From 593b94c1205fd46d2ef981685bd8ba866e3dd6ab Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:49:20 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .actrc | 1 + .claude/commands/ludwig_review.md | 70 + .claude/scheduled_tasks.lock | 1 + .claude/settings.json | 5 + .deepsource.toml | 10 + .devcontainer/Dockerfile | 24 + .devcontainer/devcontainer.json | 24 + .flake8 | 20 + .github/ISSUE_TEMPLATE/bug_report.md | 37 + .github/ISSUE_TEMPLATE/feature_request.md | 22 + .github/pull_request_template.md | 15 + .github/workflows/docker.yml | 86 + .github/workflows/pytest.yml | 267 + .github/workflows/pytest_slow.yml | 72 + .github/workflows/schema.yml | 73 + .github/workflows/test-results.yml | 70 + .github/workflows/upload-pypi.yml | 36 + .gitignore | 158 + .nojekyll | 0 .plan | 315 + .plan-lazy-preprocessing.md | 320 + .pre-commit-config.yaml | 61 + .protolint.yaml | 53 + .vscode/settings.json | 20 + CODEOWNERS | 2 + CODE_OF_CONDUCT.md | 3 + CONTRIBUTING.md | 220 + IMPROVEMENT_PLAN.md | 302 + LICENSE | 252 + NOTICE | 107 + README.md | 353 + README.wehub.md | 7 + README_KR.md | 311 + RELEASES.md | 50 + docker/README.md | 137 + docker/build_and_push.sh | 74 + docker/ludwig-gpu/Dockerfile | 43 + docker/ludwig-ray-gpu/Dockerfile | 40 + docker/ludwig-ray/Dockerfile | 39 + docker/ludwig/Dockerfile | 39 + docs/developer_guide/adding_a_feature_type.md | 348 + examples/README.md | 13 + examples/alignment/README.md | 113 + examples/alignment/alignment_dpo.ipynb | 477 + examples/alignment/config_dpo.yaml | 27 + examples/alignment/config_grpo.yaml | 28 + examples/alignment/config_kto.yaml | 29 + examples/alignment/config_orpo.yaml | 28 + examples/alignment/prepare_dataset.py | 132 + examples/alignment/train_dpo.py | 96 + examples/alignment/train_grpo.py | 392 + examples/anomaly_detection/README.md | 119 + .../anomaly_detection/anomaly_detection.ipynb | 486 + .../anomaly_detection/config_deep_sad.yaml | 44 + .../anomaly_detection/config_deep_svdd.yaml | 38 + examples/anomaly_detection/config_drocc.yaml | 41 + examples/anomaly_detection/train.py | 190 + examples/calibration/README.md | 22 + .../train_forest_cover_calibrated.py | 98 + .../train_mushroom_edibility_calibrated.py | 124 + examples/class_imbalance/README.md | 50 + .../balanced_model_config.yaml | 34 + examples/class_imbalance/model_training.py | 57 + .../model_training_results.ipynb | 246 + .../standard_model_config.yaml | 32 + examples/forecasting/README.md | 3 + examples/forecasting/config.yaml | 20 + examples/getting_started/rotten_tomatoes.yaml | 20 + examples/getting_started/run.sh | 20 + examples/hypernetwork/README.md | 73 + examples/hypernetwork/config_concat.yaml | 31 + .../hypernetwork/config_hypernetwork.yaml | 31 + examples/hypernetwork/hypernetwork.ipynb | 427 + examples/hypernetwork/train_hypernetwork.py | 160 + examples/hyperopt/README.md | 28 + examples/hyperopt/README_optuna.md | 105 + examples/hyperopt/config_optuna.yaml | 83 + .../hyperopt/model_hyperopt_example.ipynb | 1124 + examples/hyperopt/optuna_executor.ipynb | 453 + examples/hyperopt/optuna_executor.py | 145 + examples/image_encoders/README.md | 78 + examples/image_encoders/compare_encoders.py | 183 + examples/image_encoders/config_clip.yaml | 14 + .../config_dinov2_finetuned.yaml | 14 + .../config_dinov2_linear_probe.yaml | 14 + examples/image_encoders/config_siglip.yaml | 14 + .../image_encoders/config_stacked_cnn.yaml | 13 + examples/image_encoders/image_encoders.ipynb | 741 + examples/insurance_lite/config.yaml | 64 + examples/insurance_lite/train.py | 33 + examples/kfold_cv/README.md | 74 + examples/kfold_cv/display_kfold_cv_results.py | 33 + examples/kfold_cv/k-fold_cv_classification.sh | 21 + .../prepare_classification_data_set.py | 86 + examples/kfold_cv/regression_example.ipynb | 686 + examples/lbfgs/config.yaml | 40 + examples/lbfgs/model.py | 32 + examples/llama2_7b_finetuning_4bit/README.md | 43 + .../llama2_7b_4bit.yaml | 28 + .../llama2_7b_finetuning_4bit/run_train.sh | 10 + .../llama2_7b_finetuning_4bit/train_alpaca.py | 56 + .../llm_base_model_dequantization/README.md | 74 + .../phi_2_dequantization.py | 44 + examples/llm_config_generation/README.md | 93 + .../example_description.txt | 21 + .../generate_and_train.py | 240 + .../llm_config_generation.ipynb | 394 + .../simple_model_training.py | 110 + examples/llm_finetuning/README.md | 57 + .../llm_finetuning/imdb_deepspeed_zero3.yaml | 25 + .../imdb_deepspeed_zero3_ray.yaml | 29 + examples/llm_finetuning/run_train_dsz3.sh | 10 + examples/llm_finetuning/run_train_dsz3_ray.sh | 10 + examples/llm_finetuning/train_imdb_ray.py | 58 + .../train_alpaca_ray.py | 58 + examples/llm_structured_output/README.md | 71 + .../config_constrained.yaml | 29 + .../config_json_schema.yaml | 49 + .../llm_structured_output/run_structured.py | 255 + .../structured_output.ipynb | 382 + .../simple_model_training.py | 79 + .../simple_model_training.py | 102 + examples/mamba_encoders/README.md | 73 + examples/mamba_encoders/jamba_sequence.yaml | 54 + .../mamba2_text_classification.yaml | 51 + examples/mnist/README.md | 12 + examples/mnist/advanced_model_training.py | 86 + examples/mnist/assess_model_performance.py | 37 + examples/mnist/config.yaml | 26 + examples/mnist/simple_model_training.py | 35 + .../mnist/visualize_model_test_results.ipynb | 391 + examples/multi_task/README.md | 96 + examples/multi_task/compare_balancers.py | 113 + examples/multi_task/config_famo.yaml | 65 + examples/multi_task/config_nash_mtl.yaml | 65 + examples/multi_task/config_no_balancing.yaml | 65 + examples/multi_task/config_uncertainty.yaml | 65 + examples/multi_task/multi_task.ipynb | 459 + examples/multi_task/train_multi_task.py | 237 + examples/open_set_recognition/README.md | 117 + .../open_set_recognition/config_baseline.yaml | 20 + .../config_baseline_mnist.yaml | 39 + .../open_set_recognition/config_entropic.yaml | 28 + .../config_entropic_mnist.yaml | 49 + .../config_objectosphere.yaml | 31 + .../config_objectosphere_mnist.yaml | 53 + .../open_set_recognition/open_set_mnist.ipynb | 677 + .../open_set_recognition/train_open_set.py | 268 + examples/optimizers/README.md | 75 + examples/optimizers/config_adafactor.yaml | 33 + examples/optimizers/config_adamw.yaml | 34 + examples/optimizers/config_muon.yaml | 34 + examples/optimizers/config_radam.yaml | 34 + .../config_schedule_free_adamw.yaml | 34 + .../optimizers/optimizer_comparison.ipynb | 583 + examples/optimizers/optimizer_comparison.py | 153 + examples/peft_advanced/README.md | 55 + examples/peft_advanced/compare_adapters.py | 73 + examples/peft_advanced/corda_lora.yaml | 26 + examples/peft_advanced/eva_lora.yaml | 34 + examples/peft_advanced/ln_tuning_llm.yaml | 24 + examples/peft_advanced/loftq_lora.yaml | 28 + examples/peft_advanced/oft_llm.yaml | 27 + examples/peft_advanced/pissa_lora.yaml | 30 + examples/peft_advanced/tinylora_llm.yaml | 26 + examples/peft_advanced/train_example.py | 75 + examples/peft_advanced/vblora_llm.yaml | 26 + examples/ray/job_submission/README.md | 107 + examples/ray/job_submission/config.yaml | 38 + examples/ray/job_submission/submit_job.py | 231 + .../ray/job_submission/train_on_cluster.py | 76 + examples/ray/kubernetes/README.md | 70 + .../clusters/ludwig-ray-cpu-cluster.yaml | 136 + .../clusters/ludwig-ray-gpu-cluster.yaml | 138 + examples/ray/kubernetes/utils/attach.sh | 5 + examples/ray/kubernetes/utils/dashboard.sh | 5 + examples/ray/kubernetes/utils/krsync.sh | 23 + examples/ray/kubernetes/utils/ray_down.sh | 4 + examples/ray/kubernetes/utils/ray_up.sh | 4 + examples/ray/kubernetes/utils/rsync_up.sh | 16 + examples/ray/kubernetes/utils/submit.sh | 9 + examples/ray/kubernetes/utils/upload.sh | 10 + .../ecd_freezing_with_regex_training.py | 69 + .../llm_freezing_with_regex_training.py | 62 + examples/semantic_segmentation/README.md | 112 + examples/semantic_segmentation/camseq.py | 57 + .../semantic_segmentation/config_camseq.yaml | 33 + .../semantic_segmentation/config_fpn.yaml | 37 + .../config_segformer.yaml | 38 + .../config_unet_depth.yaml | 36 + .../semantic_segmentation.ipynb | 578 + .../semantic_segmentation/unet_depth_sweep.py | 124 + examples/serve/README.md | 87 + examples/serve/client_program.py | 93 + .../serve/prometheus_monitoring/README.md | 82 + .../prometheus_monitoring/docker-compose.yml | 69 + .../prometheus_monitoring/prometheus.yml | 8 + examples/serve/vllm_client.py | 172 + examples/serve/vllm_serving.ipynb | 391 + examples/serving/kserve/README.md | 92 + examples/serving/kserve/serving_config.yaml | 44 + examples/serving/ray_serve/README.md | 85 + examples/serving/ray_serve/deploy.py | 87 + examples/synthetic/train.py | 26 + examples/tabnet/higgs/medium_config.yaml | 85 + examples/tabnet/higgs/small_config.yaml | 85 + examples/tabnet/higgs/train_higgs_medium.py | 12 + examples/tabnet/higgs/train_higgs_small.py | 12 + .../nbeats_example.yaml | 20 + .../patchtst_example.yaml | 22 + examples/titanic/README.md | 42 + examples/titanic/model1_config.yaml | 23 + examples/titanic/model2_config.yaml | 32 + examples/titanic/model_training_results.ipynb | 373 + examples/titanic/multiple_model_training.py | 49 + examples/titanic/simple_model_training.py | 69 + examples/twitter_bots/README.md | 40 + examples/twitter_bots/train_twitter_bots.py | 92 + .../train_twitter_bots_text_only.py | 105 + examples/uncertainty/README.md | 81 + examples/uncertainty/config_baseline.yaml | 69 + examples/uncertainty/config_calibrated.yaml | 74 + examples/uncertainty/config_mc_dropout.yaml | 77 + examples/uncertainty/train.py | 339 + examples/uncertainty/uncertainty.ipynb | 552 + examples/vlm_finetuning/README.md | 65 + examples/vlm_finetuning/run.py | 71 + examples/vlm_finetuning/vlm_config.yaml | 49 + examples/wine_quality/README.md | 24 + .../wine_quality/model_defaults_example.ipynb | 272 + examples/wmt15/config_large.yaml | 10 + examples/wmt15/config_small.yaml | 8 + examples/wmt15/train_nmt.py | 24 + ludwig/__init__.py | 38 + ludwig/accounting/__init__.py | 0 ludwig/accounting/used_tokens.py | 35 + ludwig/api.py | 2044 + ludwig/api_annotations.py | 183 + ludwig/api_types.py | 141 + ludwig/automl/__init__.py | 32 + ludwig/automl/auto_tune_config.py | 286 + ludwig/automl/automl.py | 521 + ludwig/automl/base_config.py | 439 + ludwig/automl/config_enumerator.py | 255 + ludwig/automl/config_sampler.py | 394 + ludwig/automl/config_validator.py | 300 + .../automl/defaults/base_automl_config.yaml | 20 + .../defaults/combiner/concat_config.yaml | 26 + .../combiner/ft_transformer_config.yaml | 39 + .../defaults/combiner/tabnet_config.yaml | 46 + .../combiner/tabtransformer_config.yaml | 39 + .../defaults/combiner/transformer_config.yaml | 36 + ludwig/automl/defaults/reference_configs.yaml | 4217 + ludwig/automl/defaults/text/bert_config.yaml | 29 + ludwig/automl/search_space.py | 472 + ludwig/automl/target_detection.py | 261 + ludwig/backend/__init__.py | 103 + ludwig/backend/base.py | 337 + ludwig/backend/datasource.py | 67 + ludwig/backend/ray.py | 1092 + ludwig/backend/utils/__init__.py | 0 ludwig/backend/utils/storage.py | 69 + ludwig/benchmarking/README.md | 276 + ludwig/benchmarking/__init__.py | 0 ludwig/benchmarking/artifacts.py | 66 + ludwig/benchmarking/benchmark.py | 155 + .../examples/benchmarking_config.yaml | 45 + .../benchmarking/examples/process_config.py | 95 + ludwig/benchmarking/profiler.py | 268 + ludwig/benchmarking/profiler_callbacks.py | 59 + ludwig/benchmarking/profiler_dataclasses.py | 89 + ludwig/benchmarking/reporting.py | 240 + ludwig/benchmarking/summarize.py | 108 + ludwig/benchmarking/summary_dataclasses.py | 470 + ludwig/benchmarking/utils.py | 304 + ludwig/callbacks/__init__.py | 404 + ludwig/callbacks/studio.py | 251 + ludwig/check.py | 61 + ludwig/cli.py | 201 + ludwig/collect.py | 457 + ludwig/combiners/__init__.py | 2 + ludwig/combiners/combiners.py | 1428 + ludwig/combiners/tabpfn_v2_combiner.py | 105 + ludwig/config_generation.py | 265 + ludwig/config_sampling/__init__.py | 0 ludwig/config_sampling/explore_schema.py | 315 + ludwig/config_sampling/parameter_sampling.py | 145 + ludwig/config_validation/__init__.py | 0 ludwig/config_validation/checks.py | 738 + ludwig/config_validation/preprocessing.py | 20 + ludwig/config_validation/validation.py | 75 + ludwig/constants.py | 351 + ludwig/contrib.py | 48 + ludwig/contribs/__init__.py | 85 + ludwig/contribs/aim.py | 105 + ludwig/contribs/comet.py | 140 + ludwig/contribs/mlflow/__init__.py | 279 + ludwig/contribs/mlflow/mlflow3.py | 206 + ludwig/contribs/mlflow/model.py | 248 + ludwig/contribs/wandb.py | 72 + ludwig/data/__init__.py | 0 ludwig/data/batcher/__init__.py | 0 ludwig/data/batcher/base.py | 33 + ludwig/data/batcher/bucketed.py | 114 + ludwig/data/batcher/iterable.py | 46 + ludwig/data/batcher/random_access.py | 271 + ludwig/data/batcher/test_batcher.py | 109 + ludwig/data/cache/__init__.py | 0 ludwig/data/cache/manager.py | 165 + ludwig/data/cache/types.py | 108 + ludwig/data/cache/util.py | 34 + ludwig/data/concatenate_datasets.py | 101 + ludwig/data/dataframe/__init__.py | 15 + ludwig/data/dataframe/base.py | 105 + ludwig/data/dataframe/dask.py | 317 + ludwig/data/dataframe/modin.py | 115 + ludwig/data/dataframe/pandas.py | 118 + ludwig/data/dataset/__init__.py | 38 + ludwig/data/dataset/base.py | 76 + ludwig/data/dataset/pandas.py | 397 + ludwig/data/dataset/ray.py | 491 + ludwig/data/dataset_synthesizer.py | 641 + ludwig/data/format_registry.py | 77 + ludwig/data/lazy_utils.py | 310 + ludwig/data/multimodal_collator.py | 93 + ludwig/data/negative_sampling.py | 106 + ludwig/data/postprocessing.py | 139 + ludwig/data/preprocessing.py | 1821 + ludwig/data/preprocessing_progress.py | 204 + ludwig/data/prompt.py | 244 + ludwig/data/sampler.py | 81 + ludwig/data/split.py | 391 + ludwig/data/split_dataset.py | 48 + ludwig/data/split_utils.py | 81 + ludwig/data/statistics.py | 149 + ludwig/data/types.py | 239 + ludwig/data/utils.py | 64 + ludwig/datasets/README.md | 140 + ludwig/datasets/__init__.py | 399 + ludwig/datasets/archives.py | 140 + ludwig/datasets/configs/__init__.py | 0 ludwig/datasets/configs/abjad_kids.yaml | 14 + .../configs/acronym_identification.yaml | 14 + .../datasets/configs/adult_census_income.yaml | 48 + ludwig/datasets/configs/adult_income_hf.yaml | 39 + .../datasets/configs/ae_price_prediction.yaml | 46 + ludwig/datasets/configs/aegis_safety.yaml | 16 + ludwig/datasets/configs/aeslc.yaml | 14 + ludwig/datasets/configs/ag_news_hf.yaml | 14 + ludwig/datasets/configs/agnews.yaml | 23 + ludwig/datasets/configs/ai2d_diagrams.yaml | 17 + .../configs/ai_generated_ecommerce.yaml | 14 + ludwig/datasets/configs/allocine.yaml | 14 + .../configs/allstate_claims_severity.yaml | 15 + ludwig/datasets/configs/alpaca.yaml | 16 + ludwig/datasets/configs/alpaca_cleaned.yaml | 16 + ludwig/datasets/configs/alpaca_gpt4.yaml | 16 + ludwig/datasets/configs/alpaca_gpt4_zh.yaml | 16 + .../amazon_employee_access_challenge.yaml | 16 + .../configs/amazon_massive_intent.yaml | 16 + .../configs/amazon_massive_scenario.yaml | 16 + ludwig/datasets/configs/amazon_polarity.yaml | 16 + .../configs/amazon_review_polarity.yaml | 27 + ludwig/datasets/configs/amazon_reviews.yaml | 27 + .../datasets/configs/amazon_reviews_2023.yaml | 16 + .../configs/amazon_science_massive.yaml | 22 + ludwig/datasets/configs/ambig_qa.yaml | 14 + ludwig/datasets/configs/ames_housing.yaml | 14 + ludwig/datasets/configs/ami_asr.yaml | 17 + ludwig/datasets/configs/anli.yaml | 20 + ludwig/datasets/configs/app_reviews.yaml | 14 + ludwig/datasets/configs/aqua_rat.yaml | 17 + ludwig/datasets/configs/arc_challenge.yaml | 17 + ludwig/datasets/configs/arc_easy.yaml | 17 + .../configs/arxiv_abstracts_2021.yaml | 14 + .../datasets/configs/arxiv_summarization.yaml | 15 + .../datasets/configs/audioset_balanced.yaml | 17 + ludwig/datasets/configs/banking77.yaml | 16 + ludwig/datasets/configs/banking77_legacy.yaml | 14 + ludwig/datasets/configs/bbcnews.yaml | 14 + ludwig/datasets/configs/bbh.yaml | 15 + ludwig/datasets/configs/beans.yaml | 16 + ludwig/datasets/configs/beavertails.yaml | 18 + ludwig/datasets/configs/belebele.yaml | 34 + ludwig/datasets/configs/belebele_fr.yaml | 34 + ludwig/datasets/configs/big_patent.yaml | 15 + ludwig/datasets/configs/bigbench.yaml | 15 + ludwig/datasets/configs/billsum.yaml | 16 + .../configs/bitext_customer_intent.yaml | 16 + .../configs/bitext_customer_support.yaml | 16 + ludwig/datasets/configs/blimp.yaml | 17 + .../configs/bnp_claims_management.yaml | 14 + .../configs/bookprice_prediction.yaml | 37 + ludwig/datasets/configs/boolq.yaml | 19 + ludwig/datasets/configs/boolq_standalone.yaml | 16 + ludwig/datasets/configs/bornholm_bitext.yaml | 14 + .../configs/brazilian_toxic_tweets.yaml | 14 + .../configs/california_house_price.yaml | 100 + .../datasets/configs/california_housing.yaml | 32 + ludwig/datasets/configs/camseq.yaml | 21 + ludwig/datasets/configs/cantonese_asr.yaml | 21 + ludwig/datasets/configs/ccnews.yaml | 16 + ludwig/datasets/configs/cifar10.yaml | 14 + ludwig/datasets/configs/cifar100.yaml | 16 + ludwig/datasets/configs/civil_comments.yaml | 26 + ludwig/datasets/configs/climate_fever.yaml | 15 + .../datasets/configs/climate_sentiment.yaml | 14 + ludwig/datasets/configs/clinc_oos.yaml | 15 + ludwig/datasets/configs/clue_afqmc.yaml | 17 + ludwig/datasets/configs/cmrc2018.yaml | 16 + ludwig/datasets/configs/cnn_dailymail.yaml | 15 + ludwig/datasets/configs/cnn_dm_hf.yaml | 15 + ludwig/datasets/configs/code_alpaca.yaml | 18 + ludwig/datasets/configs/code_contests.yaml | 18 + .../configs/code_defect_detection.yaml | 14 + ludwig/datasets/configs/code_search_net.yaml | 17 + ludwig/datasets/configs/codex_thinking.yaml | 14 + .../configs/codexglue_code_to_text.yaml | 15 + ludwig/datasets/configs/coig_cqia.yaml | 19 + ludwig/datasets/configs/cola.yaml | 17 + ludwig/datasets/configs/commitment_bank.yaml | 19 + ludwig/datasets/configs/commonsense_qa.yaml | 16 + .../datasets/configs/compas_recidivism.yaml | 53 + ludwig/datasets/configs/connect4.yaml | 101 + .../datasets/configs/consumer_complaints.yaml | 50 + ludwig/datasets/configs/copa.yaml | 23 + ludwig/datasets/configs/cord_v2.yaml | 15 + .../datasets/configs/credit_card_default.yaml | 79 + ludwig/datasets/configs/creditcard_fraud.yaml | 76 + .../configs/customer_churn_prediction.yaml | 15 + ludwig/datasets/configs/customer_reviews.yaml | 16 + ludwig/datasets/configs/dair_emotion.yaml | 14 + .../configs/data_scientist_salary.yaml | 32 + ludwig/datasets/configs/databench_qa.yaml | 17 + .../configs/databricks_dolly_15k.yaml | 18 + ludwig/datasets/configs/dbpedia.yaml | 26 + ludwig/datasets/configs/dbpedia_14.yaml | 16 + .../configs/diabetes_readmission.yaml | 313 + ludwig/datasets/configs/dialogsum.yaml | 14 + ludwig/datasets/configs/docvqa.yaml | 17 + ludwig/datasets/configs/dolly_15k.yaml | 18 + ludwig/datasets/configs/drop.yaml | 16 + ludwig/datasets/configs/duorc.yaml | 17 + ludwig/datasets/configs/electricity.yaml | 22 + .../datasets/configs/electricity_tabular.yaml | 29 + ludwig/datasets/configs/emodb.yaml | 18 + ludwig/datasets/configs/emotion.yaml | 14 + ludwig/datasets/configs/enron_spam.yaml | 24 + ludwig/datasets/configs/esc50.yaml | 31 + ludwig/datasets/configs/europarl_bg_cs.yaml | 15 + ludwig/datasets/configs/europarl_bg_en.yaml | 15 + ludwig/datasets/configs/europarl_cs_en.yaml | 15 + ludwig/datasets/configs/europarl_da_en.yaml | 15 + ludwig/datasets/configs/europarl_de_en.yaml | 15 + ludwig/datasets/configs/europarl_el_en.yaml | 15 + ludwig/datasets/configs/europarl_en_es.yaml | 15 + ludwig/datasets/configs/europarl_en_fr.yaml | 15 + ludwig/datasets/configs/europarl_en_it.yaml | 15 + ludwig/datasets/configs/europarl_en_nl.yaml | 15 + ludwig/datasets/configs/europarl_en_pl.yaml | 15 + ludwig/datasets/configs/europarl_en_pt.yaml | 15 + ludwig/datasets/configs/europarl_en_ro.yaml | 15 + ludwig/datasets/configs/europarl_en_sv.yaml | 15 + ludwig/datasets/configs/eurosat.yaml | 14 + ludwig/datasets/configs/eurosat_rgb.yaml | 14 + ludwig/datasets/configs/factcheck.yaml | 20 + .../datasets/configs/fake_job_postings2.yaml | 31 + .../datasets/configs/fake_news_detection.yaml | 16 + ludwig/datasets/configs/farstail_nli.yaml | 16 + ludwig/datasets/configs/fashion_mnist.yaml | 14 + ludwig/datasets/configs/fever.yaml | 31 + ludwig/datasets/configs/fever_gold.yaml | 14 + ludwig/datasets/configs/few_nerd.yaml | 15 + .../configs/financial_phrasebank.yaml | 15 + ludwig/datasets/configs/fineweb_edu.yaml | 16 + .../configs/flashrag_2wikimultihop.yaml | 15 + ludwig/datasets/configs/flickr8k.yaml | 29 + ludwig/datasets/configs/flores_101.yaml | 20 + ludwig/datasets/configs/food101.yaml | 14 + ludwig/datasets/configs/forest_cover.yaml | 41 + ludwig/datasets/configs/gaia_cepheids.yaml | 16 + ludwig/datasets/configs/gaia_rrlyrae.yaml | 16 + .../configs/gaia_spectroscopic_binaries.yaml | 18 + .../configs/gaia_young_stellar_objects.yaml | 16 + ludwig/datasets/configs/gaiasky_qa.yaml | 14 + ludwig/datasets/configs/germeval18.yaml | 14 + ludwig/datasets/configs/go_emotions.yaml | 31 + .../configs/go_emotions_multiclass.yaml | 15 + ludwig/datasets/configs/goemotions.yaml | 27 + ludwig/datasets/configs/goodbooks_books.yaml | 75 + ...gle_qa_answer_type_reason_explanation.yaml | 104 + ...e_qa_question_type_reason_explanation.yaml | 104 + ludwig/datasets/configs/google_quest_qa.yaml | 104 + .../configs/govreport_summarization.yaml | 14 + ludwig/datasets/configs/graid_bdd.yaml | 18 + ludwig/datasets/configs/gsm8k.yaml | 15 + ludwig/datasets/configs/gsm8k_openai.yaml | 15 + ludwig/datasets/configs/gtsrb.yaml | 14 + .../configs/handwritten_crossouts.yaml | 14 + ludwig/datasets/configs/hate_speech18.yaml | 14 + .../configs/hatespeech_offensive.yaml | 15 + ludwig/datasets/configs/hc3.yaml | 21 + ludwig/datasets/configs/hc3_chinese.yaml | 20 + ludwig/datasets/configs/heart_failure.yaml | 37 + ludwig/datasets/configs/hellaswag.yaml | 18 + ludwig/datasets/configs/helpsteer2.yaml | 16 + .../configs/hermes_function_calling.yaml | 17 + ludwig/datasets/configs/hh_rlhf.yaml | 14 + ludwig/datasets/configs/higgs.yaml | 77 + ludwig/datasets/configs/hotpot_qa.yaml | 15 + ludwig/datasets/configs/hugging_face.yaml | 5 + ludwig/datasets/configs/ieee_fraud.yaml | 19 + ludwig/datasets/configs/imagenet_100.yaml | 14 + .../configs/imbalanced_insurance.yaml | 40 + ludwig/datasets/configs/imdb.yaml | 17 + .../configs/imdb_genre_prediction.yaml | 42 + ludwig/datasets/configs/imdb_mteb.yaml | 14 + ludwig/datasets/configs/imdb_sentiment.yaml | 14 + ludwig/datasets/configs/indic_glue.yaml | 15 + ludwig/datasets/configs/insurance_lite.yaml | 33 + .../datasets/configs/intuitive_physics.yaml | 14 + ludwig/datasets/configs/invoice_data.yaml | 15 + ludwig/datasets/configs/iris.yaml | 22 + ludwig/datasets/configs/iris_sklearn.yaml | 20 + ludwig/datasets/configs/irony.yaml | 17 + .../datasets/configs/jc_penney_products.yaml | 30 + .../configs/jigsaw_unintended_bias.yaml | 111 + .../configs/jigsaw_unintended_bias100k.yaml | 108 + ludwig/datasets/configs/kdd_appetency.yaml | 485 + ludwig/datasets/configs/kdd_churn.yaml | 485 + ludwig/datasets/configs/kdd_upselling.yaml | 485 + .../configs/kick_starter_funding.yaml | 38 + ludwig/datasets/configs/kilt_nq.yaml | 15 + ludwig/datasets/configs/klue_sts.yaml | 17 + ludwig/datasets/configs/klue_topic.yaml | 21 + ludwig/datasets/configs/kmmlu.yaml | 23 + ludwig/datasets/configs/kvasir_vqa.yaml | 18 + .../configs/language_identification.yaml | 14 + ludwig/datasets/configs/lex_glue_ecthr.yaml | 15 + ludwig/datasets/configs/lex_glue_eurlex.yaml | 15 + ludwig/datasets/configs/librispeech.yaml | 23 + ludwig/datasets/configs/lmsys_arena.yaml | 18 + ludwig/datasets/configs/m3cot.yaml | 16 + ludwig/datasets/configs/m_mmlu.yaml | 23 + ludwig/datasets/configs/map_trace.yaml | 16 + ludwig/datasets/configs/math500.yaml | 22 + ludwig/datasets/configs/mathvista.yaml | 14 + ludwig/datasets/configs/mbpp.yaml | 14 + .../configs/measuring_hate_speech.yaml | 296 + .../datasets/configs/medical_flashcards.yaml | 16 + ludwig/datasets/configs/medmcqa.yaml | 32 + ludwig/datasets/configs/melbourne_airbnb.yaml | 226 + .../configs/mercari_price_suggestion.yaml | 45 + .../configs/mercari_price_suggestion100K.yaml | 42 + .../configs/mercedes_benz_greener.yaml | 14 + ludwig/datasets/configs/merit.yaml | 15 + ludwig/datasets/configs/minds14.yaml | 17 + ludwig/datasets/configs/mini_imagenet.yaml | 14 + ludwig/datasets/configs/mls_german.yaml | 32 + ludwig/datasets/configs/mmlu.yaml | 17 + ludwig/datasets/configs/mmlu_lighteval.yaml | 19 + ludwig/datasets/configs/mmlu_pro.yaml | 18 + ludwig/datasets/configs/mmsulab.yaml | 14 + ludwig/datasets/configs/mnist.yaml | 33 + ludwig/datasets/configs/mnist_ylecun.yaml | 14 + ludwig/datasets/configs/mnli.yaml | 19 + ludwig/datasets/configs/mobile_mold.yaml | 14 + ludwig/datasets/configs/moral_stories.yaml | 22 + ludwig/datasets/configs/mrpc.yaml | 19 + ludwig/datasets/configs/msmarco_passage.yaml | 16 + .../configs/mteb_amazon_polarity.yaml | 14 + .../configs/mteb_amazon_reviews_class_de.yaml | 15 + .../configs/mteb_amazon_reviews_class_en.yaml | 15 + .../configs/mteb_amazon_reviews_class_es.yaml | 15 + .../configs/mteb_amazon_reviews_class_fr.yaml | 15 + .../configs/mteb_amazon_reviews_class_ja.yaml | 15 + .../configs/mteb_amazon_reviews_class_zh.yaml | 15 + ludwig/datasets/configs/mteb_biosses.yaml | 16 + .../configs/mteb_cyrillic_turkic.yaml | 14 + ludwig/datasets/configs/mteb_emotion.yaml | 16 + .../configs/mteb_financial_phrasebank.yaml | 14 + ludwig/datasets/configs/mteb_frenk_en.yaml | 14 + ludwig/datasets/configs/mteb_frenk_hr.yaml | 14 + ludwig/datasets/configs/mteb_frenk_sl.yaml | 14 + ludwig/datasets/configs/mteb_georeview.yaml | 14 + ludwig/datasets/configs/mteb_greek_legal.yaml | 14 + ludwig/datasets/configs/mteb_imdb.yaml | 14 + .../datasets/configs/mteb_ita_casehold.yaml | 14 + ludwig/datasets/configs/mteb_jd_review.yaml | 14 + ludwig/datasets/configs/mteb_kor_sarcasm.yaml | 14 + .../datasets/configs/mteb_language_class.yaml | 14 + .../configs/mteb_massive_intent_af.yaml | 15 + .../configs/mteb_massive_intent_am.yaml | 15 + .../configs/mteb_massive_intent_ar.yaml | 15 + .../configs/mteb_massive_intent_az.yaml | 15 + .../configs/mteb_massive_intent_bn.yaml | 15 + .../configs/mteb_massive_intent_cy.yaml | 15 + .../configs/mteb_massive_intent_da.yaml | 15 + .../configs/mteb_massive_intent_de.yaml | 15 + .../configs/mteb_massive_intent_el.yaml | 15 + .../configs/mteb_massive_intent_en.yaml | 15 + .../configs/mteb_massive_intent_es.yaml | 15 + .../configs/mteb_massive_intent_fa.yaml | 15 + .../configs/mteb_massive_intent_fi.yaml | 15 + .../configs/mteb_massive_intent_fr.yaml | 15 + .../configs/mteb_massive_intent_he.yaml | 15 + .../configs/mteb_massive_intent_hi.yaml | 15 + .../configs/mteb_massive_intent_hu.yaml | 15 + .../configs/mteb_massive_intent_hy.yaml | 15 + .../configs/mteb_massive_intent_id.yaml | 15 + .../configs/mteb_massive_intent_is.yaml | 15 + .../configs/mteb_massive_intent_it.yaml | 15 + .../configs/mteb_massive_intent_ja.yaml | 15 + .../configs/mteb_massive_intent_jv.yaml | 15 + .../configs/mteb_massive_intent_ka.yaml | 15 + .../configs/mteb_massive_intent_km.yaml | 15 + .../configs/mteb_massive_intent_kn.yaml | 15 + .../configs/mteb_massive_intent_ko.yaml | 15 + .../configs/mteb_massive_intent_lv.yaml | 15 + .../configs/mteb_massive_intent_ml.yaml | 15 + .../configs/mteb_massive_intent_mn.yaml | 15 + .../configs/mteb_massive_intent_ms.yaml | 15 + .../configs/mteb_massive_intent_my.yaml | 15 + .../configs/mteb_massive_intent_nb.yaml | 15 + .../configs/mteb_massive_intent_nl.yaml | 15 + .../configs/mteb_massive_intent_pl.yaml | 15 + .../configs/mteb_massive_intent_pt.yaml | 15 + .../configs/mteb_massive_intent_ro.yaml | 15 + .../configs/mteb_massive_intent_ru.yaml | 15 + .../configs/mteb_massive_intent_sl.yaml | 15 + .../configs/mteb_massive_intent_sq.yaml | 15 + .../configs/mteb_massive_intent_sv.yaml | 15 + .../configs/mteb_massive_intent_sw.yaml | 15 + .../configs/mteb_massive_intent_ta.yaml | 15 + .../configs/mteb_massive_intent_te.yaml | 15 + .../configs/mteb_massive_intent_th.yaml | 15 + .../configs/mteb_massive_intent_tl.yaml | 15 + .../configs/mteb_massive_intent_tr.yaml | 15 + .../configs/mteb_massive_intent_ur.yaml | 15 + .../configs/mteb_massive_intent_vi.yaml | 15 + .../configs/mteb_massive_intent_zh_cn.yaml | 15 + .../configs/mteb_massive_intent_zh_tw.yaml | 15 + .../configs/mteb_massive_scenario_af.yaml | 15 + .../configs/mteb_massive_scenario_am.yaml | 15 + .../configs/mteb_massive_scenario_ar.yaml | 15 + .../configs/mteb_massive_scenario_az.yaml | 15 + .../configs/mteb_massive_scenario_bn.yaml | 15 + .../configs/mteb_massive_scenario_cy.yaml | 15 + .../configs/mteb_massive_scenario_da.yaml | 15 + .../configs/mteb_massive_scenario_de.yaml | 15 + .../configs/mteb_massive_scenario_el.yaml | 15 + .../configs/mteb_massive_scenario_en.yaml | 15 + .../configs/mteb_massive_scenario_es.yaml | 15 + .../configs/mteb_massive_scenario_fa.yaml | 15 + .../configs/mteb_massive_scenario_fi.yaml | 15 + .../configs/mteb_massive_scenario_fr.yaml | 15 + .../configs/mteb_massive_scenario_he.yaml | 15 + .../configs/mteb_massive_scenario_hi.yaml | 15 + .../configs/mteb_massive_scenario_hu.yaml | 15 + .../configs/mteb_massive_scenario_hy.yaml | 15 + .../configs/mteb_massive_scenario_id.yaml | 15 + .../configs/mteb_massive_scenario_is.yaml | 15 + .../configs/mteb_massive_scenario_it.yaml | 15 + .../configs/mteb_massive_scenario_ja.yaml | 15 + .../configs/mteb_massive_scenario_jv.yaml | 15 + .../configs/mteb_massive_scenario_ka.yaml | 15 + .../configs/mteb_massive_scenario_km.yaml | 15 + .../configs/mteb_massive_scenario_kn.yaml | 15 + .../configs/mteb_massive_scenario_ko.yaml | 15 + .../configs/mteb_massive_scenario_lv.yaml | 15 + .../configs/mteb_massive_scenario_ml.yaml | 15 + .../configs/mteb_massive_scenario_mn.yaml | 15 + .../configs/mteb_massive_scenario_ms.yaml | 15 + .../configs/mteb_massive_scenario_my.yaml | 15 + .../configs/mteb_massive_scenario_nb.yaml | 15 + .../configs/mteb_massive_scenario_nl.yaml | 15 + .../configs/mteb_massive_scenario_pl.yaml | 15 + .../configs/mteb_massive_scenario_pt.yaml | 15 + .../configs/mteb_massive_scenario_ro.yaml | 15 + .../configs/mteb_massive_scenario_ru.yaml | 15 + .../configs/mteb_massive_scenario_sl.yaml | 15 + .../configs/mteb_massive_scenario_sq.yaml | 15 + .../configs/mteb_massive_scenario_sv.yaml | 15 + .../configs/mteb_massive_scenario_sw.yaml | 15 + .../configs/mteb_massive_scenario_ta.yaml | 15 + .../configs/mteb_massive_scenario_te.yaml | 15 + .../configs/mteb_massive_scenario_th.yaml | 15 + .../configs/mteb_massive_scenario_tl.yaml | 15 + .../configs/mteb_massive_scenario_tr.yaml | 15 + .../configs/mteb_massive_scenario_ur.yaml | 15 + .../configs/mteb_massive_scenario_vi.yaml | 15 + .../configs/mteb_massive_scenario_zh_cn.yaml | 15 + .../configs/mteb_massive_scenario_zh_tw.yaml | 15 + .../datasets/configs/mteb_mtop_domain_de.yaml | 15 + .../datasets/configs/mteb_mtop_domain_en.yaml | 15 + .../datasets/configs/mteb_mtop_domain_es.yaml | 15 + .../datasets/configs/mteb_mtop_domain_fr.yaml | 15 + .../datasets/configs/mteb_mtop_domain_hi.yaml | 15 + .../datasets/configs/mteb_mtop_domain_th.yaml | 15 + .../configs/mteb_mtop_intent_de2.yaml | 15 + .../datasets/configs/mteb_mtop_intent_en.yaml | 15 + .../configs/mteb_mtop_intent_es2.yaml | 15 + .../configs/mteb_mtop_intent_fr2.yaml | 15 + .../configs/mteb_mtop_intent_hi2.yaml | 15 + .../configs/mteb_mtop_intent_th2.yaml | 15 + .../configs/mteb_multilingual_sentiment.yaml | 14 + .../configs/mteb_naija_senti_hau.yaml | 15 + .../configs/mteb_naija_senti_ibo.yaml | 15 + .../configs/mteb_naija_senti_pcm.yaml | 15 + .../configs/mteb_naija_senti_yor.yaml | 15 + ludwig/datasets/configs/mteb_nepali_news.yaml | 14 + ludwig/datasets/configs/mteb_nordic_lang.yaml | 14 + .../configs/mteb_online_shopping.yaml | 14 + .../datasets/configs/mteb_poem_sentiment.yaml | 14 + .../configs/mteb_sensitive_topics.yaml | 14 + .../configs/mteb_sentiment_hindi.yaml | 14 + ludwig/datasets/configs/mteb_sts17_ar.yaml | 17 + ludwig/datasets/configs/mteb_sts17_de.yaml | 17 + ludwig/datasets/configs/mteb_sts17_en.yaml | 17 + ludwig/datasets/configs/mteb_sts17_es.yaml | 17 + ludwig/datasets/configs/mteb_sts17_fr.yaml | 17 + ludwig/datasets/configs/mteb_sts22_ar.yaml | 17 + ludwig/datasets/configs/mteb_sts22_de.yaml | 17 + ludwig/datasets/configs/mteb_sts22_de_en.yaml | 17 + ludwig/datasets/configs/mteb_sts22_de_fr.yaml | 17 + ludwig/datasets/configs/mteb_sts22_en.yaml | 17 + ludwig/datasets/configs/mteb_sts22_es.yaml | 17 + ludwig/datasets/configs/mteb_sts22_es_en.yaml | 17 + ludwig/datasets/configs/mteb_sts22_es_it.yaml | 17 + ludwig/datasets/configs/mteb_sts22_fr.yaml | 17 + ludwig/datasets/configs/mteb_sts22_it.yaml | 17 + ludwig/datasets/configs/mteb_sts22_pl.yaml | 17 + ludwig/datasets/configs/mteb_sts22_pl_en.yaml | 17 + ludwig/datasets/configs/mteb_sts22_ru.yaml | 17 + ludwig/datasets/configs/mteb_sts22_tr.yaml | 17 + ludwig/datasets/configs/mteb_sts22_zh.yaml | 17 + ludwig/datasets/configs/mteb_sts22_zh_en.yaml | 17 + .../datasets/configs/mteb_stsbenchmark.yaml | 16 + .../configs/mteb_swiss_judgement_de.yaml | 15 + .../configs/mteb_swiss_judgement_fr.yaml | 15 + .../configs/mteb_swiss_judgement_it.yaml | 15 + ludwig/datasets/configs/mteb_tnews.yaml | 14 + ludwig/datasets/configs/mteb_toxic_convo.yaml | 14 + .../configs/mteb_turkish_product.yaml | 14 + .../configs/mteb_tweet_sentiment.yaml | 14 + ludwig/datasets/configs/mteb_tweet_topic.yaml | 14 + ludwig/datasets/configs/mteb_waimai.yaml | 14 + .../datasets/configs/mteb_yahoo_answers.yaml | 14 + ludwig/datasets/configs/multi30k.yaml | 14 + ludwig/datasets/configs/multi_nli.yaml | 30 + ludwig/datasets/configs/multinerd.yaml | 14 + ludwig/datasets/configs/multirc.yaml | 19 + ludwig/datasets/configs/multiun_ar_en.yaml | 15 + .../datasets/configs/mushroom_edibility.yaml | 62 + .../datasets/configs/natural_questions.yaml | 15 + .../natural_questions_hard_negatives.yaml | 14 + ludwig/datasets/configs/naturelm_audio.yaml | 17 + ludwig/datasets/configs/naval.yaml | 50 + .../datasets/configs/naver_news_summary.yaml | 24 + ludwig/datasets/configs/nemotron_pii.yaml | 16 + ludwig/datasets/configs/nemotron_safety.yaml | 18 + ludwig/datasets/configs/news_category.yaml | 16 + ludwig/datasets/configs/news_channel.yaml | 56 + ludwig/datasets/configs/news_popularity2.yaml | 30 + .../configs/newyorker_caption_contest.yaml | 31 + ludwig/datasets/configs/nli_zh_all.yaml | 20 + ludwig/datasets/configs/no_robots.yaml | 14 + .../datasets/configs/noshow_appointments.yaml | 43 + ludwig/datasets/configs/nq_open.yaml | 15 + ludwig/datasets/configs/numerai28pt6.yaml | 56 + ludwig/datasets/configs/numinamath.yaml | 18 + ludwig/datasets/configs/oasst1.yaml | 16 + ludwig/datasets/configs/ohsumed_7400.yaml | 23 + ludwig/datasets/configs/ohsumed_cmu.yaml | 19 + ludwig/datasets/configs/openbookqa.yaml | 17 + ludwig/datasets/configs/openfake.yaml | 15 + ludwig/datasets/configs/opus100_en_es.yaml | 15 + ludwig/datasets/configs/opus100_en_fr.yaml | 15 + ludwig/datasets/configs/opus_books_en_fr.yaml | 15 + ludwig/datasets/configs/or_bench.yaml | 15 + ludwig/datasets/configs/orca_dpo_pairs.yaml | 16 + ludwig/datasets/configs/orca_math.yaml | 14 + .../datasets/configs/otto_group_product.yaml | 14 + ludwig/datasets/configs/oxford_pets.yaml | 18 + ludwig/datasets/configs/path_vqa.yaml | 16 + ludwig/datasets/configs/paws.yaml | 19 + ludwig/datasets/configs/paws_x.yaml | 17 + ludwig/datasets/configs/peoples_speech.yaml | 19 + ludwig/datasets/configs/persuasion.yaml | 16 + ludwig/datasets/configs/phinc.yaml | 14 + ludwig/datasets/configs/pii_masking.yaml | 14 + ludwig/datasets/configs/poem_sentiment.yaml | 15 + .../datasets/configs/poem_sentiment_hf.yaml | 14 + ludwig/datasets/configs/poker_hand.yaml | 44 + .../configs/porto_seguro_safe_driver.yaml | 14 + .../product_sentiment_machine_hack.yaml | 27 + ludwig/datasets/configs/protein.yaml | 32 + ludwig/datasets/configs/pubmed_qa.yaml | 17 + .../configs/pubmed_summarization.yaml | 14 + .../configs/python_code_instructions.yaml | 16 + ludwig/datasets/configs/qasc.yaml | 20 + ludwig/datasets/configs/qnli.yaml | 19 + ludwig/datasets/configs/qqp.yaml | 19 + ludwig/datasets/configs/race.yaml | 19 + ludwig/datasets/configs/ravnursson_asr.yaml | 19 + ludwig/datasets/configs/rendered_sst2.yaml | 14 + ludwig/datasets/configs/repid.yaml | 39 + ludwig/datasets/configs/reuters_cmu.yaml | 18 + ludwig/datasets/configs/reuters_r8.yaml | 21 + ludwig/datasets/configs/reward_bench.yaml | 18 + .../datasets/configs/rossman_store_sales.yaml | 16 + ludwig/datasets/configs/rotten_tomatoes.yaml | 14 + ludwig/datasets/configs/rte.yaml | 19 + ludwig/datasets/configs/samsum.yaml | 14 + .../santander_customer_satisfaction.yaml | 14 + .../santander_customer_transaction.yaml | 14 + .../configs/santander_value_prediction.yaml | 14 + .../datasets/configs/sarcastic_headlines.yaml | 19 + ludwig/datasets/configs/sarcos.yaml | 78 + ludwig/datasets/configs/scandisent.yaml | 16 + ludwig/datasets/configs/scienceqa_vqa.yaml | 16 + ludwig/datasets/configs/sciq.yaml | 16 + ludwig/datasets/configs/scitail.yaml | 19 + .../configs/scotus_classification.yaml | 15 + ludwig/datasets/configs/setfit_ag_news.yaml | 14 + .../configs/setfit_amazon_polarity.yaml | 14 + ludwig/datasets/configs/setfit_emotion.yaml | 14 + ludwig/datasets/configs/setfit_mrpc.yaml | 16 + ludwig/datasets/configs/setfit_sst2.yaml | 14 + ludwig/datasets/configs/setfit_subj.yaml | 14 + .../datasets/configs/setfit_yelp_review.yaml | 14 + ludwig/datasets/configs/setimes_bg_bs.yaml | 15 + ludwig/datasets/configs/sib200.yaml | 16 + ludwig/datasets/configs/sickr.yaml | 16 + ludwig/datasets/configs/sms_spam.yaml | 14 + ludwig/datasets/configs/snli.yaml | 16 + ludwig/datasets/configs/speech_massive.yaml | 19 + ludwig/datasets/configs/spotify_tracks.yaml | 24 + ludwig/datasets/configs/squad.yaml | 16 + ludwig/datasets/configs/squad_v2.yaml | 16 + ludwig/datasets/configs/sst2.yaml | 19 + ludwig/datasets/configs/sst2_hf.yaml | 16 + ludwig/datasets/configs/sst3.yaml | 21 + ludwig/datasets/configs/sst5.yaml | 19 + ludwig/datasets/configs/sst5_setfit.yaml | 16 + .../datasets/configs/stackoverflow_posts.yaml | 16 + ludwig/datasets/configs/stanford_cars.yaml | 14 + .../datasets/configs/stocks_daily_price.yaml | 22 + ludwig/datasets/configs/sts12.yaml | 16 + ludwig/datasets/configs/sts13.yaml | 16 + ludwig/datasets/configs/sts14.yaml | 16 + ludwig/datasets/configs/sts15.yaml | 16 + ludwig/datasets/configs/sts16.yaml | 16 + ludwig/datasets/configs/sts17.yaml | 18 + ludwig/datasets/configs/sts22.yaml | 18 + ludwig/datasets/configs/sts_benchmark.yaml | 24 + ludwig/datasets/configs/stsb.yaml | 19 + ludwig/datasets/configs/stsb_de.yaml | 17 + .../configs/stsb_sentencetransformers.yaml | 16 + .../datasets/configs/student_performance.yaml | 27 + ludwig/datasets/configs/sun397.yaml | 14 + ludwig/datasets/configs/superglue_rte.yaml | 19 + ludwig/datasets/configs/svhn.yaml | 15 + ludwig/datasets/configs/synthetic_fraud.yaml | 35 + ludwig/datasets/configs/synthia.yaml | 16 + ludwig/datasets/configs/tadabur.yaml | 16 + ludwig/datasets/configs/taix_ray.yaml | 14 + .../configs/talkingdata_adtrack_fraud.yaml | 13 + .../configs/telco_customer_churn.yaml | 58 + ludwig/datasets/configs/temperature.yaml | 88 + ludwig/datasets/configs/textvqa.yaml | 17 + ludwig/datasets/configs/tiny_imagenet.yaml | 14 + ludwig/datasets/configs/titanic.yaml | 15 + ludwig/datasets/configs/tobacco_document.yaml | 16 + ludwig/datasets/configs/tofu.yaml | 15 + ludwig/datasets/configs/toxic_chat.yaml | 17 + ludwig/datasets/configs/trivia_qa.yaml | 15 + ludwig/datasets/configs/truthful_qa.yaml | 17 + ludwig/datasets/configs/tweet_eval_emoji.yaml | 15 + .../configs/tweet_sentiment_extraction.yaml | 18 + .../datasets/configs/tweeteval_emotion.yaml | 15 + ludwig/datasets/configs/tweeteval_hate.yaml | 15 + ludwig/datasets/configs/tweeteval_irony.yaml | 15 + .../datasets/configs/tweeteval_offensive.yaml | 15 + .../datasets/configs/tweeteval_sentiment.yaml | 15 + ludwig/datasets/configs/tweeteval_stance.yaml | 15 + ludwig/datasets/configs/twitter_bots.yaml | 59 + .../configs/twitter_financial_news_topic.yaml | 14 + .../configs/universal_dependencies.yaml | 15 + ludwig/datasets/configs/voxpopuli.yaml | 28 + ludwig/datasets/configs/vqa_rad.yaml | 16 + ludwig/datasets/configs/vqav2.yaml | 17 + ludwig/datasets/configs/vukuzenzele.yaml | 15 + .../datasets/configs/walmart_recruiting.yaml | 13 + ludwig/datasets/configs/web_questions.yaml | 14 + ludwig/datasets/configs/wic.yaml | 29 + ludwig/datasets/configs/wiki_qa.yaml | 16 + ludwig/datasets/configs/wikiann.yaml | 28 + ludwig/datasets/configs/wikiann_de.yaml | 15 + ludwig/datasets/configs/wikiann_en.yaml | 15 + ludwig/datasets/configs/wikiann_zh.yaml | 15 + ludwig/datasets/configs/wikiart.yaml | 18 + ludwig/datasets/configs/wildchat.yaml | 16 + ludwig/datasets/configs/wine_reviews.yaml | 30 + ludwig/datasets/configs/winobias.yaml | 15 + ludwig/datasets/configs/winograd_schema.yaml | 25 + ludwig/datasets/configs/winogrande.yaml | 19 + ludwig/datasets/configs/winogrande_hf.yaml | 19 + ludwig/datasets/configs/wmt14_de_en.yaml | 15 + ludwig/datasets/configs/wmt15.yaml | 13 + ludwig/datasets/configs/wmt16_de_en.yaml | 15 + ludwig/datasets/configs/wmt19_de_en.yaml | 15 + ludwig/datasets/configs/wmt_t2t_de_en.yaml | 15 + ludwig/datasets/configs/wnli.yaml | 19 + .../configs/women_clothing_review.yaml | 38 + ludwig/datasets/configs/world_speech_asr.yaml | 17 + ludwig/datasets/configs/xnli.yaml | 17 + ludwig/datasets/configs/xnli_de.yaml | 18 + ludwig/datasets/configs/xnli_en.yaml | 18 + ludwig/datasets/configs/xnli_es.yaml | 17 + ludwig/datasets/configs/xnli_fr.yaml | 18 + ludwig/datasets/configs/xnli_zh.yaml | 17 + ludwig/datasets/configs/xsum.yaml | 14 + ludwig/datasets/configs/xsum_hf.yaml | 14 + ludwig/datasets/configs/yahoo_answers.yaml | 30 + .../configs/yahoo_answers_topics.yaml | 18 + ludwig/datasets/configs/yelp_polarity.yaml | 14 + ludwig/datasets/configs/yelp_review_full.yaml | 14 + .../configs/yelp_review_polarity.yaml | 24 + ludwig/datasets/configs/yelp_reviews.yaml | 24 + ludwig/datasets/configs/yosemite.yaml | 16 + ludwig/datasets/dataset_config.py | 105 + ludwig/datasets/kaggle.py | 46 + ludwig/datasets/loaders/__init__.py | 0 .../datasets/loaders/adult_census_income.py | 31 + ludwig/datasets/loaders/agnews.py | 34 + .../loaders/allstate_claims_severity.py | 31 + ludwig/datasets/loaders/california_housing.py | 33 + ludwig/datasets/loaders/camseq.py | 60 + ludwig/datasets/loaders/code_alpaca_loader.py | 27 + ludwig/datasets/loaders/code_loader.py | 68 + .../loaders/consumer_complaints_loader.py | 45 + ludwig/datasets/loaders/creditcard_fraud.py | 28 + ludwig/datasets/loaders/dataset_loader.py | 553 + ludwig/datasets/loaders/esc50.py | 28 + ludwig/datasets/loaders/ethos_binary.py | 30 + ludwig/datasets/loaders/flickr8k.py | 54 + ludwig/datasets/loaders/forest_cover.py | 148 + ludwig/datasets/loaders/go_emotions.py | 72 + ludwig/datasets/loaders/goemotions.py | 25 + ludwig/datasets/loaders/higgs.py | 45 + ludwig/datasets/loaders/hugging_face.py | 99 + ludwig/datasets/loaders/ieee_fraud.py | 44 + ludwig/datasets/loaders/insurance_lite.py | 32 + ludwig/datasets/loaders/kdd_loader.py | 159 + ludwig/datasets/loaders/misc_loaders.py | 140 + ludwig/datasets/loaders/mnist.py | 125 + ludwig/datasets/loaders/multilabel_loader.py | 142 + .../loaders/multiple_choice_loader.py | 167 + ludwig/datasets/loaders/naval.py | 23 + ludwig/datasets/loaders/ner_loader.py | 175 + .../loaders/newyorker_caption_contest.py | 63 + ludwig/datasets/loaders/openml_loader.py | 195 + ludwig/datasets/loaders/qa_loader.py | 172 + .../datasets/loaders/rossman_store_sales.py | 102 + .../loaders/santander_value_prediction.py | 31 + .../datasets/loaders/sarcastic_headlines.py | 24 + ludwig/datasets/loaders/sarcos.py | 61 + ludwig/datasets/loaders/split_loaders.py | 34 + ludwig/datasets/loaders/sst.py | 382 + ludwig/datasets/loaders/translation_loader.py | 159 + ludwig/datasets/loaders/vqa_loader.py | 184 + ludwig/datasets/loaders/wikiann.py | 39 + ludwig/datasets/model_configs/__init__.py | 0 .../adult_census_income_default.yaml | 42 + .../allstate_claims_severity_default.yaml | 273 + .../model_configs/ames_housing_default.yaml | 171 + .../bnp_claims_management_default.yaml | 275 + .../model_configs/forest_cover_default.yaml | 37 + ludwig/datasets/model_configs/higgs_best.yaml | 89 + .../datasets/model_configs/higgs_default.yaml | 70 + .../model_configs/ieee_fraud_default.yaml | 877 + .../mercedes_benz_greener_default.yaml | 765 + .../datasets/model_configs/mnist_default.yaml | 24 + .../mushroom_edibility_default.yaml | 57 + .../otto_group_product_default.yaml | 199 + .../model_configs/poker_hand_default.yaml | 33 + .../porto_seguro_safe_driver_default.yaml | 127 + .../synthetic_fraud_default.yaml | 27 + .../model_configs/titanic_default.yaml | 25 + ludwig/datasets/utils.py | 44 + ludwig/decoders/__init__.py | 6 + ludwig/decoders/base.py | 31 + ludwig/decoders/generic_decoders.py | 439 + ludwig/decoders/image_decoders.py | 351 + ludwig/decoders/llm_decoders.py | 522 + ludwig/decoders/registry.py | 35 + ludwig/decoders/sequence_decoder_utils.py | 117 + ludwig/decoders/sequence_decoders.py | 818 + ludwig/decoders/sequence_tagger.py | 92 + ludwig/decoders/utils.py | 41 + ludwig/distributed/__init__.py | 54 + ludwig/distributed/accelerate.py | 247 + ludwig/distributed/base.py | 270 + ludwig/encoders/__init__.py | 14 + ludwig/encoders/audio_encoders.py | 265 + ludwig/encoders/bag_encoders.py | 115 + ludwig/encoders/base.py | 41 + ludwig/encoders/category_encoders.py | 319 + ludwig/encoders/date_encoders.py | 340 + ludwig/encoders/generic_encoders.py | 120 + ludwig/encoders/h3_encoders.py | 412 + ludwig/encoders/image/__init__.py | 4 + ludwig/encoders/image/base.py | 275 + ludwig/encoders/image/pretrained.py | 185 + ludwig/encoders/image/timm.py | 163 + ludwig/encoders/image/torchvision.py | 540 + ludwig/encoders/mamba_hybrid.py | 344 + ludwig/encoders/number_encoders.py | 181 + ludwig/encoders/registry.py | 49 + ludwig/encoders/sequence_encoders.py | 1943 + ludwig/encoders/set_encoders.py | 117 + ludwig/encoders/text_encoders.py | 2683 + ludwig/encoders/timeseries_encoders.py | 216 + ludwig/encoders/types.py | 9 + ludwig/error.py | 72 + ludwig/evaluate.py | 242 + ludwig/experiment.py | 472 + ludwig/experiment_utils.py | 105 + ludwig/explain/__init__.py | 0 ludwig/explain/captum.py | 540 + ludwig/explain/captum_ray.py | 212 + ludwig/explain/explainer.py | 72 + ludwig/explain/explanation.py | 97 + ludwig/explain/util.py | 111 + ludwig/export.py | 127 + ludwig/features/__init__.py | 0 ludwig/features/anomaly_feature.py | 414 + ludwig/features/audio_feature.py | 675 + ludwig/features/bag_feature.py | 130 + ludwig/features/base_feature.py | 655 + ludwig/features/binary_feature.py | 409 + ludwig/features/category_feature.py | 554 + ludwig/features/date_feature.py | 167 + ludwig/features/feature_registries.py | 140 + ludwig/features/feature_utils.py | 210 + ludwig/features/h3_feature.py | 159 + ludwig/features/image_feature.py | 1384 + ludwig/features/number_feature.py | 545 + ludwig/features/passthrough_feature.py | 104 + ludwig/features/sequence_feature.py | 524 + ludwig/features/set_feature.py | 360 + ludwig/features/text_feature.py | 515 + ludwig/features/timeseries_feature.py | 381 + ludwig/features/transforms.py | 50 + ludwig/features/vector_feature.py | 259 + ludwig/forecast.py | 157 + ludwig/globals.py | 59 + ludwig/hyperopt/__init__.py | 0 ludwig/hyperopt/execution.py | 1225 + ludwig/hyperopt/optuna_executor.py | 296 + ludwig/hyperopt/results.py | 26 + ludwig/hyperopt/run.py | 372 + ludwig/hyperopt/search_algos.py | 170 + ludwig/hyperopt/search_space_generator.py | 131 + ludwig/hyperopt/utils.py | 235 + ludwig/hyperopt_cli.py | 364 + ludwig/inspect_model.py | 89 + ludwig/model_export/base_model_exporter.py | 24 + ludwig/model_export/onnx_exporter.py | 50 + ludwig/model_inspector.py | 103 + ludwig/models/__init__.py | 0 ludwig/models/base.py | 350 + ludwig/models/calibrator.py | 60 + ludwig/models/ecd.py | 282 + ludwig/models/embedder.py | 135 + ludwig/models/llm.py | 901 + ludwig/models/predictor.py | 571 + ludwig/models/registry.py | 13 + ludwig/models/retrieval.py | 222 + ludwig/modules/__init__.py | 0 ludwig/modules/attention_modules.py | 234 + ludwig/modules/batch_ensemble.py | 76 + ludwig/modules/contrastive_alignment.py | 112 + ludwig/modules/convolutional_modules.py | 1502 + ludwig/modules/dpo_loss.py | 108 + ludwig/modules/embedding_modules.py | 408 + ludwig/modules/fully_connected_modules.py | 220 + ludwig/modules/gated_cross_attention.py | 105 + ludwig/modules/initializer_modules.py | 45 + ludwig/modules/loss_balancing.py | 318 + .../modules/loss_implementations/__init__.py | 0 ludwig/modules/loss_implementations/corn.py | 73 + ludwig/modules/loss_modules.py | 766 + ludwig/modules/lr_scheduler.py | 452 + ludwig/modules/metric_modules.py | 770 + ludwig/modules/metric_registry.py | 88 + ludwig/modules/mlp_mixer_modules.py | 183 + ludwig/modules/modality_dropout.py | 55 + ludwig/modules/normalization_modules.py | 125 + ludwig/modules/optimization_modules.py | 162 + ludwig/modules/preference_losses.py | 175 + ludwig/modules/recurrent_modules.py | 81 + ludwig/modules/reduction_modules.py | 173 + ludwig/modules/tabnet_modules.py | 353 + ludwig/modules/training_hooks.py | 102 + ludwig/predict.py | 228 + ludwig/preprocess.py | 212 + ludwig/presets.py | 98 + ludwig/progress_bar.py | 49 + ludwig/py.typed | 0 ludwig/schema/__init__.py | 5 + ludwig/schema/combiners/__init__.py | 14 + ludwig/schema/combiners/base.py | 9 + .../combiners/common_transformer_options.py | 83 + ludwig/schema/combiners/comparator.py | 83 + ludwig/schema/combiners/concat.py | 69 + ludwig/schema/combiners/cross_attention.py | 65 + ludwig/schema/combiners/ft_transformer.py | 19 + ludwig/schema/combiners/gated_fusion.py | 55 + ludwig/schema/combiners/hypernetwork.py | 33 + ludwig/schema/combiners/perceiver.py | 75 + ludwig/schema/combiners/project_aggregate.py | 90 + ludwig/schema/combiners/sequence.py | 48 + ludwig/schema/combiners/sequence_concat.py | 43 + ludwig/schema/combiners/tab_transformer.py | 30 + ludwig/schema/combiners/tabnet.py | 111 + ludwig/schema/combiners/tabpfn_v2.py | 50 + ludwig/schema/combiners/transformer.py | 23 + ludwig/schema/combiners/utils.py | 106 + ludwig/schema/common_fields.py | 293 + ludwig/schema/decoders/__init__.py | 5 + ludwig/schema/decoders/base.py | 429 + ludwig/schema/decoders/image_decoders.py | 220 + ludwig/schema/decoders/llm_decoders.py | 99 + ludwig/schema/decoders/sequence_decoders.py | 237 + ludwig/schema/decoders/utils.py | 121 + ludwig/schema/defaults/__init__.py | 0 ludwig/schema/defaults/base.py | 7 + ludwig/schema/defaults/ecd.py | 55 + ludwig/schema/defaults/llm.py | 18 + ludwig/schema/defaults/utils.py | 66 + ludwig/schema/encoders/__init__.py | 13 + ludwig/schema/encoders/audio_encoders.py | 105 + ludwig/schema/encoders/bag_encoders.py | 142 + ludwig/schema/encoders/base.py | 111 + ludwig/schema/encoders/category_encoders.py | 158 + ludwig/schema/encoders/date_encoders.py | 173 + ludwig/schema/encoders/h3_encoders.py | 319 + ludwig/schema/encoders/image/__init__.py | 4 + ludwig/schema/encoders/image/base.py | 429 + ludwig/schema/encoders/image/pretrained.py | 80 + ludwig/schema/encoders/image/timm.py | 187 + ludwig/schema/encoders/image/torchvision.py | 375 + ludwig/schema/encoders/mamba_hybrid.py | 133 + ludwig/schema/encoders/number_encoders.py | 112 + ludwig/schema/encoders/sequence_encoders.py | 987 + ludwig/schema/encoders/set_encoders.py | 135 + ludwig/schema/encoders/text/__init__.py | 0 .../schema/encoders/text/hf_model_params.py | 160 + ludwig/schema/encoders/text_encoders.py | 2724 + ludwig/schema/encoders/timeseries_encoders.py | 123 + ludwig/schema/encoders/utils.py | 132 + ludwig/schema/export_schema.py | 106 + ludwig/schema/features/__init__.py | 14 + ludwig/schema/features/anomaly_feature.py | 151 + ludwig/schema/features/audio_feature.py | 34 + .../schema/features/augmentation/__init__.py | 2 + ludwig/schema/features/augmentation/base.py | 9 + ludwig/schema/features/augmentation/image.py | 122 + ludwig/schema/features/augmentation/utils.py | 195 + ludwig/schema/features/bag_feature.py | 34 + ludwig/schema/features/base.py | 343 + ludwig/schema/features/binary_feature.py | 141 + ludwig/schema/features/category_feature.py | 180 + ludwig/schema/features/date_feature.py | 34 + ludwig/schema/features/h3_feature.py | 34 + ludwig/schema/features/image_feature.py | 126 + ludwig/schema/features/loss/__init__.py | 1 + ludwig/schema/features/loss/loss.py | 927 + ludwig/schema/features/loss/utils.py | 45 + ludwig/schema/features/number_feature.py | 136 + .../schema/features/preprocessing/__init__.py | 16 + ludwig/schema/features/preprocessing/audio.py | 140 + ludwig/schema/features/preprocessing/bag.py | 60 + ludwig/schema/features/preprocessing/base.py | 12 + .../schema/features/preprocessing/binary.py | 83 + .../schema/features/preprocessing/category.py | 130 + ludwig/schema/features/preprocessing/date.py | 41 + ludwig/schema/features/preprocessing/h3.py | 33 + ludwig/schema/features/preprocessing/image.py | 196 + .../schema/features/preprocessing/number.py | 109 + .../schema/features/preprocessing/sequence.py | 175 + ludwig/schema/features/preprocessing/set.py | 94 + ludwig/schema/features/preprocessing/text.py | 234 + .../features/preprocessing/timeseries.py | 118 + ludwig/schema/features/preprocessing/utils.py | 83 + .../schema/features/preprocessing/vector.py | 61 + ludwig/schema/features/sequence_feature.py | 107 + ludwig/schema/features/set_feature.py | 114 + ludwig/schema/features/text_feature.py | 206 + ludwig/schema/features/timeseries_feature.py | 113 + ludwig/schema/features/utils.py | 143 + ludwig/schema/features/vector_feature.py | 119 + ludwig/schema/hyperopt/__init__.py | 111 + ludwig/schema/hyperopt/executor.py | 114 + ludwig/schema/hyperopt/parameter.py | 183 + ludwig/schema/hyperopt/scheduler.py | 560 + ludwig/schema/hyperopt/search_algorithm.py | 532 + ludwig/schema/hyperopt/utils.py | 170 + ludwig/schema/jsonschema.py | 35 + ludwig/schema/llms/__init__.py | 0 ludwig/schema/llms/base_model.py | 158 + ludwig/schema/llms/generation.py | 334 + ludwig/schema/llms/model_parameters.py | 93 + ludwig/schema/llms/peft.py | 1532 + ludwig/schema/llms/prompt.py | 106 + ludwig/schema/llms/quantization.py | 172 + ludwig/schema/lr_scheduler.py | 298 + ludwig/schema/metadata/__init__.py | 39 + ludwig/schema/metadata/configs/combiners.yaml | 1639 + ludwig/schema/metadata/configs/common.yaml | 348 + ludwig/schema/metadata/configs/decoders.yaml | 703 + ludwig/schema/metadata/configs/encoders.yaml | 9224 + ludwig/schema/metadata/configs/features.yaml | 1035 + ludwig/schema/metadata/configs/llm.yaml | 246 + ludwig/schema/metadata/configs/loss.yaml | 85 + .../schema/metadata/configs/optimizers.yaml | 59 + .../metadata/configs/preprocessing.yaml | 175 + ludwig/schema/metadata/configs/trainer.yaml | 894 + ludwig/schema/metadata/feature_metadata.py | 0 ludwig/schema/metadata/parameter_metadata.py | 124 + ludwig/schema/model_config.py | 2 + ludwig/schema/model_types/__init__.py | 2 + ludwig/schema/model_types/base.py | 195 + ludwig/schema/model_types/ecd.py | 49 + ludwig/schema/model_types/llm.py | 130 + ludwig/schema/model_types/utils.py | 512 + ludwig/schema/optimizers.py | 1357 + ludwig/schema/preprocessing.py | 59 + ludwig/schema/profiler.py | 113 + ludwig/schema/split.py | 193 + ludwig/schema/trainer.py | 909 + ludwig/schema/utils.py | 1530 + ludwig/serve.py | 532 + ludwig/serve_kserve.py | 143 + ludwig/serve_ray_serve.py | 128 + ludwig/serve_v2.py | 402 + ludwig/serve_vllm.py | 199 + ludwig/train.py | 355 + ludwig/trainers/__init__.py | 9 + ludwig/trainers/base.py | 99 + ludwig/trainers/mixins.py | 168 + ludwig/trainers/registry.py | 121 + ludwig/trainers/trainer.py | 1848 + ludwig/trainers/trainer_dpo.py | 187 + ludwig/trainers/trainer_llm.py | 530 + ludwig/types.py | 164 + ludwig/upload.py | 153 + ludwig/utils/__init__.py | 0 ludwig/utils/algorithms_utils.py | 88 + ludwig/utils/audio_utils.py | 397 + ludwig/utils/augmentation_utils.py | 56 + ludwig/utils/automl/__init__.py | 0 ludwig/utils/automl/data_source.py | 137 + ludwig/utils/automl/field_info.py | 42 + ludwig/utils/automl/ray_utils.py | 18 + ludwig/utils/automl/type_inference.py | 104 + ludwig/utils/automl/utils.py | 160 + ludwig/utils/backward_compatibility.py | 900 + ludwig/utils/batch_size_tuner.py | 184 + ludwig/utils/calibration.py | 321 + ludwig/utils/checkpoint_utils.py | 407 + ludwig/utils/config_utils.py | 196 + ludwig/utils/data_utils.py | 1152 + ludwig/utils/dataframe_utils.py | 159 + ludwig/utils/dataset_quality.py | 497 + ludwig/utils/dataset_utils.py | 118 + ludwig/utils/date_utils.py | 95 + ludwig/utils/defaults.py | 98 + ludwig/utils/entmax/LICENSE | 21 + ludwig/utils/entmax/README.md | 94 + ludwig/utils/entmax/__init__.py | 33 + ludwig/utils/entmax/activations.py | 329 + ludwig/utils/entmax/losses.py | 284 + ludwig/utils/entmax/root_finding.py | 298 + ludwig/utils/error_handling_utils.py | 18 + ludwig/utils/eval_utils.py | 269 + ludwig/utils/fs_utils.py | 412 + ludwig/utils/h3_util.py | 113 + ludwig/utils/heuristics.py | 26 + ludwig/utils/hf_utils.py | 186 + ludwig/utils/html_utils.py | 95 + ludwig/utils/image_utils.py | 537 + ludwig/utils/llm_quantization_utils.py | 61 + ludwig/utils/llm_utils.py | 830 + ludwig/utils/logging_utils.py | 15 + ludwig/utils/loss_utils.py | 23 + ludwig/utils/math_utils.py | 65 + ludwig/utils/metric_utils.py | 125 + ludwig/utils/metrics_printed_table.py | 98 + ludwig/utils/misc_utils.py | 220 + ludwig/utils/model_card.py | 189 + ludwig/utils/model_export.py | 199 + ludwig/utils/model_soup.py | 81 + ludwig/utils/model_utils.py | 126 + ludwig/utils/nlp_utils.py | 214 + ludwig/utils/numerical_test_utils.py | 71 + ludwig/utils/output_feature_utils.py | 122 + ludwig/utils/package_utils.py | 39 + ludwig/utils/preference_data.py | 131 + ludwig/utils/print_utils.py | 102 + ludwig/utils/quantization.py | 184 + ludwig/utils/registry.py | 117 + ludwig/utils/sequence_packing.py | 151 + ludwig/utils/server_utils.py | 168 + .../state_dict_backward_compatibility.py | 44 + ludwig/utils/strings_utils.py | 591 + ludwig/utils/structural_warning.py | 29 + ludwig/utils/system_utils.py | 26 + ludwig/utils/time_utils.py | 98 + ludwig/utils/tokenizers.py | 1088 + ludwig/utils/torch_utils.py | 421 + ludwig/utils/trainer_utils.py | 594 + ludwig/utils/training_report.py | 172 + ludwig/utils/types.py | 16 + ludwig/utils/upload_utils.py | 310 + ludwig/utils/version_transformation.py | 171 + ludwig/utils/visualization_utils.py | 1568 + ludwig/vector_index/__init__.py | 28 + ludwig/vector_index/base.py | 23 + ludwig/vector_index/faiss.py | 27 + ludwig/visualize/__init__.py | 228 + ludwig/visualize/__main__.py | 5 + ludwig/visualize/_utils.py | 369 + ludwig/visualize/cli.py | 201 + ludwig/visualize/confusion.py | 155 + ludwig/visualize/curves.py | 618 + ludwig/visualize/hyperopt.py | 125 + ludwig/visualize/performance.py | 1227 + ludwig/visualize/threshold.py | 1123 + ludwig/visualize/training.py | 112 + notebooks/advanced_peft_adapters.ipynb | 471 + pyproject.toml | 277 + pytest.ini | 29 + schemastore/README.md | 21 + schemastore/catalog-entry.json | 15 + schemastore/test/ludwig.yaml | 23 + schemastore/test/ludwig_config.yaml | 24 + scripts/benchmark_lazy_decode.py | 276 + scripts/benchmark_lazy_preprocessing.py | 330 + scripts/benchmark_results.json | 59 + scripts/benchmark_training_pipeline.py | 493 + scripts/dataset_smoke_test.py | 610 + scripts/hf_dataset_candidates.json | 3759 + scripts/probe_hf_datasets.py | 341 + scripts/probe_results.json | 5592 + scripts/smoke_results.json | 3542 + tests/README.md | 114 + tests/__init__.py | 14 + tests/conftest.py | 246 + tests/docker-compose.yml | 16 + tests/integration_tests/__init__.py | 0 .../parameter_update_utils.py | 129 + .../scripts/run_train_aim.py | 46 + .../scripts/run_train_comet.py | 79 + .../scripts/run_train_wandb.py | 56 + .../integration_tests/synthetic_test_data.py | 71 + tests/integration_tests/test_api.py | 804 + tests/integration_tests/test_audio_feature.py | 40 + tests/integration_tests/test_automl.py | 382 + tests/integration_tests/test_cache_manager.py | 106 + .../test_cached_preprocessing.py | 148 + .../test_class_imbalance_feature.py | 184 + tests/integration_tests/test_cli.py | 418 + tests/integration_tests/test_collect.py | 112 + .../test_config_global_defaults.py | 88 + tests/integration_tests/test_contrib_aim.py | 28 + tests/integration_tests/test_contrib_comet.py | 34 + tests/integration_tests/test_contrib_wandb.py | 27 + .../test_custom_components.py | 187 + tests/integration_tests/test_date_feature.py | 105 + tests/integration_tests/test_dependencies.py | 140 + tests/integration_tests/test_experiment.py | 1172 + tests/integration_tests/test_explain.py | 267 + .../integration_tests/test_graph_execution.py | 67 + tests/integration_tests/test_hyperopt.py | 661 + tests/integration_tests/test_hyperopt_ray.py | 380 + .../test_input_feature_tied.py | 150 + tests/integration_tests/test_kfold_cv.py | 271 + tests/integration_tests/test_llm.py | 1514 + .../test_missing_value_strategy.py | 202 + tests/integration_tests/test_mlflow.py | 201 + .../test_model_save_and_load.py | 446 + .../test_model_training_options.py | 496 + .../integration_tests/test_number_feature.py | 31 + tests/integration_tests/test_peft.py | 44 + .../integration_tests/test_postprocessing.py | 255 + tests/integration_tests/test_preprocessing.py | 1194 + tests/integration_tests/test_ray.py | 1152 + tests/integration_tests/test_reducers.py | 17 + tests/integration_tests/test_regularizers.py | 109 + tests/integration_tests/test_remote.py | 115 + .../integration_tests/test_reproducibility.py | 357 + .../test_sequence_decoders.py | 54 + .../test_sequence_encoders.py | 182 + .../test_sequence_features.py | 171 + tests/integration_tests/test_server.py | 322 + .../integration_tests/test_simple_features.py | 110 + .../test_timeseries_feature.py | 85 + tests/integration_tests/test_trainer.py | 294 + ...former_combiner_with_attention_reduce.yaml | 19 + tests/integration_tests/test_visualization.py | 1639 + .../test_visualization_api.py | 936 + tests/integration_tests/utils.py | 1185 + tests/ludwig/__init__.py | 0 tests/ludwig/accounting/test_used_tokens.py | 27 + .../test_augmentation_pipeline.py | 342 + .../augmentation/test_auto_augmentation.py | 27 + .../augmentation/test_image_augmentation.py | 31 + tests/ludwig/automl/__init__.py | 0 tests/ludwig/automl/test_base_config.py | 186 + tests/ludwig/automl/test_config_enumerator.py | 164 + tests/ludwig/automl/test_config_sampler.py | 117 + tests/ludwig/automl/test_config_validator.py | 183 + tests/ludwig/automl/test_data_source.py | 39 + tests/ludwig/automl/test_target_detection.py | 168 + tests/ludwig/automl/test_tune_config.py | 64 + tests/ludwig/automl/test_utils.py | 27 + tests/ludwig/backend/__init__.py | 0 .../ludwig/backend/test_batch_infer_model.py | 148 + tests/ludwig/backend/test_ray.py | 115 + .../invalid/benchmarking_config_1.yaml | 19 + .../invalid/benchmarking_config_2.yaml | 17 + .../invalid/benchmarking_config_3.yaml | 18 + .../example_files/process_config.py | 29 + .../valid/benchmarking_config_1.yaml | 24 + .../valid/benchmarking_config_2.yaml | 13 + .../valid/benchmarking_config_3.yaml | 19 + .../ludwig/benchmarking/test_benchmarking.py | 31 + tests/ludwig/benchmarking/test_profiler.py | 70 + tests/ludwig/combiners/test_combiners.py | 831 + .../ludwig/config_sampling/static_schema.json | 146652 +++++++++++++++ .../config_sampling/test_config_sampling.py | 49 + tests/ludwig/config_validation/test_checks.py | 523 + .../test_validate_config_combiner.py | 175 + .../test_validate_config_encoder.py | 59 + .../test_validate_config_features.py | 108 + .../test_validate_config_hyperopt.py | 296 + .../test_validate_config_misc.py | 439 + .../test_validate_config_preprocessing.py | 55 + .../test_validate_config_trainer.py | 152 + tests/ludwig/contrib/test_contrib.py | 31 + tests/ludwig/data/__init__.py | 0 tests/ludwig/data/dataframe/test_dask.py | 81 + tests/ludwig/data/test_cache_util.py | 138 + tests/ludwig/data/test_dask_preprocessing.py | 292 + tests/ludwig/data/test_dataset_synthesizer.py | 20 + .../ludwig/data/test_format_preprocessors.py | 177 + tests/ludwig/data/test_format_registry.py | 36 + tests/ludwig/data/test_lazy_cache.py | 361 + tests/ludwig/data/test_lazy_utils.py | 153 + tests/ludwig/data/test_multimodal_collator.py | 125 + tests/ludwig/data/test_negative_sampling.py | 25 + tests/ludwig/data/test_postprocessing.py | 31 + tests/ludwig/data/test_prefetch_batcher.py | 333 + tests/ludwig/data/test_preprocessing.py | 9 + .../data/test_preprocessing_progress.py | 305 + tests/ludwig/data/test_ray_data.py | 395 + tests/ludwig/data/test_split.py | 396 + tests/ludwig/data/test_split_utils.py | 51 + tests/ludwig/data/test_statistics.py | 135 + tests/ludwig/data/test_types.py | 90 + tests/ludwig/datasets/__init__.py | 14 + .../ludwig/datasets/download_all_datasets.py | 28 + .../datasets/mnist/test_mnist_workflow.py | 65 + .../model_configs/train_all_model_configs.py | 148 + tests/ludwig/datasets/test_dataset_configs.py | 37 + tests/ludwig/datasets/test_dataset_links.py | 49 + tests/ludwig/datasets/test_datasets.py | 252 + tests/ludwig/datasets/test_model_configs.py | 31 + .../ludwig/datasets/test_smoke_regression.py | 377 + .../datasets/titanic/test_titanic_workflow.py | 110 + tests/ludwig/decoders/test_image_decoder.py | 41 + tests/ludwig/decoders/test_llm_decoders.py | 69 + .../ludwig/decoders/test_sequence_decoder.py | 133 + .../decoders/test_sequence_decoder_utils.py | 72 + tests/ludwig/decoders/test_sequence_tagger.py | 34 + tests/ludwig/distributed/__init__.py | 0 .../distributed/test_accelerate_strategy.py | 62 + tests/ludwig/encoders/__init__.py | 0 tests/ludwig/encoders/test_bag_encoders.py | 43 + .../ludwig/encoders/test_category_encoders.py | 69 + tests/ludwig/encoders/test_date_encoders.py | 50 + .../ludwig/encoders/test_generic_encoders.py | 30 + tests/ludwig/encoders/test_h3_encoders.py | 80 + tests/ludwig/encoders/test_image_encoders.py | 583 + tests/ludwig/encoders/test_llm_encoders.py | 247 + tests/ludwig/encoders/test_mamba_hybrid.py | 173 + tests/ludwig/encoders/test_number_encoders.py | 121 + tests/ludwig/encoders/test_phase_1_5.py | 540 + .../ludwig/encoders/test_sequence_encoders.py | 56 + tests/ludwig/encoders/test_set_encoders.py | 41 + tests/ludwig/encoders/test_text_encoders.py | 314 + tests/ludwig/encoders/test_timm_encoder.py | 50 + tests/ludwig/evaluation/test_evaluation.py | 81 + tests/ludwig/explain/test_captum.py | 60 + tests/ludwig/explain/test_util.py | 93 + tests/ludwig/features/__init__.py | 0 tests/ludwig/features/test_audio_feature.py | 96 + tests/ludwig/features/test_bag_feature.py | 44 + tests/ludwig/features/test_binary_feature.py | 91 + .../ludwig/features/test_category_feature.py | 56 + tests/ludwig/features/test_date_feature.py | 182 + tests/ludwig/features/test_feature_utils.py | 63 + tests/ludwig/features/test_h3_feature.py | 68 + tests/ludwig/features/test_image_feature.py | 314 + tests/ludwig/features/test_number_feature.py | 55 + .../ludwig/features/test_sequence_features.py | 336 + tests/ludwig/features/test_set_feature.py | 61 + tests/ludwig/features/test_text_feature.py | 141 + .../features/test_timeseries_feature.py | 42 + tests/ludwig/hyperopt/test_hyperopt.py | 168 + tests/ludwig/hyperopt/test_optuna_executor.py | 110 + .../hyperopt/test_search_space_generator.py | 20 + .../ludwig/model_export/test_onnx_exporter.py | 33 + tests/ludwig/models/__init__.py | 14 + tests/ludwig/models/test_predictor.py | 139 + .../models/test_trainable_image_layers.py | 90 + .../models/test_training_determinism.py | 142 + tests/ludwig/models/test_training_success.py | 48 + tests/ludwig/modules/__init__.py | 14 + tests/ludwig/modules/test_attention.py | 160 + tests/ludwig/modules/test_batch_ensemble.py | 48 + .../modules/test_contrastive_alignment.py | 152 + .../modules/test_convolutional_modules.py | 514 + tests/ludwig/modules/test_dpo_loss.py | 102 + .../ludwig/modules/test_embedding_modules.py | 95 + tests/ludwig/modules/test_encoder.py | 330 + .../modules/test_fully_connected_modules.py | 87 + .../modules/test_gated_cross_attention.py | 149 + .../modules/test_initializer_modules.py | 22 + tests/ludwig/modules/test_loss_balancing.py | 259 + tests/ludwig/modules/test_loss_modules.py | 439 + tests/ludwig/modules/test_lr_scheduler.py | 276 + tests/ludwig/modules/test_metric_modules.py | 561 + .../ludwig/modules/test_mlp_mixer_modules.py | 27 + tests/ludwig/modules/test_modality_dropout.py | 86 + .../modules/test_normalization_modules.py | 55 + tests/ludwig/modules/test_pareto_mtl.py | 125 + .../ludwig/modules/test_preference_losses.py | 86 + .../ludwig/modules/test_recurrent_modules.py | 23 + .../ludwig/modules/test_reduction_modules.py | 23 + tests/ludwig/modules/test_regex_freezing.py | 124 + tests/ludwig/modules/test_tabnet_modules.py | 190 + tests/ludwig/modules/test_utils.py | 15 + .../ludwig/schema/hyperopt/test_scheduler.py | 39 + .../schema/hyperopt/test_search_algorithm.py | 39 + tests/ludwig/schema/test_model_config.py | 1066 + tests/ludwig/schema/test_peft_adapters.py | 534 + tests/ludwig/schema/test_phase_6_5_tabular.py | 144 + tests/ludwig/schema/test_schema_utils.py | 9 + .../ludwig/schema/test_timeseries_encoders.py | 118 + .../ludwig/schema_fields/test_fields_misc.py | 243 + .../schema_fields/test_fields_optimization.py | 97 + .../test_fields_preprocessing.py | 23 + .../schema_fields/test_marshmallow_misc.py | 40 + tests/ludwig/test_api_unit.py | 346 + tests/ludwig/test_collect.py | 82 + tests/ludwig/test_config_generation.py | 109 + tests/ludwig/test_model_inspector.py | 49 + tests/ludwig/test_presets.py | 86 + tests/ludwig/test_serve_deployment_shims.py | 89 + tests/ludwig/test_serve_v2.py | 72 + tests/ludwig/trainers/__init__.py | 0 tests/ludwig/trainers/test_dpo_trainers.py | 127 + tests/ludwig/trainers/test_mixins.py | 109 + tests/ludwig/trainers/test_trainer_helpers.py | 91 + tests/ludwig/utils/__init__.py | 14 + .../utils/automl/test_type_inference.py | 127 + tests/ludwig/utils/automl/test_utils.py | 18 + tests/ludwig/utils/entmax/test_losses.py | 53 + tests/ludwig/utils/entmax/test_mask.py | 45 + .../ludwig/utils/entmax/test_root_finding.py | 99 + tests/ludwig/utils/entmax/test_topk.py | 41 + tests/ludwig/utils/test_algorithm_utils.py | 20 + tests/ludwig/utils/test_audio_utils.py | 23 + .../utils/test_backward_compatibility.py | 864 + tests/ludwig/utils/test_calibration.py | 67 + tests/ludwig/utils/test_class_balancing.py | 51 + tests/ludwig/utils/test_config_utils.py | 334 + tests/ludwig/utils/test_data_utils.py | 251 + tests/ludwig/utils/test_dataframe_utils.py | 85 + tests/ludwig/utils/test_dataset_quality.py | 190 + tests/ludwig/utils/test_dataset_utils.py | 225 + tests/ludwig/utils/test_date_utils.py | 49 + tests/ludwig/utils/test_defaults.py | 247 + .../ludwig/utils/test_error_handling_utils.py | 88 + tests/ludwig/utils/test_errors.py | 25 + tests/ludwig/utils/test_fs_utils.py | 109 + tests/ludwig/utils/test_heuristics.py | 48 + tests/ludwig/utils/test_hf_utils.py | 90 + tests/ludwig/utils/test_hyperopt_ray_utils.py | 92 + tests/ludwig/utils/test_image_utils.py | 389 + tests/ludwig/utils/test_import_safety.py | 92 + tests/ludwig/utils/test_llm_utils.py | 345 + tests/ludwig/utils/test_metric_utils.py | 77 + tests/ludwig/utils/test_model_card.py | 78 + tests/ludwig/utils/test_model_export.py | 59 + tests/ludwig/utils/test_model_soup.py | 42 + tests/ludwig/utils/test_model_utils.py | 139 + tests/ludwig/utils/test_normalization.py | 132 + .../ludwig/utils/test_numerical_test_utils.py | 40 + .../ludwig/utils/test_output_feature_utils.py | 19 + tests/ludwig/utils/test_registry_v2.py | 91 + tests/ludwig/utils/test_sequence_packing.py | 111 + tests/ludwig/utils/test_server_utils.py | 31 + .../test_state_dict_backward_compatibility.py | 31 + tests/ludwig/utils/test_strings_utils.py | 306 + tests/ludwig/utils/test_tokenizers.py | 79 + tests/ludwig/utils/test_torch_utils.py | 108 + tests/ludwig/utils/test_trainer_utils.py | 367 + tests/ludwig/utils/test_training_report.py | 105 + tests/ludwig/utils/test_upload_utils.py | 178 + .../utils/test_version_transformation.py | 46 + .../golden/adult_census_income.types.json | 77 + .../automl/golden/mnist.types.json | 15 + .../automl/scripts/update_golden_types.py | 29 + .../automl/test_auto_type_inference.py | 31 + tests/regression_tests/automl/utils.py | 17 + .../configs/adult_census_income.ecd.yaml | 44 + .../benchmark/configs/ames_housing.ecd.yaml | 174 + .../configs/mercedes_benz_greener.ecd.yaml | 768 + .../benchmark/configs/sarcos.ecd.yaml | 101 + .../benchmark/expected_metric.py | 19 + .../adult_census_income.ecd.yaml | 5 + .../expected_metrics/ames_housing.ecd.yaml | 5 + .../mercedes_benz_greener.ecd.yaml | 5 + .../expected_metrics/sarcos.ecd.yaml | 5 + .../benchmark/test_model_performance.py | 84 + .../regression_tests/model/test_old_models.py | 63 + tests/training_success/__init__.py | 0 tests/training_success/configs.py | 158 + .../training_success/test_training_success.py | 274 + tests/ultra_slow/__init__.py | 0 tests/ultra_slow/test_ultra_slow.py | 1168 + tests/ultra_slow/test_ultra_slow_peft.py | 90 + 1647 files changed, 382489 insertions(+) create mode 100644 .actrc create mode 100644 .claude/commands/ludwig_review.md create mode 100644 .claude/scheduled_tasks.lock create mode 100644 .claude/settings.json create mode 100644 .deepsource.toml create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .flake8 create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/pytest.yml create mode 100644 .github/workflows/pytest_slow.yml create mode 100644 .github/workflows/schema.yml create mode 100644 .github/workflows/test-results.yml create mode 100644 .github/workflows/upload-pypi.yml create mode 100644 .gitignore create mode 100644 .nojekyll create mode 100644 .plan create mode 100644 .plan-lazy-preprocessing.md create mode 100644 .pre-commit-config.yaml create mode 100644 .protolint.yaml create mode 100644 .vscode/settings.json create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 IMPROVEMENT_PLAN.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README_KR.md create mode 100644 RELEASES.md create mode 100644 docker/README.md create mode 100755 docker/build_and_push.sh create mode 100644 docker/ludwig-gpu/Dockerfile create mode 100644 docker/ludwig-ray-gpu/Dockerfile create mode 100644 docker/ludwig-ray/Dockerfile create mode 100644 docker/ludwig/Dockerfile create mode 100644 docs/developer_guide/adding_a_feature_type.md create mode 100644 examples/README.md create mode 100644 examples/alignment/README.md create mode 100644 examples/alignment/alignment_dpo.ipynb create mode 100644 examples/alignment/config_dpo.yaml create mode 100644 examples/alignment/config_grpo.yaml create mode 100644 examples/alignment/config_kto.yaml create mode 100644 examples/alignment/config_orpo.yaml create mode 100644 examples/alignment/prepare_dataset.py create mode 100644 examples/alignment/train_dpo.py create mode 100644 examples/alignment/train_grpo.py create mode 100644 examples/anomaly_detection/README.md create mode 100644 examples/anomaly_detection/anomaly_detection.ipynb create mode 100644 examples/anomaly_detection/config_deep_sad.yaml create mode 100644 examples/anomaly_detection/config_deep_svdd.yaml create mode 100644 examples/anomaly_detection/config_drocc.yaml create mode 100644 examples/anomaly_detection/train.py create mode 100644 examples/calibration/README.md create mode 100644 examples/calibration/train_forest_cover_calibrated.py create mode 100644 examples/calibration/train_mushroom_edibility_calibrated.py create mode 100644 examples/class_imbalance/README.md create mode 100644 examples/class_imbalance/balanced_model_config.yaml create mode 100644 examples/class_imbalance/model_training.py create mode 100644 examples/class_imbalance/model_training_results.ipynb create mode 100644 examples/class_imbalance/standard_model_config.yaml create mode 100644 examples/forecasting/README.md create mode 100644 examples/forecasting/config.yaml create mode 100644 examples/getting_started/rotten_tomatoes.yaml create mode 100755 examples/getting_started/run.sh create mode 100644 examples/hypernetwork/README.md create mode 100644 examples/hypernetwork/config_concat.yaml create mode 100644 examples/hypernetwork/config_hypernetwork.yaml create mode 100644 examples/hypernetwork/hypernetwork.ipynb create mode 100644 examples/hypernetwork/train_hypernetwork.py create mode 100644 examples/hyperopt/README.md create mode 100644 examples/hyperopt/README_optuna.md create mode 100644 examples/hyperopt/config_optuna.yaml create mode 100644 examples/hyperopt/model_hyperopt_example.ipynb create mode 100644 examples/hyperopt/optuna_executor.ipynb create mode 100644 examples/hyperopt/optuna_executor.py create mode 100644 examples/image_encoders/README.md create mode 100644 examples/image_encoders/compare_encoders.py create mode 100644 examples/image_encoders/config_clip.yaml create mode 100644 examples/image_encoders/config_dinov2_finetuned.yaml create mode 100644 examples/image_encoders/config_dinov2_linear_probe.yaml create mode 100644 examples/image_encoders/config_siglip.yaml create mode 100644 examples/image_encoders/config_stacked_cnn.yaml create mode 100644 examples/image_encoders/image_encoders.ipynb create mode 100644 examples/insurance_lite/config.yaml create mode 100644 examples/insurance_lite/train.py create mode 100644 examples/kfold_cv/README.md create mode 100644 examples/kfold_cv/display_kfold_cv_results.py create mode 100755 examples/kfold_cv/k-fold_cv_classification.sh create mode 100644 examples/kfold_cv/prepare_classification_data_set.py create mode 100644 examples/kfold_cv/regression_example.ipynb create mode 100644 examples/lbfgs/config.yaml create mode 100644 examples/lbfgs/model.py create mode 100644 examples/llama2_7b_finetuning_4bit/README.md create mode 100644 examples/llama2_7b_finetuning_4bit/llama2_7b_4bit.yaml create mode 100755 examples/llama2_7b_finetuning_4bit/run_train.sh create mode 100644 examples/llama2_7b_finetuning_4bit/train_alpaca.py create mode 100644 examples/llm_base_model_dequantization/README.md create mode 100644 examples/llm_base_model_dequantization/phi_2_dequantization.py create mode 100644 examples/llm_config_generation/README.md create mode 100644 examples/llm_config_generation/example_description.txt create mode 100644 examples/llm_config_generation/generate_and_train.py create mode 100644 examples/llm_config_generation/llm_config_generation.ipynb create mode 100644 examples/llm_few_shot_learning/simple_model_training.py create mode 100644 examples/llm_finetuning/README.md create mode 100644 examples/llm_finetuning/imdb_deepspeed_zero3.yaml create mode 100644 examples/llm_finetuning/imdb_deepspeed_zero3_ray.yaml create mode 100755 examples/llm_finetuning/run_train_dsz3.sh create mode 100755 examples/llm_finetuning/run_train_dsz3_ray.sh create mode 100644 examples/llm_finetuning/train_imdb_ray.py create mode 100644 examples/llm_instruction_tuning/train_alpaca_ray.py create mode 100644 examples/llm_structured_output/README.md create mode 100644 examples/llm_structured_output/config_constrained.yaml create mode 100644 examples/llm_structured_output/config_json_schema.yaml create mode 100644 examples/llm_structured_output/run_structured.py create mode 100644 examples/llm_structured_output/structured_output.ipynb create mode 100644 examples/llm_text_generation/simple_model_training.py create mode 100644 examples/llm_zero_shot_learning/simple_model_training.py create mode 100644 examples/mamba_encoders/README.md create mode 100644 examples/mamba_encoders/jamba_sequence.yaml create mode 100644 examples/mamba_encoders/mamba2_text_classification.yaml create mode 100644 examples/mnist/README.md create mode 100644 examples/mnist/advanced_model_training.py create mode 100644 examples/mnist/assess_model_performance.py create mode 100644 examples/mnist/config.yaml create mode 100644 examples/mnist/simple_model_training.py create mode 100644 examples/mnist/visualize_model_test_results.ipynb create mode 100644 examples/multi_task/README.md create mode 100644 examples/multi_task/compare_balancers.py create mode 100644 examples/multi_task/config_famo.yaml create mode 100644 examples/multi_task/config_nash_mtl.yaml create mode 100644 examples/multi_task/config_no_balancing.yaml create mode 100644 examples/multi_task/config_uncertainty.yaml create mode 100644 examples/multi_task/multi_task.ipynb create mode 100644 examples/multi_task/train_multi_task.py create mode 100644 examples/open_set_recognition/README.md create mode 100644 examples/open_set_recognition/config_baseline.yaml create mode 100644 examples/open_set_recognition/config_baseline_mnist.yaml create mode 100644 examples/open_set_recognition/config_entropic.yaml create mode 100644 examples/open_set_recognition/config_entropic_mnist.yaml create mode 100644 examples/open_set_recognition/config_objectosphere.yaml create mode 100644 examples/open_set_recognition/config_objectosphere_mnist.yaml create mode 100644 examples/open_set_recognition/open_set_mnist.ipynb create mode 100644 examples/open_set_recognition/train_open_set.py create mode 100644 examples/optimizers/README.md create mode 100644 examples/optimizers/config_adafactor.yaml create mode 100644 examples/optimizers/config_adamw.yaml create mode 100644 examples/optimizers/config_muon.yaml create mode 100644 examples/optimizers/config_radam.yaml create mode 100644 examples/optimizers/config_schedule_free_adamw.yaml create mode 100644 examples/optimizers/optimizer_comparison.ipynb create mode 100644 examples/optimizers/optimizer_comparison.py create mode 100644 examples/peft_advanced/README.md create mode 100644 examples/peft_advanced/compare_adapters.py create mode 100644 examples/peft_advanced/corda_lora.yaml create mode 100644 examples/peft_advanced/eva_lora.yaml create mode 100644 examples/peft_advanced/ln_tuning_llm.yaml create mode 100644 examples/peft_advanced/loftq_lora.yaml create mode 100644 examples/peft_advanced/oft_llm.yaml create mode 100644 examples/peft_advanced/pissa_lora.yaml create mode 100644 examples/peft_advanced/tinylora_llm.yaml create mode 100644 examples/peft_advanced/train_example.py create mode 100644 examples/peft_advanced/vblora_llm.yaml create mode 100644 examples/ray/job_submission/README.md create mode 100644 examples/ray/job_submission/config.yaml create mode 100644 examples/ray/job_submission/submit_job.py create mode 100644 examples/ray/job_submission/train_on_cluster.py create mode 100644 examples/ray/kubernetes/README.md create mode 100644 examples/ray/kubernetes/clusters/ludwig-ray-cpu-cluster.yaml create mode 100644 examples/ray/kubernetes/clusters/ludwig-ray-gpu-cluster.yaml create mode 100755 examples/ray/kubernetes/utils/attach.sh create mode 100755 examples/ray/kubernetes/utils/dashboard.sh create mode 100755 examples/ray/kubernetes/utils/krsync.sh create mode 100755 examples/ray/kubernetes/utils/ray_down.sh create mode 100755 examples/ray/kubernetes/utils/ray_up.sh create mode 100755 examples/ray/kubernetes/utils/rsync_up.sh create mode 100755 examples/ray/kubernetes/utils/submit.sh create mode 100755 examples/ray/kubernetes/utils/upload.sh create mode 100644 examples/regex_freezing/ecd_freezing_with_regex_training.py create mode 100644 examples/regex_freezing/llm_freezing_with_regex_training.py create mode 100644 examples/semantic_segmentation/README.md create mode 100644 examples/semantic_segmentation/camseq.py create mode 100644 examples/semantic_segmentation/config_camseq.yaml create mode 100644 examples/semantic_segmentation/config_fpn.yaml create mode 100644 examples/semantic_segmentation/config_segformer.yaml create mode 100644 examples/semantic_segmentation/config_unet_depth.yaml create mode 100644 examples/semantic_segmentation/semantic_segmentation.ipynb create mode 100644 examples/semantic_segmentation/unet_depth_sweep.py create mode 100644 examples/serve/README.md create mode 100644 examples/serve/client_program.py create mode 100644 examples/serve/prometheus_monitoring/README.md create mode 100644 examples/serve/prometheus_monitoring/docker-compose.yml create mode 100644 examples/serve/prometheus_monitoring/prometheus.yml create mode 100644 examples/serve/vllm_client.py create mode 100644 examples/serve/vllm_serving.ipynb create mode 100644 examples/serving/kserve/README.md create mode 100644 examples/serving/kserve/serving_config.yaml create mode 100644 examples/serving/ray_serve/README.md create mode 100644 examples/serving/ray_serve/deploy.py create mode 100644 examples/synthetic/train.py create mode 100644 examples/tabnet/higgs/medium_config.yaml create mode 100644 examples/tabnet/higgs/small_config.yaml create mode 100644 examples/tabnet/higgs/train_higgs_medium.py create mode 100644 examples/tabnet/higgs/train_higgs_small.py create mode 100644 examples/timeseries_forecasting/nbeats_example.yaml create mode 100644 examples/timeseries_forecasting/patchtst_example.yaml create mode 100644 examples/titanic/README.md create mode 100644 examples/titanic/model1_config.yaml create mode 100644 examples/titanic/model2_config.yaml create mode 100644 examples/titanic/model_training_results.ipynb create mode 100644 examples/titanic/multiple_model_training.py create mode 100644 examples/titanic/simple_model_training.py create mode 100644 examples/twitter_bots/README.md create mode 100644 examples/twitter_bots/train_twitter_bots.py create mode 100644 examples/twitter_bots/train_twitter_bots_text_only.py create mode 100644 examples/uncertainty/README.md create mode 100644 examples/uncertainty/config_baseline.yaml create mode 100644 examples/uncertainty/config_calibrated.yaml create mode 100644 examples/uncertainty/config_mc_dropout.yaml create mode 100644 examples/uncertainty/train.py create mode 100644 examples/uncertainty/uncertainty.ipynb create mode 100644 examples/vlm_finetuning/README.md create mode 100644 examples/vlm_finetuning/run.py create mode 100644 examples/vlm_finetuning/vlm_config.yaml create mode 100644 examples/wine_quality/README.md create mode 100644 examples/wine_quality/model_defaults_example.ipynb create mode 100644 examples/wmt15/config_large.yaml create mode 100644 examples/wmt15/config_small.yaml create mode 100644 examples/wmt15/train_nmt.py create mode 100644 ludwig/__init__.py create mode 100644 ludwig/accounting/__init__.py create mode 100644 ludwig/accounting/used_tokens.py create mode 100644 ludwig/api.py create mode 100644 ludwig/api_annotations.py create mode 100644 ludwig/api_types.py create mode 100644 ludwig/automl/__init__.py create mode 100644 ludwig/automl/auto_tune_config.py create mode 100644 ludwig/automl/automl.py create mode 100644 ludwig/automl/base_config.py create mode 100644 ludwig/automl/config_enumerator.py create mode 100644 ludwig/automl/config_sampler.py create mode 100644 ludwig/automl/config_validator.py create mode 100644 ludwig/automl/defaults/base_automl_config.yaml create mode 100644 ludwig/automl/defaults/combiner/concat_config.yaml create mode 100644 ludwig/automl/defaults/combiner/ft_transformer_config.yaml create mode 100644 ludwig/automl/defaults/combiner/tabnet_config.yaml create mode 100644 ludwig/automl/defaults/combiner/tabtransformer_config.yaml create mode 100644 ludwig/automl/defaults/combiner/transformer_config.yaml create mode 100644 ludwig/automl/defaults/reference_configs.yaml create mode 100644 ludwig/automl/defaults/text/bert_config.yaml create mode 100644 ludwig/automl/search_space.py create mode 100644 ludwig/automl/target_detection.py create mode 100644 ludwig/backend/__init__.py create mode 100644 ludwig/backend/base.py create mode 100644 ludwig/backend/datasource.py create mode 100644 ludwig/backend/ray.py create mode 100644 ludwig/backend/utils/__init__.py create mode 100644 ludwig/backend/utils/storage.py create mode 100644 ludwig/benchmarking/README.md create mode 100644 ludwig/benchmarking/__init__.py create mode 100644 ludwig/benchmarking/artifacts.py create mode 100644 ludwig/benchmarking/benchmark.py create mode 100644 ludwig/benchmarking/examples/benchmarking_config.yaml create mode 100644 ludwig/benchmarking/examples/process_config.py create mode 100644 ludwig/benchmarking/profiler.py create mode 100644 ludwig/benchmarking/profiler_callbacks.py create mode 100644 ludwig/benchmarking/profiler_dataclasses.py create mode 100644 ludwig/benchmarking/reporting.py create mode 100644 ludwig/benchmarking/summarize.py create mode 100644 ludwig/benchmarking/summary_dataclasses.py create mode 100644 ludwig/benchmarking/utils.py create mode 100644 ludwig/callbacks/__init__.py create mode 100644 ludwig/callbacks/studio.py create mode 100644 ludwig/check.py create mode 100644 ludwig/cli.py create mode 100644 ludwig/collect.py create mode 100644 ludwig/combiners/__init__.py create mode 100644 ludwig/combiners/combiners.py create mode 100644 ludwig/combiners/tabpfn_v2_combiner.py create mode 100644 ludwig/config_generation.py create mode 100644 ludwig/config_sampling/__init__.py create mode 100644 ludwig/config_sampling/explore_schema.py create mode 100644 ludwig/config_sampling/parameter_sampling.py create mode 100644 ludwig/config_validation/__init__.py create mode 100644 ludwig/config_validation/checks.py create mode 100644 ludwig/config_validation/preprocessing.py create mode 100644 ludwig/config_validation/validation.py create mode 100644 ludwig/constants.py create mode 100644 ludwig/contrib.py create mode 100644 ludwig/contribs/__init__.py create mode 100644 ludwig/contribs/aim.py create mode 100644 ludwig/contribs/comet.py create mode 100644 ludwig/contribs/mlflow/__init__.py create mode 100644 ludwig/contribs/mlflow/mlflow3.py create mode 100644 ludwig/contribs/mlflow/model.py create mode 100644 ludwig/contribs/wandb.py create mode 100644 ludwig/data/__init__.py create mode 100644 ludwig/data/batcher/__init__.py create mode 100644 ludwig/data/batcher/base.py create mode 100644 ludwig/data/batcher/bucketed.py create mode 100644 ludwig/data/batcher/iterable.py create mode 100644 ludwig/data/batcher/random_access.py create mode 100644 ludwig/data/batcher/test_batcher.py create mode 100644 ludwig/data/cache/__init__.py create mode 100644 ludwig/data/cache/manager.py create mode 100644 ludwig/data/cache/types.py create mode 100644 ludwig/data/cache/util.py create mode 100644 ludwig/data/concatenate_datasets.py create mode 100644 ludwig/data/dataframe/__init__.py create mode 100644 ludwig/data/dataframe/base.py create mode 100644 ludwig/data/dataframe/dask.py create mode 100644 ludwig/data/dataframe/modin.py create mode 100644 ludwig/data/dataframe/pandas.py create mode 100644 ludwig/data/dataset/__init__.py create mode 100644 ludwig/data/dataset/base.py create mode 100644 ludwig/data/dataset/pandas.py create mode 100644 ludwig/data/dataset/ray.py create mode 100644 ludwig/data/dataset_synthesizer.py create mode 100644 ludwig/data/format_registry.py create mode 100644 ludwig/data/lazy_utils.py create mode 100644 ludwig/data/multimodal_collator.py create mode 100644 ludwig/data/negative_sampling.py create mode 100644 ludwig/data/postprocessing.py create mode 100644 ludwig/data/preprocessing.py create mode 100644 ludwig/data/preprocessing_progress.py create mode 100644 ludwig/data/prompt.py create mode 100644 ludwig/data/sampler.py create mode 100644 ludwig/data/split.py create mode 100644 ludwig/data/split_dataset.py create mode 100644 ludwig/data/split_utils.py create mode 100644 ludwig/data/statistics.py create mode 100644 ludwig/data/types.py create mode 100644 ludwig/data/utils.py create mode 100644 ludwig/datasets/README.md create mode 100644 ludwig/datasets/__init__.py create mode 100644 ludwig/datasets/archives.py create mode 100644 ludwig/datasets/configs/__init__.py create mode 100644 ludwig/datasets/configs/abjad_kids.yaml create mode 100644 ludwig/datasets/configs/acronym_identification.yaml create mode 100644 ludwig/datasets/configs/adult_census_income.yaml create mode 100644 ludwig/datasets/configs/adult_income_hf.yaml create mode 100644 ludwig/datasets/configs/ae_price_prediction.yaml create mode 100644 ludwig/datasets/configs/aegis_safety.yaml create mode 100644 ludwig/datasets/configs/aeslc.yaml create mode 100644 ludwig/datasets/configs/ag_news_hf.yaml create mode 100644 ludwig/datasets/configs/agnews.yaml create mode 100644 ludwig/datasets/configs/ai2d_diagrams.yaml create mode 100644 ludwig/datasets/configs/ai_generated_ecommerce.yaml create mode 100644 ludwig/datasets/configs/allocine.yaml create mode 100644 ludwig/datasets/configs/allstate_claims_severity.yaml create mode 100644 ludwig/datasets/configs/alpaca.yaml create mode 100644 ludwig/datasets/configs/alpaca_cleaned.yaml create mode 100644 ludwig/datasets/configs/alpaca_gpt4.yaml create mode 100644 ludwig/datasets/configs/alpaca_gpt4_zh.yaml create mode 100644 ludwig/datasets/configs/amazon_employee_access_challenge.yaml create mode 100644 ludwig/datasets/configs/amazon_massive_intent.yaml create mode 100644 ludwig/datasets/configs/amazon_massive_scenario.yaml create mode 100644 ludwig/datasets/configs/amazon_polarity.yaml create mode 100644 ludwig/datasets/configs/amazon_review_polarity.yaml create mode 100644 ludwig/datasets/configs/amazon_reviews.yaml create mode 100644 ludwig/datasets/configs/amazon_reviews_2023.yaml create mode 100644 ludwig/datasets/configs/amazon_science_massive.yaml create mode 100644 ludwig/datasets/configs/ambig_qa.yaml create mode 100644 ludwig/datasets/configs/ames_housing.yaml create mode 100644 ludwig/datasets/configs/ami_asr.yaml create mode 100644 ludwig/datasets/configs/anli.yaml create mode 100644 ludwig/datasets/configs/app_reviews.yaml create mode 100644 ludwig/datasets/configs/aqua_rat.yaml create mode 100644 ludwig/datasets/configs/arc_challenge.yaml create mode 100644 ludwig/datasets/configs/arc_easy.yaml create mode 100644 ludwig/datasets/configs/arxiv_abstracts_2021.yaml create mode 100644 ludwig/datasets/configs/arxiv_summarization.yaml create mode 100644 ludwig/datasets/configs/audioset_balanced.yaml create mode 100644 ludwig/datasets/configs/banking77.yaml create mode 100644 ludwig/datasets/configs/banking77_legacy.yaml create mode 100644 ludwig/datasets/configs/bbcnews.yaml create mode 100644 ludwig/datasets/configs/bbh.yaml create mode 100644 ludwig/datasets/configs/beans.yaml create mode 100644 ludwig/datasets/configs/beavertails.yaml create mode 100644 ludwig/datasets/configs/belebele.yaml create mode 100644 ludwig/datasets/configs/belebele_fr.yaml create mode 100644 ludwig/datasets/configs/big_patent.yaml create mode 100644 ludwig/datasets/configs/bigbench.yaml create mode 100644 ludwig/datasets/configs/billsum.yaml create mode 100644 ludwig/datasets/configs/bitext_customer_intent.yaml create mode 100644 ludwig/datasets/configs/bitext_customer_support.yaml create mode 100644 ludwig/datasets/configs/blimp.yaml create mode 100644 ludwig/datasets/configs/bnp_claims_management.yaml create mode 100644 ludwig/datasets/configs/bookprice_prediction.yaml create mode 100644 ludwig/datasets/configs/boolq.yaml create mode 100644 ludwig/datasets/configs/boolq_standalone.yaml create mode 100644 ludwig/datasets/configs/bornholm_bitext.yaml create mode 100644 ludwig/datasets/configs/brazilian_toxic_tweets.yaml create mode 100644 ludwig/datasets/configs/california_house_price.yaml create mode 100644 ludwig/datasets/configs/california_housing.yaml create mode 100644 ludwig/datasets/configs/camseq.yaml create mode 100644 ludwig/datasets/configs/cantonese_asr.yaml create mode 100644 ludwig/datasets/configs/ccnews.yaml create mode 100644 ludwig/datasets/configs/cifar10.yaml create mode 100644 ludwig/datasets/configs/cifar100.yaml create mode 100644 ludwig/datasets/configs/civil_comments.yaml create mode 100644 ludwig/datasets/configs/climate_fever.yaml create mode 100644 ludwig/datasets/configs/climate_sentiment.yaml create mode 100644 ludwig/datasets/configs/clinc_oos.yaml create mode 100644 ludwig/datasets/configs/clue_afqmc.yaml create mode 100644 ludwig/datasets/configs/cmrc2018.yaml create mode 100644 ludwig/datasets/configs/cnn_dailymail.yaml create mode 100644 ludwig/datasets/configs/cnn_dm_hf.yaml create mode 100644 ludwig/datasets/configs/code_alpaca.yaml create mode 100644 ludwig/datasets/configs/code_contests.yaml create mode 100644 ludwig/datasets/configs/code_defect_detection.yaml create mode 100644 ludwig/datasets/configs/code_search_net.yaml create mode 100644 ludwig/datasets/configs/codex_thinking.yaml create mode 100644 ludwig/datasets/configs/codexglue_code_to_text.yaml create mode 100644 ludwig/datasets/configs/coig_cqia.yaml create mode 100644 ludwig/datasets/configs/cola.yaml create mode 100644 ludwig/datasets/configs/commitment_bank.yaml create mode 100644 ludwig/datasets/configs/commonsense_qa.yaml create mode 100644 ludwig/datasets/configs/compas_recidivism.yaml create mode 100644 ludwig/datasets/configs/connect4.yaml create mode 100644 ludwig/datasets/configs/consumer_complaints.yaml create mode 100644 ludwig/datasets/configs/copa.yaml create mode 100644 ludwig/datasets/configs/cord_v2.yaml create mode 100644 ludwig/datasets/configs/credit_card_default.yaml create mode 100644 ludwig/datasets/configs/creditcard_fraud.yaml create mode 100644 ludwig/datasets/configs/customer_churn_prediction.yaml create mode 100644 ludwig/datasets/configs/customer_reviews.yaml create mode 100644 ludwig/datasets/configs/dair_emotion.yaml create mode 100644 ludwig/datasets/configs/data_scientist_salary.yaml create mode 100644 ludwig/datasets/configs/databench_qa.yaml create mode 100644 ludwig/datasets/configs/databricks_dolly_15k.yaml create mode 100644 ludwig/datasets/configs/dbpedia.yaml create mode 100644 ludwig/datasets/configs/dbpedia_14.yaml create mode 100644 ludwig/datasets/configs/diabetes_readmission.yaml create mode 100644 ludwig/datasets/configs/dialogsum.yaml create mode 100644 ludwig/datasets/configs/docvqa.yaml create mode 100644 ludwig/datasets/configs/dolly_15k.yaml create mode 100644 ludwig/datasets/configs/drop.yaml create mode 100644 ludwig/datasets/configs/duorc.yaml create mode 100644 ludwig/datasets/configs/electricity.yaml create mode 100644 ludwig/datasets/configs/electricity_tabular.yaml create mode 100644 ludwig/datasets/configs/emodb.yaml create mode 100644 ludwig/datasets/configs/emotion.yaml create mode 100644 ludwig/datasets/configs/enron_spam.yaml create mode 100644 ludwig/datasets/configs/esc50.yaml create mode 100644 ludwig/datasets/configs/europarl_bg_cs.yaml create mode 100644 ludwig/datasets/configs/europarl_bg_en.yaml create mode 100644 ludwig/datasets/configs/europarl_cs_en.yaml create mode 100644 ludwig/datasets/configs/europarl_da_en.yaml create mode 100644 ludwig/datasets/configs/europarl_de_en.yaml create mode 100644 ludwig/datasets/configs/europarl_el_en.yaml create mode 100644 ludwig/datasets/configs/europarl_en_es.yaml create mode 100644 ludwig/datasets/configs/europarl_en_fr.yaml create mode 100644 ludwig/datasets/configs/europarl_en_it.yaml create mode 100644 ludwig/datasets/configs/europarl_en_nl.yaml create mode 100644 ludwig/datasets/configs/europarl_en_pl.yaml create mode 100644 ludwig/datasets/configs/europarl_en_pt.yaml create mode 100644 ludwig/datasets/configs/europarl_en_ro.yaml create mode 100644 ludwig/datasets/configs/europarl_en_sv.yaml create mode 100644 ludwig/datasets/configs/eurosat.yaml create mode 100644 ludwig/datasets/configs/eurosat_rgb.yaml create mode 100644 ludwig/datasets/configs/factcheck.yaml create mode 100644 ludwig/datasets/configs/fake_job_postings2.yaml create mode 100644 ludwig/datasets/configs/fake_news_detection.yaml create mode 100644 ludwig/datasets/configs/farstail_nli.yaml create mode 100644 ludwig/datasets/configs/fashion_mnist.yaml create mode 100644 ludwig/datasets/configs/fever.yaml create mode 100644 ludwig/datasets/configs/fever_gold.yaml create mode 100644 ludwig/datasets/configs/few_nerd.yaml create mode 100644 ludwig/datasets/configs/financial_phrasebank.yaml create mode 100644 ludwig/datasets/configs/fineweb_edu.yaml create mode 100644 ludwig/datasets/configs/flashrag_2wikimultihop.yaml create mode 100644 ludwig/datasets/configs/flickr8k.yaml create mode 100644 ludwig/datasets/configs/flores_101.yaml create mode 100644 ludwig/datasets/configs/food101.yaml create mode 100644 ludwig/datasets/configs/forest_cover.yaml create mode 100644 ludwig/datasets/configs/gaia_cepheids.yaml create mode 100644 ludwig/datasets/configs/gaia_rrlyrae.yaml create mode 100644 ludwig/datasets/configs/gaia_spectroscopic_binaries.yaml create mode 100644 ludwig/datasets/configs/gaia_young_stellar_objects.yaml create mode 100644 ludwig/datasets/configs/gaiasky_qa.yaml create mode 100644 ludwig/datasets/configs/germeval18.yaml create mode 100644 ludwig/datasets/configs/go_emotions.yaml create mode 100644 ludwig/datasets/configs/go_emotions_multiclass.yaml create mode 100644 ludwig/datasets/configs/goemotions.yaml create mode 100644 ludwig/datasets/configs/goodbooks_books.yaml create mode 100644 ludwig/datasets/configs/google_qa_answer_type_reason_explanation.yaml create mode 100644 ludwig/datasets/configs/google_qa_question_type_reason_explanation.yaml create mode 100644 ludwig/datasets/configs/google_quest_qa.yaml create mode 100644 ludwig/datasets/configs/govreport_summarization.yaml create mode 100644 ludwig/datasets/configs/graid_bdd.yaml create mode 100644 ludwig/datasets/configs/gsm8k.yaml create mode 100644 ludwig/datasets/configs/gsm8k_openai.yaml create mode 100644 ludwig/datasets/configs/gtsrb.yaml create mode 100644 ludwig/datasets/configs/handwritten_crossouts.yaml create mode 100644 ludwig/datasets/configs/hate_speech18.yaml create mode 100644 ludwig/datasets/configs/hatespeech_offensive.yaml create mode 100644 ludwig/datasets/configs/hc3.yaml create mode 100644 ludwig/datasets/configs/hc3_chinese.yaml create mode 100644 ludwig/datasets/configs/heart_failure.yaml create mode 100644 ludwig/datasets/configs/hellaswag.yaml create mode 100644 ludwig/datasets/configs/helpsteer2.yaml create mode 100644 ludwig/datasets/configs/hermes_function_calling.yaml create mode 100644 ludwig/datasets/configs/hh_rlhf.yaml create mode 100644 ludwig/datasets/configs/higgs.yaml create mode 100644 ludwig/datasets/configs/hotpot_qa.yaml create mode 100644 ludwig/datasets/configs/hugging_face.yaml create mode 100644 ludwig/datasets/configs/ieee_fraud.yaml create mode 100644 ludwig/datasets/configs/imagenet_100.yaml create mode 100644 ludwig/datasets/configs/imbalanced_insurance.yaml create mode 100644 ludwig/datasets/configs/imdb.yaml create mode 100644 ludwig/datasets/configs/imdb_genre_prediction.yaml create mode 100644 ludwig/datasets/configs/imdb_mteb.yaml create mode 100644 ludwig/datasets/configs/imdb_sentiment.yaml create mode 100644 ludwig/datasets/configs/indic_glue.yaml create mode 100644 ludwig/datasets/configs/insurance_lite.yaml create mode 100644 ludwig/datasets/configs/intuitive_physics.yaml create mode 100644 ludwig/datasets/configs/invoice_data.yaml create mode 100644 ludwig/datasets/configs/iris.yaml create mode 100644 ludwig/datasets/configs/iris_sklearn.yaml create mode 100644 ludwig/datasets/configs/irony.yaml create mode 100644 ludwig/datasets/configs/jc_penney_products.yaml create mode 100644 ludwig/datasets/configs/jigsaw_unintended_bias.yaml create mode 100644 ludwig/datasets/configs/jigsaw_unintended_bias100k.yaml create mode 100644 ludwig/datasets/configs/kdd_appetency.yaml create mode 100644 ludwig/datasets/configs/kdd_churn.yaml create mode 100644 ludwig/datasets/configs/kdd_upselling.yaml create mode 100644 ludwig/datasets/configs/kick_starter_funding.yaml create mode 100644 ludwig/datasets/configs/kilt_nq.yaml create mode 100644 ludwig/datasets/configs/klue_sts.yaml create mode 100644 ludwig/datasets/configs/klue_topic.yaml create mode 100644 ludwig/datasets/configs/kmmlu.yaml create mode 100644 ludwig/datasets/configs/kvasir_vqa.yaml create mode 100644 ludwig/datasets/configs/language_identification.yaml create mode 100644 ludwig/datasets/configs/lex_glue_ecthr.yaml create mode 100644 ludwig/datasets/configs/lex_glue_eurlex.yaml create mode 100644 ludwig/datasets/configs/librispeech.yaml create mode 100644 ludwig/datasets/configs/lmsys_arena.yaml create mode 100644 ludwig/datasets/configs/m3cot.yaml create mode 100644 ludwig/datasets/configs/m_mmlu.yaml create mode 100644 ludwig/datasets/configs/map_trace.yaml create mode 100644 ludwig/datasets/configs/math500.yaml create mode 100644 ludwig/datasets/configs/mathvista.yaml create mode 100644 ludwig/datasets/configs/mbpp.yaml create mode 100644 ludwig/datasets/configs/measuring_hate_speech.yaml create mode 100644 ludwig/datasets/configs/medical_flashcards.yaml create mode 100644 ludwig/datasets/configs/medmcqa.yaml create mode 100644 ludwig/datasets/configs/melbourne_airbnb.yaml create mode 100644 ludwig/datasets/configs/mercari_price_suggestion.yaml create mode 100644 ludwig/datasets/configs/mercari_price_suggestion100K.yaml create mode 100644 ludwig/datasets/configs/mercedes_benz_greener.yaml create mode 100644 ludwig/datasets/configs/merit.yaml create mode 100644 ludwig/datasets/configs/minds14.yaml create mode 100644 ludwig/datasets/configs/mini_imagenet.yaml create mode 100644 ludwig/datasets/configs/mls_german.yaml create mode 100644 ludwig/datasets/configs/mmlu.yaml create mode 100644 ludwig/datasets/configs/mmlu_lighteval.yaml create mode 100644 ludwig/datasets/configs/mmlu_pro.yaml create mode 100644 ludwig/datasets/configs/mmsulab.yaml create mode 100644 ludwig/datasets/configs/mnist.yaml create mode 100644 ludwig/datasets/configs/mnist_ylecun.yaml create mode 100644 ludwig/datasets/configs/mnli.yaml create mode 100644 ludwig/datasets/configs/mobile_mold.yaml create mode 100644 ludwig/datasets/configs/moral_stories.yaml create mode 100644 ludwig/datasets/configs/mrpc.yaml create mode 100644 ludwig/datasets/configs/msmarco_passage.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_polarity.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_reviews_class_de.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_reviews_class_en.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_reviews_class_es.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_reviews_class_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_reviews_class_ja.yaml create mode 100644 ludwig/datasets/configs/mteb_amazon_reviews_class_zh.yaml create mode 100644 ludwig/datasets/configs/mteb_biosses.yaml create mode 100644 ludwig/datasets/configs/mteb_cyrillic_turkic.yaml create mode 100644 ludwig/datasets/configs/mteb_emotion.yaml create mode 100644 ludwig/datasets/configs/mteb_financial_phrasebank.yaml create mode 100644 ludwig/datasets/configs/mteb_frenk_en.yaml create mode 100644 ludwig/datasets/configs/mteb_frenk_hr.yaml create mode 100644 ludwig/datasets/configs/mteb_frenk_sl.yaml create mode 100644 ludwig/datasets/configs/mteb_georeview.yaml create mode 100644 ludwig/datasets/configs/mteb_greek_legal.yaml create mode 100644 ludwig/datasets/configs/mteb_imdb.yaml create mode 100644 ludwig/datasets/configs/mteb_ita_casehold.yaml create mode 100644 ludwig/datasets/configs/mteb_jd_review.yaml create mode 100644 ludwig/datasets/configs/mteb_kor_sarcasm.yaml create mode 100644 ludwig/datasets/configs/mteb_language_class.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_af.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_am.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ar.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_az.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_bn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_cy.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_da.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_de.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_el.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_en.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_es.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_fa.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_fi.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_he.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_hi.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_hu.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_hy.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_id.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_is.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_it.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ja.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_jv.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ka.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_km.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_kn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ko.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_lv.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ml.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_mn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ms.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_my.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_nb.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_nl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_pl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_pt.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ro.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ru.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_sl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_sq.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_sv.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_sw.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ta.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_te.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_th.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_tl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_tr.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_ur.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_vi.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_zh_cn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_intent_zh_tw.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_af.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_am.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ar.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_az.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_bn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_cy.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_da.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_de.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_el.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_en.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_es.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_fa.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_fi.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_he.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_hi.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_hu.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_hy.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_id.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_is.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_it.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ja.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_jv.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ka.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_km.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_kn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ko.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_lv.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ml.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_mn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ms.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_my.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_nb.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_nl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_pl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_pt.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ro.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ru.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_sl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_sq.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_sv.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_sw.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ta.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_te.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_th.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_tl.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_tr.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_ur.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_vi.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_zh_cn.yaml create mode 100644 ludwig/datasets/configs/mteb_massive_scenario_zh_tw.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_domain_de.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_domain_en.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_domain_es.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_domain_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_domain_hi.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_domain_th.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_intent_de2.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_intent_en.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_intent_es2.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_intent_fr2.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_intent_hi2.yaml create mode 100644 ludwig/datasets/configs/mteb_mtop_intent_th2.yaml create mode 100644 ludwig/datasets/configs/mteb_multilingual_sentiment.yaml create mode 100644 ludwig/datasets/configs/mteb_naija_senti_hau.yaml create mode 100644 ludwig/datasets/configs/mteb_naija_senti_ibo.yaml create mode 100644 ludwig/datasets/configs/mteb_naija_senti_pcm.yaml create mode 100644 ludwig/datasets/configs/mteb_naija_senti_yor.yaml create mode 100644 ludwig/datasets/configs/mteb_nepali_news.yaml create mode 100644 ludwig/datasets/configs/mteb_nordic_lang.yaml create mode 100644 ludwig/datasets/configs/mteb_online_shopping.yaml create mode 100644 ludwig/datasets/configs/mteb_poem_sentiment.yaml create mode 100644 ludwig/datasets/configs/mteb_sensitive_topics.yaml create mode 100644 ludwig/datasets/configs/mteb_sentiment_hindi.yaml create mode 100644 ludwig/datasets/configs/mteb_sts17_ar.yaml create mode 100644 ludwig/datasets/configs/mteb_sts17_de.yaml create mode 100644 ludwig/datasets/configs/mteb_sts17_en.yaml create mode 100644 ludwig/datasets/configs/mteb_sts17_es.yaml create mode 100644 ludwig/datasets/configs/mteb_sts17_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_ar.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_de.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_de_en.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_de_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_en.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_es.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_es_en.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_es_it.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_it.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_pl.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_pl_en.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_ru.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_tr.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_zh.yaml create mode 100644 ludwig/datasets/configs/mteb_sts22_zh_en.yaml create mode 100644 ludwig/datasets/configs/mteb_stsbenchmark.yaml create mode 100644 ludwig/datasets/configs/mteb_swiss_judgement_de.yaml create mode 100644 ludwig/datasets/configs/mteb_swiss_judgement_fr.yaml create mode 100644 ludwig/datasets/configs/mteb_swiss_judgement_it.yaml create mode 100644 ludwig/datasets/configs/mteb_tnews.yaml create mode 100644 ludwig/datasets/configs/mteb_toxic_convo.yaml create mode 100644 ludwig/datasets/configs/mteb_turkish_product.yaml create mode 100644 ludwig/datasets/configs/mteb_tweet_sentiment.yaml create mode 100644 ludwig/datasets/configs/mteb_tweet_topic.yaml create mode 100644 ludwig/datasets/configs/mteb_waimai.yaml create mode 100644 ludwig/datasets/configs/mteb_yahoo_answers.yaml create mode 100644 ludwig/datasets/configs/multi30k.yaml create mode 100644 ludwig/datasets/configs/multi_nli.yaml create mode 100644 ludwig/datasets/configs/multinerd.yaml create mode 100644 ludwig/datasets/configs/multirc.yaml create mode 100644 ludwig/datasets/configs/multiun_ar_en.yaml create mode 100644 ludwig/datasets/configs/mushroom_edibility.yaml create mode 100644 ludwig/datasets/configs/natural_questions.yaml create mode 100644 ludwig/datasets/configs/natural_questions_hard_negatives.yaml create mode 100644 ludwig/datasets/configs/naturelm_audio.yaml create mode 100644 ludwig/datasets/configs/naval.yaml create mode 100644 ludwig/datasets/configs/naver_news_summary.yaml create mode 100644 ludwig/datasets/configs/nemotron_pii.yaml create mode 100644 ludwig/datasets/configs/nemotron_safety.yaml create mode 100644 ludwig/datasets/configs/news_category.yaml create mode 100644 ludwig/datasets/configs/news_channel.yaml create mode 100644 ludwig/datasets/configs/news_popularity2.yaml create mode 100644 ludwig/datasets/configs/newyorker_caption_contest.yaml create mode 100644 ludwig/datasets/configs/nli_zh_all.yaml create mode 100644 ludwig/datasets/configs/no_robots.yaml create mode 100644 ludwig/datasets/configs/noshow_appointments.yaml create mode 100644 ludwig/datasets/configs/nq_open.yaml create mode 100644 ludwig/datasets/configs/numerai28pt6.yaml create mode 100644 ludwig/datasets/configs/numinamath.yaml create mode 100644 ludwig/datasets/configs/oasst1.yaml create mode 100644 ludwig/datasets/configs/ohsumed_7400.yaml create mode 100644 ludwig/datasets/configs/ohsumed_cmu.yaml create mode 100644 ludwig/datasets/configs/openbookqa.yaml create mode 100644 ludwig/datasets/configs/openfake.yaml create mode 100644 ludwig/datasets/configs/opus100_en_es.yaml create mode 100644 ludwig/datasets/configs/opus100_en_fr.yaml create mode 100644 ludwig/datasets/configs/opus_books_en_fr.yaml create mode 100644 ludwig/datasets/configs/or_bench.yaml create mode 100644 ludwig/datasets/configs/orca_dpo_pairs.yaml create mode 100644 ludwig/datasets/configs/orca_math.yaml create mode 100644 ludwig/datasets/configs/otto_group_product.yaml create mode 100644 ludwig/datasets/configs/oxford_pets.yaml create mode 100644 ludwig/datasets/configs/path_vqa.yaml create mode 100644 ludwig/datasets/configs/paws.yaml create mode 100644 ludwig/datasets/configs/paws_x.yaml create mode 100644 ludwig/datasets/configs/peoples_speech.yaml create mode 100644 ludwig/datasets/configs/persuasion.yaml create mode 100644 ludwig/datasets/configs/phinc.yaml create mode 100644 ludwig/datasets/configs/pii_masking.yaml create mode 100644 ludwig/datasets/configs/poem_sentiment.yaml create mode 100644 ludwig/datasets/configs/poem_sentiment_hf.yaml create mode 100644 ludwig/datasets/configs/poker_hand.yaml create mode 100644 ludwig/datasets/configs/porto_seguro_safe_driver.yaml create mode 100644 ludwig/datasets/configs/product_sentiment_machine_hack.yaml create mode 100644 ludwig/datasets/configs/protein.yaml create mode 100644 ludwig/datasets/configs/pubmed_qa.yaml create mode 100644 ludwig/datasets/configs/pubmed_summarization.yaml create mode 100644 ludwig/datasets/configs/python_code_instructions.yaml create mode 100644 ludwig/datasets/configs/qasc.yaml create mode 100644 ludwig/datasets/configs/qnli.yaml create mode 100644 ludwig/datasets/configs/qqp.yaml create mode 100644 ludwig/datasets/configs/race.yaml create mode 100644 ludwig/datasets/configs/ravnursson_asr.yaml create mode 100644 ludwig/datasets/configs/rendered_sst2.yaml create mode 100644 ludwig/datasets/configs/repid.yaml create mode 100644 ludwig/datasets/configs/reuters_cmu.yaml create mode 100644 ludwig/datasets/configs/reuters_r8.yaml create mode 100644 ludwig/datasets/configs/reward_bench.yaml create mode 100644 ludwig/datasets/configs/rossman_store_sales.yaml create mode 100644 ludwig/datasets/configs/rotten_tomatoes.yaml create mode 100644 ludwig/datasets/configs/rte.yaml create mode 100644 ludwig/datasets/configs/samsum.yaml create mode 100644 ludwig/datasets/configs/santander_customer_satisfaction.yaml create mode 100644 ludwig/datasets/configs/santander_customer_transaction.yaml create mode 100644 ludwig/datasets/configs/santander_value_prediction.yaml create mode 100644 ludwig/datasets/configs/sarcastic_headlines.yaml create mode 100644 ludwig/datasets/configs/sarcos.yaml create mode 100644 ludwig/datasets/configs/scandisent.yaml create mode 100644 ludwig/datasets/configs/scienceqa_vqa.yaml create mode 100644 ludwig/datasets/configs/sciq.yaml create mode 100644 ludwig/datasets/configs/scitail.yaml create mode 100644 ludwig/datasets/configs/scotus_classification.yaml create mode 100644 ludwig/datasets/configs/setfit_ag_news.yaml create mode 100644 ludwig/datasets/configs/setfit_amazon_polarity.yaml create mode 100644 ludwig/datasets/configs/setfit_emotion.yaml create mode 100644 ludwig/datasets/configs/setfit_mrpc.yaml create mode 100644 ludwig/datasets/configs/setfit_sst2.yaml create mode 100644 ludwig/datasets/configs/setfit_subj.yaml create mode 100644 ludwig/datasets/configs/setfit_yelp_review.yaml create mode 100644 ludwig/datasets/configs/setimes_bg_bs.yaml create mode 100644 ludwig/datasets/configs/sib200.yaml create mode 100644 ludwig/datasets/configs/sickr.yaml create mode 100644 ludwig/datasets/configs/sms_spam.yaml create mode 100644 ludwig/datasets/configs/snli.yaml create mode 100644 ludwig/datasets/configs/speech_massive.yaml create mode 100644 ludwig/datasets/configs/spotify_tracks.yaml create mode 100644 ludwig/datasets/configs/squad.yaml create mode 100644 ludwig/datasets/configs/squad_v2.yaml create mode 100644 ludwig/datasets/configs/sst2.yaml create mode 100644 ludwig/datasets/configs/sst2_hf.yaml create mode 100644 ludwig/datasets/configs/sst3.yaml create mode 100644 ludwig/datasets/configs/sst5.yaml create mode 100644 ludwig/datasets/configs/sst5_setfit.yaml create mode 100644 ludwig/datasets/configs/stackoverflow_posts.yaml create mode 100644 ludwig/datasets/configs/stanford_cars.yaml create mode 100644 ludwig/datasets/configs/stocks_daily_price.yaml create mode 100644 ludwig/datasets/configs/sts12.yaml create mode 100644 ludwig/datasets/configs/sts13.yaml create mode 100644 ludwig/datasets/configs/sts14.yaml create mode 100644 ludwig/datasets/configs/sts15.yaml create mode 100644 ludwig/datasets/configs/sts16.yaml create mode 100644 ludwig/datasets/configs/sts17.yaml create mode 100644 ludwig/datasets/configs/sts22.yaml create mode 100644 ludwig/datasets/configs/sts_benchmark.yaml create mode 100644 ludwig/datasets/configs/stsb.yaml create mode 100644 ludwig/datasets/configs/stsb_de.yaml create mode 100644 ludwig/datasets/configs/stsb_sentencetransformers.yaml create mode 100644 ludwig/datasets/configs/student_performance.yaml create mode 100644 ludwig/datasets/configs/sun397.yaml create mode 100644 ludwig/datasets/configs/superglue_rte.yaml create mode 100644 ludwig/datasets/configs/svhn.yaml create mode 100644 ludwig/datasets/configs/synthetic_fraud.yaml create mode 100644 ludwig/datasets/configs/synthia.yaml create mode 100644 ludwig/datasets/configs/tadabur.yaml create mode 100644 ludwig/datasets/configs/taix_ray.yaml create mode 100644 ludwig/datasets/configs/talkingdata_adtrack_fraud.yaml create mode 100644 ludwig/datasets/configs/telco_customer_churn.yaml create mode 100644 ludwig/datasets/configs/temperature.yaml create mode 100644 ludwig/datasets/configs/textvqa.yaml create mode 100644 ludwig/datasets/configs/tiny_imagenet.yaml create mode 100644 ludwig/datasets/configs/titanic.yaml create mode 100644 ludwig/datasets/configs/tobacco_document.yaml create mode 100644 ludwig/datasets/configs/tofu.yaml create mode 100644 ludwig/datasets/configs/toxic_chat.yaml create mode 100644 ludwig/datasets/configs/trivia_qa.yaml create mode 100644 ludwig/datasets/configs/truthful_qa.yaml create mode 100644 ludwig/datasets/configs/tweet_eval_emoji.yaml create mode 100644 ludwig/datasets/configs/tweet_sentiment_extraction.yaml create mode 100644 ludwig/datasets/configs/tweeteval_emotion.yaml create mode 100644 ludwig/datasets/configs/tweeteval_hate.yaml create mode 100644 ludwig/datasets/configs/tweeteval_irony.yaml create mode 100644 ludwig/datasets/configs/tweeteval_offensive.yaml create mode 100644 ludwig/datasets/configs/tweeteval_sentiment.yaml create mode 100644 ludwig/datasets/configs/tweeteval_stance.yaml create mode 100644 ludwig/datasets/configs/twitter_bots.yaml create mode 100644 ludwig/datasets/configs/twitter_financial_news_topic.yaml create mode 100644 ludwig/datasets/configs/universal_dependencies.yaml create mode 100644 ludwig/datasets/configs/voxpopuli.yaml create mode 100644 ludwig/datasets/configs/vqa_rad.yaml create mode 100644 ludwig/datasets/configs/vqav2.yaml create mode 100644 ludwig/datasets/configs/vukuzenzele.yaml create mode 100644 ludwig/datasets/configs/walmart_recruiting.yaml create mode 100644 ludwig/datasets/configs/web_questions.yaml create mode 100644 ludwig/datasets/configs/wic.yaml create mode 100644 ludwig/datasets/configs/wiki_qa.yaml create mode 100644 ludwig/datasets/configs/wikiann.yaml create mode 100644 ludwig/datasets/configs/wikiann_de.yaml create mode 100644 ludwig/datasets/configs/wikiann_en.yaml create mode 100644 ludwig/datasets/configs/wikiann_zh.yaml create mode 100644 ludwig/datasets/configs/wikiart.yaml create mode 100644 ludwig/datasets/configs/wildchat.yaml create mode 100644 ludwig/datasets/configs/wine_reviews.yaml create mode 100644 ludwig/datasets/configs/winobias.yaml create mode 100644 ludwig/datasets/configs/winograd_schema.yaml create mode 100644 ludwig/datasets/configs/winogrande.yaml create mode 100644 ludwig/datasets/configs/winogrande_hf.yaml create mode 100644 ludwig/datasets/configs/wmt14_de_en.yaml create mode 100644 ludwig/datasets/configs/wmt15.yaml create mode 100644 ludwig/datasets/configs/wmt16_de_en.yaml create mode 100644 ludwig/datasets/configs/wmt19_de_en.yaml create mode 100644 ludwig/datasets/configs/wmt_t2t_de_en.yaml create mode 100644 ludwig/datasets/configs/wnli.yaml create mode 100644 ludwig/datasets/configs/women_clothing_review.yaml create mode 100644 ludwig/datasets/configs/world_speech_asr.yaml create mode 100644 ludwig/datasets/configs/xnli.yaml create mode 100644 ludwig/datasets/configs/xnli_de.yaml create mode 100644 ludwig/datasets/configs/xnli_en.yaml create mode 100644 ludwig/datasets/configs/xnli_es.yaml create mode 100644 ludwig/datasets/configs/xnli_fr.yaml create mode 100644 ludwig/datasets/configs/xnli_zh.yaml create mode 100644 ludwig/datasets/configs/xsum.yaml create mode 100644 ludwig/datasets/configs/xsum_hf.yaml create mode 100644 ludwig/datasets/configs/yahoo_answers.yaml create mode 100644 ludwig/datasets/configs/yahoo_answers_topics.yaml create mode 100644 ludwig/datasets/configs/yelp_polarity.yaml create mode 100644 ludwig/datasets/configs/yelp_review_full.yaml create mode 100644 ludwig/datasets/configs/yelp_review_polarity.yaml create mode 100644 ludwig/datasets/configs/yelp_reviews.yaml create mode 100644 ludwig/datasets/configs/yosemite.yaml create mode 100644 ludwig/datasets/dataset_config.py create mode 100644 ludwig/datasets/kaggle.py create mode 100644 ludwig/datasets/loaders/__init__.py create mode 100644 ludwig/datasets/loaders/adult_census_income.py create mode 100644 ludwig/datasets/loaders/agnews.py create mode 100644 ludwig/datasets/loaders/allstate_claims_severity.py create mode 100644 ludwig/datasets/loaders/california_housing.py create mode 100644 ludwig/datasets/loaders/camseq.py create mode 100644 ludwig/datasets/loaders/code_alpaca_loader.py create mode 100644 ludwig/datasets/loaders/code_loader.py create mode 100644 ludwig/datasets/loaders/consumer_complaints_loader.py create mode 100644 ludwig/datasets/loaders/creditcard_fraud.py create mode 100644 ludwig/datasets/loaders/dataset_loader.py create mode 100644 ludwig/datasets/loaders/esc50.py create mode 100644 ludwig/datasets/loaders/ethos_binary.py create mode 100644 ludwig/datasets/loaders/flickr8k.py create mode 100644 ludwig/datasets/loaders/forest_cover.py create mode 100644 ludwig/datasets/loaders/go_emotions.py create mode 100644 ludwig/datasets/loaders/goemotions.py create mode 100644 ludwig/datasets/loaders/higgs.py create mode 100644 ludwig/datasets/loaders/hugging_face.py create mode 100644 ludwig/datasets/loaders/ieee_fraud.py create mode 100644 ludwig/datasets/loaders/insurance_lite.py create mode 100644 ludwig/datasets/loaders/kdd_loader.py create mode 100644 ludwig/datasets/loaders/misc_loaders.py create mode 100644 ludwig/datasets/loaders/mnist.py create mode 100644 ludwig/datasets/loaders/multilabel_loader.py create mode 100644 ludwig/datasets/loaders/multiple_choice_loader.py create mode 100644 ludwig/datasets/loaders/naval.py create mode 100644 ludwig/datasets/loaders/ner_loader.py create mode 100644 ludwig/datasets/loaders/newyorker_caption_contest.py create mode 100644 ludwig/datasets/loaders/openml_loader.py create mode 100644 ludwig/datasets/loaders/qa_loader.py create mode 100644 ludwig/datasets/loaders/rossman_store_sales.py create mode 100644 ludwig/datasets/loaders/santander_value_prediction.py create mode 100644 ludwig/datasets/loaders/sarcastic_headlines.py create mode 100644 ludwig/datasets/loaders/sarcos.py create mode 100644 ludwig/datasets/loaders/split_loaders.py create mode 100644 ludwig/datasets/loaders/sst.py create mode 100644 ludwig/datasets/loaders/translation_loader.py create mode 100644 ludwig/datasets/loaders/vqa_loader.py create mode 100644 ludwig/datasets/loaders/wikiann.py create mode 100644 ludwig/datasets/model_configs/__init__.py create mode 100644 ludwig/datasets/model_configs/adult_census_income_default.yaml create mode 100644 ludwig/datasets/model_configs/allstate_claims_severity_default.yaml create mode 100644 ludwig/datasets/model_configs/ames_housing_default.yaml create mode 100644 ludwig/datasets/model_configs/bnp_claims_management_default.yaml create mode 100644 ludwig/datasets/model_configs/forest_cover_default.yaml create mode 100644 ludwig/datasets/model_configs/higgs_best.yaml create mode 100644 ludwig/datasets/model_configs/higgs_default.yaml create mode 100644 ludwig/datasets/model_configs/ieee_fraud_default.yaml create mode 100644 ludwig/datasets/model_configs/mercedes_benz_greener_default.yaml create mode 100644 ludwig/datasets/model_configs/mnist_default.yaml create mode 100644 ludwig/datasets/model_configs/mushroom_edibility_default.yaml create mode 100644 ludwig/datasets/model_configs/otto_group_product_default.yaml create mode 100644 ludwig/datasets/model_configs/poker_hand_default.yaml create mode 100644 ludwig/datasets/model_configs/porto_seguro_safe_driver_default.yaml create mode 100644 ludwig/datasets/model_configs/synthetic_fraud_default.yaml create mode 100644 ludwig/datasets/model_configs/titanic_default.yaml create mode 100644 ludwig/datasets/utils.py create mode 100644 ludwig/decoders/__init__.py create mode 100644 ludwig/decoders/base.py create mode 100644 ludwig/decoders/generic_decoders.py create mode 100644 ludwig/decoders/image_decoders.py create mode 100644 ludwig/decoders/llm_decoders.py create mode 100644 ludwig/decoders/registry.py create mode 100644 ludwig/decoders/sequence_decoder_utils.py create mode 100644 ludwig/decoders/sequence_decoders.py create mode 100644 ludwig/decoders/sequence_tagger.py create mode 100644 ludwig/decoders/utils.py create mode 100644 ludwig/distributed/__init__.py create mode 100644 ludwig/distributed/accelerate.py create mode 100644 ludwig/distributed/base.py create mode 100644 ludwig/encoders/__init__.py create mode 100644 ludwig/encoders/audio_encoders.py create mode 100644 ludwig/encoders/bag_encoders.py create mode 100644 ludwig/encoders/base.py create mode 100644 ludwig/encoders/category_encoders.py create mode 100644 ludwig/encoders/date_encoders.py create mode 100644 ludwig/encoders/generic_encoders.py create mode 100644 ludwig/encoders/h3_encoders.py create mode 100644 ludwig/encoders/image/__init__.py create mode 100644 ludwig/encoders/image/base.py create mode 100644 ludwig/encoders/image/pretrained.py create mode 100644 ludwig/encoders/image/timm.py create mode 100644 ludwig/encoders/image/torchvision.py create mode 100644 ludwig/encoders/mamba_hybrid.py create mode 100644 ludwig/encoders/number_encoders.py create mode 100644 ludwig/encoders/registry.py create mode 100644 ludwig/encoders/sequence_encoders.py create mode 100644 ludwig/encoders/set_encoders.py create mode 100644 ludwig/encoders/text_encoders.py create mode 100644 ludwig/encoders/timeseries_encoders.py create mode 100644 ludwig/encoders/types.py create mode 100644 ludwig/error.py create mode 100644 ludwig/evaluate.py create mode 100644 ludwig/experiment.py create mode 100644 ludwig/experiment_utils.py create mode 100644 ludwig/explain/__init__.py create mode 100644 ludwig/explain/captum.py create mode 100644 ludwig/explain/captum_ray.py create mode 100644 ludwig/explain/explainer.py create mode 100644 ludwig/explain/explanation.py create mode 100644 ludwig/explain/util.py create mode 100644 ludwig/export.py create mode 100644 ludwig/features/__init__.py create mode 100644 ludwig/features/anomaly_feature.py create mode 100644 ludwig/features/audio_feature.py create mode 100644 ludwig/features/bag_feature.py create mode 100644 ludwig/features/base_feature.py create mode 100644 ludwig/features/binary_feature.py create mode 100644 ludwig/features/category_feature.py create mode 100644 ludwig/features/date_feature.py create mode 100644 ludwig/features/feature_registries.py create mode 100644 ludwig/features/feature_utils.py create mode 100644 ludwig/features/h3_feature.py create mode 100644 ludwig/features/image_feature.py create mode 100644 ludwig/features/number_feature.py create mode 100644 ludwig/features/passthrough_feature.py create mode 100644 ludwig/features/sequence_feature.py create mode 100644 ludwig/features/set_feature.py create mode 100644 ludwig/features/text_feature.py create mode 100644 ludwig/features/timeseries_feature.py create mode 100644 ludwig/features/transforms.py create mode 100644 ludwig/features/vector_feature.py create mode 100644 ludwig/forecast.py create mode 100644 ludwig/globals.py create mode 100644 ludwig/hyperopt/__init__.py create mode 100644 ludwig/hyperopt/execution.py create mode 100644 ludwig/hyperopt/optuna_executor.py create mode 100644 ludwig/hyperopt/results.py create mode 100644 ludwig/hyperopt/run.py create mode 100644 ludwig/hyperopt/search_algos.py create mode 100644 ludwig/hyperopt/search_space_generator.py create mode 100644 ludwig/hyperopt/utils.py create mode 100644 ludwig/hyperopt_cli.py create mode 100644 ludwig/inspect_model.py create mode 100644 ludwig/model_export/base_model_exporter.py create mode 100644 ludwig/model_export/onnx_exporter.py create mode 100644 ludwig/model_inspector.py create mode 100644 ludwig/models/__init__.py create mode 100644 ludwig/models/base.py create mode 100644 ludwig/models/calibrator.py create mode 100644 ludwig/models/ecd.py create mode 100644 ludwig/models/embedder.py create mode 100644 ludwig/models/llm.py create mode 100644 ludwig/models/predictor.py create mode 100644 ludwig/models/registry.py create mode 100644 ludwig/models/retrieval.py create mode 100644 ludwig/modules/__init__.py create mode 100644 ludwig/modules/attention_modules.py create mode 100644 ludwig/modules/batch_ensemble.py create mode 100644 ludwig/modules/contrastive_alignment.py create mode 100644 ludwig/modules/convolutional_modules.py create mode 100644 ludwig/modules/dpo_loss.py create mode 100644 ludwig/modules/embedding_modules.py create mode 100644 ludwig/modules/fully_connected_modules.py create mode 100644 ludwig/modules/gated_cross_attention.py create mode 100644 ludwig/modules/initializer_modules.py create mode 100644 ludwig/modules/loss_balancing.py create mode 100644 ludwig/modules/loss_implementations/__init__.py create mode 100644 ludwig/modules/loss_implementations/corn.py create mode 100644 ludwig/modules/loss_modules.py create mode 100644 ludwig/modules/lr_scheduler.py create mode 100644 ludwig/modules/metric_modules.py create mode 100644 ludwig/modules/metric_registry.py create mode 100644 ludwig/modules/mlp_mixer_modules.py create mode 100644 ludwig/modules/modality_dropout.py create mode 100644 ludwig/modules/normalization_modules.py create mode 100644 ludwig/modules/optimization_modules.py create mode 100644 ludwig/modules/preference_losses.py create mode 100644 ludwig/modules/recurrent_modules.py create mode 100644 ludwig/modules/reduction_modules.py create mode 100644 ludwig/modules/tabnet_modules.py create mode 100644 ludwig/modules/training_hooks.py create mode 100644 ludwig/predict.py create mode 100644 ludwig/preprocess.py create mode 100644 ludwig/presets.py create mode 100644 ludwig/progress_bar.py create mode 100644 ludwig/py.typed create mode 100644 ludwig/schema/__init__.py create mode 100644 ludwig/schema/combiners/__init__.py create mode 100644 ludwig/schema/combiners/base.py create mode 100644 ludwig/schema/combiners/common_transformer_options.py create mode 100644 ludwig/schema/combiners/comparator.py create mode 100644 ludwig/schema/combiners/concat.py create mode 100644 ludwig/schema/combiners/cross_attention.py create mode 100644 ludwig/schema/combiners/ft_transformer.py create mode 100644 ludwig/schema/combiners/gated_fusion.py create mode 100644 ludwig/schema/combiners/hypernetwork.py create mode 100644 ludwig/schema/combiners/perceiver.py create mode 100644 ludwig/schema/combiners/project_aggregate.py create mode 100644 ludwig/schema/combiners/sequence.py create mode 100644 ludwig/schema/combiners/sequence_concat.py create mode 100644 ludwig/schema/combiners/tab_transformer.py create mode 100644 ludwig/schema/combiners/tabnet.py create mode 100644 ludwig/schema/combiners/tabpfn_v2.py create mode 100644 ludwig/schema/combiners/transformer.py create mode 100644 ludwig/schema/combiners/utils.py create mode 100644 ludwig/schema/common_fields.py create mode 100644 ludwig/schema/decoders/__init__.py create mode 100644 ludwig/schema/decoders/base.py create mode 100644 ludwig/schema/decoders/image_decoders.py create mode 100644 ludwig/schema/decoders/llm_decoders.py create mode 100644 ludwig/schema/decoders/sequence_decoders.py create mode 100644 ludwig/schema/decoders/utils.py create mode 100644 ludwig/schema/defaults/__init__.py create mode 100644 ludwig/schema/defaults/base.py create mode 100644 ludwig/schema/defaults/ecd.py create mode 100644 ludwig/schema/defaults/llm.py create mode 100644 ludwig/schema/defaults/utils.py create mode 100644 ludwig/schema/encoders/__init__.py create mode 100644 ludwig/schema/encoders/audio_encoders.py create mode 100644 ludwig/schema/encoders/bag_encoders.py create mode 100644 ludwig/schema/encoders/base.py create mode 100644 ludwig/schema/encoders/category_encoders.py create mode 100644 ludwig/schema/encoders/date_encoders.py create mode 100644 ludwig/schema/encoders/h3_encoders.py create mode 100644 ludwig/schema/encoders/image/__init__.py create mode 100644 ludwig/schema/encoders/image/base.py create mode 100644 ludwig/schema/encoders/image/pretrained.py create mode 100644 ludwig/schema/encoders/image/timm.py create mode 100644 ludwig/schema/encoders/image/torchvision.py create mode 100644 ludwig/schema/encoders/mamba_hybrid.py create mode 100644 ludwig/schema/encoders/number_encoders.py create mode 100644 ludwig/schema/encoders/sequence_encoders.py create mode 100644 ludwig/schema/encoders/set_encoders.py create mode 100644 ludwig/schema/encoders/text/__init__.py create mode 100644 ludwig/schema/encoders/text/hf_model_params.py create mode 100644 ludwig/schema/encoders/text_encoders.py create mode 100644 ludwig/schema/encoders/timeseries_encoders.py create mode 100644 ludwig/schema/encoders/utils.py create mode 100644 ludwig/schema/export_schema.py create mode 100644 ludwig/schema/features/__init__.py create mode 100644 ludwig/schema/features/anomaly_feature.py create mode 100644 ludwig/schema/features/audio_feature.py create mode 100644 ludwig/schema/features/augmentation/__init__.py create mode 100644 ludwig/schema/features/augmentation/base.py create mode 100644 ludwig/schema/features/augmentation/image.py create mode 100644 ludwig/schema/features/augmentation/utils.py create mode 100644 ludwig/schema/features/bag_feature.py create mode 100644 ludwig/schema/features/base.py create mode 100644 ludwig/schema/features/binary_feature.py create mode 100644 ludwig/schema/features/category_feature.py create mode 100644 ludwig/schema/features/date_feature.py create mode 100644 ludwig/schema/features/h3_feature.py create mode 100644 ludwig/schema/features/image_feature.py create mode 100644 ludwig/schema/features/loss/__init__.py create mode 100644 ludwig/schema/features/loss/loss.py create mode 100644 ludwig/schema/features/loss/utils.py create mode 100644 ludwig/schema/features/number_feature.py create mode 100644 ludwig/schema/features/preprocessing/__init__.py create mode 100644 ludwig/schema/features/preprocessing/audio.py create mode 100644 ludwig/schema/features/preprocessing/bag.py create mode 100644 ludwig/schema/features/preprocessing/base.py create mode 100644 ludwig/schema/features/preprocessing/binary.py create mode 100644 ludwig/schema/features/preprocessing/category.py create mode 100644 ludwig/schema/features/preprocessing/date.py create mode 100644 ludwig/schema/features/preprocessing/h3.py create mode 100644 ludwig/schema/features/preprocessing/image.py create mode 100644 ludwig/schema/features/preprocessing/number.py create mode 100644 ludwig/schema/features/preprocessing/sequence.py create mode 100644 ludwig/schema/features/preprocessing/set.py create mode 100644 ludwig/schema/features/preprocessing/text.py create mode 100644 ludwig/schema/features/preprocessing/timeseries.py create mode 100644 ludwig/schema/features/preprocessing/utils.py create mode 100644 ludwig/schema/features/preprocessing/vector.py create mode 100644 ludwig/schema/features/sequence_feature.py create mode 100644 ludwig/schema/features/set_feature.py create mode 100644 ludwig/schema/features/text_feature.py create mode 100644 ludwig/schema/features/timeseries_feature.py create mode 100644 ludwig/schema/features/utils.py create mode 100644 ludwig/schema/features/vector_feature.py create mode 100644 ludwig/schema/hyperopt/__init__.py create mode 100644 ludwig/schema/hyperopt/executor.py create mode 100644 ludwig/schema/hyperopt/parameter.py create mode 100644 ludwig/schema/hyperopt/scheduler.py create mode 100644 ludwig/schema/hyperopt/search_algorithm.py create mode 100644 ludwig/schema/hyperopt/utils.py create mode 100644 ludwig/schema/jsonschema.py create mode 100644 ludwig/schema/llms/__init__.py create mode 100644 ludwig/schema/llms/base_model.py create mode 100644 ludwig/schema/llms/generation.py create mode 100644 ludwig/schema/llms/model_parameters.py create mode 100644 ludwig/schema/llms/peft.py create mode 100644 ludwig/schema/llms/prompt.py create mode 100644 ludwig/schema/llms/quantization.py create mode 100644 ludwig/schema/lr_scheduler.py create mode 100644 ludwig/schema/metadata/__init__.py create mode 100644 ludwig/schema/metadata/configs/combiners.yaml create mode 100644 ludwig/schema/metadata/configs/common.yaml create mode 100644 ludwig/schema/metadata/configs/decoders.yaml create mode 100644 ludwig/schema/metadata/configs/encoders.yaml create mode 100644 ludwig/schema/metadata/configs/features.yaml create mode 100644 ludwig/schema/metadata/configs/llm.yaml create mode 100644 ludwig/schema/metadata/configs/loss.yaml create mode 100644 ludwig/schema/metadata/configs/optimizers.yaml create mode 100644 ludwig/schema/metadata/configs/preprocessing.yaml create mode 100644 ludwig/schema/metadata/configs/trainer.yaml create mode 100644 ludwig/schema/metadata/feature_metadata.py create mode 100644 ludwig/schema/metadata/parameter_metadata.py create mode 100644 ludwig/schema/model_config.py create mode 100644 ludwig/schema/model_types/__init__.py create mode 100644 ludwig/schema/model_types/base.py create mode 100644 ludwig/schema/model_types/ecd.py create mode 100644 ludwig/schema/model_types/llm.py create mode 100644 ludwig/schema/model_types/utils.py create mode 100644 ludwig/schema/optimizers.py create mode 100644 ludwig/schema/preprocessing.py create mode 100644 ludwig/schema/profiler.py create mode 100644 ludwig/schema/split.py create mode 100644 ludwig/schema/trainer.py create mode 100644 ludwig/schema/utils.py create mode 100644 ludwig/serve.py create mode 100644 ludwig/serve_kserve.py create mode 100644 ludwig/serve_ray_serve.py create mode 100644 ludwig/serve_v2.py create mode 100644 ludwig/serve_vllm.py create mode 100644 ludwig/train.py create mode 100644 ludwig/trainers/__init__.py create mode 100644 ludwig/trainers/base.py create mode 100644 ludwig/trainers/mixins.py create mode 100644 ludwig/trainers/registry.py create mode 100644 ludwig/trainers/trainer.py create mode 100644 ludwig/trainers/trainer_dpo.py create mode 100644 ludwig/trainers/trainer_llm.py create mode 100644 ludwig/types.py create mode 100644 ludwig/upload.py create mode 100644 ludwig/utils/__init__.py create mode 100644 ludwig/utils/algorithms_utils.py create mode 100644 ludwig/utils/audio_utils.py create mode 100644 ludwig/utils/augmentation_utils.py create mode 100644 ludwig/utils/automl/__init__.py create mode 100644 ludwig/utils/automl/data_source.py create mode 100644 ludwig/utils/automl/field_info.py create mode 100644 ludwig/utils/automl/ray_utils.py create mode 100644 ludwig/utils/automl/type_inference.py create mode 100644 ludwig/utils/automl/utils.py create mode 100644 ludwig/utils/backward_compatibility.py create mode 100644 ludwig/utils/batch_size_tuner.py create mode 100644 ludwig/utils/calibration.py create mode 100644 ludwig/utils/checkpoint_utils.py create mode 100644 ludwig/utils/config_utils.py create mode 100644 ludwig/utils/data_utils.py create mode 100644 ludwig/utils/dataframe_utils.py create mode 100644 ludwig/utils/dataset_quality.py create mode 100644 ludwig/utils/dataset_utils.py create mode 100644 ludwig/utils/date_utils.py create mode 100644 ludwig/utils/defaults.py create mode 100644 ludwig/utils/entmax/LICENSE create mode 100644 ludwig/utils/entmax/README.md create mode 100644 ludwig/utils/entmax/__init__.py create mode 100644 ludwig/utils/entmax/activations.py create mode 100644 ludwig/utils/entmax/losses.py create mode 100644 ludwig/utils/entmax/root_finding.py create mode 100644 ludwig/utils/error_handling_utils.py create mode 100644 ludwig/utils/eval_utils.py create mode 100644 ludwig/utils/fs_utils.py create mode 100644 ludwig/utils/h3_util.py create mode 100644 ludwig/utils/heuristics.py create mode 100644 ludwig/utils/hf_utils.py create mode 100644 ludwig/utils/html_utils.py create mode 100644 ludwig/utils/image_utils.py create mode 100644 ludwig/utils/llm_quantization_utils.py create mode 100644 ludwig/utils/llm_utils.py create mode 100644 ludwig/utils/logging_utils.py create mode 100644 ludwig/utils/loss_utils.py create mode 100644 ludwig/utils/math_utils.py create mode 100644 ludwig/utils/metric_utils.py create mode 100644 ludwig/utils/metrics_printed_table.py create mode 100644 ludwig/utils/misc_utils.py create mode 100644 ludwig/utils/model_card.py create mode 100644 ludwig/utils/model_export.py create mode 100644 ludwig/utils/model_soup.py create mode 100644 ludwig/utils/model_utils.py create mode 100644 ludwig/utils/nlp_utils.py create mode 100644 ludwig/utils/numerical_test_utils.py create mode 100644 ludwig/utils/output_feature_utils.py create mode 100644 ludwig/utils/package_utils.py create mode 100644 ludwig/utils/preference_data.py create mode 100644 ludwig/utils/print_utils.py create mode 100644 ludwig/utils/quantization.py create mode 100644 ludwig/utils/registry.py create mode 100644 ludwig/utils/sequence_packing.py create mode 100644 ludwig/utils/server_utils.py create mode 100644 ludwig/utils/state_dict_backward_compatibility.py create mode 100644 ludwig/utils/strings_utils.py create mode 100644 ludwig/utils/structural_warning.py create mode 100644 ludwig/utils/system_utils.py create mode 100644 ludwig/utils/time_utils.py create mode 100644 ludwig/utils/tokenizers.py create mode 100644 ludwig/utils/torch_utils.py create mode 100644 ludwig/utils/trainer_utils.py create mode 100644 ludwig/utils/training_report.py create mode 100644 ludwig/utils/types.py create mode 100644 ludwig/utils/upload_utils.py create mode 100644 ludwig/utils/version_transformation.py create mode 100644 ludwig/utils/visualization_utils.py create mode 100644 ludwig/vector_index/__init__.py create mode 100644 ludwig/vector_index/base.py create mode 100644 ludwig/vector_index/faiss.py create mode 100644 ludwig/visualize/__init__.py create mode 100644 ludwig/visualize/__main__.py create mode 100644 ludwig/visualize/_utils.py create mode 100644 ludwig/visualize/cli.py create mode 100644 ludwig/visualize/confusion.py create mode 100644 ludwig/visualize/curves.py create mode 100644 ludwig/visualize/hyperopt.py create mode 100644 ludwig/visualize/performance.py create mode 100644 ludwig/visualize/threshold.py create mode 100644 ludwig/visualize/training.py create mode 100644 notebooks/advanced_peft_adapters.ipynb create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 schemastore/README.md create mode 100644 schemastore/catalog-entry.json create mode 100644 schemastore/test/ludwig.yaml create mode 100644 schemastore/test/ludwig_config.yaml create mode 100644 scripts/benchmark_lazy_decode.py create mode 100644 scripts/benchmark_lazy_preprocessing.py create mode 100644 scripts/benchmark_results.json create mode 100644 scripts/benchmark_training_pipeline.py create mode 100644 scripts/dataset_smoke_test.py create mode 100644 scripts/hf_dataset_candidates.json create mode 100644 scripts/probe_hf_datasets.py create mode 100644 scripts/probe_results.json create mode 100644 scripts/smoke_results.json create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/docker-compose.yml create mode 100644 tests/integration_tests/__init__.py create mode 100644 tests/integration_tests/parameter_update_utils.py create mode 100644 tests/integration_tests/scripts/run_train_aim.py create mode 100644 tests/integration_tests/scripts/run_train_comet.py create mode 100644 tests/integration_tests/scripts/run_train_wandb.py create mode 100644 tests/integration_tests/synthetic_test_data.py create mode 100644 tests/integration_tests/test_api.py create mode 100644 tests/integration_tests/test_audio_feature.py create mode 100644 tests/integration_tests/test_automl.py create mode 100644 tests/integration_tests/test_cache_manager.py create mode 100644 tests/integration_tests/test_cached_preprocessing.py create mode 100644 tests/integration_tests/test_class_imbalance_feature.py create mode 100644 tests/integration_tests/test_cli.py create mode 100644 tests/integration_tests/test_collect.py create mode 100644 tests/integration_tests/test_config_global_defaults.py create mode 100644 tests/integration_tests/test_contrib_aim.py create mode 100644 tests/integration_tests/test_contrib_comet.py create mode 100644 tests/integration_tests/test_contrib_wandb.py create mode 100644 tests/integration_tests/test_custom_components.py create mode 100644 tests/integration_tests/test_date_feature.py create mode 100644 tests/integration_tests/test_dependencies.py create mode 100644 tests/integration_tests/test_experiment.py create mode 100644 tests/integration_tests/test_explain.py create mode 100644 tests/integration_tests/test_graph_execution.py create mode 100644 tests/integration_tests/test_hyperopt.py create mode 100644 tests/integration_tests/test_hyperopt_ray.py create mode 100644 tests/integration_tests/test_input_feature_tied.py create mode 100644 tests/integration_tests/test_kfold_cv.py create mode 100644 tests/integration_tests/test_llm.py create mode 100644 tests/integration_tests/test_missing_value_strategy.py create mode 100644 tests/integration_tests/test_mlflow.py create mode 100644 tests/integration_tests/test_model_save_and_load.py create mode 100644 tests/integration_tests/test_model_training_options.py create mode 100644 tests/integration_tests/test_number_feature.py create mode 100644 tests/integration_tests/test_peft.py create mode 100644 tests/integration_tests/test_postprocessing.py create mode 100644 tests/integration_tests/test_preprocessing.py create mode 100644 tests/integration_tests/test_ray.py create mode 100644 tests/integration_tests/test_reducers.py create mode 100644 tests/integration_tests/test_regularizers.py create mode 100644 tests/integration_tests/test_remote.py create mode 100644 tests/integration_tests/test_reproducibility.py create mode 100644 tests/integration_tests/test_sequence_decoders.py create mode 100644 tests/integration_tests/test_sequence_encoders.py create mode 100644 tests/integration_tests/test_sequence_features.py create mode 100644 tests/integration_tests/test_server.py create mode 100644 tests/integration_tests/test_simple_features.py create mode 100644 tests/integration_tests/test_timeseries_feature.py create mode 100644 tests/integration_tests/test_trainer.py create mode 100644 tests/integration_tests/test_triton_configs/transformer_combiner_with_attention_reduce.yaml create mode 100644 tests/integration_tests/test_visualization.py create mode 100644 tests/integration_tests/test_visualization_api.py create mode 100644 tests/integration_tests/utils.py create mode 100644 tests/ludwig/__init__.py create mode 100644 tests/ludwig/accounting/test_used_tokens.py create mode 100644 tests/ludwig/augmentation/test_augmentation_pipeline.py create mode 100644 tests/ludwig/augmentation/test_auto_augmentation.py create mode 100644 tests/ludwig/augmentation/test_image_augmentation.py create mode 100644 tests/ludwig/automl/__init__.py create mode 100644 tests/ludwig/automl/test_base_config.py create mode 100644 tests/ludwig/automl/test_config_enumerator.py create mode 100644 tests/ludwig/automl/test_config_sampler.py create mode 100644 tests/ludwig/automl/test_config_validator.py create mode 100644 tests/ludwig/automl/test_data_source.py create mode 100644 tests/ludwig/automl/test_target_detection.py create mode 100644 tests/ludwig/automl/test_tune_config.py create mode 100644 tests/ludwig/automl/test_utils.py create mode 100644 tests/ludwig/backend/__init__.py create mode 100644 tests/ludwig/backend/test_batch_infer_model.py create mode 100644 tests/ludwig/backend/test_ray.py create mode 100644 tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_1.yaml create mode 100644 tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_2.yaml create mode 100644 tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_3.yaml create mode 100644 tests/ludwig/benchmarking/example_files/process_config.py create mode 100644 tests/ludwig/benchmarking/example_files/valid/benchmarking_config_1.yaml create mode 100644 tests/ludwig/benchmarking/example_files/valid/benchmarking_config_2.yaml create mode 100644 tests/ludwig/benchmarking/example_files/valid/benchmarking_config_3.yaml create mode 100644 tests/ludwig/benchmarking/test_benchmarking.py create mode 100644 tests/ludwig/benchmarking/test_profiler.py create mode 100644 tests/ludwig/combiners/test_combiners.py create mode 100644 tests/ludwig/config_sampling/static_schema.json create mode 100644 tests/ludwig/config_sampling/test_config_sampling.py create mode 100644 tests/ludwig/config_validation/test_checks.py create mode 100644 tests/ludwig/config_validation/test_validate_config_combiner.py create mode 100644 tests/ludwig/config_validation/test_validate_config_encoder.py create mode 100644 tests/ludwig/config_validation/test_validate_config_features.py create mode 100644 tests/ludwig/config_validation/test_validate_config_hyperopt.py create mode 100644 tests/ludwig/config_validation/test_validate_config_misc.py create mode 100644 tests/ludwig/config_validation/test_validate_config_preprocessing.py create mode 100644 tests/ludwig/config_validation/test_validate_config_trainer.py create mode 100644 tests/ludwig/contrib/test_contrib.py create mode 100644 tests/ludwig/data/__init__.py create mode 100644 tests/ludwig/data/dataframe/test_dask.py create mode 100644 tests/ludwig/data/test_cache_util.py create mode 100644 tests/ludwig/data/test_dask_preprocessing.py create mode 100644 tests/ludwig/data/test_dataset_synthesizer.py create mode 100644 tests/ludwig/data/test_format_preprocessors.py create mode 100644 tests/ludwig/data/test_format_registry.py create mode 100644 tests/ludwig/data/test_lazy_cache.py create mode 100644 tests/ludwig/data/test_lazy_utils.py create mode 100644 tests/ludwig/data/test_multimodal_collator.py create mode 100644 tests/ludwig/data/test_negative_sampling.py create mode 100644 tests/ludwig/data/test_postprocessing.py create mode 100644 tests/ludwig/data/test_prefetch_batcher.py create mode 100644 tests/ludwig/data/test_preprocessing.py create mode 100644 tests/ludwig/data/test_preprocessing_progress.py create mode 100644 tests/ludwig/data/test_ray_data.py create mode 100644 tests/ludwig/data/test_split.py create mode 100644 tests/ludwig/data/test_split_utils.py create mode 100644 tests/ludwig/data/test_statistics.py create mode 100644 tests/ludwig/data/test_types.py create mode 100644 tests/ludwig/datasets/__init__.py create mode 100644 tests/ludwig/datasets/download_all_datasets.py create mode 100644 tests/ludwig/datasets/mnist/test_mnist_workflow.py create mode 100644 tests/ludwig/datasets/model_configs/train_all_model_configs.py create mode 100644 tests/ludwig/datasets/test_dataset_configs.py create mode 100644 tests/ludwig/datasets/test_dataset_links.py create mode 100644 tests/ludwig/datasets/test_datasets.py create mode 100644 tests/ludwig/datasets/test_model_configs.py create mode 100644 tests/ludwig/datasets/test_smoke_regression.py create mode 100644 tests/ludwig/datasets/titanic/test_titanic_workflow.py create mode 100644 tests/ludwig/decoders/test_image_decoder.py create mode 100644 tests/ludwig/decoders/test_llm_decoders.py create mode 100644 tests/ludwig/decoders/test_sequence_decoder.py create mode 100644 tests/ludwig/decoders/test_sequence_decoder_utils.py create mode 100644 tests/ludwig/decoders/test_sequence_tagger.py create mode 100644 tests/ludwig/distributed/__init__.py create mode 100644 tests/ludwig/distributed/test_accelerate_strategy.py create mode 100644 tests/ludwig/encoders/__init__.py create mode 100644 tests/ludwig/encoders/test_bag_encoders.py create mode 100644 tests/ludwig/encoders/test_category_encoders.py create mode 100644 tests/ludwig/encoders/test_date_encoders.py create mode 100644 tests/ludwig/encoders/test_generic_encoders.py create mode 100644 tests/ludwig/encoders/test_h3_encoders.py create mode 100644 tests/ludwig/encoders/test_image_encoders.py create mode 100644 tests/ludwig/encoders/test_llm_encoders.py create mode 100644 tests/ludwig/encoders/test_mamba_hybrid.py create mode 100644 tests/ludwig/encoders/test_number_encoders.py create mode 100644 tests/ludwig/encoders/test_phase_1_5.py create mode 100644 tests/ludwig/encoders/test_sequence_encoders.py create mode 100644 tests/ludwig/encoders/test_set_encoders.py create mode 100644 tests/ludwig/encoders/test_text_encoders.py create mode 100644 tests/ludwig/encoders/test_timm_encoder.py create mode 100644 tests/ludwig/evaluation/test_evaluation.py create mode 100644 tests/ludwig/explain/test_captum.py create mode 100644 tests/ludwig/explain/test_util.py create mode 100644 tests/ludwig/features/__init__.py create mode 100644 tests/ludwig/features/test_audio_feature.py create mode 100644 tests/ludwig/features/test_bag_feature.py create mode 100644 tests/ludwig/features/test_binary_feature.py create mode 100644 tests/ludwig/features/test_category_feature.py create mode 100644 tests/ludwig/features/test_date_feature.py create mode 100644 tests/ludwig/features/test_feature_utils.py create mode 100644 tests/ludwig/features/test_h3_feature.py create mode 100644 tests/ludwig/features/test_image_feature.py create mode 100644 tests/ludwig/features/test_number_feature.py create mode 100644 tests/ludwig/features/test_sequence_features.py create mode 100644 tests/ludwig/features/test_set_feature.py create mode 100644 tests/ludwig/features/test_text_feature.py create mode 100644 tests/ludwig/features/test_timeseries_feature.py create mode 100644 tests/ludwig/hyperopt/test_hyperopt.py create mode 100644 tests/ludwig/hyperopt/test_optuna_executor.py create mode 100644 tests/ludwig/hyperopt/test_search_space_generator.py create mode 100644 tests/ludwig/model_export/test_onnx_exporter.py create mode 100644 tests/ludwig/models/__init__.py create mode 100644 tests/ludwig/models/test_predictor.py create mode 100644 tests/ludwig/models/test_trainable_image_layers.py create mode 100644 tests/ludwig/models/test_training_determinism.py create mode 100644 tests/ludwig/models/test_training_success.py create mode 100644 tests/ludwig/modules/__init__.py create mode 100644 tests/ludwig/modules/test_attention.py create mode 100644 tests/ludwig/modules/test_batch_ensemble.py create mode 100644 tests/ludwig/modules/test_contrastive_alignment.py create mode 100644 tests/ludwig/modules/test_convolutional_modules.py create mode 100644 tests/ludwig/modules/test_dpo_loss.py create mode 100644 tests/ludwig/modules/test_embedding_modules.py create mode 100644 tests/ludwig/modules/test_encoder.py create mode 100644 tests/ludwig/modules/test_fully_connected_modules.py create mode 100644 tests/ludwig/modules/test_gated_cross_attention.py create mode 100644 tests/ludwig/modules/test_initializer_modules.py create mode 100644 tests/ludwig/modules/test_loss_balancing.py create mode 100644 tests/ludwig/modules/test_loss_modules.py create mode 100644 tests/ludwig/modules/test_lr_scheduler.py create mode 100644 tests/ludwig/modules/test_metric_modules.py create mode 100644 tests/ludwig/modules/test_mlp_mixer_modules.py create mode 100644 tests/ludwig/modules/test_modality_dropout.py create mode 100644 tests/ludwig/modules/test_normalization_modules.py create mode 100644 tests/ludwig/modules/test_pareto_mtl.py create mode 100644 tests/ludwig/modules/test_preference_losses.py create mode 100644 tests/ludwig/modules/test_recurrent_modules.py create mode 100644 tests/ludwig/modules/test_reduction_modules.py create mode 100644 tests/ludwig/modules/test_regex_freezing.py create mode 100644 tests/ludwig/modules/test_tabnet_modules.py create mode 100644 tests/ludwig/modules/test_utils.py create mode 100644 tests/ludwig/schema/hyperopt/test_scheduler.py create mode 100644 tests/ludwig/schema/hyperopt/test_search_algorithm.py create mode 100644 tests/ludwig/schema/test_model_config.py create mode 100644 tests/ludwig/schema/test_peft_adapters.py create mode 100644 tests/ludwig/schema/test_phase_6_5_tabular.py create mode 100644 tests/ludwig/schema/test_schema_utils.py create mode 100644 tests/ludwig/schema/test_timeseries_encoders.py create mode 100644 tests/ludwig/schema_fields/test_fields_misc.py create mode 100644 tests/ludwig/schema_fields/test_fields_optimization.py create mode 100644 tests/ludwig/schema_fields/test_fields_preprocessing.py create mode 100644 tests/ludwig/schema_fields/test_marshmallow_misc.py create mode 100644 tests/ludwig/test_api_unit.py create mode 100644 tests/ludwig/test_collect.py create mode 100644 tests/ludwig/test_config_generation.py create mode 100644 tests/ludwig/test_model_inspector.py create mode 100644 tests/ludwig/test_presets.py create mode 100644 tests/ludwig/test_serve_deployment_shims.py create mode 100644 tests/ludwig/test_serve_v2.py create mode 100644 tests/ludwig/trainers/__init__.py create mode 100644 tests/ludwig/trainers/test_dpo_trainers.py create mode 100644 tests/ludwig/trainers/test_mixins.py create mode 100644 tests/ludwig/trainers/test_trainer_helpers.py create mode 100644 tests/ludwig/utils/__init__.py create mode 100644 tests/ludwig/utils/automl/test_type_inference.py create mode 100644 tests/ludwig/utils/automl/test_utils.py create mode 100644 tests/ludwig/utils/entmax/test_losses.py create mode 100644 tests/ludwig/utils/entmax/test_mask.py create mode 100644 tests/ludwig/utils/entmax/test_root_finding.py create mode 100644 tests/ludwig/utils/entmax/test_topk.py create mode 100644 tests/ludwig/utils/test_algorithm_utils.py create mode 100644 tests/ludwig/utils/test_audio_utils.py create mode 100644 tests/ludwig/utils/test_backward_compatibility.py create mode 100644 tests/ludwig/utils/test_calibration.py create mode 100644 tests/ludwig/utils/test_class_balancing.py create mode 100644 tests/ludwig/utils/test_config_utils.py create mode 100644 tests/ludwig/utils/test_data_utils.py create mode 100644 tests/ludwig/utils/test_dataframe_utils.py create mode 100644 tests/ludwig/utils/test_dataset_quality.py create mode 100644 tests/ludwig/utils/test_dataset_utils.py create mode 100644 tests/ludwig/utils/test_date_utils.py create mode 100644 tests/ludwig/utils/test_defaults.py create mode 100644 tests/ludwig/utils/test_error_handling_utils.py create mode 100644 tests/ludwig/utils/test_errors.py create mode 100644 tests/ludwig/utils/test_fs_utils.py create mode 100644 tests/ludwig/utils/test_heuristics.py create mode 100644 tests/ludwig/utils/test_hf_utils.py create mode 100644 tests/ludwig/utils/test_hyperopt_ray_utils.py create mode 100644 tests/ludwig/utils/test_image_utils.py create mode 100644 tests/ludwig/utils/test_import_safety.py create mode 100644 tests/ludwig/utils/test_llm_utils.py create mode 100644 tests/ludwig/utils/test_metric_utils.py create mode 100644 tests/ludwig/utils/test_model_card.py create mode 100644 tests/ludwig/utils/test_model_export.py create mode 100644 tests/ludwig/utils/test_model_soup.py create mode 100644 tests/ludwig/utils/test_model_utils.py create mode 100644 tests/ludwig/utils/test_normalization.py create mode 100644 tests/ludwig/utils/test_numerical_test_utils.py create mode 100644 tests/ludwig/utils/test_output_feature_utils.py create mode 100644 tests/ludwig/utils/test_registry_v2.py create mode 100644 tests/ludwig/utils/test_sequence_packing.py create mode 100644 tests/ludwig/utils/test_server_utils.py create mode 100644 tests/ludwig/utils/test_state_dict_backward_compatibility.py create mode 100644 tests/ludwig/utils/test_strings_utils.py create mode 100644 tests/ludwig/utils/test_tokenizers.py create mode 100644 tests/ludwig/utils/test_torch_utils.py create mode 100644 tests/ludwig/utils/test_trainer_utils.py create mode 100644 tests/ludwig/utils/test_training_report.py create mode 100644 tests/ludwig/utils/test_upload_utils.py create mode 100644 tests/ludwig/utils/test_version_transformation.py create mode 100644 tests/regression_tests/automl/golden/adult_census_income.types.json create mode 100644 tests/regression_tests/automl/golden/mnist.types.json create mode 100644 tests/regression_tests/automl/scripts/update_golden_types.py create mode 100644 tests/regression_tests/automl/test_auto_type_inference.py create mode 100644 tests/regression_tests/automl/utils.py create mode 100644 tests/regression_tests/benchmark/configs/adult_census_income.ecd.yaml create mode 100644 tests/regression_tests/benchmark/configs/ames_housing.ecd.yaml create mode 100644 tests/regression_tests/benchmark/configs/mercedes_benz_greener.ecd.yaml create mode 100644 tests/regression_tests/benchmark/configs/sarcos.ecd.yaml create mode 100644 tests/regression_tests/benchmark/expected_metric.py create mode 100644 tests/regression_tests/benchmark/expected_metrics/adult_census_income.ecd.yaml create mode 100644 tests/regression_tests/benchmark/expected_metrics/ames_housing.ecd.yaml create mode 100644 tests/regression_tests/benchmark/expected_metrics/mercedes_benz_greener.ecd.yaml create mode 100644 tests/regression_tests/benchmark/expected_metrics/sarcos.ecd.yaml create mode 100644 tests/regression_tests/benchmark/test_model_performance.py create mode 100644 tests/regression_tests/model/test_old_models.py create mode 100644 tests/training_success/__init__.py create mode 100644 tests/training_success/configs.py create mode 100644 tests/training_success/test_training_success.py create mode 100644 tests/ultra_slow/__init__.py create mode 100644 tests/ultra_slow/test_ultra_slow.py create mode 100644 tests/ultra_slow/test_ultra_slow_peft.py diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..68c3d77 --- /dev/null +++ b/.actrc @@ -0,0 +1 @@ +-P ubuntu-latest=ludwigai/ludwig-ray diff --git a/.claude/commands/ludwig_review.md b/.claude/commands/ludwig_review.md new file mode 100644 index 0000000..4d8fa9f --- /dev/null +++ b/.claude/commands/ludwig_review.md @@ -0,0 +1,70 @@ +# Ludwig Codebase Review + +Perform a thorough, opinionated code review of the Ludwig codebase (or a specified subsystem if an argument is given). + +## Scope + +If $ARGUMENTS is provided, scope the review to that subsystem or file pattern (e.g. `ludwig/features/`, `data pipeline`, `ray backend`). +Otherwise review the entire codebase. + +## Review Dimensions + +Evaluate each area across ALL of the following axes: + +### Technical axes + +- **Code smells**: long methods, god objects, feature envy, primitive obsession, data clumps, shotgun surgery, dead code +- **Duplication**: copy-paste logic, structural duplication, near-duplicate classes that should share a base +- **Abstraction level**: too low (leaking internals), too high (over-engineered), mismatched levels within a single function +- **Naming**: violate "naming things" rules — misleading names, abbreviations, overly generic names (`utils`, `helper`, `Manager`), names that lie about what a thing does, names that describe implementation not intent +- **Type hints**: missing, incomplete, `Any`-abuse, wrong (e.g. `dict` where `dict[str, float]` is knowable) +- **Docstrings**: missing on public API, wrong (describe what not why), stale (describe removed behavior) +- **Test coverage**: untested public surface, tests that only test the happy path, tests that mock away the thing being tested, missing edge cases +- **Performance**: unnecessary copies, redundant I/O, blocking the event loop, O(N²) in disguise, missing caching +- **Consistency**: same concept named differently in different files, different patterns for the same operation, inconsistent error handling styles + +### Persona axes + +Rate severity from each perspective and explain why it matters to that audience: + +- **ML Engineer** (building production pipelines): Does this cause silent failures? Surprise OOMs? Hard-to-debug errors? Bad default choices? +- **ML Researcher** (running experiments): Is the config surface clear? Can they reproduce results? Do names match paper terminology? Is the API discoverable? +- **Open Source Contributor** (first PR): Is the code navigable? Is there a clear pattern to follow? Are there unexplained magic constants? Is test setup obvious? +- **Social Media ML Reader** (HN/Reddit/X): Would they call this "spaghetti"? Is there obvious NIH syndrome? Would they praise the architecture or cringe at it? + +## Output Format + +Structure the review as: + +### Executive Summary + +2-3 sentences on overall health and the single most important thing to fix. + +### Critical Issues (must fix) + +Numbered list. Each entry: file:line_range, what's wrong, why it matters, concrete fix. + +### Major Issues (should fix) + +Same format. Things that hurt quality but aren't blocking. + +### Minor Issues (nice to fix) + +Grouped by category (naming, type hints, docstrings, etc.). + +### Persona Verdicts + +One paragraph per persona with their honest take. + +### Improvement Plan + +Ordered list of PRs/tasks to address everything, with rough size estimate (S/M/L/XL). + +## Instructions + +- Be specific: always cite file paths and line numbers (or ranges) +- Be opinionated: don't hedge with "consider maybe possibly" +- Don't praise things that are merely adequate +- Distinguish between subjective style and objective bugs +- Focus on patterns, not one-off issues — if the same problem appears in 10 files, name the pattern once and give 3 examples +- Use the Explore subagent for broad searches, then Read for deep dives on critical files diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..0277352 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"733fdee3-4ca3-414d-9f48-1cfce3f2259d","pid":132110,"procStart":"14515772","acquiredAt":1779640200070} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9030888 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true + } +} diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..dbe6d2a --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,10 @@ +version = 1 + +test_patterns = [ + "tests/**" +] + +[[analyzers]] +name = "python" +enabled = true +runtime_version = "3.x.x" diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..758d071 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get install -y --no-install-recommends \ + git \ + build-essential \ + curl \ + libsndfile1 \ + ffmpeg \ + sox \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Create non-root user +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME + +USER $USERNAME +ENV PATH="/home/$USERNAME/.local/bin:$PATH" + +# Pre-install pip tools so editable install is fast +RUN pip install --user --no-cache-dir --upgrade pip setuptools wheel diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..56731a5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +{ + "name": "Ludwig Dev", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python" + }, + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ] + } + }, + "postCreateCommand": "pip install --user -e '.[test]'", + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/git:1": {} + } +} diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5301646 --- /dev/null +++ b/.flake8 @@ -0,0 +1,20 @@ +[flake8] +max-line-length = 120 +exclude = + .tox, + *.egg, + *_pb2.py, + build, + temp + +select = E,W,F +doctests = True +verbose = 2 +format = pylint +# E731: Do not assign a lambda expression, use a def +# W503: Line break occurred before a binary operator +# E203: whitespace before ':' +ignore = + E731, + W503, + E203 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..fbea11f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +1. Click on '....' +1. Scroll down to '....' +1. See error + +Please provide code, yaml config file and a sample of data in order to entirely reproduce the issue. +Issues that are not reproducible will be ignored. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment (please complete the following information):** + +- OS: [e.g. iOS] +- Version [e.g. 22] +- Python version +- Ludwig version + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4531012 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the use case** +A clear and concise description of what the use case for this feature is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..eabd847 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +# Code Pull Requests + +Please provide the following: + +- a clear explanation of what your code does +- if applicable, a reference to an issue +- a reproducible test for your PR (code, config and data sample) + +# Documentation Pull Requests + +Note that the documentation HTML files are in `docs/` while the Markdown sources are in `mkdocs/docs`. + +If you are proposing a modification to the documentation you should change only the Markdown files. + +`api.md` is automatically generated from the docstrings in the code, so if you want to change something in that file, first modify `ludwig/api.py` docstring, then run `mkdocs/code_docs_autogen.py`, which will create `mkdocs/docs/api.md` . diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..c558db3 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,86 @@ +name: docker + +on: + push: + tags: ["v*.*.*"] + workflow_dispatch: + inputs: + ludwig_version: + description: "PyPI version to install, e.g. 0.13.0 (leave empty to build from source)" + required: false + default: "" + latest: + description: "Also tag as :latest" + required: false + type: boolean + default: false + +jobs: + docker: + name: Build docker image ${{ matrix.docker-image }} + if: github.repository == 'ludwig-ai/ludwig' + runs-on: ubuntu-latest + + # cancel in-progress runs for the same branch/tag + concurrency: + group: docker-${{ matrix.docker-image }}-${{ inputs.ludwig_version || github.head_ref || github.sha }} + cancel-in-progress: true + + strategy: + fail-fast: false + matrix: + docker-image: + - ludwig + - ludwig-gpu + - ludwig-ray + - ludwig-ray-gpu + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Derive minor version tag + id: version + if: inputs.ludwig_version != '' + run: | + minor=$(echo "${{ inputs.ludwig_version }}" | cut -d. -f1-2) + echo "minor=${minor}" >> "$GITHUB_OUTPUT" + + - name: Docker meta + id: meta + uses: docker/metadata-action@v6 + with: + images: | + ludwigai/${{ matrix.docker-image }} + tags: | + # versioned dispatch: use explicit PyPI version tags + type=raw,value=${{ inputs.ludwig_version }},enable=${{ inputs.ludwig_version != '' }} + type=raw,value=${{ steps.version.outputs.minor }},enable=${{ inputs.ludwig_version != '' }} + type=raw,value=latest,enable=${{ inputs.ludwig_version != '' && inputs.latest }} + # tag push: derive tags from git ref + type=semver,pattern={{version}},enable=${{ inputs.ludwig_version == '' }} + type=semver,pattern={{major}}.{{minor}},enable=${{ inputs.ludwig_version == '' }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + file: ./docker/${{ matrix.docker-image }}/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + LUDWIG_VERSION=${{ inputs.ludwig_version }} diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..971f649 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,267 @@ +name: pytest + +on: + push: + branches: ["main", "release-*", "future-capabilities"] + pull_request: + branches: ["main", "release-*", "future-capabilities"] + +concurrency: + group: pytest-${{ github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + unit-tests: + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ secrets.LUDWIG_TESTS_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.LUDWIG_TESTS_AWS_SECRET_ACCESS_KEY }} + KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }} + KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }} + IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }} + UV_SYSTEM_PYTHON: "1" + + name: Unit Tests + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup Linux + run: | + sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev ffmpeg + + - name: uv cache + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-unit-${{ hashFiles('pyproject.toml', '.github/workflows/pytest.yml') }} + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: | + uv pip install torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 torchcodec --extra-index-url https://download.pytorch.org/whl/cpu + uv pip install '.[test]' + uv pip list + + - name: Unit Tests + run: | + RUN_PRIVATE=$IS_NOT_FORK LUDWIG_TEST_SUITE_TIMEOUT_S=5400 pytest -v --timeout 300 --durations 100 -m "not distributed and not slow and not combinatorial and not llm" --junitxml pytest.xml tests/ludwig + + - name: Regression Tests + run: | + RUN_PRIVATE=$IS_NOT_FORK LUDWIG_TEST_SUITE_TIMEOUT_S=5400 pytest -v --timeout 300 --durations 100 -m "not distributed and not slow and not combinatorial and not llm" --junitxml pytest-regression.xml tests/regression_tests + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Unit Test Results + path: pytest*.xml + + integration-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + test-markers: + - "integration_tests_a" + - "integration_tests_b" + - "integration_tests_c" + - "integration_tests_d" + - "integration_tests_e" + - "integration_tests_f" + - "integration_tests_g" + - "integration_tests_h" + - "integration_tests_i" + - "integration_tests_j" + + env: + AWS_ACCESS_KEY_ID: ${{ secrets.LUDWIG_TESTS_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.LUDWIG_TESTS_AWS_SECRET_ACCESS_KEY }} + KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }} + KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }} + IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }} + MARKERS: ${{ matrix.test-markers }} + UV_SYSTEM_PYTHON: "1" + MLFLOW_ALLOW_FILE_STORE: "true" + + name: Integration (${{ matrix.test-markers }}) + services: + minio: + image: fclairamb/minio-github-actions + env: + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + ports: + - 9000:9000 + + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup Linux + run: | + sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev ffmpeg + + - name: uv cache + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-integration-${{ hashFiles('pyproject.toml', '.github/workflows/pytest.yml') }} + + - name: Install dependencies + run: | + pip install uv + uv pip install torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 torchcodec --extra-index-url https://download.pytorch.org/whl/cpu + uv pip install '.[test]' + uv pip list + + - name: Free Disk Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: true + + - name: Integration Tests + run: | + RUN_PRIVATE=$IS_NOT_FORK LUDWIG_TEST_SUITE_TIMEOUT_S=7200 pytest -v --timeout 300 --durations 100 -m "not slow and not combinatorial and not llm and $MARKERS" --junitxml pytest.xml tests/integration_tests + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Integration Test Results (${{ matrix.test-markers }}) + path: pytest.xml + + distributed-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + distributed-group: + - "distributed_a" + - "distributed_b" + - "distributed_c" + - "distributed_d" + - "distributed_e" + - "distributed_f" + + env: + AWS_ACCESS_KEY_ID: ${{ secrets.LUDWIG_TESTS_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.LUDWIG_TESTS_AWS_SECRET_ACCESS_KEY }} + KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }} + KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }} + IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }} + DIST_GROUP: ${{ matrix.distributed-group }} + UV_SYSTEM_PYTHON: "1" + MLFLOW_ALLOW_FILE_STORE: "true" + + name: Distributed (${{ matrix.distributed-group }}) + services: + minio: + image: fclairamb/minio-github-actions + env: + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + ports: + - 9000:9000 + + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup Linux + run: | + sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev ffmpeg + + - name: uv cache + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-distributed-${{ hashFiles('pyproject.toml', '.github/workflows/pytest.yml') }} + + - name: Install dependencies + run: | + pip install uv + uv pip install torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 torchcodec --extra-index-url https://download.pytorch.org/whl/cpu + uv pip install '.[test]' + uv pip list + + - name: Free Disk Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: true + + - name: Distributed Tests + run: | + RUN_PRIVATE=$IS_NOT_FORK LUDWIG_TEST_SUITE_TIMEOUT_S=5400 pytest -v --timeout 300 --durations 100 -m "distributed and not slow and not combinatorial and not llm and $DIST_GROUP" --ignore=tests/integration_tests/test_server.py --junitxml pytest.xml tests/ludwig tests/integration_tests + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Distributed Test Results (${{ matrix.distributed-group }}) + path: pytest.xml + + test-minimal-install: + name: Minimal Install + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + UV_SYSTEM_PYTHON: "1" + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup Linux + run: | + sudo apt-get update && sudo apt-get install -y cmake libsndfile1 + + - name: Install dependencies + run: | + pip install uv + uv pip install torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 torchcodec --extra-index-url https://download.pytorch.org/whl/cpu + uv pip install . + uv pip list + + - name: Check Install + run: | + ludwig check_install + + event_file: + name: "Event File" + runs-on: ubuntu-latest + steps: + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: Event File + path: ${{ github.event_path }} diff --git a/.github/workflows/pytest_slow.yml b/.github/workflows/pytest_slow.yml new file mode 100644 index 0000000..5295535 --- /dev/null +++ b/.github/workflows/pytest_slow.yml @@ -0,0 +1,72 @@ +# This workflow will install Python dependencies and run all tests marked as `slow` on a single Python version. + +name: pytest (slow) + +on: + push: + branches: ["main", "release-*"] + +jobs: + slow-pytest: + name: py-slow + runs-on: ubuntu-latest + env: + # Use Minio credentials for all S3 operations in tests. + # PyArrow/Ray S3 clients use these env vars directly, so they must point to Minio. + AWS_ACCESS_KEY_ID: minio + AWS_SECRET_ACCESS_KEY: minio123 + AWS_ENDPOINT_URL: http://localhost:9000 + KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }} + KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }} + IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }} + MLFLOW_ALLOW_FILE_STORE: "true" + + services: + minio: + image: fclairamb/minio-github-actions + env: + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: minio123 + ports: + - 9000:9000 + + timeout-minutes: 150 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Setup Linux + if: runner.os == 'linux' + run: | + sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev ffmpeg + + - name: Install dependencies + run: | + python --version + pip --version + pip install uv + uv pip install --system torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 torchcodec --extra-index-url https://download.pytorch.org/whl/cpu + uv pip install --system '.[test]' + pip list + shell: bash + + - name: Create Minio test bucket + run: | + python -c " + import boto3 + s3 = boto3.client('s3', endpoint_url='http://localhost:9000', + aws_access_key_id='minio', aws_secret_access_key='minio123') + try: + s3.create_bucket(Bucket='ludwig-tests') + except s3.exceptions.BucketAlreadyOwnedByYou: + pass + " + shell: bash + + - name: Tests + run: | + RUN_PRIVATE=$IS_NOT_FORK LUDWIG_TEST_SUITE_TIMEOUT_S=7200 pytest -v --timeout 600 --durations 100 -m "slow" --junitxml pytest.xml tests/integration_tests/ diff --git a/.github/workflows/schema.yml b/.github/workflows/schema.yml new file mode 100644 index 0000000..0918c93 --- /dev/null +++ b/.github/workflows/schema.yml @@ -0,0 +1,73 @@ +name: Publish JSON Schema + +on: + push: + branches: [main] + paths: + - "ludwig/schema/**" + - "ludwig/config_validation/**" + - "ludwig/constants.py" + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish-schema: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Ludwig + run: pip install -e ".[test]" + + - name: Export schemas + run: | + mkdir -p schema-out + ludwig export_schema --model-type combined -o schema-out/ludwig-config.json + ludwig export_schema --model-type ecd -o schema-out/ludwig-config-ecd.json + ludwig export_schema --model-type llm -o schema-out/ludwig-config-llm.json + + - name: Generate index.html + run: | + cat > schema-out/index.html << 'HTMLEOF' + + + + + Ludwig JSON Schema + + + +

Ludwig JSON Schema

+

JSON Schema files for Ludwig config validation and IDE auto-complete.

+ +

Usage

+

Add to your Ludwig YAML config:

+
# yaml-language-server: $schema=https://ludwig-ai.github.io/schema/ludwig-config.json
+

Or see SchemaStore for automatic IDE integration.

+ + + HTMLEOF + + - name: Publish to ludwig-ai/schema + uses: cpina/github-action-push-to-another-repository@v1.7.2 + env: + SSH_DEPLOY_KEY: ${{ secrets.SCHEMA_REPO_DEPLOY_KEY }} + with: + source-directory: schema-out + destination-github-username: ludwig-ai + destination-repository-name: schema + target-branch: main + commit-message: "Update Ludwig JSON schema from ${{ github.sha }}" diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml new file mode 100644 index 0000000..c378c40 --- /dev/null +++ b/.github/workflows/test-results.yml @@ -0,0 +1,70 @@ +name: test results + +# Post a clean check-run summary per test suite after the pytest workflow finishes. +# +# Replaces the older EnricoMi/publish-unit-test-result-action bot that posted busy +# emoji-table PR comments accumulating across runs. dorny/test-reporter@v2 instead +# posts a single well-formatted check run per suite with a table of passed / failed +# tests, collapsible stack traces for failures, and source-line annotations. No +# PR-comment spam; the result lives in the checks UI and on the "Details" page. +# +# Runs inside a workflow_run triggered by pytest so the downloaded JUnit XML +# artifacts come with the write permissions needed to post check runs from +# forked PRs (where the triggering workflow itself is read-only). + +on: + workflow_run: + workflows: ["pytest"] + types: + - completed + +permissions: + contents: read + checks: write + actions: read + +jobs: + test-results: + name: Test Results + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion != 'skipped' + + steps: + - name: Unit Tests report + uses: dorny/test-reporter@v2 + with: + artifact: Unit Test Results + name: Unit Tests + path: "*.xml" + reporter: java-junit + fail-on-error: false + list-suites: failed + list-tests: failed + max-annotations: 50 + + - name: Integration Tests report + uses: dorny/test-reporter@v2 + with: + # Six matrix jobs upload artifacts named + # "Integration Test Results (integration_tests_a)" .. _f. Match them all with + # a regex and aggregate into a single report. + artifact: /Integration Test Results .*/ + name: Integration Tests + path: "*.xml" + reporter: java-junit + fail-on-error: false + list-suites: failed + list-tests: failed + max-annotations: 50 + + - name: Distributed Tests report + uses: dorny/test-reporter@v2 + with: + artifact: /Distributed Test Results .*/ + name: Distributed Tests + path: "*.xml" + reporter: java-junit + fail-on-error: false + list-suites: failed + list-tests: failed + max-annotations: 50 diff --git a/.github/workflows/upload-pypi.yml b/.github/workflows/upload-pypi.yml new file mode 100644 index 0000000..6252801 --- /dev/null +++ b/.github/workflows/upload-pypi.yml @@ -0,0 +1,36 @@ +name: Upload to PyPI + +on: + # Triggers the workflow when a release or draft of a release is published, + # or a pre-release is changed to a release + release: + types: [released] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + # Specifying a GitHub environment is optional, but strongly encouraged + environment: release + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: "recursive" + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build source distribution and wheel + run: | + pip install build + python -m build + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6f670f --- /dev/null +++ b/.gitignore @@ -0,0 +1,158 @@ +################### +# ludwig specific # +################### + +*.lock_preprocessing +.plans/ +results/ +ludwig/results/ +results_*/ +ludwig_arm64/ + +# ailabs-utils +ailabs_util +docker_assets + +# data +mnist_data/ +profile_images/ +./profile_images/ + +########### +# General # +########### + +# Mac stuff +.DS_Store + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +env* +build/ +develop-eggs/ +dist/ +downloads/ +./downloads/ +./dataset/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Data +*.csv +*.hdf5 +*.meta.json +*.parquet + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv* +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# PyCharm +.idea + +# ctags +tags + +# examples +examples/*/data/ +examples/*/visualizations/ + +# benchmarking configs +ludwig/benchmarking/configs/ + +# Aim tracking +.aim/ + +# Comet +.comet.config + +# Test-generated artifacts (image/audio features) +*.png +*.wav +generated_audio/ +generated_images/ + +# Claude Code worktrees (should never be committed) +.claude/worktrees/ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/.plan b/.plan new file mode 100644 index 0000000..d9045b9 --- /dev/null +++ b/.plan @@ -0,0 +1,315 @@ +# Ludwig Examples & Tutorials Expansion Plan + +## Context + +Two releases (0.12 → 0.13) added substantial new surface area. The table below maps every +significant new feature to its current documentation coverage and the example gap that needs +filling. The final section covers features in open PRs that are not yet merged. + +### Merged — what's in main right now + +| Feature | PR | Example gap | +|---------|----|-------------| +| Anomaly detection (Deep SVDD, SAD, DROCC) | #4109 | No runnable example, doc page only | +| Image decoders: SegFormer, FPN, configurable UNet | #4100 | `semantic_segmentation/` is stale | +| Focal, Dice, Lovász, NT-Xent, PolyLoss, Entmax losses | #4107 | Nothing | +| LLM logits extraction, structured output, constrained decoding | #4103 | Nothing | +| FastAPI: auto-schemas, Prometheus metrics, structured logging | #4101 | Nothing | +| New LR schedulers: OneCycleLR, inverse_sqrt, WSD, polynomial | #4106 | Nothing | +| New optimizers: RAdam, Adafactor, Schedule-Free AdamW, Muon, SOAP | #4105 | Nothing | +| MLPClassifier decoder, temperature calibration, MC Dropout | #4108 | Nothing | +| TransformerDecoder, scheduled sampling, beam search | #4102 | Nothing | +| CLIP, DINOv2, SigLIP pretrained image encoders | standalone | Nothing | +| RLHF alignment trainers: DPO, KTO, ORPO, GRPO | earlier | Config snippets only | +| vLLM serving backend | #4089 | Doc section only | +| Entropic Open-Set & Objectosphere losses | standalone | Standalone script, no doc tutorial | + +### Open PRs — not yet merged + +| Feature | PR | Branch | +|---------|----|----| +| LLM-driven config generation (`generate_config`) | #4092 | `future-capabilities` | +| HyperNetworkCombiner | #4092 | `future-capabilities` | +| Nash-MTL loss balancer | #4092 | `future-capabilities` | +| ModelInspector, trainer mixins, registry modernization | #4091 | `api-code-quality` | +| Native Optuna HPO executor | #4090 | `data-pipeline-hyperopt-modernization` | +| Typed feature metadata classes | #4090 | `data-pipeline-hyperopt-modernization` | + +--- + +## Tier 1 — High impact, completely uncharted (start here) + +### 1. Anomaly Detection Tutorial +**Source:** PR #4109 +**Files:** +- `examples/anomaly_detection/train_deep_svdd.py` +- `examples/anomaly_detection/train_deep_sad.py` +- `examples/anomaly_detection/train_drocc.py` +- `examples/anomaly_detection/config_deep_svdd.yaml` +- `examples/anomaly_detection/config_deep_sad.yaml` +- `examples/anomaly_detection/config_drocc.yaml` +- `examples/anomaly_detection/README.md` +- `ludwig-docs/docs/examples/anomaly_detection.md` + +**Content:** +- Synthetic sensor dataset (no download required), all three loss variants +- Score histogram plots showing separation between normal and anomalous samples +- Colab notebook: multimodal anomaly detection (tabular + text log messages) +- Guidance on choosing the threshold (95th percentile of training scores) + +--- + +### 2. Alignment / RLHF Tutorial +**Source:** alignment trainers (DPO, KTO, ORPO, GRPO) +**Files:** +- `examples/alignment/train_dpo.py` — Llama-3.1-8B, public preference dataset, 4-bit QLoRA +- `examples/alignment/train_kto.py` +- `examples/alignment/train_orpo.py` +- `examples/alignment/config_dpo.yaml` +- `examples/alignment/config_kto.yaml` +- `examples/alignment/config_orpo.yaml` +- `examples/alignment/README.md` +- `ludwig-docs/docs/examples/llm/alignment.md` (new) +- `ludwig-docs/docs/user_guide/llms/finetuning.md` (expand with end-to-end walkthrough) + +**Content:** +- DPO walkthrough from dataset prep to serving +- DPO vs KTO comparison on the same preference dataset +- When to use each method (table: data requirements, training cost, use case) +- GRPO section with a custom reward function example (separate entry below) + +--- + +### 3. vLLM Serving Tutorial +**Source:** PR #4089 / `--backend vllm` +**Files:** +- `examples/serve/vllm_serving.py` +- `examples/serve/vllm_client.py` +- `examples/serve/prometheus_monitoring/docker-compose.yml` +- `examples/serve/prometheus_monitoring/grafana_dashboard.json` +- `ludwig-docs/docs/user_guide/serving.md` (expand existing) + +**Content:** +- Model prep → launch → benchmark vs. default backend +- Latency/throughput comparison chart +- Multi-GPU launch with `--num_gpus N` +- OpenAI-compatible endpoint usage +- Prometheus scrape config + sample Grafana dashboard + +--- + +### 4. Structured / Constrained LLM Decoding +**Source:** PR #4103 +**Files:** +- `examples/llm_structured_output/entity_extraction.py` +- `examples/llm_structured_output/constrained_classification.py` +- `examples/llm_structured_output/config_json_schema.yaml` +- `examples/llm_structured_output/README.md` +- `ludwig-docs/docs/user_guide/llms/structured_output.md` (new) + +**Content:** +- Schema-constrained JSON generation (entity extraction → validated output) +- Regex-constrained token generation (forced classification labels) +- Logits extraction and custom post-processing +- Comparison: unconstrained vs. constrained output quality + +--- + +## Tier 2 — High impact, extends existing areas + +### 5. Open-Set Recognition — Full Tutorial +**Source:** Entropic Open-Set & Objectosphere losses (already in `examples/open_set_recognition/`) +**Files:** +- `examples/open_set_recognition/train_open_set_mnist.py` (add MNIST-based Colab notebook) +- `examples/open_set_recognition/open_set_recognition.ipynb` (Colab) +- `ludwig-docs/docs/examples/open_set_recognition.md` (new, add to examples index) + +**Content:** +- MNIST: known classes 0–7, unknown classes 8–9 +- Confidence histogram: CE vs. Entropic vs. Objectosphere +- Full Ludwig YAML + CLI walkthrough (not just raw PyTorch) +- Inference-time threshold selection from validation set +- Links to the standalone PyTorch script as a "how it works" explainer + +--- + +### 6. Uncertainty Quantification +**Source:** PR #4108 (MC Dropout + temperature scaling calibration) +**Files:** +- `examples/uncertainty/mc_dropout.py` +- `examples/uncertainty/temperature_calibration.py` +- `examples/uncertainty/config_mc_dropout.yaml` +- `examples/uncertainty/README.md` +- `ludwig-docs/docs/user_guide/uncertainty.md` (new) + +**Content:** +- Wine quality dataset (already in examples, no download) +- Calibration curves and reliability diagrams before/after temperature scaling +- MC Dropout: how many forward passes, variance as uncertainty proxy +- OOD detection: uncertainty on held-out distribution shift samples +- When to use each method + +--- + +### 7. Pretrained Image Encoders — CLIP / DINOv2 / SigLIP +**Source:** CLIP, DINOv2, SigLIP encoder integration +**Files:** +- `examples/image_encoders/compare_encoders.py` +- `examples/image_encoders/few_shot_dinov2.py` +- `examples/image_encoders/config_dinov2_linear_probe.yaml` +- `examples/image_encoders/config_clip.yaml` +- `examples/image_encoders/README.md` +- `ludwig-docs/docs/configuration/features/pretrained_image_encoders.md` (new) + +**Content:** +- Side-by-side: `stacked_cnn` vs. `dinov2` vs. `clip` on a small image classification task +- Linear probing pattern: `use_pretrained: true`, `trainable: false` +- Colab notebook: 5-shot image classification with DINOv2 (no fine-tuning) +- Performance vs. training time trade-off table + +--- + +### 8. Semantic Segmentation Refresh +**Source:** PR #4100 (SegFormer, FPN, configurable UNet depth) +**Files:** +- `examples/semantic_segmentation/config_segformer.yaml` (replace stale config) +- `examples/semantic_segmentation/config_fpn.yaml` +- `examples/semantic_segmentation/unet_depth_sweep.py` +- `examples/semantic_segmentation/README.md` (update) + +**Content:** +- SegFormer as the new recommended default +- FPN as the lightweight alternative +- UNet depth sweep showing mIoU vs. parameter count +- Update `docs/configuration/features/image_features.md` decoder section + +--- + +## Tier 3 — Narrower audience, pending PR features + +### 9. LLM-Driven Config Generation +**Source:** PR #4092 (`generate_config`) +*Wait for #4092 to merge before building.* +**Files:** +- `examples/llm_config_generation/generate_and_train.py` +- `ludwig-docs/docs/user_guide/llm_config_generation.md` (new) + +**Content:** +- Natural language task description → validated Ludwig config → train +- Show the validation step catching bad LLM suggestions +- Cover both Anthropic and OpenAI backends +- Tips for writing good task descriptions + +--- + +### 10. HyperNetworkCombiner Tutorial +**Source:** PR #4092 +*Wait for #4092 to merge before building.* +**Files:** +- `examples/hypernetwork/train_conditioned_model.py` +- `examples/hypernetwork/config_hypernetwork.yaml` +- `ludwig-docs/docs/configuration/combiners/hypernetwork.md` (new) + +**Content:** +- Multimodal dataset where one modality (e.g. sensor type) should *condition* processing of others +- Comparison: concat combiner vs. hypernetwork combiner on the same task +- Reference to HyperFusion paper (arXiv 2403.13319) + +--- + +### 11. Native Optuna HPO +**Source:** PR #4090 (`OptunaExecutor`) +*Wait for #4090 to merge before building.* +**Files:** +- `examples/hyperopt/optuna_executor.py` +- `examples/hyperopt/config_optuna.yaml` +- `ludwig-docs/docs/user_guide/hyperopt.md` (expand with Optuna section) + +**Content:** +- Drop-in replacement for Ray Tune executor +- Sampler comparison: TPE vs. CMA-ES vs. GPSampler +- Persistence with SQLite for resumable HPO runs +- Pruner (Hyperband) for early stopping of bad trials + +--- + +### 12. GRPO — Reward-Based Alignment +**Source:** GRPO trainer +**Files:** +- `examples/alignment/train_grpo.py` +- `examples/alignment/config_grpo.yaml` +- `ludwig-docs/docs/user_guide/llms/grpo.md` (new) + +**Content:** +- Custom reward function (response length + factuality check) +- When to use GRPO vs. DPO (no preference pairs needed) +- Group normalisation: why it stabilises training vs. vanilla RL + +--- + +### 13. Optimizer Guide +**Source:** PR #4105 (Schedule-Free AdamW, Muon, Adafactor) +**Files:** +- `examples/optimizers/optimizer_comparison.py` +- `ludwig-docs/docs/configuration/trainer.md` (add "Choosing an optimizer" section) + +**Content:** +- Training curve comparison on a standard benchmark (MNIST or wine quality) +- Schedule-Free AdamW: why no LR scheduler is needed +- Muon: weight-matrix-only updates, when beneficial +- Decision tree: Adam → AdamW → Schedule-Free → Muon/SOAP + +--- + +### 14. Nash-MTL Multi-Task Loss Balancing +**Source:** PR #4092 (Nash-MTL loss balancer) +*Wait for #4092 to merge before building.* +**Files:** +- `examples/multi_task/nash_mtl.py` +- `ludwig-docs/docs/user_guide/multi_task.md` (expand) + +**Content:** +- Multi-output model (classification + regression simultaneously) +- Comparison: fixed weights vs. FAMO vs. Nash-MTL loss balancing +- When Nash-MTL is worth the overhead vs. simpler methods + +--- + +## Execution order + +``` +Phase A (no GPU needed, self-contained): + 1. Anomaly detection tutorial + 5. Open-set recognition → full tutorial + 6. Uncertainty quantification + 13. Optimizer guide + +Phase B (needs GPU, builds on existing LLM work): + 2. Alignment / RLHF tutorial (DPO + KTO) + 3. vLLM serving tutorial + 4. Structured / constrained LLM decoding + 12. GRPO + +Phase C (vision, needs pretrained model weights): + 7. CLIP / DINOv2 / SigLIP encoders + 8. Semantic segmentation refresh + +Phase D (blocked on open PRs merging): + 9. LLM-driven config generation [blocked on #4092] + 10. HyperNetworkCombiner [blocked on #4092] + 14. Nash-MTL multi-task [blocked on #4092] + 11. Native Optuna HPO [blocked on #4090] +``` + +--- + +## Deliverable locations + +| Type | Repo | Path | +|------|------|------| +| Runnable scripts | `ludwig` | `examples//` | +| YAML configs | `ludwig` | `examples//config*.yaml` | +| Colab notebooks | `ludwig` | `examples//*.ipynb` | +| Doc tutorials | `ludwig-docs` | `docs/examples/.md` | +| User guide pages | `ludwig-docs` | `docs/user_guide/.md` | +| Config reference pages | `ludwig-docs` | `docs/configuration/**/.md` | diff --git a/.plan-lazy-preprocessing.md b/.plan-lazy-preprocessing.md new file mode 100644 index 0000000..c92718c --- /dev/null +++ b/.plan-lazy-preprocessing.md @@ -0,0 +1,320 @@ +# Ludwig Lazy / Streaming Preprocessing Plan + +## Problem Statement + +Ludwig's preprocessing pipeline is **not lazy**: before the training loop starts, it decodes and +transforms every sample in the dataset into in-memory NumPy arrays. This causes: + +1. **OOM on media datasets** — 1 000 images at original resolution × their tensor size saturates + RAM before a single batch reaches the GPU. +1. **Fixed preprocessing bottleneck** — transforms run single-threaded on the CPU ahead of training, + blocking the GPU. +1. **No online statistics** — normalization stats (mean/std) require a full pass that materialises + the entire dataset. +1. **Impossible streaming** — datasets that exceed RAM (or that arrive over a network) cannot be + used at all today. + +______________________________________________________________________ + +## How Other Frameworks Solve This + +### PyTorch / torchvision + +`Dataset.__getitem__` is the canonical lazy primitive. Each DataLoader worker calls it +independently, so only `batch_size × num_workers × prefetch_factor` samples are alive at once. +Transforms (resize, normalize, augment) are composed in the Dataset constructor and run inside +worker processes, overlapping with the GPU forward pass. + +Statistics (ImageNet mean/std) are pre-computed offline and shipped as constants. For custom +datasets a single Welford accumulation scan (O(1) memory) is standard. + +### TensorFlow `tf.data` + +Declarative lazy pipeline: `.map(fn, num_parallel_calls=AUTOTUNE)` → `.batch()` → +`.prefetch(AUTOTUNE)`. Nothing is executed until the training iterator pulls a batch. +`.cache()` optionally persists the decoded dataset to disk after the first epoch. + +### HuggingFace `datasets` + +Arrow IPC memory-map: only the pages actually accessed are loaded from disk. Audio/image columns +store raw bytes or file paths — decoded waveforms are produced per-sample at access time via +`cast_column("audio", Audio())`. Passing `decode=False` gives raw bytes with zero decoding cost. + +### MONAI (3D medical imaging) + +Lazy resampling accumulates sequential spatial transforms (rotate → crop → resize) as pending +homogeneous-matrix operations and fuses them into a single interpolation, eliminating intermediate +materialisation and reducing quality loss. + +### torchaudio + +Transforms (`MelSpectrogram`, `Resample`, `MFCC`) are `nn.Module` subclasses — composable, +JIT-scriptable, and GPU-movable. Applying them post-batch on GPU is 10-20× faster than per-sample +on CPU. + +______________________________________________________________________ + +## Design Principles for Ludwig's New Pipeline + +1. **Store paths, not arrays.** The Arrow dataset (or in-memory DataFrame) holds file paths / + metadata, never decoded tensors. Decode happens inside the DataLoader worker. +1. **Transforms as `nn.Module`.** All feature-specific transforms become PyTorch modules: + composable, testable, batchable, GPU-ready. +1. **Online statistics with Welford's algorithm.** Replace full-dataset decode-then-accumulate + with a single streaming pass that needs O(1) memory per feature. +1. **Prefetching and parallelism.** DataLoader `num_workers > 0` + `prefetch_factor` provides + CPU/GPU overlap for free once decode is inside `__getitem__`. +1. **Backward compatibility.** Existing configs that work today must continue to work. New + behaviour is opt-in at the feature level via `lazy: true` or automatic for audio/image features. + +______________________________________________________________________ + +## Implementation Phases + +______________________________________________________________________ + +### Phase 0 — Foundations (no user-facing change) + +**Goal:** establish shared infrastructure that later phases build on. + +#### 0-A. Welford online stats accumulator + +- Add `ludwig/data/statistics.py` with `WelfordAccumulator` (supports merge across shards). +- API: `acc.update(x: np.ndarray)`, `acc.result() → (mean, std, min, max, count)`, + `acc.merge(other)`. +- Unit-test with known distributions. + +#### 0-B. Feature transform protocol + +- Define `FeatureTransform(nn.Module)` base class in `ludwig/features/transforms.py`. +- Existing preprocessing logic stays unchanged for now; this just establishes the interface. +- `__call__(self, x: Tensor) → Tensor`; serialisable via `torch.save`. + +#### 0-C. DataLoader worker-safe file handle pool + +- Research whether Ludwig's existing multiprocessing setup needs `worker_init_fn` to reset + open file handles or RNG state (same issue as HuggingFace's `IterableDataset` shard split). +- Document the policy; no code change yet. + +**Exit criteria:** `WelfordAccumulator` tests pass; `FeatureTransform` ABC exists. + +______________________________________________________________________ + +### Phase 1 — Lazy Audio (highest OOM risk) + +**Goal:** audio columns decode inside the DataLoader worker, not during preprocessing. + +#### 1-A. `AudioFeature` path-only mode + +- After the preprocessing step, the Arrow/DataFrame column holds **file paths** (strings), not + decoded tensors. +- Remove `_process_in_memory` eager decode; replace with a lightweight path-validity check. +- The existing `read_audio_files_to_dict` path (used today) becomes a fallback for non-lazy mode. + +#### 1-B. `AudioDataset.__getitem__` decode + +- Create `ludwig/data/datasets/audio_dataset.py` (or extend `LudwigDataset`). +- In `__getitem__`, call `torchaudio.load(path)` → resample → pad/trim to target length. +- Apply `FeatureTransform` chain (mel spectrogram, normalization). + +#### 1-C. Online stats for audio normalization + +- Replace current two-pass (decode all → compute stats) with a streaming Welford scan: + `torchaudio.load` one file at a time, update accumulator, discard tensor. +- Triggered once at `prepare_data` time; stats stored in the feature config cache. + +#### 1-D. Integration test + +- Run `pytest tests/integration_tests/test_audio_feature.py` — no OOM on the medium-size + audio fixtures. +- Add a test that processes 10 000 short clips and checks RSS stays under 2 GB. + +**Exit criteria:** audio smoke tests pass on datasets with >10 h of audio; RSS bounded by +`batch_size × clip_length × sr × 4 bytes`. + +______________________________________________________________________ + +### Phase 2 — Lazy Image + +**Goal:** image columns store paths; decode + augment happens per-sample in DataLoader workers. + +#### 2-A. `ImageFeature` path-only mode + +- Preprocessing stores file paths (already partially the case for CSV/HDF5 workflows). +- Remove upfront `_process_in_memory` full-batch decode; keep only metadata inference + (height, width, channels) via a single sampled read. + +#### 2-B. `ImageDataset.__getitem__` decode + +- `PIL.Image.open(path).convert("RGB")` → `torchvision.transforms` pipeline. +- Apply `FeatureTransform` (resize, center crop, normalize). +- Multi-channel and TIFF support via existing `read_image_from_path` helper. + +#### 2-C. Online stats for image normalization + +- Welford accumulator over pixel values per channel; one forward pass, batch-level updates + (read image → reshape to (H×W, C) → `acc.update`). +- Offer `use_imagenet_stats: true` shortcut (ships precomputed constants) for the common case. + +#### 2-D. Augmentation support + +- `ImageFeature` config gains an optional `augmentation:` block (equivalent to + `torchvision.transforms.v2`). +- Augmentation runs inside `__getitem__` → free via DataLoader parallelism. + +**Exit criteria:** image smoke tests pass; openfake and synthia complete without OOM at +original resolution. + +______________________________________________________________________ + +### Phase 3 — Lazy Text / Tabular (lower priority) + +**Goal:** tokenisation and numerical transforms move into the DataLoader worker. + +#### 3-A. Text tokenisation in `__getitem__` + +- Currently: tokenise all text upfront into integer id arrays → store in HDF5. +- New: store raw strings; tokenise on-the-fly in `__getitem__`. +- Tradeoff: tokenisation is cheap but repeated; cache tokenised output to Arrow after first epoch + if `cache_encoder_embeddings: true`. + +#### 3-B. Numerical normalisation as `nn.Module` + +- `NormalizationTransform(mean, std)` replaces the current in-place column mutation. +- Stats computed once via Welford in `prepare_data`; transform applied in `__getitem__`. + +#### 3-C. Category embedding stays eager + +- Category integer encoding is O(1) per sample and zero-memory; no change needed. + +**Exit criteria:** existing tabular test suite passes unchanged. + +______________________________________________________________________ + +### Phase 4 — Streaming Dataset Source + +**Goal:** support HuggingFace streaming datasets (`streaming=True`) as a first-class source, +enabling datasets that exceed local disk space. + +#### 4-A. `HFStreamingDataset(IterableDataset)` + +- Wraps a `datasets.IterableDataset` in a PyTorch `IterableDataset`. +- Shard splitting: `worker_init_fn` slices the stream across DataLoader workers (HF provides + `dataset.shard(num_shards=N, index=i)`). +- Shuffle buffer: configurable per-dataset (small for media, large for text). + +#### 4-B. Online stats for streaming sources + +- Welford accumulator updated during the stats-scan pre-training pass (a full iteration over + the stream without gradient accumulation). +- Stats cached to disk keyed by dataset id + revision + split. + +#### 4-C. No-shuffle streaming mode + +- For datasets too large even for a shuffle buffer, expose `shuffle: false` (warn user). + +**Exit criteria:** `AudioSet`, `VoxPopuli`, `LibriSpeech` stream through without touching disk; +stats computed in one pass. + +______________________________________________________________________ + +### Phase 5 — MONAI-style Transform Fusion (stretch goal) + +**Goal:** fuse adjacent spatial transforms (resize → crop → normalize) into a single tensor +operation to eliminate intermediate materialisation. + +#### 5-A. Transform graph analysis + +- At `setup` time, walk the `FeatureTransform` chain; identify composable sequences. +- A sequence is composable when all transforms implement `as_matrix() → Tensor[4×4]`. + +#### 5-B. Single-pass spatial transform + +- Fuse the homogeneous matrices; apply via `F.grid_sample` once. +- Benchmark vs. sequential PIL transforms on ImageNet validation set. + +#### 5-C. Augmentation randomness + +- Random transforms (flip, rotation jitter) break deterministic fusion; keep them as + post-fusion `nn.Module` steps. + +**Exit criteria:** ≥ 15% wall-clock speedup on image-only training runs vs. Phase 2 baseline. + +______________________________________________________________________ + +## Data-Flow Diagram (target state) + +``` +Disk / HF Hub + │ + ▼ +┌──────────────────────────────────────────────────────┐ +│ prepare_data (single process, once per dataset) │ +│ │ +│ • Build Arrow metadata table (paths, labels, ids) │ +│ • Welford scan → normalization stats │ +│ • Write stats to feature config cache │ +└───────────────────────────┬──────────────────────────┘ + │ Arrow file / paths + ▼ +┌──────────────────────────────────────────────────────┐ +│ LudwigDataset.__getitem__ (per DataLoader worker) │ +│ │ +│ • Read path from Arrow row │ +│ • torchaudio.load / PIL.Image.open │ +│ • FeatureTransform chain (resize, mel spec, norm) │ +│ • Return sample dict of tensors │ +└───────────────────────────┬──────────────────────────┘ + │ batch of tensors (pinned RAM) + ▼ +┌──────────────────────────────────────────────────────┐ +│ GPU training loop │ +│ • H2D transfer (pin_memory → non-blocking) │ +│ • Forward pass │ +│ • Gradient accumulation │ +└──────────────────────────────────────────────────────┘ +``` + +______________________________________________________________________ + +## Key Files to Create / Modify + +| File | Action | +| ---------------------------------------------- | --------------------------------------------------- | +| `ludwig/data/statistics.py` | NEW — `WelfordAccumulator` | +| `ludwig/features/transforms.py` | NEW — `FeatureTransform` base + concrete transforms | +| `ludwig/features/audio_feature.py` | MODIFY — remove eager decode, add lazy path | +| `ludwig/features/image_feature.py` | MODIFY — remove eager decode, add lazy path | +| `ludwig/data/datasets/base_dataset.py` | MODIFY — add `__getitem__` decode hooks | +| `ludwig/data/datasets/audio_dataset.py` | NEW (or extend base) | +| `ludwig/data/datasets/image_dataset.py` | NEW (or extend base) | +| `ludwig/data/datasets/hf_streaming_dataset.py` | NEW — Phase 4 | +| `tests/unit/data/test_statistics.py` | NEW — Welford unit tests | +| `tests/integration_tests/test_lazy_audio.py` | NEW | +| `tests/integration_tests/test_lazy_image.py` | NEW | + +______________________________________________________________________ + +## Risks and Mitigations + +| Risk | Mitigation | +| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| DataLoader worker forking copies open file handles / CUDA contexts | `worker_init_fn` resets handles; delay CUDA init until after fork | +| Welford online stats diverge from current batch stats | Unit-test Welford against numpy batch stats on synthetic data | +| Per-sample disk I/O is slower than batched HDF5 read | Benchmark; add optional post-first-epoch disk cache for decoded tensors | +| Existing HDF5 preprocessing cache invalidated | Introduce cache version key; fall back to eager mode on cache miss | +| `IterableDataset` with shuffle buffer OOMs on large-media streams | Default shuffle buffer scales with declared feature size; audio → 500, image → 200, text → 10 000 | + +______________________________________________________________________ + +## Open Questions + +1. **HDF5 cache vs. Arrow:** Should the decoded-tensor cache (post-Phase 2) write Arrow or HDF5? + Arrow supports mmap natively; HDF5 has chunked access. Arrow preferred for new code. +1. **Normalization stat freshness:** If the dataset changes between runs, cached Welford stats + are stale. Hash the dataset id + revision + split into the cache key. +1. **Multi-output features:** When one column feeds multiple output features (e.g. image → + classification + detection), the decode must happen once. Ensure `LudwigDataset` returns a + shared tensor, not independent copies. +1. **Tokenisation caching:** Tokenising every sample every epoch wastes CPU. Cache to Arrow after + the first epoch under `${cache_dir}//tokenized/`. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..75ca3e8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,61 @@ +# Apply to all files without committing: +# pre-commit run --all-files +# Apply to changed files: +# pre-commit run +# Update this file: +# pre-commit autoupdate +# Run a specific hook: +# pre-commit run + +ci: + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] pre-commit suggestions" + autoupdate_schedule: weekly + +# Pin to the system Python version (3.14 post OS upgrade; previously 3.12). +# Note: pre-commit.ci hosted runners default to 3.14 which is now also local. +default_language_version: + python: python3.14 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-ast + - id: fix-byte-order-marker + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.14 + hooks: + - id: ruff + name: Lint and fix (ruff) + args: [--fix] + - id: ruff-format + name: Format code (ruff) + - repo: https://github.com/asottile/blacken-docs + rev: 1.20.0 + hooks: + - id: blacken-docs + args: [--line-length=120] + exclude: "IMPROVEMENT_PLAN.md" + - repo: https://github.com/hukkin/mdformat + rev: 1.0.0 + hooks: + - id: mdformat + additional_dependencies: + - mdformat-gfm==1.0.0 + - mdformat_frontmatter==2.0.10 + exclude: "CHANGELOG.md|IMPROVEMENT_PLAN.md" + - repo: https://github.com/yoheimuta/protolint + rev: v0.56.4 + hooks: + - id: protolint diff --git a/.protolint.yaml b/.protolint.yaml new file mode 100644 index 0000000..8016862 --- /dev/null +++ b/.protolint.yaml @@ -0,0 +1,53 @@ +# Adapted from +# https://github.com/yoheimuta/protolint/blob/master/_example/config/.protolint.yaml +--- +# Lint directives. +lint: + # Linter files to walk. + files: + # The specific files to exclude. + exclude: + # NOTE: UNIX paths will be properly accepted by both UNIX and Windows. + - ../proto/invalidFileName.proto + + # Linter rules. + # Run `protolint list` to see all available rules. + rules: + # Set the default to all linters. This option works the other way around as no_default does. + # If you want to enable this option, delete the comment out below and no_default. + # all_default: true + + # The specific linters to add. + add: + - FIELD_NAMES_LOWER_SNAKE_CASE + - MESSAGE_NAMES_UPPER_CAMEL_CASE + - MAX_LINE_LENGTH + - INDENT + - FIELD_NAMES_EXCLUDE_PREPOSITIONS + - FILE_NAMES_LOWER_SNAKE_CASE + - IMPORTS_SORTED + - PACKAGE_NAME_LOWER_CASE + - ORDER + - PROTO3_FIELDS_AVOID_REQUIRED + - PROTO3_GROUPS_AVOID + - REPEATED_FIELD_NAMES_PLURALIZED + - QUOTE_CONSISTENT + + # Linter rules option. + rules_option: + # MAX_LINE_LENGTH rule option. + max_line_length: + # Enforces a maximum line length + max_chars: 120 + # Specifies the character count for tab characters + tab_chars: 2 + + # FILE_NAMES_LOWER_SNAKE_CASE rule option. + file_names_lower_snake_case: + excludes: + - ../proto/invalidFileName.proto + + # QUOTE_CONSISTENT rule option. + quote_consistent: + # Available quote are "double" or "single". + quote: double diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bd803cd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.rulers": [ + 120 + ], + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + }, + "black-formatter.args": [ + "--line-length", + "120" + ], + "flake8.args": [ + "--config=setup.cfg" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": false, + "python.envFile": "${workspaceFolder}/.env" +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..2a1bea4 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Default code owners for the entire repository +* @w4nderlust diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..58efc53 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of conduct + +Ludwig adopts the [Linux Foundation code of conduct](https://lfprojects.org/policies/code-of-conduct/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9886ae8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,220 @@ +# Contributing + +Everyone is welcome to contribute, and we value everybody’s contribution. Code is thus not the only +way to help the community. Answering questions, helping others, reaching out and improving the +documentation are immensely valuable contributions as well. + +It also helps us if you spread the word: reference the library from blog posts on the awesome +projects it made possible, shout out on X every time it has helped you, or simply star the +repo to say "thank you". + +Check out the official [ludwig docs](https://ludwig-ai.github.io/ludwig-docs/) to get oriented +around the codebase, and join the community! + +## Open Issues + +Issues are listed at: + +If you would like to work on any of them, make sure it is not already assigned to someone else. + +You can self-assign it by commenting on the Issue page with one of the keywords: `#take` or +`#self-assign`. + +Work on your self-assigned issue and eventually create a Pull Request. + +## Creating Pull Requests + +1. Fork the [repository](https://github.com/ludwig-ai/ludwig) by clicking on the "Fork" button on + the repository's page. This creates a copy of the code under your GitHub user account. + +1. Clone your fork to your local disk, and add the base repository as a remote: + + ```bash + git clone git@github.com:/ludwig.git + cd ludwig + git remote add upstream https://github.com/ludwig-ai/ludwig.git + ``` + +1. Create a new branch to hold your development changes: + + ```bash + git checkout -b a-descriptive-name-for-my-changes + ``` + + *Do not*\* work on the `master` branch. + +1. Set up a development environment by running the following command in a virtual environment: + + ```bash + pip install -e . + ``` + + The above command will install the core dependencies in developer mode. If you would like to + be able to potentially make changes to the overall Ludwig codebase, then use the following command: + + ```bash + pip install -e .[full] + ``` + + Please note that in certain Shell environments (e.g., the `Z shell`), the dependencies in brackets have to be quoted: + + ```bash + pip install -e ."[full]" + ``` + + If you do not need access to the entire Ludwig codebase, but just want to be able to run `pytest` on the essential + functionality, then you would replace the above command with: + + ```bash + pip install -e .[test] + ``` + + (Please use `pip install -e ."[test]"` where your Shell environment requires quotes around the square brackets.) + + For the full list of the optional dependencies available in Ludwig, please see + [Installation Guide](https://ludwig.ai/latest/getting_started/installation/) and `pyproject.toml` in the root of the Ludwig + repository. + +1. On MacOS with Apple Silicon, if this installation approach runs into errors, you may need to install the following + prerequisites: + + ```bash + brew install cmake libomp + ``` + + This step requires `homebrew` to be installed on your development machine. + +1. Install and run `pre-commit`: + + ```bash + pip install pre-commit + pre-commit install + ``` + +1. Develop features on your branch. + +1. Format your code by running pre-commits so that your newly added files look nice: + + ```bash + pre-commit run + ``` + + Pre-commits also run automatically when committing. + +1. Once you're happy with your changes, make a commit to record your changes locally: + + ```bash + git add . + git commit + ``` + + It is a good idea to sync your copy of the code with the original repository regularly. This + way you can quickly account for changes: + + ```bash + git fetch upstream + git rebase upstream/master + ``` + + Push the changes to your account using: + + ```bash + git push -u origin a-descriptive-name-for-my-changes + ``` + +1. Once you are satisfied, go the webpage of your fork on GitHub. Click on "Pull request" to send + your contribution to the project maintainers for review. + +## Running Tests + +Ludwig's test suite lives in `tests/`. The main categories are: + +| Directory | What it covers | +| -------------------------- | ------------------------------------------------------- | +| `tests/ludwig/` | Unit tests for individual modules (fast, no GPU needed) | +| `tests/integration_tests/` | End-to-end training/prediction pipelines | +| `tests/regression_tests/` | Accuracy regression checks | + +**Run the full unit test suite:** + +```bash +pytest tests/ludwig/ +``` + +**Run a single test file:** + +```bash +pytest tests/ludwig/encoders/test_image_encoder.py -v +``` + +**Run integration tests (slow, uses GPUs if available):** + +```bash +pytest tests/integration_tests/ -v +``` + +**Run tests matching a keyword:** + +```bash +pytest tests/ -k "test_binary_feature" -v +``` + +Tests that require GPUs, Ray, or heavy optional dependencies are marked with pytest marks +(`@pytest.mark.slow`, `@pytest.mark.distributed`, etc.) and are automatically skipped in CI +unless the appropriate resources are available. + +## Codebase Overview + +A quick map of the most important modules: + +| Path | Purpose | +| ------------------------------ | --------------------------------------------------------------------------- | +| `ludwig/api.py` | `LudwigModel` — the main public API (train, predict, evaluate) | +| `ludwig/schema/` | Pydantic v2 config schema — `ModelConfig`, feature configs, trainer configs | +| `ludwig/features/` | Feature types: preprocessing, encoding, decoding, metrics | +| `ludwig/encoders/` | Encoder implementations (text, image, audio, tabular, …) | +| `ludwig/decoders/` | Decoder implementations | +| `ludwig/models/` | `ECD` (encoder–combiner–decoder) and `LLM` model classes | +| `ludwig/trainers/` | Trainer implementations (ECD, LLM fine-tune, inference-only) | +| `ludwig/data/preprocessing.py` | Tabular data loading and preprocessing pipeline | +| `ludwig/backend/` | Execution backends (`LocalBackend`, `RayBackend`) | +| `ludwig/utils/data_utils.py` | File format readers (CSV, Parquet, JSON, …) | +| `ludwig/collect.py` | CLI tools: collect activations, weights | + +**Key abstractions:** + +- `BaseFeature` → `InputFeature` / `OutputFeature` — every feature type inherits from one of these. +- `Encoder` / `Decoder` — registered via `@register_encoder` / `@register_decoder`; looked up by + `encoder.type` / `decoder.type` from config. +- `DataFormatPreprocessor` — dispatches file loading per format via a strategy (reader function). + `FileBasedPreprocessor(read_fn)` covers all tabular file types; `HDF5Preprocessor` and the + in-memory `DictPreprocessor` / `DataFramePreprocessor` are separate. +- `BackendCapabilities` — frozen dataclass of feature flags advertised by each backend. +- `InferenceOnlyTrainer` (config type `"none"`) — runs evaluation without training; used for + zero-shot / few-shot LLM inference. + +**Adding a new feature type:** + +1. Create `ludwig/features/_feature.py` with `FeatureMixin`, `InputFeature`, + and/or `OutputFeature`. +1. Create `ludwig/schema/features/_feature.py` with the Pydantic config classes. +1. Register them with `@register_input_feature("")` / `@register_output_feature("")`. +1. Add a preprocessing config class to `ludwig/schema/features/preprocessing/`. +1. Add tests in `tests/ludwig/features/test__feature.py`. + +## Other Tips + +- Add unit tests for any new code you write. +- Make sure tests pass. See the [Developer Guide](https://ludwig-ai.github.io/ludwig-docs/latest/developer_guide/style_guidelines_and_tests/) for more details. +- Keep `except Exception:` blocks narrow: use `except ImportError:` for optional imports and + `except pydantic.ValidationError:` for schema fallbacks. Broad exception swallowing makes + production bugs invisible. + +## Attribution + +This contributing guideline is adapted from `huggingface`, available at . + +## Code of Conduct + +Please be mindful of and adhere to the Linux Foundation's +[Code of Conduct](https://lfprojects.org/policies/code-of-conduct) when contributing to Ludwig. diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..436807d --- /dev/null +++ b/IMPROVEMENT_PLAN.md @@ -0,0 +1,302 @@ +# Ludwig Codebase Improvement Plan + +Generated: 2026-05-16. Based on a thorough review of the full codebase (110k lines, ~400 files). + +--- + +## Executive Summary + +Ludwig is **architecturally sound at the macro level** — the Backend abstraction, modular encoder/decoder registries, and schema-driven config system are genuinely well-designed. The problem is at the **meso level**: a handful of files have become god objects that grow without bound (`preprocessing.py` 2407 lines, `api.py` 2237 lines, `trainer.py` 1766 lines), there are 208 untyped `Any` fields hiding data contracts, 112 bare `except Exception:` handlers silencing failures, and 172 TODOs indicating unresolved design decisions. The single most impactful structural fix is decomposing the 18-class `DataFormatPreprocessor` hierarchy into a reader-strategy pattern (~900 lines of duplication removed). The single most impactful production fix is auditing the 112 bare `except Exception:` handlers. + +--- + +## Critical Issues (Must Fix) + +### C1 — `FatherPreprocessor` typo (`preprocessing.py:689`) +The Feather format preprocessor is named `FatherPregressor`. It is mapped to `FEATHER_FORMATS` at line 1149. Any contributor looking for the Feather reader would never find it. +- **Fix:** Rename to `FeatherPreprocessor` (class + format map). + +### C2 — Duplicate `keys()` on `TrainingStats` (`api.py:150,168`) +`TrainingStats.keys()` is defined twice. The second definition silently overrides the first; suppressed with `# noqa: F811`. Breaks the Mapping protocol contract. +- **Fix:** Delete lines 150-153 (the first definition). + +### C3 — 18 nearly-identical `DataFormatPreprocessor` subclasses (`preprocessing.py:186-1046`) +Every subclass (`DictPreprocessor`, `CSVPreprocessor`, `JSONPreprocessor`, `ParquetPreprocessor`, ...) implements `preprocess_for_training()`, `preprocess_for_prediction()`, `prepare_processed_data()` with bodies that differ only in the pandas read call. ~900 lines of structural duplication. +- **Fix:** See PR-3. + +### C4 — Silent failure on missing files (`backend/base.py:191-212`) +`LocalPreprocessingMixin.read_binary_files()` passes `None` paths to `map_fn` without error. Datasets silently drop samples at scale. +- **Fix:** Add `None` guard before calling `map_fn`; raise `ValueError` with path context. + +### C5 — 112 bare `except Exception:` handlers throughout codebase +Examples: `except Exception: return None` in `strings_utils.py:138`, `except Exception: print("FAILURE")` in `check.py:32`. Makes production debugging impossible. +- **Fix:** See PR-7. + +### C6 — OOM footgun in `collect_activations()` (`collect.py:186`) +Marked `# TODO -> Fix OOM on large models e.g. llama 3 8B`. Loads all activations for all samples into RAM at once. Will OOM on any LLM with >10k samples. +- **Fix:** Stream activations in chunks; write intermediate results to disk. + +--- + +## Major Issues (Should Fix) + +### M1 — `dict[str, Any]` type aliases hide all data contracts (`types.py:5-47`) +All public typedefs (`FeatureConfigDict`, `ModelConfigDict`, `TrainingSetMetadataDict`, etc.) are `dict[str, Any]`. 208 total `Any` usages. Disables IDE completion and static analysis. +- **Fix:** See PR-1. + +### M2 — Trainer is a 1766-line god object (`trainer.py:104-1766`) +`Trainer` inherits from 4 mixins + `BaseTrainer`. `__init__` has 24 parameters. `train_loop()` is ~240 lines mixing batching, backprop, gradient accumulation, metrics, checkpointing, early stopping. +- **Fix:** See PR-5, PR-6. + +### M3 — `LudwigModel` is a 2237-line god object (`api.py:160-2237`) +Mixes model lifecycle, serialization, experiment logging, hyperopt, and serving. +- **Fix:** See PR-8. + +### M4 — Feature preprocessing modules have no shared base +`_ImagePreprocessing`, `_AudioPreprocessing`, `_CategoryPreprocessing` etc. each re-implement missing value handling, dtype casting, reshaping with no common base class. +- **Fix:** See PR-4. + +### M5 — `create_passthrough_input_feature()` is an invisible feature type (`base_feature.py:648-703`) +Defines an inline class that bypasses the feature registry entirely. Undiscoverable. +- **Fix:** See PR-9. + +### M6 — 92 suppressed type errors (`# type: ignore`, `# noqa`) +Including bugs: `api.py:288` has `# type: ignore [assignment]` because `self.config_fp = None` when type expects `str` — that's an uninitialised-state bug. +- **Fix:** Audit all 92; fix root causes. + +### M7 — Test coverage gaps on critical paths +- `preprocessing.py` — no unit tests for individual `DataFormatPreprocessor` subclasses +- `backend/ray.py:816` — `BatchInferModel` inner class untested +- `trainers/trainer_dpo.py` — `KTOTrainer`, `ORPOTrainer`, `GRPOTrainer` have no unit tests +- `collect.py` — zero unit tests + +### M8 — Stale TODOs that are actually bugs +- `trainer.py:302` — "loading an existing model loses metric values" — known data loss on resume +- `models/base.py:147` — "Remove dummy implementation" — dummy property returns wrong values +- `api.py:1980` — model type check duplicated between LLM and ECD paths + +--- + +## Minor Issues + +### Naming (violating "naming things" rules) + +| File:Line | Problem | Fix | +|-----------|---------|-----| +| `preprocessing.py:689` | `FatherPreprocessor` — typo for Feather | `FeatherPreprocessor` | +| `backend/base.py:181` | `LocalPreprocessingMixin` — also handles binary reads | `LocalDataProcessingMixin` | +| `features/base_feature.py:57` | `BaseFeatureMixin` — vague, has state | `FeaturePreprocessingMixin` | +| `models/base.py:125` | `ModuleWrapper` — says nothing about purpose | `NonPropertyModuleWrapper` | +| `trainers/trainer_llm.py:44` | `NoneTrainer` — confusing name for inference-only | `InferenceOnlyTrainer` | +| `data/cache/manager.py:101` | `CacheManager` — generic Manager anti-pattern | `PreprocessedDataCache` | + +### Type Hints +- `backend/base.py:79` — `capabilities: dict[str, Any]` → `dict[str, bool]` +- `api.py:145-147` — `TrainingStats` fields → `dict[str, float]` +- `features/base_feature.py` — 12 methods missing return type annotations +- `encoders/text_encoders.py:184` — abstract method `get_hf_config_param_names` never enforced + +### Docstrings +- `features/base_feature.py:100` — `add_feature_data()` doesn't describe `proc_df` contract +- `models/ecd.py:145` — `forward()` doesn't explain `targets` in train vs. predict mode +- `data/preprocessing.py:143` — `DataFormatPreprocessor` has no docstring for the 3-method contract + +### Performance +- `preprocessing.py:207-214` — converts `dataset` to `pd.DataFrame` 3× in same function +- `utils/data_utils.py:452` — `hash_dict()` runs `pickle.dumps()` + SHA256 on every call; not cached +- `features/base_feature.py:178` — `create_sample_input()` generates random tensors every call + +### Magic Constants +- `data/lazy_utils.py:29` — `min(16, (os.cpu_count() or 4) + 4)` — why `+4`? why cap 16? Add comment. +- `features/image_feature.py:98-99` — ImageNet1K mean/std hardcoded instead of from torchvision + +### Dead Code +- `features/base_feature.py:648-703` — `create_passthrough_input_feature()` inline class factory +- `utils/visualization_utils.py` — 1568 lines of custom plotting duplicating pandas/plotly + +--- + +## Persona Verdicts + +### ML Engineer (Production Pipelines) +**HIGH RISK** — do not deploy without fixing C4, C5, C6. Silent failures in `read_binary_files()` (C4) silently shrink datasets at scale. 112 bare `except Exception:` (C5) mean production tracebacks are useless. The OOM footgun in `collect.py` (C6) hits every practitioner who runs feature analysis on an LLM. The preprocessing monolith (2407 lines, 18 classes) makes debugging data pipelines require holding an enormous mental model. The trainer mixes concerns so tightly that adding distributed debugging hooks requires touching 6 different mixins. + +### ML Researcher (Running Experiments) +**MODERATE** — good for prototyping, risky for long-running experiments. The declarative YAML config and pydantic schema validation are the right idea, well-executed. However: the known metric-loss-on-resume bug (M8, `trainer.py:302`) is a real reproducibility hazard for multi-day training runs. The HuggingFace encoder schema (`schema/encoders/text_encoders.py`: 2714 lines) is so large that understanding available params for a given model requires reading 100+ lines. Hyperopt is tightly coupled to trainer internals, making custom search spaces fragile across versions. + +### Open Source Contributor (First PR) +**UNWELCOMING** — needs a contributor guide and smaller files. Adding a new feature type requires understanding: (1) `BaseFeatureMixin` vs `InputFeature`/`OutputFeature` split, (2) the schema-vs-feature-module duality (every feature has a matching schema class in `ludwig/schema/features/`), (3) the inner preprocessing module pattern, (4) the encoder/decoder registry. None of this is documented in one place. Feature files are enormous (image: 1378 lines, 66 methods; audio: 675 lines). `create_passthrough_input_feature()` is an invisible feature type that bypasses the registry — a contributor following the normal pattern would never know it exists. + +### Social Media ML Reader (HN/Reddit/X) +**MIXED** — impressive scope, cringe-worthy internals. The feature set is genuinely impressive (600+ datasets, 50+ encoders, Ray distributed, LLM fine-tuning, multimodal). But: `preprocessing.py` has 18 classes with copy-pasted method bodies. `api.py` is 2237 lines. `FatherPreprocessor` (a Feather reader named "Father") has been in production. The `dict[str, Any]` typedefs look like hastily-migrated Python 2 code. 172 TODOs suggest active development paralysis. The architecture deserves better than the execution. + +--- + +## Improvement Plan (Ordered PRs) + +### Phase 0 — Quick Wins (1-2 days, zero risk) + +**PR-0a: Fix typos + silent bugs** (S) +- `FatherPreprocessor` → `FeatherPreprocessor` (`preprocessing.py:689,1149`) +- Remove duplicate `keys()` from `TrainingStats` (`api.py:150-153`) +- Add `None` guard in `read_binary_files()` (`backend/base.py:207`) + +**PR-0b: Fix TODO bugs** (S) +- Fix metric loss on resume (`trainer.py:302`) — reload metrics from checkpoint +- Remove fake `input_shape` dummy property (`models/base.py:147`) +- Fix `check.py:32` silent failure — add `logger.exception()` + +--- + +### Phase 1 — Type System (1 week) + +**PR-1: TypedDict for data contracts** (L) +Replace `dict[str, Any]` aliases in `types.py` with `TypedDict` subclasses. Update callsites. Run mypy; fix revealed type errors. +- `FeatureConfigDict`, `TrainingSetMetadataDict`, `FeatureMetadataDict` etc. +- Files: `types.py`, `api.py`, `features/base_feature.py`, `data/preprocessing.py` +- Impact: ~50 latent bugs caught by mypy; IDE completion for configs + +**PR-2: Backend capabilities as frozen dataclass** (S) +```python +@dataclass(frozen=True) +class BackendCapabilities: + distributed: bool = False + hyperopt: bool = False + async_execution: bool = False +``` +Replace all string-key capability lookups. +- Files: `backend/base.py`, `backend/ray.py`, `backend/local.py` + +--- + +### Phase 2 — Preprocessing Refactoring (1-2 weeks) + +**PR-3: Reader strategy pattern for `DataFormatPreprocessor`** (L) +Collapse 18 subclasses into one `DataFormatPreprocessor` with injected format reader: +```python +class DataFormatReader(ABC): + @abstractmethod + def read(self, path: str, **kwargs) -> pd.DataFrame: ... + +class CSVReader(DataFormatReader): + def read(self, path, **kwargs): return pd.read_csv(path, **kwargs) + +# One 5-line reader per format instead of one 50-line class per format +``` +`preprocessing.py` drops from 2407 → ~1000 lines. +- New package: `ludwig/data/readers/` +- Add unit tests per reader (normal, missing file, malformed, empty) + +**PR-4: Base preprocessing module** (M) +Extract shared logic (missing value handling, dtype casting, reshaping) into `BasePreprocessingModule`. Have all feature `_Preprocessing` inner classes inherit from it. +- Files: `features/base_feature.py`, `features/image_feature.py`, `features/audio_feature.py`, `features/category_feature.py` + +--- + +### Phase 3 — Trainer Modularization (1-2 weeks) + +**PR-5: Trainer composition over mixin inheritance** (L) +```python +# Before: class Trainer(CheckpointMixin, EarlyStoppingMixin, MetricsMixin, ProfilingMixin, BaseTrainer) +# After: +class Trainer(BaseTrainer): + def __init__(self, config, backend, + checkpointer: CheckpointService, + early_stopper: EarlyStoppingService, + metrics_collector: MetricsCollectionService, + profiler: ProfilingService | None = None): ... +``` +- `Trainer.__init__` shrinks from 24 params to 5 +- Each service is independently testable +- Files: `trainers/trainer.py`, `trainers/mixins.py` → `trainers/services/` + +**PR-6: Decompose `train_loop()`** (M) +Break 240-line method into `_forward_pass()`, `_backward_pass()`, `_update_metrics()`, `_maybe_checkpoint()`, `_maybe_early_stop()` — each <50 lines. +- Files: `trainers/trainer.py` + +--- + +### Phase 4 — Error Handling (1 week) + +**PR-7: Fix bare `except Exception:` handlers** (M) +112 instances. Replace with specific exception types + logging. At minimum: +- `check.py:32` — add `logger.exception()` +- `strings_utils.py:138` — replace `return None` with typed exception +- `image_utils.py:98` — replace `return None` with logged warning +- All handlers that discard errors silently in preprocessing/data loading paths + +**PR-7b: Fix `collect_activations()` OOM** (M) +Add chunked streaming with disk offload. `collect.py`. + +--- + +### Phase 5 — Structure & Dead Code (1 week) + +**PR-8: Split `LudwigModel` (`api.py`)** (XL) +``` +api.py (~600 lines) — train(), evaluate(), predict(), save(), load() only +experiment.py — run_experiment(), hyperopt integration +serve_v2.py — already partially extracted; complete it +explain.py — already partially extracted +``` + +**PR-9: Promote `PassthroughInputFeature`** (S) +Remove `create_passthrough_input_feature()` factory (`base_feature.py:648-703`). Create `features/passthrough_feature.py` with a proper class in the feature registry. + +**PR-10: Rename misleading identifiers** (S) +`NoneTrainer` → `InferenceOnlyTrainer`, `LocalPreprocessingMixin` → `LocalDataProcessingMixin`, `CacheManager` → `PreprocessedDataCache` + +--- + +### Phase 6 — Test Coverage (Ongoing) + +**PR-11: Unit tests for format readers** (M) +After PR-3: `tests/ludwig/data/readers/test_*.py` — normal read, missing file, malformed, empty. + +**PR-12: Unit tests for `collect.py`** (S) +Test with 2-layer model; verify output shapes; verify chunked mode. + +**PR-13: Unit tests for `BatchInferModel`** (S) +Test inner class at `backend/ray.py:816`. + +**PR-14: Unit tests for RLHF trainers** (M) +`KTOTrainer`, `ORPOTrainer`, `GRPOTrainer` — unit tests with tiny models, no GPU required. + +--- + +### Phase 7 — Documentation & Contributor Experience (1 week) + +**PR-15: Feature contributor guide** (S) +`docs/developer_guide/adding_a_feature_type.md` — schema class, feature module, mixin pattern, required vs optional methods, test template. + +**PR-16: Resolve all TODOs** (M) +Audit all 172. Fix bugs; convert design questions to GitHub issues; delete the comment. + +--- + +## Summary Table + +| PR | Description | Size | Priority | +|----|-------------|------|----------| +| PR-0a | Fix typos + silent bugs | S | P0 | +| PR-0b | Fix TODO bugs | S | P0 | +| PR-7 | Fix bare `except Exception:` | M | P0 | +| PR-7b | Fix `collect_activations()` OOM | M | P0 | +| PR-1 | TypedDict for data contracts | L | P0 | +| PR-3 | Reader strategy for format preprocessors | L | P0 | +| PR-2 | Backend capabilities dataclass | S | P1 | +| PR-4 | Base preprocessing module | M | P1 | +| PR-5 | Trainer composition over inheritance | L | P1 | +| PR-6 | Decompose `train_loop()` | M | P1 | +| PR-11 | Unit tests for format readers | M | P1 | +| PR-12 | Unit tests for `collect.py` | S | P1 | +| PR-13 | Unit tests for `BatchInferModel` | S | P1 | +| PR-14 | Unit tests for RLHF trainers | M | P1 | +| PR-8 | Split `LudwigModel` | XL | P2 | +| PR-9 | Promote `PassthroughInputFeature` | S | P2 | +| PR-10 | Rename misleading identifiers | S | P2 | +| PR-15 | Feature contributor guide | S | P2 | +| PR-16 | Resolve all TODOs | M | P2 | + +**Recommended start order:** PR-0a → PR-7 → PR-3 → PR-1 → PR-5 +**Total estimated effort:** 6-8 weeks of focused engineering (PRs within phases can be parallelized). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abd635e --- /dev/null +++ b/LICENSE @@ -0,0 +1,252 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------- + +Code in ludwig/api_annotations.py adapted from +https://github.com/ray-project/ray (Apache-2.0 License) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +Code in ludwig/utils/structural_warnings.py adapted from +https://github.com/ray-project/ray (Apache-2.0 License) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +Code in ludwig/utils/logging_utils.py adapted from +https://github.com/ray-project/ray (Apache-2.0 License) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..71f114a --- /dev/null +++ b/NOTICE @@ -0,0 +1,107 @@ +Ludwig includes derived work from TensorFlow(https://github.com/tensorflow/tensorflow) under the Apache License 2.0: + +Copyright 2016 The prometheus-operator Authors + + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + +http://www.apache.org/licenses/LICENSE-2.0 + + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. + + +The derived work can be found in the files: ludwig/models/modules/convolutional_modules.py + + +------ + +Ludwig includes derived work from Keras(https://github.com/keras-team/keras) under the MIT License: + + +COPYRIGHT + +All contributions by François Chollet: + +Copyright (c) 2015 - 2018, François Chollet. + +All rights reserved. + + +All contributions by Google: + +Copyright (c) 2015 - 2018, Google, Inc. + +All rights reserved. + + +All contributions by Microsoft: + +Copyright (c) 2017 - 2018, Microsoft, Inc. + +All rights reserved. + + +All other contributions: + +Copyright (c) 2015 - 2018, the respective contributors. + +All rights reserved. + + +Each contributor holds copyright over their respective contributions. + +The project versioning (Git) records all such contribution source information. + +LICENSE + +The MIT License (MIT) + + +Permission is hereby granted, free of charge, to any person obtaining a copy + +of this software and associated documentation files (the "Software"), to deal + +in the Software without restriction, including without limitation the rights + +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + +copies of the Software, and to permit persons to whom the Software is + +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all + +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + +SOFTWARE. + + +The derived work can be found in the files: mkdocs/code_docs_autogen.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..d68e43a --- /dev/null +++ b/README.md @@ -0,0 +1,353 @@ +

+ + + +

+ +
+ +**Declarative deep learning framework for LLMs, multimodal models, and tabular AI.** + +[![PyPI version](https://badge.fury.io/py/ludwig.svg)](https://badge.fury.io/py/ludwig) +[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/CBgdrGnZjy) +[![DockerHub](https://img.shields.io/docker/pulls/ludwigai/ludwig.svg)](https://hub.docker.com/r/ludwigai) +[![Downloads](https://pepy.tech/badge/ludwig)](https://pepy.tech/project/ludwig) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ludwig-ai/ludwig/blob/main/LICENSE) +[![X](https://img.shields.io/twitter/follow/ludwig_ai.svg?style=social&logo=twitter)](https://twitter.com/ludwig_ai) + +[**Docs**](https://ludwig.ai) · [**Getting Started**](https://ludwig.ai/latest/getting_started/) · [**Examples**](https://ludwig.ai/latest/examples) · [**Discord**](https://discord.gg/CBgdrGnZjy) + +
+ +______________________________________________________________________ + +## What is Ludwig? + +Ludwig is a **declarative deep learning framework** that lets you train, fine-tune, and deploy AI models — from LLM fine-tuning to tabular classification — using a YAML config file and zero boilerplate Python. + +```yaml +# Fine-tune Llama-3.1 with LoRA in one config file +model_type: llm +base_model: meta-llama/Llama-3.1-8B +adapter: + type: lora +trainer: + type: finetune + epochs: 3 +input_features: + - name: instruction + type: text +output_features: + - name: response + type: text +``` + +```bash +ludwig train --config model.yaml --dataset my_data.csv +``` + +**Tech stack:** Python 3.12 · PyTorch 2.7+ · Pydantic 2 · Transformers 5 · Ray 2.54 + +Ludwig is hosted by the [Linux Foundation AI & Data](https://lfaidata.foundation/). + +______________________________________________________________________ + +## What's New in Ludwig 0.16 + +| Feature | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **PatchTST & N-BEATS encoders** | State-of-the-art timeseries forecasting encoders with MASE/sMAPE metrics | +| **Advanced PEFT adapters** | PiSSA, EVA, CorDA/LoftQ initializers; TinyLoRA, OFT, HRA, WaveFT, LN-Tuning, VBLoRA, C3A adapter types | +| **VLM fine-tuning** | Train LLaVA, Qwen2-VL, InternVL via `is_multimodal: true` with gated cross-attention | +| **HyperNetwork combiner** | Conditioning-based feature fusion — one feature generates weights for others | +| **Nash-MTL & Pareto-MTL** | Game-theoretic and preference-based multi-task loss balancing | +| **LLM config generation** | `ludwig generate_config "describe your task"` — LLM writes the YAML for you | +| **ModelInspector** | Architecture analysis, weight collection, feature importance proxy | +| **Ray Serve & KServe** | Distributed and Kubernetes-native model deployment shims | +| **GRPO alignment** | Reward-model-free RLHF via Group Relative Policy Optimization | +| **torchao quantization + QAT** | PyTorch-native `int4/int8/float8` with Quantization-Aware Training | +| **Multi-adapter PEFT** | Multiple named LoRA adapters with weighted merging (TIES, DARE, SVD) | +| **Native Optuna executor** | GPT/TPE/CMA-ES samplers, pruning, resumable SQLite/PostgreSQL storage | +| **Timeseries forecasting** | `model.forecast(dataset, horizon=N)` API with `TimeseriesOutputFeature` | +| **Muon & ScheduleFreeAdamW** | New optimizers for large-scale pretraining and fine-tuning | +| **Image segmentation decoders** | UNet, SegFormer, FPN decoders for semantic segmentation | + +______________________________________________________________________ + +## Installation + +```bash +pip install ludwig # core +pip install ludwig[full] # all optional dependencies +pip install ludwig[llm] # LLM fine-tuning only +``` + +Requires Python 3.12+. See [contributing](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md) for a full dependency matrix. + +______________________________________________________________________ + +## Quick Start + +### Fine-tune an LLM (instruction tuning) + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1c3AO8l_H6V_x37RwQ8V7M6A-RmcBf2tG?usp=sharing) + +Ludwig supports the full LLM fine-tuning spectrum: + +| Technique | Config key | +| --------------------------------- | ------------------------------------------------------------------------ | +| Supervised fine-tuning (SFT) | `trainer.type: finetune` | +| DPO / KTO / ORPO / GRPO alignment | `trainer.type: dpo` (or `kto`, `orpo`, `grpo`) | +| LoRA / DoRA / VeRA / PiSSA | `adapter.type: lora` (or `dora`, `vera`, `lora` + `init_weights: pissa`) | +| 4-bit QLoRA (bitsandbytes) | `quantization.bits: 4` | +| torchao + QAT | `quantization.backend: torchao` | +| Multi-adapter with merging | `adapters:` dict + `merge:` block | +| VLM (vision-language) | `is_multimodal: true` | + +```yaml +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +quantization: + bits: 4 + +adapter: + type: lora + +prompt: + template: | + ### Instruction: {instruction} + ### Input: {input} + ### Response: + +input_features: + - name: prompt + type: text + +output_features: + - name: output + type: text + +trainer: + type: finetune + learning_rate: 0.0001 + batch_size: 1 + gradient_accumulation_steps: 16 + epochs: 3 + learning_rate_scheduler: + decay: cosine + warmup_fraction: 0.01 + +backend: + type: local +``` + +```bash +export HUGGING_FACE_HUB_TOKEN="" +ludwig train --config model.yaml --dataset "ludwig://alpaca" +``` + +### Train a multimodal classifier + +```yaml +input_features: + - name: review_text + type: text + encoder: + type: bert + - name: star_rating + type: number + - name: product_image + type: image + encoder: + type: dinov2 + +output_features: + - name: recommended + type: binary +``` + +```bash +ludwig train --config model.yaml --dataset reviews.csv +``` + +### Generate a config from natural language + +```bash +ludwig generate_config "I have a CSV with age, income, education level, and I want to predict loan default" +``` + +### Make predictions + +```bash +ludwig predict --model_path results/experiment_run/model --dataset new_data.csv +``` + +### Launch a REST API + +```bash +ludwig serve --model_path results/experiment_run/model +# POST http://localhost:8000/predict +``` + +______________________________________________________________________ + +## Capabilities + +
+LLM Fine-Tuning + +- **Supervised fine-tuning (SFT)** on instruction/response pairs +- **Alignment training**: DPO, KTO, ORPO, GRPO (reward-model-free RLHF) +- **PEFT adapters**: LoRA, DoRA, VeRA, LoRA+, TinyLoRA, OFT, HRA, WaveFT, LN-Tuning, VBLoRA, C3A +- **LoRA initializers**: PiSSA, EVA, CorDA, LoftQ for improved convergence +- **Multi-adapter PEFT**: multiple named adapters on one base model, switchable at runtime; merge with TIES, DARE, SVD, magnitude pruning +- **Quantization**: 4-bit/8-bit QLoRA (bitsandbytes), torchao int4/int8/float8 with QAT +- **VLM fine-tuning**: LLaVA, Qwen2-VL, InternVL via `is_multimodal: true` +- **Sequence packing** for efficient training on variable-length inputs +- **Paged and 8-bit optimizers** for memory-efficient training + +
+ +
+Multimodal & Tabular Models + +- **Input modalities**: text, numbers, categories, binary, sets, bags, sequences, images, audio, timeseries, vectors, dates +- **Text encoders**: any HuggingFace Transformer (BERT, RoBERTa, ModernBERT, Qwen3, Llama-3.1, etc.), plus Mamba-2, Jamba +- **Image encoders**: DINOv2, ConvNeXt, EfficientNet, ViT, CAFormer, ConvFormer, PoolFormer, TIMM (1000+ models) +- **Timeseries encoders**: PatchTST, N-BEATS, CNN, RNN, Transformer; MASE and sMAPE metrics; `model.forecast()` API +- **Combiners**: concat, transformer, tab_transformer, FT-Transformer, TabNet, TabPFN v2, HyperNetwork, ProjectAggregate, GatedFusion, Perceiver +- **Multi-task learning**: multiple output features in a single model; Nash-MTL, Pareto-MTL, FAMO, GradNorm, uncertainty loss balancing +- **Image segmentation**: UNet, SegFormer, FPN decoders + +
+ +
+Training Infrastructure + +- **Distributed training**: HuggingFace Accelerate with DDP, FSDP, DeepSpeed (zero-code changes) +- **Ray backend**: training across a Ray cluster, larger-than-memory datasets via Ray Data +- **Automatic batch size selection** and learning rate range test +- **Mixed precision** (fp16/bf16), gradient checkpointing, gradient accumulation +- **Optimizers**: AdamW, Adafactor, SGD, Muon, ScheduleFreeAdamW, Lion, paged/8-bit variants +- **Learning rate schedulers**: cosine, linear, polynomial, reduce-on-plateau, OneCycleLR +- **Model Soup**: uniform and greedy checkpoint averaging for better generalization at zero inference cost +- **Modality dropout** for robust multimodal models + +
+ +
+Hyperparameter Optimization + +- **Executors**: Ray Tune (ASHA, PBT, Bayesian) and native Optuna (auto/GP/TPE/CMA-ES) +- **Optuna persistence**: SQLite or PostgreSQL for resumable HPO runs +- **Pruning** with Optuna's MedianPruner and HyperbandPruner +- **Search spaces**: uniform, log-uniform, choice, randint, quantized +- **Full Ludwig config** is searchable — any nested parameter can be a hyperparameter + +
+ +
+Production & Deployment + +- **REST API**: FastAPI server with Prometheus metrics and structured logging (`ludwig serve`) +- **vLLM serving**: OpenAI-compatible API with PagedAttention and continuous batching +- **Ray Serve**: distributed deployment with auto-scaling and traffic splitting +- **KServe**: Kubernetes-native deployment with Open Inference Protocol v2 +- **Model export**: SafeTensors (default), `torch.export` `.pt2` bundles, ONNX +- **HuggingFace Hub**: `ludwig upload hf_hub` — push model + auto-generated model card +- **Docker**: prebuilt containers at [ludwigai/ludwig](https://hub.docker.com/u/ludwigai) + +
+ +
+Tooling & Integrations + +- **Experiment tracking**: TensorBoard, Weights & Biases, Comet ML, MLflow, Aim Stack +- **Model inspection**: `ModelInspector` — weight enumeration, architecture summary, feature importance proxy +- **Visualizations**: learning curves, confusion matrices, calibration plots, ROC curves, hyperopt analysis +- **AutoML**: `ludwig.automl.auto_train()` — give it a dataset and a time budget; the YAML-driven search space samples encoder/combiner/decoder combinations and validates them before training +- **Dataset quality checks**: `from ludwig.utils.dataset_quality import check_dataset_quality` — validates a DataFrame before training (missing values, class imbalance, near-duplicate columns, ID leakage, …) +- **OpenML integration**: load any OpenML task directly — `OpenMLLoader` fetches by task ID and caches locally as Parquet +- **LLM config generation**: `ludwig generate_config "describe your task"` — LLM writes the YAML +- **K-fold cross-validation**: `ludwig experiment --k_fold N` +- **Dataset Zoo**: 70+ built-in benchmark datasets (`ludwig://mnist`, `ludwig://alpaca`, …) + +
+ +______________________________________________________________________ + +## Examples + +### LLM & Alignment + +| Use Case | Link | +| ------------------------------------- | ----------------------------------------------------------------------------------- | +| LLM instruction tuning (LoRA + QLoRA) | [examples/llm](https://ludwig.ai/latest/examples/llm/llm_finetuning) | +| DPO / GRPO alignment | [examples/llm/alignment](https://ludwig.ai/latest/examples/llm/alignment) | +| Advanced PEFT (PiSSA, OFT, VBLoRA, …) | [examples/llms/peft_advanced](https://ludwig.ai/latest/examples/llms/peft_advanced) | +| VLM fine-tuning (LLaVA, Qwen2-VL) | [examples/vlm](https://github.com/ludwig-ai/ludwig/tree/main/examples/vlm) | + +### Tabular & Multimodal + +| Use Case | Link | +| -------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Binary classification (Titanic) | [examples/titanic](https://ludwig.ai/latest/examples/titanic) | +| Tabular classification (census income) | [examples/adult_census_income](https://ludwig.ai/latest/examples/adult_census_income) | +| Multimodal classification | [examples/multimodal_classification](https://ludwig.ai/latest/examples/multimodal_classification) | +| Multi-task learning | [examples/multi_task](https://ludwig.ai/latest/examples/multi_task) | + +### Timeseries & Vision + +| Use Case | Link | +| ------------------------------------------ | ----------------------------------------------------------------------------------------- | +| Timeseries forecasting (PatchTST, N-BEATS) | [examples/forecasting](https://ludwig.ai/latest/examples/forecasting) | +| Weather forecasting | [examples/weather](https://ludwig.ai/latest/examples/weather) | +| Image classification (MNIST) | [examples/mnist](https://ludwig.ai/latest/examples/mnist) | +| Semantic segmentation | [examples/semantic_segmentation](https://ludwig.ai/latest/examples/semantic_segmentation) | + +### NLP & Audio + +| Use Case | Link | +| ------------------------ | --------------------------------------------------------------------------------------- | +| Text classification | [examples/text_classification](https://ludwig.ai/latest/examples/text_classification) | +| Named entity recognition | [examples/ner_tagging](https://ludwig.ai/latest/examples/ner_tagging) | +| Machine translation | [examples/machine_translation](https://ludwig.ai/latest/examples/machine_translation) | +| Speech recognition | [examples/speech_recognition](https://ludwig.ai/latest/examples/speech_recognition) | +| Speaker verification | [examples/speaker_verification](https://ludwig.ai/latest/examples/speaker_verification) | + +______________________________________________________________________ + +## Why Ludwig? + +- **Zero boilerplate** — no training loop, no data pipeline, no evaluation code. The YAML config is the entire program. +- **Best-in-class LLM support** — full spectrum from LoRA to GRPO alignment, torchao QAT, and VLM fine-tuning, all in config. +- **Multimodal out of the box** — mix text, images, numbers, audio, and timeseries with one config change. +- **Scale without code changes** — go from laptop → multi-GPU → Ray cluster by changing `backend.type`. +- **Expert control when you need it** — every activation function, scheduler, and optimizer is configurable. +- **Reproducible research** — every run is logged and the full config is saved. Compare experiments with `ludwig visualize`. + +______________________________________________________________________ + +## Publications + +- [Ludwig: A Type-Based Declarative Deep Learning Toolbox](https://arxiv.org/pdf/1909.07930.pdf) (2019) +- [Declarative Machine Learning Systems](https://arxiv.org/pdf/2107.08148.pdf) (2021) +- [Ludwig's State-of-the-Art Benchmarks](https://openreview.net/pdf?id=hwjnu6qW7E4) + +______________________________________________________________________ + +## Community + +[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/CBgdrGnZjy) + +- [Discord](https://discord.gg/CBgdrGnZjy) — ask questions, share what you've built +- [GitHub Issues](https://github.com/ludwig-ai/ludwig/issues) — bugs and feature requests +- [X / Twitter](https://twitter.com/ludwig_ai) — announcements +- [Medium](https://medium.com/ludwig-ai) — tutorials and deep-dives + + + + + +[![Star History Chart](https://api.star-history.com/svg?repos=ludwig-ai/ludwig&type=Date)](https://star-history.com/#ludwig-ai/ludwig&Date) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..ffe5569 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`ludwig-ai/ludwig` +- 原始仓库:https://github.com/ludwig-ai/ludwig +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_KR.md b/README_KR.md new file mode 100644 index 0000000..4ba102c --- /dev/null +++ b/README_KR.md @@ -0,0 +1,311 @@ +

+ + + +

+ +
+ +_확장성과 효율성을 위해 설계된 선언적 딥러닝 프레임워크_ + +[![PyPI version](https://badge.fury.io/py/ludwig.svg)](https://badge.fury.io/py/ludwig) +[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/CBgdrGnZjy) +[![DockerHub](https://img.shields.io/docker/pulls/ludwigai/ludwig.svg)](https://hub.docker.com/r/ludwigai) +[![Downloads](https://pepy.tech/badge/ludwig)](https://pepy.tech/project/ludwig) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ludwig-ai/ludwig/blob/main/LICENSE) +[![X](https://img.shields.io/twitter/follow/ludwig_ai.svg?style=social&logo=twitter)](https://twitter.com/ludwig_ai) + +
+ +# 📖 Ludwig란? + +Ludwig는 **LLM** 및 기타 심층 신경망과 같은 **맞춤형** AI 모델을 구축하기 위한 **로우코드** 프레임워크입니다. + +주요 기능: + +- 🛠 **손쉬운 맞춤형 모델 구축:** 선언적 YAML 설정 파일만으로 최신 LLM을 데이터에 맞춰 학습시킬 수 있습니다. 멀티태스크 및 멀티모달 학습을 지원합니다. 포괄적인 설정 검증으로 잘못된 매개변수 조합을 감지하고 런타임 오류를 방지합니다. +- ⚡ **확장성과 효율성 최적화:** 자동 배치 크기 선택, 분산 학습([DDP](https://pytorch.org/tutorials/beginner/ddp_series_theory.html), [DeepSpeed](https://github.com/microsoft/DeepSpeed)), 매개변수 효율적 미세 조정([PEFT](https://github.com/huggingface/peft)), 4비트 양자화(QLoRA), 페이지 및 8비트 옵티마이저, 메모리 초과 데이터셋 지원. +- 📐 **전문가 수준의 제어:** 활성화 함수까지 모델을 완전히 제어할 수 있습니다. 하이퍼파라미터 최적화, 설명 가능성, 풍부한 메트릭 시각화를 지원합니다. +- 🧱 **모듈식 및 확장 가능:** 설정에서 몇 가지 매개변수만 변경하여 다양한 모델 아키텍처, 태스크, 피처, 모달리티를 실험할 수 있습니다. 딥러닝을 위한 빌딩 블록이라고 생각하세요. +- 🚢 **프로덕션을 위한 설계:** 사전 빌드된 [Docker](https://hub.docker.com/u/ludwigai) 컨테이너, [Kubernetes](https://github.com/ray-project/kuberay)에서 [Ray](https://www.ray.io/) 실행 네이티브 지원, [Torchscript](https://pytorch.org/docs/stable/jit.html) 및 [Triton](https://developer.nvidia.com/triton-inference-server)으로 모델 내보내기, 한 번의 명령으로 [HuggingFace](https://huggingface.co/models)에 업로드. + +Ludwig는 [Linux Foundation AI & Data](https://lfaidata.foundation/)에서 호스팅합니다. + +**기술 스택:** Python 3.12 | PyTorch 2.6 | Pydantic 2 | Transformers 5 | Ray 2.54 + +![img](https://raw.githubusercontent.com/ludwig-ai/ludwig-docs/master/docs/images/ludwig_legos_unanimated.gif) + +# 💾 설치 + +PyPI에서 설치합니다. Ludwig는 Python 3.12 이상을 요구합니다. + +```shell +pip install ludwig +``` + +모든 선택적 의존성을 포함하여 설치: + +```shell +pip install ludwig[full] +``` + +더 자세한 설치 방법은 [기여 가이드](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md)를 참조하세요. + +# 🚂 시작하기 + +Ludwig의 기능을 빠르게 살펴보고 싶으시다면 이 Colab 노트북을 확인하세요 🚀 [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1lB4ALmEyvcMycE3Mlnsd7I3bc0zxvk39) + +LLM 미세 조정을 원하시나요? 다음 노트북을 확인하세요: + +1. Fine-Tune Llama-2-7b: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1r4oSEwRJpYKBPM0M0RSh0pBEYK_gBKbe) +1. Fine-Tune Llama-2-13b: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1zmSEzqZ7v4twBrXagj1TE_C--RNyVAyu) +1. Fine-Tune Mistral-7b: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1i_8A1n__b7ljRWHzIsAdhO7u7r49vUm4) + +전체 튜토리얼은 공식 [시작 가이드](https://ludwig.ai/latest/getting_started/)를 확인하시거나, 엔드투엔드 [예제](https://ludwig.ai/latest/examples)를 살펴보세요. + +## 대규모 언어 모델 미세 조정 + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1c3AO8l_H6V_x37RwQ8V7M6A-RmcBf2tG?usp=sharing) + +사전 학습된 LLM을 챗봇처럼 지시를 따르도록 미세 조정("인스트럭션 튜닝")해 봅시다. + +### 사전 요구 사항 + +- [HuggingFace API 토큰](https://huggingface.co/docs/hub/security-tokens) +- 선택한 베이스 모델에 대한 접근 승인 (예: [Llama-3.1-8B](https://huggingface.co/meta-llama/Llama-3.1-8B)) +- 최소 12 GiB VRAM의 GPU (테스트에서는 Nvidia T4를 사용했습니다) + +### 실행 + +[Stanford Alpaca](https://crfm.stanford.edu/2023/03/13/alpaca.html) 데이터셋을 사용합니다. 다음과 같은 테이블 형식의 파일로 구성됩니다: + +| instruction | input | output | +| :-----------------------------------------------: | :--------------: | :-----------------------------------------------: | +| Give three tips for staying healthy. | | 1.Eat a balanced diet and make sure to include... | +| Arrange the items given below in the order to ... | cake, me, eating | I eating cake. | +| Write an introductory paragraph about a famous... | Michelle Obama | Michelle Obama is an inspirational woman who r... | +| ... | ... | ... | + +`model.yaml`이라는 YAML 설정 파일을 다음 내용으로 생성하세요: + +```yaml +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +quantization: + bits: 4 + +adapter: + type: lora + +prompt: + template: | + Below is an instruction that describes a task, paired with an input that may provide further context. + Write a response that appropriately completes the request. + + ### Instruction: + {instruction} + + ### Input: + {input} + + ### Response: + +input_features: + - name: prompt + type: text + +output_features: + - name: output + type: text + +trainer: + type: finetune + learning_rate: 0.0001 + batch_size: 1 + gradient_accumulation_steps: 16 + epochs: 3 + learning_rate_scheduler: + decay: cosine + warmup_fraction: 0.01 + +preprocessing: + sample_ratio: 0.1 + +backend: + type: local +``` + +이제 모델을 학습시켜 봅시다: + +```bash +export HUGGING_FACE_HUB_TOKEN = "" + +ludwig train --config model.yaml --dataset "ludwig://alpaca" +``` + +## 지도 학습 ML + +[Rotten Tomatoes](https://www.kaggle.com/stefanoleone992/rotten-tomatoes-movies-and-critic-reviews-dataset) 영화 평론가의 리뷰가 긍정적인지 부정적인지 예측하는 신경망을 만들어 봅시다. + +데이터셋은 다음과 같은 CSV 파일입니다: + +| movie_title | content_rating | genres | runtime | top_critic | review_content | recommended | +| :------------------: | :------------: | :------------------------------: | :-----: | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| Deliver Us from Evil | R | Action & Adventure, Horror | 117.0 | TRUE | Director Scott Derrickson and his co-writer, Paul Harris Boardman, deliver a routine procedural with unremarkable frights. | 0 | +| Barbara | PG-13 | Art House & International, Drama | 105.0 | FALSE | Somehow, in this stirring narrative, Barbara manages to keep hold of her principles, and her humanity and courage, and battles to save a dissident teenage girl whose life the Communists are trying to destroy. | 1 | +| Horrible Bosses | R | Comedy | 98.0 | FALSE | These bosses cannot justify either murder or lasting comic memories, fatally compromising a farce that could have been great but ends up merely mediocre. | 0 | +| ... | ... | ... | ... | ... | ... | ... | + +[여기](https://ludwig.ai/latest/data/rotten_tomatoes.csv)에서 데이터셋 샘플을 다운로드하세요. + +```bash +wget https://ludwig.ai/latest/data/rotten_tomatoes.csv +``` + +다음으로 `model.yaml`이라는 YAML 설정 파일을 생성하세요: + +```yaml +input_features: + - name: genres + type: set + preprocessing: + tokenizer: comma + - name: content_rating + type: category + - name: top_critic + type: binary + - name: runtime + type: number + - name: review_content + type: text + encoder: + type: embed +output_features: + - name: recommended + type: binary +``` + +이게 전부입니다! 이제 모델을 학습시켜 봅시다: + +```bash +ludwig train --config model.yaml --dataset rotten_tomatoes.csv +``` + +**즐거운 모델링 되세요** + +Ludwig를 여러분의 데이터에 적용해 보세요. 질문이 있으시면 [Discord에서 문의](https://discord.gg/CBgdrGnZjy)해 주세요. + +# ❓ Ludwig를 사용해야 하는 이유 + +- **최소한의 머신러닝 보일러플레이트** + + Ludwig는 머신러닝의 엔지니어링 복잡성을 기본으로 처리하여, 연구자들이 가장 높은 수준의 추상화에서 모델 구축에 집중할 수 있게 합니다. `torch.nn.Module` 모델에 대한 데이터 전처리, 하이퍼파라미터 최적화, 디바이스 관리, 분산 학습이 완전히 무료로 제공됩니다. + +- **손쉬운 벤치마크 구축** + + 최신 기준 모델을 만들고 새 모델과 비교하는 것이 간단한 설정 변경만으로 가능합니다. + +- **새로운 아키텍처를 여러 문제와 데이터셋에 쉽게 적용** + + Ludwig가 지원하는 광범위한 태스크 및 데이터셋 세트에 새 모델을 적용하세요. Ludwig에는 간단한 설정만으로 여러 데이터셋에서 여러 모델 실험을 실행할 수 있는 [전체 벤치마킹 도구](https://arxiv.org/abs/2111.04260)가 모든 사용자에게 제공됩니다. + +- **데이터 전처리, 모델링, 메트릭의 높은 설정 가능성** + + 모델 아키텍처, 학습 루프, 하이퍼파라미터 검색, 백엔드 인프라의 모든 측면을 선언적 설정에서 추가 필드로 수정하여 파이프라인을 요구 사항에 맞게 커스터마이즈할 수 있습니다. 설정 가능한 항목에 대한 자세한 내용은 [Ludwig 설정](https://ludwig.ai/latest/configuration/) 문서를 확인하세요. + +- **멀티모달, 멀티태스크 학습 기본 지원** + + 코드 작성 없이 테이블 데이터, 텍스트, 이미지, 오디오까지 복잡한 모델 설정으로 혼합하여 사용할 수 있습니다. + +- **풍부한 모델 내보내기 및 추적** + + Tensorboard, Comet ML, Weights & Biases, MLFlow, Aim Stack 등의 도구로 모든 시도와 메트릭을 자동으로 추적합니다. + +- **멀티 GPU, 멀티 노드 클러스터로 학습 자동 확장** + + 로컬 머신에서 클라우드로 코드 변경 없이 전환할 수 있습니다. + +- **사전 학습된 Huggingface Transformers를 포함한 최신 모델의 로우코드 인터페이스** + + Ludwig는 [Huggingface Transformers](https://huggingface.co/docs/transformers/index)에서 제공하는 사전 학습된 모델과 네이티브로 통합됩니다. 사용자는 코드를 전혀 작성하지 않고도 방대한 최신 사전 학습 PyTorch 모델을 사용할 수 있습니다. 예를 들어, Ludwig로 BERT 기반 감성 분석 모델을 학습시키는 것은 다음과 같이 간단합니다: + + ```shell + ludwig train --dataset sst5 --config_str "{input_features: [{name: sentence, type: text, encoder: bert}], output_features: [{name: label, type: category}]}" + ``` + +- **AutoML을 위한 로우코드 인터페이스** + + [Ludwig AutoML](https://ludwig.ai/latest/user_guide/automl/)을 사용하면 데이터셋, 대상 컬럼, 시간 예산만 제공하여 학습된 모델을 얻을 수 있습니다. + + ```python + auto_train_results = ludwig.automl.auto_train(dataset=my_dataset_df, target=target_column_name, time_limit_s=7200) + ``` + +- **손쉬운 프로덕션화** + + Ludwig는 GPU를 포함한 딥러닝 모델 서빙을 쉽게 만들어 줍니다. 학습된 Ludwig 모델에 대한 REST API를 실행하세요. + + ```shell + ludwig serve --model_path=/path/to/model + ``` + + Ludwig는 효율적인 Torchscript 번들로 모델 내보내기를 지원합니다. + + ```shell + ludwig export_torchscript --model_path=/path/to/model + ``` + +# 📚 튜토리얼 + +- [텍스트 분류](https://ludwig.ai/latest/examples/text_classification) +- [테이블 데이터 분류](https://ludwig.ai/latest/examples/adult_census_income) +- [이미지 분류](https://ludwig.ai/latest/examples/mnist) +- [멀티모달 분류](https://ludwig.ai/latest/examples/multimodal_classification) + +# 🔬 예제 사용 사례 + +- [개체명 인식 태깅](https://ludwig.ai/latest/examples/ner_tagging) +- [자연어 이해](https://ludwig.ai/latest/examples/nlu) +- [기계 번역](https://ludwig.ai/latest/examples/machine_translation) +- [seq2seq를 통한 대화 모델링](https://ludwig.ai/latest/examples/seq2seq) +- [감성 분석](https://ludwig.ai/latest/examples/sentiment_analysis) +- [시아미즈 네트워크를 이용한 원샷 학습](https://ludwig.ai/latest/examples/oneshot) +- [시각적 질의응답](https://ludwig.ai/latest/examples/visual_qa) +- [음성 숫자 인식](https://ludwig.ai/latest/examples/speech_recognition) +- [화자 인증](https://ludwig.ai/latest/examples/speaker_verification) +- [이진 분류 (타이타닉)](https://ludwig.ai/latest/examples/titanic) +- [시계열 예측](https://ludwig.ai/latest/examples/forecasting) +- [시계열 예측 (날씨)](https://ludwig.ai/latest/examples/weather) +- [영화 평점 예측](https://ludwig.ai/latest/examples/movie_ratings) +- [다중 레이블 분류](https://ludwig.ai/latest/examples/multi_label) +- [멀티태스크 학습](https://ludwig.ai/latest/examples/multi_task) +- [단순 회귀: 연비 예측](https://ludwig.ai/latest/examples/fuel_efficiency) +- [사기 탐지](https://ludwig.ai/latest/examples/fraud) + +# 💡 추가 정보 + +[Ludwig](https://arxiv.org/pdf/1909.07930.pdf), [선언적 ML](https://arxiv.org/pdf/2107.08148.pdf), [Ludwig의 SoTA 벤치마크](https://openreview.net/pdf?id=hwjnu6qW7E4)에 대한 논문을 읽어보세요. + +[Ludwig의 작동 방식](https://ludwig.ai/latest/user_guide/how_ludwig_works/), [시작 가이드](https://ludwig.ai/latest/getting_started/), 더 많은 [예제](https://ludwig.ai/latest/examples)를 확인하세요. + +[기여](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md)에 관심이 있으시거나, 질문, 의견, 공유하고 싶은 생각이 있으시거나, 최신 정보를 받고 싶으시다면 [Discord 커뮤니티에 참여](https://discord.gg/CBgdrGnZjy)하시고 [X](https://twitter.com/ludwig_ai)에서 팔로우해 주세요! + +# 🤝 함께 Ludwig를 만들어 갈 커뮤니티에 참여하세요 + +Ludwig는 여러분과 같은 분들의 기여에 의존하는 활발하게 관리되는 오픈소스 프로젝트입니다. Ludwig를 모든 사람이 사용할 수 있는 더 접근 가능하고 기능이 풍부한 프레임워크로 만들기 위해 활발한 Ludwig 기여자 그룹에 참여하는 것을 고려해 주세요! + + + +
+ +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=ludwig-ai/ludwig&type=Date)](https://star-history.com/#ludwig-ai/ludwig&Date) + +# 👋 참여하기 + +- [Discord](https://discord.gg/CBgdrGnZjy) +- [X (Twitter)](https://twitter.com/ludwig_ai) +- [Medium](https://medium.com/ludwig-ai) +- [GitHub Issues](https://github.com/ludwig-ai/ludwig/issues) diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..fbe3e9a --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,50 @@ +# Releasing + +## Release procedure + +1. Update version number in `ludwig/globals.py` +1. Update the `README.md` file +1. Update `ludwig-docs` +1. Commit +1. Tag the commit with the version number `vX.Y.Z` with a meaningful message +1. Push with `--tags` +1. If a non-patch release, edit the release notes +1. The PyPI upload is automated via GitHub Actions (`.github/workflows/upload-pypi.yml`) when a release is published +1. Publish Docker images (see below) + +## Docker images + +Four images are published to Docker Hub under the `ludwigai` organisation for each release: +`ludwigai/ludwig`, `ludwigai/ludwig-gpu`, `ludwigai/ludwig-ray`, `ludwigai/ludwig-ray-gpu`. + +### Automated (CI) + +The GitHub Actions workflow `.github/workflows/docker.yml` triggers on `v*.*.*` tags and builds +images from the tagged source. If CI is healthy this runs automatically after step 6 above. + +### Manual fallback + +If CI does not run or images need to be backfilled, trigger a versioned build via the workflow +dispatch input — no local Docker setup required: + +```bash +# Trigger all 4 image variants for a specific PyPI release +gh workflow run docker.yml --repo ludwig-ai/ludwig --ref main \ + -f ludwig_version=0.14.0 -f latest=true +``` + +Or build and push locally using the script at `docker/build_and_push.sh` +(requires `docker login` to a `ludwigai` Docker Hub account): + +```bash +./docker/build_and_push.sh 0.14.0 --latest +``` + +Both approaches install `ludwig[full]==` from PyPI and produce two tags per image: +the full version (`0.14.0`) and the major.minor shorthand (`0.14`), plus `latest` when requested. + +## Release policy + +Ludwig follows [Semantic Versioning](https://semver.org). +In general, for major and minor releases, maintainers should all agree on the release. +For patches, in particular time sensitive ones, a single maintainer can release without a full consensus, but this practice should be reserved for critical situations. diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..bde695e --- /dev/null +++ b/docker/README.md @@ -0,0 +1,137 @@ +# Ludwig Docker Images + +These images provide Ludwig, a toolbox to train and evaluate deep learning models +without the need to write code. Ludwig Docker images contain the full set of pre-requisite +packages to support these capabilities + +- text features +- image features +- audio features +- visualizations +- hyperparameter optimization +- distributed training +- model serving + +## Publishing images + +Images are normally published automatically by CI (`.github/workflows/docker.yml`) when a release +tag is pushed. To publish manually or backfill a release, use the script in this directory: + +```bash +# Requires: docker login to a ludwigai Docker Hub account +./docker/build_and_push.sh [--latest] + +# Examples +./docker/build_and_push.sh 0.14.0 --latest # new latest release +./docker/build_and_push.sh 0.13.0 # backfill without updating :latest +``` + +See `RELEASES.md` for the full release procedure. + +## Repositories + +These four repositories contain a version of Ludwig with full features built +from the project's `master` branch. + +- `ludwigai/ludwig` Ludwig packaged with PyTorch +- `ludwigai/ludwig-gpu` Ludwig packaged with gpu-enabled version of PyTorch +- `ludwigai/ludwig-ray` Ludwig packaged with PyTorch + and Ray 2.3.1 (https://github.com/ray-project/ray) +- `ludwigai/ludwig-ray-gpu` Ludwig packaged with gpu-enabled versions of PyTorch + and Ray 2.3.1 (https://github.com/ray-project/ray) + +## Image Tags + +- `master` - built from Ludwig's `master` branch +- `nightly` - nightly build of Ludwig's software. +- `sha-` - version of Ludwig software at designated git sha1 + 7-character commit point. + +## Running Containers + +Examples of using the `ludwigai/ludwig:master` image to: + +- run the `ludwig cli` command or +- run Python program containing Ludwig api or +- view Ludwig results with Tensorboard + +For purposes of the examples assume this host directory structure + +``` +/top/level/directory/path/ + data/ + train.csv + src/ + config.yaml + ludwig_api_program.py +``` + +### Run Ludwig CLI + +``` +# set shell variable to parent directory +parent_path=/top/level/directory/path + +# invoke docker run command to execute the ludwig cli +# map host directory ${parent_path}/data to container /data directory +# map host directory ${parent_path}/src to container /src directory +docker run -v ${parent_path}/data:/data \ + -v ${parent_path}/src:/src \ + ludwigai/ludwig:master \ + experiment --config /src/config.yaml \ + --dataset /data/train.csv \ + --output_directory /src/results +``` + +Experiment results can be found in host directory `/top/level/directory/path/src/results` + +### Run Python program using Ludwig APIs + +``` +# set shell variable to parent directory +parent_path=/top/level/directory/path + +# invoke docker run command to execute Python interpreter +# map host directory ${parent_path}/data to container /data directory +# map host directory ${parent_path}/src to container /src directory +# set current working directory to container /src directory +# change default entrypoint from ludwig to python +docker run -v ${parent_path}/data:/data \ + -v ${parent_path}/src:/src \ + -w /src \ + --entrypoint python \ + ludwigai/ludwig:master /src/ludwig_api_program.py +``` + +Ludwig results can be found in host +directory `/top/level/directory/path/src/results` + +### View Ludwig Tensorboard results + +``` +# set shell variable to parent directory +parent_path=/top/level/directory/path + +# invoke docker run command to execute Tensorboard +# map host directory ${parent_path}/src to container /src directory +# set up mapping from localhost port 6006 to container port 6006 +# change default entrypoint from ludwig to tensorboard +# --logdir container location of tenorboard logs /src/results/_/model/logs +# --bind_all Tensorboard serves on all public container interfaces +docker run -v ${parent_path}/src:/src \ + -p 6006:6006 \ + --entrypoint tensorboard \ + ludwigai/ludwig:master \ + --logdir /src/results/experiment_run/model/logs \ + --bind_all +``` + +Point browser to `http://localhost:6006` to see Tensorboard dashboard. + +### Devcontainer + +If you want to contribute to Ludwig, you can setup a Docker container with all the dependencies +installed as a full featured development environment. This can be done using devcontainers with VS Code: +https://code.visualstudio.com/docs/devcontainers/containers + +You can find the `devcontainer.json` file within the top level `.devcontainer` folder. diff --git a/docker/build_and_push.sh b/docker/build_and_push.sh new file mode 100755 index 0000000..d6b7f26 --- /dev/null +++ b/docker/build_and_push.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Build and push versioned Ludwig Docker images to Docker Hub. +# +# Usage: +# ./docker/build_and_push.sh [--latest] +# +# Examples: +# ./docker/build_and_push.sh 0.14.0 # tags: 0.14.0, 0.14 +# ./docker/build_and_push.sh 0.14.0 --latest # tags: 0.14.0, 0.14, latest +# +# Requires: docker login to ludwigai account already done. + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "Usage: $0 [--latest]" + echo " version full version to build, e.g. 0.14.0" + echo " --latest also tag as :latest" + exit 1 +fi + +VERSION="$1" +IS_LATEST=false +if [ "${2:-}" = "--latest" ]; then + IS_LATEST=true +fi + +# Derive major.minor tag from full version (e.g. "0.14.0" -> "0.14") +MINOR="${VERSION%.*}" + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# Image variants: "image_name dockerfile_dir" +VARIANTS=( + "ludwigai/ludwig docker/ludwig" + "ludwigai/ludwig-gpu docker/ludwig-gpu" + "ludwigai/ludwig-ray docker/ludwig-ray" + "ludwigai/ludwig-ray-gpu docker/ludwig-ray-gpu" +) + +build_and_push() { + local image="$1" + local dockerfile_dir="$2" + + echo "" + echo "=== Building ${image}:${VERSION} ===" + + local tag_args="-t ${image}:${VERSION} -t ${image}:${MINOR}" + if [ "${IS_LATEST}" = "true" ]; then + tag_args="${tag_args} -t ${image}:latest" + fi + + # shellcheck disable=SC2086 + docker build \ + --build-arg LUDWIG_VERSION="${VERSION}" \ + ${tag_args} \ + -f "${REPO_ROOT}/${dockerfile_dir}/Dockerfile" \ + "${REPO_ROOT}" + + echo "--- Pushing ${image}:${VERSION} ---" + docker push "${image}:${VERSION}" + docker push "${image}:${MINOR}" + if [ "${IS_LATEST}" = "true" ]; then + docker push "${image}:latest" + fi +} + +for variant_entry in "${VARIANTS[@]}"; do + read -r image dockerfile_dir <<< "${variant_entry}" + build_and_push "${image}" "${dockerfile_dir}" +done + +echo "" +echo "All images built and pushed successfully." diff --git a/docker/ludwig-gpu/Dockerfile b/docker/ludwig-gpu/Dockerfile new file mode 100644 index 0000000..0f94d74 --- /dev/null +++ b/docker/ludwig-gpu/Dockerfile @@ -0,0 +1,43 @@ +# +# Ludwig Docker image with full set of pre-requiste packages to support these capabilities +# text features +# image features +# audio features +# visualizations +# hyperparameter optimization +# distributed training +# model serving +# + +FROM nvidia/cuda:12.6.3-cudnn-devel-ubuntu24.04 + +RUN apt-get -y update && DEBIAN_FRONTEND="noninteractive" apt-get -y install \ + python3.12 \ + python3.12-venv \ + python3.12-dev \ + git \ + libsndfile1 \ + cmake \ + ffmpeg \ + sox \ + libsox-dev + +RUN python3.12 -m venv /opt/ludwig-venv +ENV PATH="/opt/ludwig-venv/bin:$PATH" +RUN pip install -U pip + +ARG LUDWIG_VERSION + +WORKDIR /ludwig + +COPY . . +RUN if [ -n "${LUDWIG_VERSION}" ]; then \ + pip install --no-cache-dir "ludwig[full]==${LUDWIG_VERSION}" --extra-index-url https://download.pytorch.org/whl/cu126; \ + else \ + pip install --no-cache-dir '.[full]' --extra-index-url https://download.pytorch.org/whl/cu126; \ + fi +RUN pip install --no-cache-dir --force-reinstall torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 --index-url https://download.pytorch.org/whl/cu126 + +WORKDIR /data + +ENTRYPOINT ["ludwig"] diff --git a/docker/ludwig-ray-gpu/Dockerfile b/docker/ludwig-ray-gpu/Dockerfile new file mode 100644 index 0000000..301f6cf --- /dev/null +++ b/docker/ludwig-ray-gpu/Dockerfile @@ -0,0 +1,40 @@ +# +# Ludwig Docker image with Ray support and full dependencies including: +# text features +# image features +# audio features +# visualizations +# hyperparameter optimization +# distributed training +# model serving +# + +FROM rayproject/ray:2.54.0-py312-cu126 + +RUN sudo apt-get update && \ + DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y \ + build-essential \ + wget \ + git \ + curl \ + libsndfile1 \ + cmake \ + tzdata \ + rsync \ + vim \ + ffmpeg \ + sox \ + libsox-dev +RUN pip install -U pip + +ARG LUDWIG_VERSION + +WORKDIR /ludwig + +COPY . . +RUN if [ -n "${LUDWIG_VERSION}" ]; then \ + pip install --no-cache-dir "ludwig[full]==${LUDWIG_VERSION}" --extra-index-url https://download.pytorch.org/whl/cu126; \ + else \ + pip install --no-cache-dir '.[full]' --extra-index-url https://download.pytorch.org/whl/cu126; \ + fi +RUN pip install --no-cache-dir --force-reinstall torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 --index-url https://download.pytorch.org/whl/cu126 diff --git a/docker/ludwig-ray/Dockerfile b/docker/ludwig-ray/Dockerfile new file mode 100644 index 0000000..bd4c580 --- /dev/null +++ b/docker/ludwig-ray/Dockerfile @@ -0,0 +1,39 @@ +# +# Ludwig Docker image with Ray support and full dependencies including: +# text features +# image features +# audio features +# visualizations +# hyperparameter optimization +# distributed training +# model serving +# + +FROM rayproject/ray:2.54.0-py312 + +RUN sudo apt-get update && DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y \ + build-essential \ + wget \ + git \ + curl \ + libsndfile1 \ + cmake \ + tzdata \ + rsync \ + vim \ + ffmpeg \ + sox \ + libsox-dev +RUN pip install -U pip + +ARG LUDWIG_VERSION + +WORKDIR /ludwig + +COPY . . +RUN if [ -n "${LUDWIG_VERSION}" ]; then \ + pip install --no-cache-dir "ludwig[full]==${LUDWIG_VERSION}" --extra-index-url https://download.pytorch.org/whl/cpu; \ + else \ + pip install --no-cache-dir '.[full]' --extra-index-url https://download.pytorch.org/whl/cpu; \ + fi +RUN pip install --no-cache-dir --force-reinstall torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 --extra-index-url https://download.pytorch.org/whl/cpu diff --git a/docker/ludwig/Dockerfile b/docker/ludwig/Dockerfile new file mode 100644 index 0000000..bc75f58 --- /dev/null +++ b/docker/ludwig/Dockerfile @@ -0,0 +1,39 @@ +# +# Ludwig Docker image with full set of pre-requiste packages to support these capabilities +# text features +# image features +# audio features +# visualizations +# hyperparameter optimization +# distributed training +# model serving +# + +FROM python:3.12-slim + +RUN apt-get -y update && apt-get -y install \ + git \ + libsndfile1 \ + build-essential \ + g++ \ + cmake \ + ffmpeg \ + sox \ + libsox-dev +RUN pip install -U pip + +ARG LUDWIG_VERSION + +WORKDIR /ludwig + +COPY . . +RUN if [ -n "${LUDWIG_VERSION}" ]; then \ + pip install --no-cache-dir "ludwig[full]==${LUDWIG_VERSION}" --extra-index-url https://download.pytorch.org/whl/cpu; \ + else \ + pip install --no-cache-dir '.[full]' --extra-index-url https://download.pytorch.org/whl/cpu; \ + fi +RUN pip install --no-cache-dir --force-reinstall torch==2.12.0 torchvision==0.27.0 torchaudio==2.11.0 --extra-index-url https://download.pytorch.org/whl/cpu + +WORKDIR /data + +ENTRYPOINT ["ludwig"] diff --git a/docs/developer_guide/adding_a_feature_type.md b/docs/developer_guide/adding_a_feature_type.md new file mode 100644 index 0000000..6d63b10 --- /dev/null +++ b/docs/developer_guide/adding_a_feature_type.md @@ -0,0 +1,348 @@ +# Adding a New Feature Type to Ludwig + +This guide walks through every file you need to touch when adding a brand-new feature type (e.g. a hypothetical `"widget"` type). Use `ludwig/features/binary_feature.py` and `ludwig/schema/features/binary_feature.py` as living reference implementations — they are among the simplest complete examples. + +______________________________________________________________________ + +## Conceptual overview + +Each feature type lives in two parallel places: + +| Layer | Location | Purpose | +| ------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- | +| **Schema** | `ludwig/schema/features/_feature.py` | Pydantic-backed config classes; declares hyperparameters and their defaults | +| **Feature module** | `ludwig/features/_feature.py` | PyTorch modules; implements preprocessing, encoding, decoding, postprocessing | + +The schema classes are used for config validation and serialization. The feature module classes are instantiated at model-build time using those configs. Neither layer knows the other exists at import time — they are wired together through the feature registry. + +______________________________________________________________________ + +## Step 1 ��� Define the constant + +Add the type string to `ludwig/constants.py`: + +```python +WIDGET = "widget" +``` + +______________________________________________________________________ + +## Step 2 — Write the schema file + +Create `ludwig/schema/features/widget_feature.py`. The minimal required structure is: + +```python +from ludwig.constants import WIDGET, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.utils import LudwigBaseConfig + + +@input_mixin_registry.register(WIDGET) +class WidgetInputFeatureConfigMixin(LudwigBaseConfig): + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=WIDGET) + + +class WidgetInputFeatureConfig(WidgetInputFeatureConfigMixin, BaseInputFeatureConfig): + type: str = schema_utils.ProtectedString(WIDGET) + encoder: BaseEncoderConfig = None + + +@ecd_input_config_registry.register(WIDGET) +class ECDWidgetInputFeatureConfig(WidgetInputFeatureConfig): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=WIDGET, + default="dense", # default encoder for this type + ) + + +# For output features only: +@output_mixin_registry.register(WIDGET) +class WidgetOutputFeatureConfigMixin(LudwigBaseConfig): + # add loss, calibration, etc. fields here + pass + + +class WidgetOutputFeatureConfig(WidgetOutputFeatureConfigMixin, BaseOutputFeatureConfig): + type: str = schema_utils.ProtectedString(WIDGET) + default_validation_metric: str = "some_metric" + + +@ecd_output_config_registry.register(WIDGET) +class ECDWidgetOutputFeatureConfig(WidgetOutputFeatureConfig): + pass +``` + +**Key rules:** + +- `type` must be a `ProtectedString` with your constant — this prevents accidental overwrite via user YAML. +- `@input_mixin_registry.register` / `@output_mixin_registry.register` make the preprocessing config available to `global_defaults` in Ludwig configs. +- `@ecd_input_config_registry.register` / `@ecd_output_config_registry.register` wire the schema into the ECD model config builder. + +______________________________________________________________________ + +## Step 3 — Write the preprocessing config + +Create `ludwig/schema/features/preprocessing/widget_feature_preprocessing.py` if your feature needs non-default preprocessing parameters, or register your type against an existing one (e.g. `number_feature` for scalars). For a new type, create the file: + +```python +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.constants import WIDGET + + +@register_preprocessor(WIDGET) +class WidgetPreprocessingConfig(BasePreprocessingConfig): + # add preprocessing hyperparameters here + pass +``` + +______________________________________________________________________ + +## Step 4 — Write the feature module + +Create `ludwig/features/widget_feature.py`. The required classes are: + +### Inner preprocessing module + +```python +import torch +from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin, InputFeature, OutputFeature + + +class _WidgetPreprocessing(BasePreprocessingModule): + """Runs inside the model graph during inference to preprocess raw input.""" + + def __init__(self, metadata: dict, preprocessing_config, is_input_feature: bool = True): + super().__init__() + # store everything needed to preprocess at inference time + + def forward(self, v): + # v is the raw column value; return a tensor + raise NotImplementedError +``` + +### FeatureMixin (shared preprocessing logic) + +`FeaturePreprocessingMixin` provides the Python-side preprocessing used during dataset preparation (not inside the model graph). You must implement `add_feature_data` and `get_preprocessing_module`: + +```python +class WidgetFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return WIDGET + + @staticmethod + def cast_column(column, backend): + """Cast the raw DataFrame column to the expected dtype.""" + return column + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters, + backend, + skip_save_processed_input, + ): + """Populate proc_df[feature_config[PROC_COLUMN]] with preprocessed values.""" + proc_df[feature_config[PROC_COLUMN]] = input_df[feature_config[COLUMN]].values + return proc_df + + @staticmethod + def fill_missing_values(feature_config, input_df, backend): + """Replace NaN/None with a fill value appropriate for this type.""" + return input_df + + @staticmethod + def feature_meta(column, preprocessing_parameters, backend): + """Compute and return the training-set-level metadata dict for this feature.""" + return {} + + @staticmethod + def get_preprocessing_module(feature_config, metadata): + """Return the _WidgetPreprocessing module for use during inference.""" + return _WidgetPreprocessing(metadata, feature_config.preprocessing) +``` + +### InputFeature class + +```python +from ludwig.schema.features.widget_feature import WidgetInputFeatureConfig + + +class WidgetInputFeature(WidgetFeatureMixin, InputFeature): + def __init__(self, input_feature_config: WidgetInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + self._input_shape = torch.Size([1]) # set to actual encoded shape + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs, mask=None): + assert inputs.dtype == torch.float32 + encoder_output = self.encoder_obj(inputs, mask=mask) + return {"encoder_output": encoder_output} + + @property + def input_dtype(self): + return torch.float32 + + @property + def input_shape(self): + return self._input_shape + + @property + def output_shape(self): + return self.encoder_obj.output_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + @staticmethod + def create_sample_input(batch_size=2): + return torch.zeros(batch_size, 1) + + @staticmethod + def get_schema_cls(): + return WidgetInputFeatureConfig +``` + +### OutputFeature class (only if this type can be a target) + +```python +from ludwig.schema.features.widget_feature import WidgetOutputFeatureConfig + + +class WidgetOutputFeature(WidgetFeatureMixin, OutputFeature): + def __init__(self, output_feature_config: WidgetOutputFeatureConfig, output_features: dict, **kwargs): + super().__init__(output_feature_config, output_features, **kwargs) + self._input_shape = torch.Size([output_feature_config.input_size]) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, target=None): + return self.decoder_obj(inputs) + + def create_predict_module(self): + return _WidgetPredict() # see PredictModule below + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS, PROBABILITIES} + + @classmethod + def update_config_with_metadata(cls, feature_config, feature_metadata, *args, **kwargs): + feature_config.input_size = feature_metadata["input_size"] + + @staticmethod + def get_schema_cls(): + return WidgetOutputFeatureConfig +``` + +### PredictModule (for output features) + +```python +from ludwig.features.base_feature import PredictModule + + +class _WidgetPredict(PredictModule): + def forward(self, inputs, feature_name): + logits = inputs[f"{feature_name}_{LOGITS}"] + predictions = (logits > 0.5).float() + return {PREDICTIONS: predictions, LOGITS: logits} +``` + +______________________________________________________________________ + +## Step 5 — Register in the feature registries + +Open `ludwig/features/feature_registries.py` and add your classes to all relevant registry functions: + +```python +# at the top — add import +from ludwig.features.widget_feature import WidgetFeatureMixin, WidgetInputFeature + +# in get_base_type_registry(), inside the returned dict: +# WIDGET: WidgetFeatureMixin, +# +# in get_input_type_registry(), inside the returned dict: +# WIDGET: WidgetInputFeature, +# +# in get_output_type_registry() if applicable, inside the returned dict: +# WIDGET: WidgetOutputFeature, +``` + +The model builder uses `get_input_type_registry()` and `get_output_type_registry()` to instantiate feature objects from config at training time. + +______________________________________________________________________ + +## Step 6 — Register the constant in constants.py (feature sets) + +If the feature appears in `FEATURE_TYPES`, `INPUT_FEATURE_TYPES`, or similar sets, add `WIDGET` there too. + +______________________________________________________________________ + +## Step 7 — Write tests + +Create `tests/ludwig/features/test_widget_feature.py`. At minimum test: + +1. `WidgetFeatureMixin.add_feature_data` — correct column values written to `proc_df` +1. `_WidgetPreprocessing.forward` — correct tensor shape for a known input +1. `WidgetInputFeature.forward` — correct output keys and shapes with a random input +1. Encoder round-trip via `create_sample_input` + +```python +import torch +import pytest +from tests.integration_tests.utils import generate_data, run_api_test + + +def test_widget_preprocessing_forward(): + meta = {} + module = _WidgetPreprocessing(meta, preprocessing_config=None) + out = module(torch.zeros(4)) + assert out.shape == (4, 1) +``` + +______________________________________________________________________ + +## Checklist + +- [ ] `ludwig/constants.py` — add `WIDGET = "widget"` +- [ ] `ludwig/schema/features/widget_feature.py` — schema classes + registry decorators +- [ ] `ludwig/schema/features/preprocessing/` — preprocessing config class (or reuse existing) +- [ ] `ludwig/features/widget_feature.py` — preprocessing module, mixin, input/output feature classes +- [ ] `ludwig/features/feature_registries.py` — add to `get_base_type_registry`, `get_input_type_registry`, optionally `get_output_type_registry` +- [ ] `tests/ludwig/features/test_widget_feature.py` — unit tests for preprocessing and forward pass + +______________________________________________________________________ + +## Common pitfalls + +**`proc_df[PROC_COLUMN]` vs `proc_df[COLUMN]`** — always write to `PROC_COLUMN` (the internal column name), not `COLUMN` (the raw user column name). They can differ when the user renames features. + +**`get_preprocessing_module` vs `add_feature_data`** — `add_feature_data` runs in Python at dataset preparation time (CPU, pandas). `get_preprocessing_module` returns a `torch.nn.Module` that runs inside the model graph at inference time. Both must produce compatible representations. + +**`input_shape` vs `output_shape`** — `InputFeature.input_shape` is the shape of the *raw preprocessed* tensor going into the encoder. `InputFeature.output_shape` is the encoder's output shape that feeds into the combiner. Return `self.encoder_obj.output_shape` for the latter. + +**Registry order matters** — the registry in `feature_registries.py` is read at import time. If you import your feature class before `feature_registries.py` is loaded, the registry will be empty. The correct order is always: define constants → define schema → define feature → add to registry. + +**Schema `type` field** — always use `schema_utils.ProtectedString(WIDGET)` not `str = WIDGET`. The protected string raises an error if a user tries to override it in their config YAML, which prevents subtle type mismatches. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..a13f193 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,13 @@ +# Examples + +This directory contains example programs demonstrating Ludwig's Python APIs. + +| Directory | Examples Provided | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| hyperopt | Demonstrates Ludwig's to hyper-parameter optimization capability. | +| kfold_cv | Provides two examples for performing a k-fold cross validation analysis. One example uses the `ludwig experiment` cli. The other example uses the `ludwig.experiment.kfold_cross_validate()` api function. | +| mnist | Creates a model config data structure from a yaml file and trains a model. Programmatically modify the model config data structure to evaluate several different neural network architectures. Jupyter notebook demonstrates using a hold-out test data set to visualize model performance for alternative model architectures. | +| titanic | Trains a simple model with model config contained in a yaml file. Trains multiple models from yaml files and generate visualizations to compare training results. Jupyter notebook demonstrating how to programmatically create visualizations. | +| serve | Demonstrates running Ludwig http model server. A sample Python program illustrates how to invoke the REST API to get predictions from input features. | +| class_imbalance | Demonstrates using our class balancing feature to over-sample an imbalanced dataset. | +| ray/job_submission | Submit Ludwig training to a remote Ray cluster via Ray Job Submission. Avoids Ray Client issues with ray.data. Works with KubeRay, Anyscale, or any Ray cluster. | diff --git a/examples/alignment/README.md b/examples/alignment/README.md new file mode 100644 index 0000000..ad18b4f --- /dev/null +++ b/examples/alignment/README.md @@ -0,0 +1,113 @@ +# LLM Alignment with DPO and KTO + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/alignment/alignment_dpo.ipynb) + +This example shows how to align a large language model with human preferences using Ludwig's +built-in preference learning trainers. Alignment training is typically applied after an initial +supervised fine-tuning (SFT) stage to improve response quality, reduce harmful outputs, and teach +the model to follow instructions more reliably. + +## What is alignment? + +Alignment refers to the process of shaping a model's behaviour to match human values and preferences. +The classic approach — Reinforcement Learning from Human Feedback (RLHF) — requires training a +separate reward model on human-ranked responses, then running a full RL loop (PPO) against it. +Modern preference learning methods like DPO bypass the reward model entirely, making alignment +cheaper and more stable to train. + +## When to use each trainer + +| Trainer | Data format | Use case | Compute | +| ------- | ------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `dpo` | `prompt`, `chosen`, `rejected` | Preference pairs from human feedback; most widely studied | Medium — requires forward passes through both policy and reference model | +| `kto` | `prompt`, `response`, `label` (bool) | Single-label feedback (thumbs up/down); no paired responses needed | Low — simpler loss than DPO | +| `orpo` | `prompt`, `chosen`, `rejected` | Single-stage SFT + alignment; no separate reference model | Low — no reference model forward passes | +| `grpo` | `prompt`, custom reward function | RL-style training with a group-normalised reward signal; used in DeepSeek-R1 | High — requires multiple rollouts per prompt | + +Choose **DPO** when you have human-ranked response pairs and want the best-studied approach. +Choose **KTO** when collecting binary per-response feedback is easier than pairwise comparisons. +Choose **ORPO** when you want to skip the SFT stage and align in one shot. +Choose **GRPO** when you have a programmatic reward function (e.g. code execution, math verification). + +## Prerequisites + +- GPU with at least 40 GiB of VRAM (A100 recommended) +- [HuggingFace API Token](https://huggingface.co/docs/hub/security-tokens) +- Access approval to [Llama-3.1-8B](https://huggingface.co/meta-llama/Meta-Llama-3.1-8B) + +## Quick start + +Install dependencies: + +```bash +pip install "ludwig[llm]" datasets +``` + +Set your HuggingFace token: + +```bash +export HUGGING_FACE_HUB_TOKEN="" +``` + +Prepare the dataset: + +```bash +python prepare_dataset.py +``` + +Run DPO training: + +```bash +python train_dpo.py +# or with the CLI: +ludwig train --config config_dpo.yaml --dataset train.csv +``` + +Run KTO training: + +```bash +ludwig train --config config_kto.yaml --dataset train_kto.csv +``` + +Run GRPO training (reuses the DPO preference-pair format): + +```bash +python train_grpo.py +# or with the CLI: +ludwig train --config config_grpo.yaml --dataset preference_data.parquet +``` + +## GRPO specifics + +GRPO (Group Relative Policy Optimization, Shao et al. 2024) is the alignment method used by +DeepSeek-R1. For each prompt it samples a group of `grpo_num_generations` completions, scores +them, normalises rewards within the group, and applies a PPO-style clipped objective — +without a separate critic model. + +Ludwig's GRPO trainer consumes the same `prompt` / `chosen` / `rejected` columns as DPO, so +a programmatic reward function is implemented as a pre-processing step: score each candidate +completion in your dataset preparation pipeline, then emit the top-scoring completion as +`chosen` and the lowest as `rejected`. See `config_grpo.yaml` for the full list of knobs +(`grpo_beta` for the KL penalty, `grpo_epsilon` for PPO clipping, +`grpo_num_generations` for the group size). + +## Files + +| File | Description | +| --------------------- | ------------------------------------------------------------------ | +| `prepare_dataset.py` | Downloads Anthropic/hh-rlhf and converts it to Ludwig format | +| `train_dpo.py` | DPO training script using the Python API | +| `train_grpo.py` | GRPO training script using the Python API | +| `config_dpo.yaml` | Ludwig config for DPO | +| `config_kto.yaml` | Ludwig config for KTO | +| `config_orpo.yaml` | Ludwig config for ORPO | +| `config_grpo.yaml` | Ludwig config for GRPO | +| `alignment_dpo.ipynb` | Colab-compatible notebook covering DPO, KTO evaluation, and upload | + +## Upload to HuggingFace + +After training, upload the aligned model: + +```bash +ludwig upload hf_hub -r / -m results/experiment_run/model +``` diff --git a/examples/alignment/alignment_dpo.ipynb b/examples/alignment/alignment_dpo.ipynb new file mode 100644 index 0000000..99dc091 --- /dev/null +++ b/examples/alignment/alignment_dpo.ipynb @@ -0,0 +1,477 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LLM Alignment with DPO and KTO\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/alignment/alignment_dpo.ipynb)\n", + "\n", + "This notebook walks through aligning **Llama-3.1-8B** with human preferences using Ludwig's built-in\n", + "**Direct Preference Optimization (DPO)** trainer, then shows how to switch to **KTO** when paired\n", + "preference data is not available.\n", + "\n", + "We use the publicly available [Anthropic HH-RLHF](https://huggingface.co/datasets/Anthropic/hh-rlhf)\n", + "dataset, which contains helpfulness and harmlessness preference pairs collected from human annotators.\n", + "\n", + "### What you will learn\n", + "- How to parse the HH-RLHF dataset into Ludwig's expected column format\n", + "- How to configure and run DPO alignment training with LoRA\n", + "- How to evaluate response quality before and after alignment\n", + "- How to switch to KTO with a one-line config change\n", + "- How to upload the aligned model to HuggingFace Hub\n", + "\n", + "### Prerequisites\n", + "- **GPU**: A100 (40 GiB) recommended. T4 will work with smaller batch sizes but is slower.\n", + "- **HuggingFace token**: Required for Llama-3.1-8B. Set `HUGGING_FACE_HUB_TOKEN` before running.\n", + "- **Model access**: Request access at https://huggingface.co/meta-llama/Meta-Llama-3.1-8B" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install \"ludwig[llm]\" datasets --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Set your HuggingFace token — required to download Llama-3.1-8B.\n", + "# In Colab: Secrets panel (key icon) -> add HF_TOKEN, then reference it here.\n", + "try:\n", + " from google.colab import userdata\n", + "\n", + " os.environ[\"HUGGING_FACE_HUB_TOKEN\"] = userdata.get(\"HF_TOKEN\")\n", + "except Exception:\n", + " pass # Running locally — export HUGGING_FACE_HUB_TOKEN in your shell instead\n", + "\n", + "assert os.environ.get(\"HUGGING_FACE_HUB_TOKEN\"), (\n", + " \"HUGGING_FACE_HUB_TOKEN is not set. Add it to Colab Secrets or export it in your shell.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "\n", + "# Verify GPU availability\n", + "result = subprocess.run(\n", + " [\"nvidia-smi\", \"--query-gpu=name,memory.total\", \"--format=csv,noheader\"], capture_output=True, text=True\n", + ")\n", + "if result.returncode == 0:\n", + " print(\"GPU(s) detected:\")\n", + " print(result.stdout.strip())\n", + "else:\n", + " print(\"WARNING: No GPU detected. Training will be very slow on CPU.\")\n", + " print(\"In Colab: Runtime -> Change runtime type -> A100 GPU\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prepare dataset\n", + "\n", + "The [Anthropic HH-RLHF](https://huggingface.co/datasets/Anthropic/hh-rlhf) dataset stores full\n", + "multi-turn conversations in two columns: `chosen` (preferred response) and `rejected` (dispreferred\n", + "response). Each value is a raw conversation string like:\n", + "\n", + "```\n", + "\\n\\nHuman: How do I make pasta?\\n\\nAssistant: Boil water, add pasta...\\n\\nHuman: What sauce?\\n\\nAssistant: Try marinara...\n", + "```\n", + "\n", + "Ludwig's DPO trainer expects three columns: `prompt`, `chosen`, `rejected`, where `prompt` is the\n", + "last human turn and `chosen`/`rejected` are the final assistant responses.\n", + "\n", + "The cell below does that extraction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "\n", + "import pandas as pd\n", + "from datasets import load_dataset\n", + "\n", + "\n", + "def extract_last_human_turn(conversation: str) -> str:\n", + " \"\"\"Extract the last Human turn from an HH-RLHF conversation string.\"\"\"\n", + " turns = re.findall(r\"\\n\\nHuman: (.*?)(?=\\n\\nAssistant:|\\Z)\", conversation, re.DOTALL)\n", + " return turns[-1].strip() if turns else conversation.strip()\n", + "\n", + "\n", + "def extract_last_assistant_turn(conversation: str) -> str:\n", + " \"\"\"Extract the last Assistant turn from an HH-RLHF conversation string.\"\"\"\n", + " turns = re.findall(r\"\\n\\nAssistant: (.*?)(?=\\n\\nHuman:|\\Z)\", conversation, re.DOTALL)\n", + " return turns[-1].strip() if turns else \"\"\n", + "\n", + "\n", + "def prepare_dpo_dataset(split, max_samples=None):\n", + " if max_samples:\n", + " split = split.select(range(min(max_samples, len(split))))\n", + " rows = []\n", + " for ex in split:\n", + " prompt = extract_last_human_turn(ex[\"chosen\"])\n", + " chosen = extract_last_assistant_turn(ex[\"chosen\"])\n", + " rejected = extract_last_assistant_turn(ex[\"rejected\"])\n", + " if prompt and chosen and rejected:\n", + " rows.append({\"prompt\": prompt, \"chosen\": chosen, \"rejected\": rejected})\n", + " return pd.DataFrame(rows)\n", + "\n", + "\n", + "print(\"Downloading Anthropic/hh-rlhf ...\")\n", + "hh = load_dataset(\"Anthropic/hh-rlhf\")\n", + "print(f\"Train size: {len(hh['train'])}, Test size: {len(hh['test'])}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use a subset for a quick experiment. Remove max_samples caps for full training.\n", + "MAX_TRAIN = 5000\n", + "MAX_TEST = 500\n", + "\n", + "train_df = prepare_dpo_dataset(hh[\"train\"], max_samples=MAX_TRAIN)\n", + "test_df = prepare_dpo_dataset(hh[\"test\"], max_samples=MAX_TEST)\n", + "\n", + "train_df.to_csv(\"train.csv\", index=False)\n", + "test_df.to_csv(\"test.csv\", index=False)\n", + "\n", + "print(f\"Saved {len(train_df)} train rows to train.csv\")\n", + "print(f\"Saved {len(test_df)} test rows to test.csv\")\n", + "train_df.head(2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Preview: show a prompt with its chosen vs rejected response\n", + "row = train_df.iloc[0]\n", + "print(\"=== PROMPT ===\")\n", + "print(row[\"prompt\"])\n", + "print(\"\\n=== CHOSEN ===\")\n", + "print(row[\"chosen\"])\n", + "print(\"\\n=== REJECTED ===\")\n", + "print(row[\"rejected\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## DPO training\n", + "\n", + "Direct Preference Optimization (DPO) treats alignment as a classification problem: given a prompt,\n", + "prefer `chosen` over `rejected`. The loss function directly optimises the log-ratio between the\n", + "policy's probability of the chosen response and the reference model's, without needing an explicit\n", + "reward model.\n", + "\n", + "Key hyperparameters:\n", + "- `beta`: KL penalty coefficient (0.1 is a typical starting point). Higher values keep the aligned\n", + " model closer to the base model.\n", + "- `learning_rate`: 5e-7 is much lower than SFT — the policy already knows how to generate text;\n", + " we're only nudging its preferences.\n", + "- `lora r=16, alpha=32`: Standard LoRA settings. Increase `r` for larger models or more complex tasks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "import yaml\n", + "\n", + "from ludwig.api import LudwigModel\n", + "\n", + "dpo_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: meta-llama/Llama-3.1-8B\n", + "\n", + "adapter:\n", + " type: lora\n", + " r: 16\n", + " alpha: 32\n", + " dropout: 0.05\n", + "\n", + "trainer:\n", + " type: dpo\n", + " epochs: 1\n", + " learning_rate: 5.0e-7\n", + " batch_size: 2\n", + " gradient_accumulation_steps: 8\n", + " beta: 0.1\n", + "\n", + "input_features:\n", + " - name: prompt\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: chosen\n", + " type: text\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "dpo_model = LudwigModel(config=dpo_config, logging_level=logging.INFO)\n", + "\n", + "train_stats, _, output_directory = dpo_model.train(\n", + " dataset=\"train.csv\",\n", + " experiment_name=\"hh_rlhf_dpo\",\n", + " output_directory=\"results\",\n", + ")\n", + "\n", + "print(f\"\\nModel saved to: {output_directory}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evaluate\n", + "\n", + "We compare responses from the base model (no alignment) and the DPO-aligned model on a few\n", + "prompts from the test set. You should see the aligned model give more helpful, less evasive answers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load the base model for comparison\n", + "base_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: meta-llama/Llama-3.1-8B\n", + "\n", + "input_features:\n", + " - name: prompt\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: chosen\n", + " type: text\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "base_model = LudwigModel(config=base_config, logging_level=logging.WARNING)\n", + "_ = base_model.train(dataset=\"train.csv\", experiment_name=\"baseline\", output_directory=\"results_base\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eval_prompts = test_df[\"prompt\"].head(3).tolist()\n", + "eval_df = pd.DataFrame({\"prompt\": eval_prompts})\n", + "\n", + "base_preds, _ = base_model.predict(dataset=eval_df)\n", + "dpo_preds, _ = dpo_model.predict(dataset=eval_df)\n", + "\n", + "for i, prompt in enumerate(eval_prompts):\n", + " print(f\"\\n{'=' * 60}\")\n", + " print(f\"PROMPT: {prompt}\")\n", + " print(\"\\n--- Base model ---\")\n", + " print(base_preds.iloc[i][\"chosen_predictions\"])\n", + " print(\"\\n--- DPO-aligned model ---\")\n", + " print(dpo_preds.iloc[i][\"chosen_predictions\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## KTO alternative\n", + "\n", + "**Kahneman-Tversky Optimization (KTO)** is a good alternative to DPO when you cannot collect\n", + "paired preference data. Instead of a (chosen, rejected) pair per prompt, KTO requires only a\n", + "single response with a boolean label indicating whether it was desirable.\n", + "\n", + "This makes it easy to use binary user feedback (thumbs up / thumbs down, click-through, etc.)\n", + "as training signal.\n", + "\n", + "### Dataset format for KTO\n", + "\n", + "We expand each DPO pair into two rows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "rows_kto = []\n", + "for _, row in train_df.iterrows():\n", + " rows_kto.append({\"prompt\": row[\"prompt\"], \"response\": row[\"chosen\"], \"label\": True})\n", + " rows_kto.append({\"prompt\": row[\"prompt\"], \"response\": row[\"rejected\"], \"label\": False})\n", + "\n", + "train_kto_df = pd.DataFrame(rows_kto)\n", + "train_kto_df.to_csv(\"train_kto.csv\", index=False)\n", + "print(f\"KTO dataset: {len(train_kto_df)} rows\")\n", + "train_kto_df.head(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "kto_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: meta-llama/Llama-3.1-8B\n", + "\n", + "adapter:\n", + " type: lora\n", + " r: 16\n", + " alpha: 32\n", + " dropout: 0.05\n", + "\n", + "trainer:\n", + " type: kto\n", + " epochs: 1\n", + " learning_rate: 5.0e-7\n", + " batch_size: 2\n", + " gradient_accumulation_steps: 8\n", + " beta: 0.1\n", + " desirable_weight: 1.0\n", + " undesirable_weight: 1.0\n", + "\n", + "input_features:\n", + " - name: prompt\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: response\n", + " type: text\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "kto_model = LudwigModel(config=kto_config, logging_level=logging.INFO)\n", + "\n", + "_, _, kto_output_dir = kto_model.train(\n", + " dataset=\"train_kto.csv\",\n", + " experiment_name=\"hh_rlhf_kto\",\n", + " output_directory=\"results_kto\",\n", + ")\n", + "\n", + "print(f\"\\nKTO model saved to: {kto_output_dir}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Save and upload\n", + "\n", + "After training, upload the aligned model to HuggingFace Hub to share it or deploy it to an endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Replace with your HuggingFace org/username and desired repo name\n", + "HF_REPO = \"your-username/llama-3.1-8b-dpo-hh-rlhf\"\n", + "\n", + "# Upload via CLI:\n", + "print(f\"ludwig upload hf_hub -r {HF_REPO} -m {output_directory}\")\n", + "\n", + "# Or via Python API:\n", + "# LudwigModel.upload_to_hf_hub(HF_REPO, output_directory)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment to actually run the upload:\n", + "# !ludwig upload hf_hub -r {HF_REPO} -m {output_directory}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "- **Scale up**: Remove the `max_samples` caps and train on the full HH-RLHF dataset (~160k examples).\n", + "- **Try ORPO**: Use `config_orpo.yaml` for single-stage SFT + alignment without a reference model.\n", + "- **Iterate on beta**: Lower `beta` (e.g. 0.05) gives more aggressive alignment but risks reward hacking.\n", + "- **Evaluation**: Use [MT-Bench](https://github.com/lm-sys/FastChat/tree/main/fastchat/llm_judge) or\n", + " [AlpacaEval](https://github.com/tatsu-lab/alpaca_eval) for rigorous alignment benchmarks.\n", + "- **GRPO**: For tasks with a programmatic reward (math, code), see the GRPO trainer docs." + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "A100", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/alignment/config_dpo.yaml b/examples/alignment/config_dpo.yaml new file mode 100644 index 0000000..426c699 --- /dev/null +++ b/examples/alignment/config_dpo.yaml @@ -0,0 +1,27 @@ +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +adapter: + type: lora + r: 16 + alpha: 32 + dropout: 0.05 + +trainer: + type: dpo + epochs: 1 + learning_rate: 5.0e-7 + batch_size: 2 + gradient_accumulation_steps: 8 + beta: 0.1 # KL penalty coefficient — higher values keep the policy closer to the reference + +input_features: + - name: prompt + type: text + +output_features: + - name: chosen + type: text + +backend: + type: local diff --git a/examples/alignment/config_grpo.yaml b/examples/alignment/config_grpo.yaml new file mode 100644 index 0000000..d870ef8 --- /dev/null +++ b/examples/alignment/config_grpo.yaml @@ -0,0 +1,28 @@ +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +adapter: + type: lora + r: 16 + alpha: 32 + +trainer: + type: grpo + epochs: 1 + learning_rate: 5.0e-7 + batch_size: 1 + gradient_accumulation_steps: 16 + grpo_beta: 0.04 # KL penalty — keep the policy close to the reference model + grpo_epsilon: 0.2 # PPO clipping — bounds how much the policy can shift per step + grpo_num_generations: 4 # completions sampled per prompt; higher = better reward estimates + +input_features: + - name: prompt + type: text + +output_features: + - name: response + type: text + +backend: + type: local diff --git a/examples/alignment/config_kto.yaml b/examples/alignment/config_kto.yaml new file mode 100644 index 0000000..8e0a126 --- /dev/null +++ b/examples/alignment/config_kto.yaml @@ -0,0 +1,29 @@ +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +adapter: + type: lora + r: 16 + alpha: 32 + dropout: 0.05 + +trainer: + type: kto + epochs: 1 + learning_rate: 5.0e-7 + batch_size: 2 + gradient_accumulation_steps: 8 + beta: 0.1 + desirable_weight: 1.0 # weight for chosen (positive) samples + undesirable_weight: 1.0 # weight for rejected (negative) samples + +input_features: + - name: prompt + type: text + +output_features: + - name: response + type: text + +backend: + type: local diff --git a/examples/alignment/config_orpo.yaml b/examples/alignment/config_orpo.yaml new file mode 100644 index 0000000..8f2f730 --- /dev/null +++ b/examples/alignment/config_orpo.yaml @@ -0,0 +1,28 @@ +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +adapter: + type: lora + r: 16 + alpha: 32 + dropout: 0.05 + +trainer: + type: orpo + epochs: 1 + learning_rate: 5.0e-7 + batch_size: 2 + gradient_accumulation_steps: 8 + # ORPO combines SFT and alignment in a single objective so no reference model + # forward passes are required and there is no beta KL penalty. + +input_features: + - name: prompt + type: text + +output_features: + - name: chosen + type: text + +backend: + type: local diff --git a/examples/alignment/prepare_dataset.py b/examples/alignment/prepare_dataset.py new file mode 100644 index 0000000..718eb0a --- /dev/null +++ b/examples/alignment/prepare_dataset.py @@ -0,0 +1,132 @@ +"""Prepare the Anthropic HH-RLHF dataset for Ludwig alignment training. + +Downloads `Anthropic/hh-rlhf` from HuggingFace and converts it into the +column format expected by Ludwig's DPO, KTO, and ORPO trainers. + +DPO / ORPO output: train.csv, test.csv + Columns: prompt, chosen, rejected + +KTO output: train_kto.csv, test_kto.csv + Columns: prompt, response, label (label is True for chosen, False for rejected) + +The HH-RLHF dataset stores full multi-turn conversations as raw text with the +pattern: + "\n\nHuman: \n\nAssistant: \n\nHuman: ...\n\nAssistant: ..." + +We extract the last Human turn as the prompt and the final Assistant turn as +the response. For DPO we do this for both `chosen` and `rejected` columns. +""" + +import argparse +import re + +import pandas as pd +from datasets import load_dataset + + +def extract_last_human_turn(conversation: str) -> str: + """Return the last Human turn from a raw HH-RLHF conversation string.""" + human_turns = re.findall(r"\n\nHuman: (.*?)(?=\n\nAssistant:|\Z)", conversation, re.DOTALL) + if human_turns: + return human_turns[-1].strip() + return conversation.strip() + + +def extract_last_assistant_turn(conversation: str) -> str: + """Return the last Assistant turn from a raw HH-RLHF conversation string.""" + assistant_turns = re.findall(r"\n\nAssistant: (.*?)(?=\n\nHuman:|\Z)", conversation, re.DOTALL) + if assistant_turns: + return assistant_turns[-1].strip() + return "" + + +def convert_split(split_data) -> tuple[pd.DataFrame, pd.DataFrame]: + """Convert one HF dataset split into DPO and KTO DataFrames.""" + rows_dpo = [] + rows_kto = [] + + for example in split_data: + chosen_conv = example["chosen"] + rejected_conv = example["rejected"] + + prompt = extract_last_human_turn(chosen_conv) + chosen_response = extract_last_assistant_turn(chosen_conv) + rejected_response = extract_last_assistant_turn(rejected_conv) + + if not prompt or not chosen_response or not rejected_response: + continue + + rows_dpo.append( + { + "prompt": prompt, + "chosen": chosen_response, + "rejected": rejected_response, + } + ) + + # KTO: expand each pair into two rows with a boolean label + rows_kto.append({"prompt": prompt, "response": chosen_response, "label": True}) + rows_kto.append({"prompt": prompt, "response": rejected_response, "label": False}) + + return pd.DataFrame(rows_dpo), pd.DataFrame(rows_kto) + + +def main(): + parser = argparse.ArgumentParser(description="Prepare Anthropic HH-RLHF for Ludwig alignment training.") + parser.add_argument("--output_dir", default=".", help="Directory to write CSV files into.") + parser.add_argument( + "--max_train_samples", + type=int, + default=None, + help="Cap the number of training examples (useful for quick experiments).", + ) + parser.add_argument( + "--max_test_samples", + type=int, + default=None, + help="Cap the number of test examples.", + ) + args = parser.parse_args() + + print("Downloading Anthropic/hh-rlhf …") + dataset = load_dataset("Anthropic/hh-rlhf") + + train_split = dataset["train"] + test_split = dataset["test"] + + if args.max_train_samples: + train_split = train_split.select(range(min(args.max_train_samples, len(train_split)))) + if args.max_test_samples: + test_split = test_split.select(range(min(args.max_test_samples, len(test_split)))) + + print(f"Converting {len(train_split)} train examples …") + train_dpo, train_kto = convert_split(train_split) + + print(f"Converting {len(test_split)} test examples …") + test_dpo, test_kto = convert_split(test_split) + + import os + + os.makedirs(args.output_dir, exist_ok=True) + + train_path = os.path.join(args.output_dir, "train.csv") + test_path = os.path.join(args.output_dir, "test.csv") + train_kto_path = os.path.join(args.output_dir, "train_kto.csv") + test_kto_path = os.path.join(args.output_dir, "test_kto.csv") + + train_dpo.to_csv(train_path, index=False) + test_dpo.to_csv(test_path, index=False) + train_kto.to_csv(train_kto_path, index=False) + test_kto.to_csv(test_kto_path, index=False) + + print(f"\nDPO dataset: {len(train_dpo)} train rows -> {train_path}") + print(f" {len(test_dpo)} test rows -> {test_path}") + print(f"KTO dataset: {len(train_kto)} train rows -> {train_kto_path}") + print(f" {len(test_kto)} test rows -> {test_kto_path}") + print("\nColumns in DPO files: prompt, chosen, rejected") + print("Columns in KTO files: prompt, response, label") + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/examples/alignment/train_dpo.py b/examples/alignment/train_dpo.py new file mode 100644 index 0000000..a3db0b8 --- /dev/null +++ b/examples/alignment/train_dpo.py @@ -0,0 +1,96 @@ +"""DPO alignment training with Ludwig. + +Usage: + python train_dpo.py --dataset train.csv + python train_dpo.py --dataset train.csv --epochs 3 --beta 0.05 + +Prerequisites: + pip install "ludwig[llm]" datasets + export HUGGING_FACE_HUB_TOKEN="" + +The dataset must have columns: prompt, chosen, rejected +Use prepare_dataset.py to produce this file from Anthropic/hh-rlhf. +""" + +import argparse +import logging +import os + +import yaml + +from ludwig.api import LudwigModel + + +def build_config(epochs: int, learning_rate: float, beta: float, batch_size: int) -> dict: + raw = f""" +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +adapter: + type: lora + r: 16 + alpha: 32 + dropout: 0.05 + +trainer: + type: dpo + epochs: {epochs} + learning_rate: {learning_rate} + batch_size: {batch_size} + gradient_accumulation_steps: 8 + beta: {beta} + +input_features: + - name: prompt + type: text + +output_features: + - name: chosen + type: text + +backend: + type: local +""" + return yaml.safe_load(raw) + + +def main(): + parser = argparse.ArgumentParser(description="Run DPO alignment training with Ludwig.") + parser.add_argument("--dataset", required=True, help="Path to the DPO CSV (prompt, chosen, rejected).") + parser.add_argument("--epochs", type=int, default=1) + parser.add_argument("--learning_rate", type=float, default=5e-7) + parser.add_argument("--beta", type=float, default=0.1, help="KL penalty coefficient.") + parser.add_argument("--batch_size", type=int, default=2) + parser.add_argument("--experiment_name", default="hh_rlhf_dpo") + parser.add_argument("--output_dir", default="results") + args = parser.parse_args() + + token = os.environ.get("HUGGING_FACE_HUB_TOKEN") or os.environ.get("HF_TOKEN") + if not token: + raise OSError( + "Set HUGGING_FACE_HUB_TOKEN (or HF_TOKEN) before running. " + "You also need access approval for meta-llama/Llama-3.1-8B." + ) + + config = build_config( + epochs=args.epochs, + learning_rate=args.learning_rate, + beta=args.beta, + batch_size=args.batch_size, + ) + + model = LudwigModel(config=config, logging_level=logging.INFO) + + train_stats, preprocessed_data, output_directory = model.train( + dataset=args.dataset, + experiment_name=args.experiment_name, + output_directory=args.output_dir, + ) + + print(f"\nTraining complete. Results saved to: {output_directory}") + print("To upload the model to HuggingFace Hub:") + print(f" ludwig upload hf_hub -r / -m {output_directory}") + + +if __name__ == "__main__": + main() diff --git a/examples/alignment/train_grpo.py b/examples/alignment/train_grpo.py new file mode 100644 index 0000000..0589c6b --- /dev/null +++ b/examples/alignment/train_grpo.py @@ -0,0 +1,392 @@ +"""GRPO alignment training with Ludwig. + +Group Relative Policy Optimization (Shao et al., 2024 — DeepSeek-R1) trains a language +model using a programmatic reward signal rather than preference pairs. Instead of +"chosen vs rejected" data, you supply a reward function that scores each generated +response. Ludwig samples grpo_num_generations completions per prompt, normalises +rewards within the group, and applies a clipped PPO-style update. + +Usage (standalone): + python train_grpo.py + python train_grpo.py --epochs 2 --lr 3e-7 --output_dir my_run + +Usage (CLI with pre-scored dataset): + ludwig train --config config_grpo.yaml --dataset grpo_train.csv + +Prerequisites: + # Colab: !pip install "ludwig[llm]" + pip install "ludwig[llm]" + export HUGGING_FACE_HUB_TOKEN="" + # You also need access approval for meta-llama/Llama-3.1-8B on HuggingFace Hub. + +How reward functions work with the Ludwig GRPO trainer +------------------------------------------------------- +The GRPO trainer expects a dataset where each row has: + - prompt : the input question + - response : the correct / reference answer (used as a training target) + +The reward function is applied *before* training, in a data preparation step that +scores each (prompt, response) pair and stores a "reward" column. During the GRPO +update the trainer uses those scores — together with the group-normalised advantages +computed across grpo_num_generations rollouts — to weight the policy gradient. + +NOTE: If a future Ludwig version adds a reward_fn parameter directly on LudwigModel +or the GRPO trainer config, you can pass the callable there instead of pre-scoring. +Check the Ludwig changelog for that API addition. +""" + +import argparse +import logging +import os +import re + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Dataset — 100 math word problems generated inline; no download needed +# --------------------------------------------------------------------------- + +MATH_PROBLEMS = [ + { + "prompt": "A baker has 48 cookies. She puts them equally into 6 bags. How many cookies are in each bag?", + "answer": "8", + }, + {"prompt": "Tom has 15 apples. He gives 7 to his friend. How many apples does Tom have now?", "answer": "8"}, + {"prompt": "A train travels 60 miles per hour. How far does it travel in 3 hours?", "answer": "180"}, + {"prompt": "There are 24 students in a class. They form groups of 4. How many groups are there?", "answer": "6"}, + {"prompt": "Maria saves $12 each week. How much does she save in 5 weeks?", "answer": "60"}, + {"prompt": "A rectangle has a length of 8 cm and a width of 5 cm. What is its area?", "answer": "40"}, + { + "prompt": "Jake has 3 boxes of crayons. Each box has 16 crayons. How many crayons does he have in total?", + "answer": "48", + }, + { + "prompt": "A shop sells 35 items on Monday and 47 items on Tuesday. How many items were sold in total?", + "answer": "82", + }, + { + "prompt": "There are 100 balloons. 38 are red and the rest are blue. How many blue balloons are there?", + "answer": "62", + }, + {"prompt": "A car travels 90 km in 2 hours. What is its average speed in km/h?", "answer": "45"}, + {"prompt": "Lucy reads 20 pages per day. How many pages does she read in 7 days?", "answer": "140"}, + {"prompt": "A box holds 12 eggs. How many eggs are in 9 boxes?", "answer": "108"}, + {"prompt": "A pool has 500 litres of water. 175 litres evaporate. How many litres remain?", "answer": "325"}, + {"prompt": "There are 7 shelves with 9 books on each shelf. How many books are there in total?", "answer": "63"}, + {"prompt": "Sam earns $8 per hour. How much does he earn in 6 hours?", "answer": "48"}, + {"prompt": "A garden is 12 m long and 7 m wide. What is its perimeter?", "answer": "38"}, + {"prompt": "There are 5 rows of chairs with 14 chairs in each row. How many chairs are there?", "answer": "70"}, + { + "prompt": "Anna bakes 4 trays of muffins. Each tray has 12 muffins. How many muffins does she bake?", + "answer": "48", + }, + {"prompt": "A rope is 72 cm long. It is cut into 8 equal pieces. How long is each piece?", "answer": "9"}, + {"prompt": "Ben has 45 stickers. He gives 18 to his sister. How many stickers does Ben have left?", "answer": "27"}, + { + "prompt": "A cinema sold 256 tickets on Saturday and 198 on Sunday. " + "How many tickets were sold over the weekend?", + "answer": "454", + }, + {"prompt": "There are 360 minutes in 6 hours. How many minutes are in 1 hour?", "answer": "60"}, + {"prompt": "A cyclist rides 15 km each day. How far does she ride in 4 days?", "answer": "60"}, + {"prompt": "A bookstore has 5 shelves with 30 books each. How many books does it have in total?", "answer": "150"}, + {"prompt": "A pizza is cut into 8 slices. If 3 slices are eaten, how many slices remain?", "answer": "5"}, + {"prompt": "There are 4 quarters in a dollar. How many quarters are in $7?", "answer": "28"}, + {"prompt": "A farmer has 120 eggs. He puts them in cartons of 12. How many cartons does he fill?", "answer": "10"}, + { + "prompt": "A school has 480 pupils split equally across 6 classes. How many pupils are in each class?", + "answer": "80", + }, + { + "prompt": "An ice cream shop sold 45 cones on Friday and 67 on Saturday. How many cones were sold in total?", + "answer": "112", + }, + {"prompt": "A factory makes 250 units per day. How many units does it make in 5 days?", "answer": "1250"}, + { + "prompt": "There are 18 players in a tournament. They are split into teams of 3. How many teams are there?", + "answer": "6", + }, + { + "prompt": "A jar holds 96 sweets. If 4 children share them equally, how many sweets does each child get?", + "answer": "24", + }, + {"prompt": "A plane flies 800 km in 2 hours. What is its average speed?", "answer": "400"}, + { + "prompt": "A garden has 5 rows of flowers with 11 flowers in each row. How many flowers are there?", + "answer": "55", + }, + {"prompt": "James has $200. He spends $74 on shoes. How much money does he have left?", "answer": "126"}, + {"prompt": "A recipe uses 3 cups of flour per cake. How many cups are needed for 7 cakes?", "answer": "21"}, + {"prompt": "There are 50 chairs in a hall. 13 are occupied. How many chairs are empty?", "answer": "37"}, + {"prompt": "A clock ticks 60 times per minute. How many times does it tick in 5 minutes?", "answer": "300"}, + { + "prompt": "A runner completes a 400 m lap in 80 seconds. How many laps does she run in 400 seconds?", + "answer": "5", + }, + {"prompt": "There are 3 packs of pens with 12 pens each. How many pens are there in total?", "answer": "36"}, + {"prompt": "A tank holds 200 gallons. It is currently 40% full. How many gallons are in the tank?", "answer": "80"}, + { + "prompt": "A store has 84 items. They are arranged in 7 equal rows. How many items are in each row?", + "answer": "12", + }, + { + "prompt": "There are 9 months until the concert. How many weeks is that (assuming 4 weeks per month)?", + "answer": "36", + }, + {"prompt": "A frog jumps 3 m each jump. How far does it jump in 15 jumps?", "answer": "45"}, + {"prompt": "A box of pencils costs $3. How much do 11 boxes cost?", "answer": "33"}, + {"prompt": "There are 144 hours in 6 days. How many hours are in 1 day?", "answer": "24"}, + {"prompt": "A pond has 300 fish. 75 are caught and released. How many fish remain?", "answer": "225"}, + {"prompt": "A school bus seats 40 students. How many students can travel in 3 buses?", "answer": "120"}, + { + "prompt": "There are 11 teams in a league. Each team plays every other team once. How many matches are there?", + "answer": "55", + }, + {"prompt": "A bakery makes 60 loaves a day. How many loaves does it make in 2 weeks?", "answer": "840"}, + {"prompt": "A square has sides of 9 cm. What is its perimeter?", "answer": "36"}, + {"prompt": "An author writes 500 words per hour. How many words does she write in 3 hours?", "answer": "1500"}, + {"prompt": "There are 72 hours in 3 days. How many hours are in 5 days?", "answer": "120"}, + {"prompt": "A store discounts a $50 item by 20%. What is the sale price?", "answer": "40"}, + {"prompt": "A farmer plants 8 seeds in each row. He has 9 rows. How many seeds does he plant?", "answer": "72"}, + {"prompt": "A car park has 6 levels with 45 spaces each. How many spaces are there in total?", "answer": "270"}, + {"prompt": "A marathon is 42 km. A runner has covered 28 km. How many km remain?", "answer": "14"}, + {"prompt": "There are 30 days in a month. How many days are in 4 months?", "answer": "120"}, + {"prompt": "A vending machine sells 15 drinks per hour. How many drinks does it sell in 8 hours?", "answer": "120"}, + {"prompt": "There are 6 strings on a guitar. How many strings are on 9 guitars?", "answer": "54"}, + {"prompt": "A worker earns $15 per hour and works 8 hours. How much does she earn?", "answer": "120"}, + {"prompt": "A square room has an area of 64 m². What is the length of each side?", "answer": "8"}, + {"prompt": "A jar has 5 red marbles and 8 blue marbles. How many marbles are there in total?", "answer": "13"}, + {"prompt": "A truck carries 2 tonnes per trip. How many tonnes does it carry in 7 trips?", "answer": "14"}, + {"prompt": "There are 32 students. Half are girls. How many girls are there?", "answer": "16"}, + {"prompt": "A patio is 6 m wide and 9 m long. What is its area?", "answer": "54"}, + {"prompt": "A swimmer does 50 laps per session. How many laps does she do in 6 sessions?", "answer": "300"}, + {"prompt": "A bag of rice weighs 5 kg. How much do 8 bags weigh?", "answer": "40"}, + {"prompt": "There are 7 days in a week. How many days are in 13 weeks?", "answer": "91"}, + {"prompt": "A factory produces 1200 items in 4 hours. How many items per hour?", "answer": "300"}, + { + "prompt": "A road is 3.5 km long. Two cars start at each end and drive toward each other " + "at 1.75 km/h each. How long until they meet (in hours)?", + "answer": "1", + }, + {"prompt": "There are 18 biscuits. Each person eats 3. How many people can eat?", "answer": "6"}, + { + "prompt": "A pool requires 8 hours to fill. After 5 hours, how much is left to fill (as a fraction)?", + "answer": "3/8", + }, + {"prompt": "A box has 4 layers with 25 chocolates per layer. How many chocolates are in the box?", "answer": "100"}, + {"prompt": "A phone battery lasts 12 hours. After 9 hours of use, what percentage remains?", "answer": "25"}, + {"prompt": "There are 40 red and 25 blue tiles. How many tiles are there in total?", "answer": "65"}, + {"prompt": "A rope is 54 m long. It is divided into 9 equal pieces. How long is each piece?", "answer": "6"}, + {"prompt": "A bag has 3 green, 4 yellow, and 5 purple balls. How many balls are there?", "answer": "12"}, + {"prompt": "Each page has 30 lines. How many lines are on 8 pages?", "answer": "240"}, + {"prompt": "A worker packs 6 boxes per hour. How many boxes in 9 hours?", "answer": "54"}, + {"prompt": "There are 1000 metres in a kilometre. How many metres in 7.5 km?", "answer": "7500"}, + {"prompt": "A container holds 5 litres. How many containers are needed for 35 litres?", "answer": "7"}, + { + "prompt": "A car travels 110 km in 2 hours. How far does it travel in 5 hours at the same speed?", + "answer": "275", + }, + {"prompt": "There are 52 cards in a deck. How many cards are in 3 decks?", "answer": "156"}, + {"prompt": "A hotel has 12 floors with 18 rooms each. How many rooms are there?", "answer": "216"}, + { + "prompt": "A pie is split into 6 equal slices. Two people each eat 2 slices. How many slices remain?", + "answer": "2", + }, + {"prompt": "A printing press produces 40 pages per minute. How many pages in 15 minutes?", "answer": "600"}, + {"prompt": "There are 200 pupils. 45% are boys. How many boys are there?", "answer": "90"}, + {"prompt": "A wall is 3 m tall and 8 m wide. What is its area?", "answer": "24"}, + {"prompt": "A car uses 6 litres of fuel per 100 km. How much fuel is needed for 300 km?", "answer": "18"}, + {"prompt": "A team scores 3 goals in each of 6 matches. How many goals in total?", "answer": "18"}, + {"prompt": "There are 11 rows of seats with 20 seats each. How many seats are there?", "answer": "220"}, + {"prompt": "A box contains 50 nails. After using 23, how many remain?", "answer": "27"}, + {"prompt": "A wheel turns 360 degrees in one full rotation. How many degrees in 5 rotations?", "answer": "1800"}, + {"prompt": "A shop receives 3 deliveries of 40 items each. How many items did it receive?", "answer": "120"}, + {"prompt": "There are 15 biscuits on a plate. 6 are eaten. How many remain?", "answer": "9"}, +] + + +# --------------------------------------------------------------------------- +# Reward function +# --------------------------------------------------------------------------- + + +def reward_fn(prompt: str, response: str) -> float: + """Score a model response by checking if it contains the correct numerical answer. + + Returns 1.0 if the response contains the expected answer, 0.0 otherwise. The correct answer is looked up from the + global ANSWER_LOOKUP dict which is built from MATH_PROBLEMS at startup. + + This is a simple exact-match reward. Real applications might use an LLM judge, a code execution sandbox, or a + symbolic verifier. + """ + expected = ANSWER_LOOKUP.get(prompt.strip()) + if expected is None: + return 0.0 + # Accept the answer if it appears as a standalone number/fraction in the response + pattern = r"(? pd.DataFrame: + """Build a training DataFrame by applying the reward function to each example. + + For GRPO the dataset needs at minimum: + - prompt : the input text + - response : the reference / target text + - reward : a float score for each (prompt, response) pair + + NOTE: The Ludwig GRPO trainer does not yet accept a reward_fn callable at + train-time (as of v0.11.dev). Instead, rewards are pre-computed here and + stored in a 'reward' column that the trainer reads from the dataset. If a + future version exposes a reward_fn parameter on LudwigModel or the GRPO + config, you can pass `reward_fn=reward_function` there directly. + """ + rows = [] + for item in MATH_PROBLEMS: + prompt = item["prompt"] + response = item["answer"] + reward = reward_function(prompt, response) + rows.append({"prompt": prompt, "response": response, "reward": reward}) + df = pd.DataFrame(rows) + logger.info( + "Dataset built: %d examples, mean reward=%.3f", + len(df), + df["reward"].mean(), + ) + return df + + +# --------------------------------------------------------------------------- +# Config builder +# --------------------------------------------------------------------------- + + +def build_config(epochs: int, learning_rate: float, batch_size: int) -> dict: + raw = f""" +model_type: llm +base_model: meta-llama/Llama-3.1-8B + +adapter: + type: lora + r: 16 + alpha: 32 + +trainer: + type: grpo + epochs: {epochs} + learning_rate: {learning_rate} + batch_size: {batch_size} + gradient_accumulation_steps: 16 + grpo_beta: 0.04 + grpo_epsilon: 0.2 + grpo_num_generations: 4 + +input_features: + - name: prompt + type: text + +output_features: + - name: response + type: text + +backend: + type: local +""" + return yaml.safe_load(raw) + + +# --------------------------------------------------------------------------- +# GPU check +# --------------------------------------------------------------------------- + + +def check_gpu(): + try: + import torch + + if torch.cuda.is_available() and torch.cuda.device_count() > 0: + name = torch.cuda.get_device_name(0) + vram = torch.cuda.get_device_properties(0).total_memory / 1024**3 + logger.info("GPU detected: %s (%.1f GiB VRAM)", name, vram) + if vram < 20: + logger.warning( + "Only %.1f GiB VRAM detected. Llama-3.1-8B requires at least 40 GiB " + "for GRPO training. Consider using a smaller base model or enabling " + "quantisation (e.g. bitsandbytes 4-bit).", + vram, + ) + else: + logger.warning( + "No GPU detected. GRPO training on a 7-8B model will be extremely slow on CPU. " + "On Colab, go to Runtime > Change runtime type and select a GPU." + ) + except ImportError: + pass + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") + + parser = argparse.ArgumentParser(description="GRPO alignment training with Ludwig.") + parser.add_argument("--epochs", type=int, default=1) + parser.add_argument("--lr", type=float, default=5e-7) + parser.add_argument("--batch_size", type=int, default=1) + parser.add_argument("--experiment_name", default="math_grpo") + parser.add_argument("--output_dir", default="results") + args = parser.parse_args() + + # --- HuggingFace token --- + token = os.environ.get("HUGGING_FACE_HUB_TOKEN") or os.environ.get("HF_TOKEN") + if not token: + raise OSError( + "Set HUGGING_FACE_HUB_TOKEN (or HF_TOKEN) before running. " + "You also need access approval for meta-llama/Llama-3.1-8B on HuggingFace Hub." + ) + + check_gpu() + + # --- Build dataset with pre-computed rewards --- + df = build_dataset(reward_fn) + + # --- Build Ludwig config --- + config = build_config( + epochs=args.epochs, + learning_rate=args.lr, + batch_size=args.batch_size, + ) + + # --- Train --- + model = LudwigModel(config=config, logging_level=logging.INFO) + + train_stats, _, output_directory = model.train( + dataset=df, + experiment_name=args.experiment_name, + output_directory=args.output_dir, + ) + + print(f"\nTraining complete. Results saved to: {output_directory}") + print("To upload the aligned model to HuggingFace Hub:") + print(f" ludwig upload hf_hub -r / -m {output_directory}") + + +if __name__ == "__main__": + main() diff --git a/examples/anomaly_detection/README.md b/examples/anomaly_detection/README.md new file mode 100644 index 0000000..164a892 --- /dev/null +++ b/examples/anomaly_detection/README.md @@ -0,0 +1,119 @@ +# Anomaly Detection with Deep SVDD, SAD, and DROCC + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/anomaly_detection/anomaly_detection.ipynb) + +This example shows how to train an anomaly detection model with Ludwig using the `anomaly` output feature type. The model learns a compact representation of "normal" sensor data using three complementary hypersphere-based objectives: + +- **Deep SVDD** — unsupervised, trains only on normal samples +- **Deep SAD** — semi-supervised, uses a small set of labeled anomalies at training time +- **DROCC** — unsupervised with adversarial robustness, recommended for expressive encoders + +At inference time each sample receives an `anomaly_score` equal to its squared distance from the learned hypersphere centre. Higher scores indicate more anomalous samples. + +## Prerequisites + +```bash +pip install ludwig +``` + +## Dataset + +The example uses a synthetic sensor dataset with four numeric features (`sensor_a`, `sensor_b`, `sensor_c`, `timestamp_hour`). Normal samples are drawn from a Gaussian distribution centred at the origin; anomalous samples have a large offset. The train split contains **only normal samples**; the test split contains both normal and anomalous samples for evaluation. + +## Loss variants + +### Deep SVDD (unsupervised) + +```yaml +output_features: + - name: anomaly + type: anomaly + loss: + type: deep_svdd + nu: 0.1 # fraction of points allowed outside the hypersphere +``` + +Hard-boundary objective: minimise the mean squared distance of all normal training representations to the hypersphere centre `c`. The `nu` parameter controls soft-boundary relaxation (set to `0` for hard SVDD). + +Full config: [`config_deep_svdd.yaml`](config_deep_svdd.yaml) + +### Deep SAD (semi-supervised) + +```yaml +output_features: + - name: anomaly + type: anomaly + loss: + type: deep_sad + eta: 1.0 # weight for the labeled anomaly repulsion term +``` + +Extends Deep SVDD with labeled anomaly support. Normal and unlabeled samples (label `0` or `-1`) are pulled toward `c`; labeled anomalies (label `1`) are pushed away. Provide a small fraction of labeled anomaly rows in the training data with `anomaly=1`. + +Full config: [`config_deep_sad.yaml`](config_deep_sad.yaml) + +### DROCC (robust unsupervised) + +```yaml +output_features: + - name: anomaly + type: anomaly + loss: + type: drocc + perturbation_strength: 0.1 + num_perturbation_steps: 5 +``` + +Prevents hypersphere collapse via an adversarial perturbation regulariser. Recommended when using expressive encoders (e.g. transformers) that are prone to degenerate solutions where all representations collapse to a single point. + +Full config: [`config_drocc.yaml`](config_drocc.yaml) + +## Running the example + +### CLI + +```bash +# Train +ludwig train --config config_deep_svdd.yaml --dataset /tmp/sensors_train.csv + +# Predict (score test samples) +ludwig predict --model_path results/experiment_run/model \ + --dataset /tmp/sensors_test.csv + +# Evaluate (requires labeled anomaly column in test CSV) +ludwig evaluate --model_path results/experiment_run/model \ + --dataset /tmp/sensors_test.csv +``` + +### Python API + +```python +import pandas as pd +from ludwig.api import LudwigModel + +# Load data +train_df = pd.read_csv("/tmp/sensors_train.csv") +test_df = pd.read_csv("/tmp/sensors_test.csv") + +# Train +model = LudwigModel("config_deep_svdd.yaml", logging_level="ERROR") +results = model.train(dataset=train_df) + +# Predict — returns a DataFrame with anomaly_score_predictions column +predictions, _ = model.predict(dataset=test_df) +print(predictions[["anomaly_anomaly_score_predictions"]].describe()) +``` + +For a full walkthrough including score distribution plots and AUC comparison, open the notebook in Colab: + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/anomaly_detection/anomaly_detection.ipynb) + +## Files + +| File | Description | +| ------------------------- | ----------------------------------------- | +| `anomaly_detection.ipynb` | End-to-end Colab notebook | +| `config_deep_svdd.yaml` | Deep SVDD config | +| `config_deep_sad.yaml` | Deep SAD (semi-supervised) config | +| `config_drocc.yaml` | DROCC config | +| `train.py` | Standalone training and evaluation script | diff --git a/examples/anomaly_detection/anomaly_detection.ipynb b/examples/anomaly_detection/anomaly_detection.ipynb new file mode 100644 index 0000000..8c89d18 --- /dev/null +++ b/examples/anomaly_detection/anomaly_detection.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "# Anomaly Detection with Deep SVDD, SAD, and DROCC\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/anomaly_detection/anomaly_detection.ipynb)\n", + "\n", + "This notebook demonstrates **unsupervised anomaly detection** using Ludwig's `anomaly` output feature.\n", + "The model learns a compact representation of normal data inside a hypersphere in latent space.\n", + "At inference time, each sample is scored by its squared distance to the centre of that hypersphere:\n", + "samples far from the centre are flagged as anomalies.\n", + "\n", + "```\n", + "anomaly_score = ||z - c||^2\n", + "```\n", + "\n", + "**What this notebook covers:**\n", + "\n", + "1. Generating a synthetic sensor dataset (normal vs anomalous readings)\n", + "2. Training with **Deep SVDD** (fully unsupervised)\n", + "3. Visualising the anomaly score distribution for normal vs anomalous samples\n", + "4. Training with **Deep SAD** (semi-supervised — uses a few labeled anomalies)\n", + "5. Training with **DROCC** (robust unsupervised — adversarial perturbations prevent collapse)\n", + "6. Comparing AUC-ROC across all three methods" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acae54e37e7d407bbb7b55eff062a284", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "9a63283cbaf04dbcab1f6479b197f3a8", + "metadata": {}, + "source": [ + "## Generate synthetic sensor data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd0d8092fe74a7c96281538738b07e2", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "RNG = np.random.default_rng(42)\n", + "\n", + "N_NORMAL = 800\n", + "N_ANOMALY = 200\n", + "\n", + "# Normal samples: Gaussian near origin\n", + "normal_df = pd.DataFrame(\n", + " {\n", + " \"sensor_a\": RNG.normal(0.0, 1.0, N_NORMAL),\n", + " \"sensor_b\": RNG.normal(0.0, 1.0, N_NORMAL),\n", + " \"sensor_c\": RNG.normal(0.0, 1.0, N_NORMAL),\n", + " \"timestamp_hour\": RNG.integers(0, 24, N_NORMAL).astype(float),\n", + " \"anomaly\": 0.0,\n", + " }\n", + ")\n", + "\n", + "# Anomalous samples: large offset from origin\n", + "anomaly_df = pd.DataFrame(\n", + " {\n", + " \"sensor_a\": RNG.normal(6.0, 1.0, N_ANOMALY),\n", + " \"sensor_b\": RNG.normal(6.0, 1.0, N_ANOMALY),\n", + " \"sensor_c\": RNG.normal(6.0, 1.0, N_ANOMALY),\n", + " \"timestamp_hour\": RNG.integers(0, 24, N_ANOMALY).astype(float),\n", + " \"anomaly\": 1.0,\n", + " }\n", + ")\n", + "\n", + "# ---------------------------------------------------------------\n", + "# Train split: ONLY normal samples (anomaly detection is unsupervised).\n", + "# Validation / test splits: mix of normal and anomalous.\n", + "# split column: 0=train, 1=validation, 2=test\n", + "# ---------------------------------------------------------------\n", + "\n", + "normal_idx = normal_df.index.tolist()\n", + "RNG.shuffle(normal_idx)\n", + "n_train = int(0.7 * len(normal_idx))\n", + "n_val = int(0.15 * len(normal_idx))\n", + "\n", + "normal_df[\"split\"] = 2 # test by default\n", + "normal_df.loc[normal_idx[:n_train], \"split\"] = 0\n", + "normal_df.loc[normal_idx[n_train : n_train + n_val], \"split\"] = 1\n", + "\n", + "anom_idx = anomaly_df.index.tolist()\n", + "RNG.shuffle(anom_idx)\n", + "n_val_anom = len(anom_idx) // 2\n", + "anomaly_df[\"split\"] = 2\n", + "anomaly_df.loc[anom_idx[:n_val_anom], \"split\"] = 1\n", + "\n", + "# Training CSV: only normal rows\n", + "train_df = normal_df[normal_df[\"split\"] == 0].copy()\n", + "\n", + "# Test CSV: validation + test rows (normal and anomalous), used for scoring\n", + "val_test_df = pd.concat([normal_df[normal_df[\"split\"] != 0], anomaly_df], ignore_index=True)\n", + "\n", + "train_df.to_csv(\"/tmp/sensors_train.csv\", index=False)\n", + "val_test_df.to_csv(\"/tmp/sensors_test.csv\", index=False)\n", + "\n", + "print(f\"Train samples : {len(train_df)} (all normal)\")\n", + "print(f\"Test samples : {len(val_test_df)}\")\n", + "print(f\" Normal : {(val_test_df['anomaly'] == 0).sum()}\")\n", + "print(f\" Anomalous : {(val_test_df['anomaly'] == 1).sum()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "72eea5119410473aa328ad9291626812", + "metadata": {}, + "source": [ + "## Train: Deep SVDD\n", + "\n", + "Deep SVDD (Ruff et al., ICML 2018) is the simplest variant: it minimises the mean squared\n", + "distance of all training representations to a hypersphere centre `c`, learned during training.\n", + "Only normal samples are used for training — no anomaly labels are required.\n", + "\n", + "The `nu` parameter controls the soft-boundary relaxation: `nu=0.1` allows up to 10% of\n", + "training points outside the hypersphere." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8edb47106e1a46a883d545849b8ab81b", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "from ludwig.api import LudwigModel\n", + "\n", + "config_svdd_str = \"\"\"\n", + "model_type: ecd\n", + "\n", + "input_features:\n", + " - name: sensor_a\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_b\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_c\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: timestamp_hour\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + "\n", + "output_features:\n", + " - name: anomaly\n", + " type: anomaly\n", + " loss:\n", + " type: deep_svdd\n", + " nu: 0.1\n", + "\n", + "trainer:\n", + " epochs: 20\n", + " learning_rate: 0.001\n", + "\n", + "combiner:\n", + " type: concat\n", + " fc_layers:\n", + " - output_size: 64\n", + " - output_size: 32\n", + "\"\"\"\n", + "\n", + "config_svdd = yaml.safe_load(config_svdd_str)\n", + "\n", + "model_svdd = LudwigModel(config_svdd, logging_level=30)\n", + "train_stats_svdd, _, _ = model_svdd.train(dataset=train_df)\n", + "\n", + "print(\"Deep SVDD training complete.\")" + ] + }, + { + "cell_type": "markdown", + "id": "10185d26023b46108eb7d9f57d49d2b3", + "metadata": {}, + "source": [ + "## Evaluate: score distribution\n", + "\n", + "Run inference on the test set (which contains both normal and anomalous samples) and plot\n", + "the distribution of `anomaly_score_predictions`. A good model will produce clearly separated\n", + "distributions for the two classes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8763a12b2bbd4a93a75aff182afb95dc", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from sklearn.metrics import roc_auc_score\n", + "\n", + "preds_svdd, _ = model_svdd.predict(dataset=val_test_df)\n", + "\n", + "score_col = \"anomaly_anomaly_score_predictions\"\n", + "scores_svdd = preds_svdd[score_col].values\n", + "true_labels = val_test_df[\"anomaly\"].values\n", + "\n", + "auc_svdd = roc_auc_score(true_labels, scores_svdd)\n", + "print(f\"Deep SVDD AUC-ROC: {auc_svdd:.4f}\")\n", + "\n", + "# --- Plot score distribution ---\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "\n", + "ax.hist(scores_svdd[true_labels == 0], bins=40, alpha=0.6, label=\"Normal\", color=\"steelblue\")\n", + "ax.hist(scores_svdd[true_labels == 1], bins=40, alpha=0.6, label=\"Anomalous\", color=\"tomato\")\n", + "\n", + "ax.set_xlabel(\"Anomaly score ||z - c||^2\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(f\"Deep SVDD — anomaly score distribution (AUC = {auc_svdd:.3f})\")\n", + "ax.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7623eae2785240b9bd12b16a66d81610", + "metadata": {}, + "source": [ + "## Try Deep SAD (semi-supervised)\n", + "\n", + "Deep SAD (Ruff et al., ICLR 2020) extends Deep SVDD by incorporating a small set of\n", + "**labeled anomalies** during training. Normal/unlabeled samples are pulled toward centre `c`;\n", + "labeled anomalies are pushed away. This typically improves score separation when even a\n", + "handful of anomaly examples are available.\n", + "\n", + "In the dataset, set `anomaly=1` for labeled anomaly rows, `anomaly=0` for normal rows, and\n", + "`anomaly=-1` (or leave the column absent) for unlabeled rows that should be ignored by the\n", + "repulsion term.\n", + "\n", + "Here we inject ~10% labeled anomalies into the training split (`labeled_anomalies_ratio=0.1`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cdc8c89c7104fffa095e18ddfef8986", + "metadata": {}, + "outputs": [], + "source": [ + "config_sad_str = \"\"\"\n", + "model_type: ecd\n", + "\n", + "input_features:\n", + " - name: sensor_a\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_b\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_c\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: timestamp_hour\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + "\n", + "output_features:\n", + " - name: anomaly\n", + " type: anomaly\n", + " loss:\n", + " type: deep_sad\n", + " eta: 1.0\n", + "\n", + "trainer:\n", + " epochs: 20\n", + " learning_rate: 0.001\n", + "\n", + "combiner:\n", + " type: concat\n", + " fc_layers:\n", + " - output_size: 64\n", + " - output_size: 32\n", + "\"\"\"\n", + "\n", + "config_sad = yaml.safe_load(config_sad_str)\n", + "\n", + "# Inject ~10% labeled anomalies into the training set\n", + "N_LABELED = max(1, int(0.1 * len(train_df)))\n", + "labeled_anom = anomaly_df.sample(n=N_LABELED, random_state=0).copy()\n", + "labeled_anom[\"split\"] = 0\n", + "sad_train_df = pd.concat([train_df, labeled_anom], ignore_index=True)\n", + "\n", + "print(\n", + " f\"Deep SAD training set: {len(sad_train_df)} rows \"\n", + " f\"({N_LABELED} labeled anomalies = \"\n", + " f\"{100 * N_LABELED / len(sad_train_df):.1f}%)\"\n", + ")\n", + "\n", + "model_sad = LudwigModel(config_sad, logging_level=30)\n", + "train_stats_sad, _, _ = model_sad.train(dataset=sad_train_df)\n", + "\n", + "preds_sad, _ = model_sad.predict(dataset=val_test_df)\n", + "scores_sad = preds_sad[score_col].values\n", + "auc_sad = roc_auc_score(true_labels, scores_sad)\n", + "print(f\"Deep SAD AUC-ROC: {auc_sad:.4f}\")\n", + "\n", + "# --- Plot ---\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.hist(scores_sad[true_labels == 0], bins=40, alpha=0.6, label=\"Normal\", color=\"steelblue\")\n", + "ax.hist(scores_sad[true_labels == 1], bins=40, alpha=0.6, label=\"Anomalous\", color=\"tomato\")\n", + "ax.set_xlabel(\"Anomaly score ||z - c||^2\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(f\"Deep SAD — score distribution (AUC = {auc_sad:.3f})\")\n", + "ax.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b118ea5561624da68c537baed56e602f", + "metadata": {}, + "source": [ + "## Try DROCC\n", + "\n", + "DROCC (Goyal et al., ICML 2020) prevents hypersphere **collapse** — a failure mode where an\n", + "expressive encoder maps all inputs to the same point, achieving a trivially low loss while\n", + "learning nothing useful. It does so by generating adversarial perturbations around each\n", + "normal training point and penalising the model if the perturbed points score as normal.\n", + "\n", + "Key parameters:\n", + "- `perturbation_strength`: magnitude of adversarial perturbations (default `0.1`)\n", + "- `num_perturbation_steps`: gradient ascent steps per sample (default `5`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "938c804e27f84196a10c8828c723f798", + "metadata": {}, + "outputs": [], + "source": [ + "config_drocc_str = \"\"\"\n", + "model_type: ecd\n", + "\n", + "input_features:\n", + " - name: sensor_a\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_b\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_c\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: timestamp_hour\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + "\n", + "output_features:\n", + " - name: anomaly\n", + " type: anomaly\n", + " loss:\n", + " type: drocc\n", + " perturbation_strength: 0.1\n", + " num_perturbation_steps: 5\n", + "\n", + "trainer:\n", + " epochs: 20\n", + " learning_rate: 0.001\n", + "\n", + "combiner:\n", + " type: concat\n", + " fc_layers:\n", + " - output_size: 64\n", + " - output_size: 32\n", + "\"\"\"\n", + "\n", + "config_drocc = yaml.safe_load(config_drocc_str)\n", + "\n", + "model_drocc = LudwigModel(config_drocc, logging_level=30)\n", + "train_stats_drocc, _, _ = model_drocc.train(dataset=train_df)\n", + "\n", + "preds_drocc, _ = model_drocc.predict(dataset=val_test_df)\n", + "scores_drocc = preds_drocc[score_col].values\n", + "auc_drocc = roc_auc_score(true_labels, scores_drocc)\n", + "print(f\"DROCC AUC-ROC: {auc_drocc:.4f}\")\n", + "\n", + "# --- Plot ---\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.hist(scores_drocc[true_labels == 0], bins=40, alpha=0.6, label=\"Normal\", color=\"steelblue\")\n", + "ax.hist(scores_drocc[true_labels == 1], bins=40, alpha=0.6, label=\"Anomalous\", color=\"tomato\")\n", + "ax.set_xlabel(\"Anomaly score ||z - c||^2\")\n", + "ax.set_ylabel(\"Count\")\n", + "ax.set_title(f\"DROCC — score distribution (AUC = {auc_drocc:.3f})\")\n", + "ax.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "504fb2a444614c0babb325280ed9130a", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "The table below compares the three loss variants on the synthetic sensor dataset.\n", + "AUC-ROC close to 1.0 means the model can almost perfectly rank anomalous samples above\n", + "normal ones. The separation ratio is the mean anomaly score divided by the mean normal score:\n", + "a ratio much greater than 1 indicates clear separation in score space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59bbdb311c014d738909a11f9e486628", + "metadata": {}, + "outputs": [], + "source": [ + "summary_rows = []\n", + "for name, scores, auc in [\n", + " (\"Deep SVDD (unsupervised)\", scores_svdd, auc_svdd),\n", + " (\"Deep SAD (semi-supervised)\", scores_sad, auc_sad),\n", + " (\"DROCC (robust unsup.)\", scores_drocc, auc_drocc),\n", + "]:\n", + " normal_s = scores[true_labels == 0]\n", + " anom_s = scores[true_labels == 1]\n", + " sep = anom_s.mean() / (normal_s.mean() + 1e-9)\n", + " summary_rows.append(\n", + " {\n", + " \"Method\": name,\n", + " \"AUC-ROC\": round(auc, 4),\n", + " \"Mean normal score\": round(float(normal_s.mean()), 4),\n", + " \"Mean anomaly score\": round(float(anom_s.mean()), 4),\n", + " \"Separation ratio\": round(float(sep), 2),\n", + " }\n", + " )\n", + "\n", + "summary_df = pd.DataFrame(summary_rows)\n", + "print(summary_df.to_string(index=False))" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/anomaly_detection/config_deep_sad.yaml b/examples/anomaly_detection/config_deep_sad.yaml new file mode 100644 index 0000000..8e190dd --- /dev/null +++ b/examples/anomaly_detection/config_deep_sad.yaml @@ -0,0 +1,44 @@ +model_type: ecd + +input_features: + - name: sensor_a + type: number + preprocessing: + normalization: zscore + - name: sensor_b + type: number + preprocessing: + normalization: zscore + - name: sensor_c + type: number + preprocessing: + normalization: zscore + - name: timestamp_hour + type: number + preprocessing: + normalization: zscore + +output_features: + - name: anomaly + type: anomaly + loss: + type: deep_sad + # eta: weight for the labeled anomaly repulsion term. + # Labeled anomalies (target=1 in the dataset) are pushed away from the hypersphere + # centre while normal/unlabeled samples are pulled toward it. + eta: 1.0 + +trainer: + epochs: 20 + learning_rate: 0.001 + +combiner: + type: concat + fc_layers: + - output_size: 64 + - output_size: 32 + +# Deep SAD is semi-supervised: include a small fraction of labeled anomaly rows +# in the training set with the anomaly column set to 1. All other training rows +# should have anomaly=0 (normal) or anomaly=-1 (unlabeled/ignored). +# labeled_anomalies_ratio: 0.1 # approximately 10% of training rows are labeled anomalies diff --git a/examples/anomaly_detection/config_deep_svdd.yaml b/examples/anomaly_detection/config_deep_svdd.yaml new file mode 100644 index 0000000..987a49d --- /dev/null +++ b/examples/anomaly_detection/config_deep_svdd.yaml @@ -0,0 +1,38 @@ +model_type: ecd + +input_features: + - name: sensor_a + type: number + preprocessing: + normalization: zscore + - name: sensor_b + type: number + preprocessing: + normalization: zscore + - name: sensor_c + type: number + preprocessing: + normalization: zscore + - name: timestamp_hour + type: number + preprocessing: + normalization: zscore + +output_features: + - name: anomaly + type: anomaly + loss: + type: deep_svdd + # nu: fraction of training samples allowed outside the hypersphere (soft-boundary). + # Set to 0 for hard-boundary SVDD where all points are pulled toward centre c. + nu: 0.1 + +trainer: + epochs: 20 + learning_rate: 0.001 + +combiner: + type: concat + fc_layers: + - output_size: 64 + - output_size: 32 diff --git a/examples/anomaly_detection/config_drocc.yaml b/examples/anomaly_detection/config_drocc.yaml new file mode 100644 index 0000000..cb3f285 --- /dev/null +++ b/examples/anomaly_detection/config_drocc.yaml @@ -0,0 +1,41 @@ +model_type: ecd + +input_features: + - name: sensor_a + type: number + preprocessing: + normalization: zscore + - name: sensor_b + type: number + preprocessing: + normalization: zscore + - name: sensor_c + type: number + preprocessing: + normalization: zscore + - name: timestamp_hour + type: number + preprocessing: + normalization: zscore + +output_features: + - name: anomaly + type: anomaly + loss: + type: drocc + # perturbation_strength: magnitude of adversarial perturbations applied to prevent + # hypersphere collapse (all representations converging to center c). + # Typical range: 0.01–0.5. Increase if the model collapses early. + perturbation_strength: 0.1 + # num_perturbation_steps: gradient ascent steps for generating each perturbation. + num_perturbation_steps: 5 + +trainer: + epochs: 20 + learning_rate: 0.001 + +combiner: + type: concat + fc_layers: + - output_size: 64 + - output_size: 32 diff --git a/examples/anomaly_detection/train.py b/examples/anomaly_detection/train.py new file mode 100644 index 0000000..897acfb --- /dev/null +++ b/examples/anomaly_detection/train.py @@ -0,0 +1,190 @@ +# Colab: !pip install ludwig +"""Anomaly detection with Ludwig using Deep SVDD, Deep SAD, and DROCC losses. + +Generates synthetic sensor data, trains all three model variants, evaluates on a +held-out test set containing both normal and anomalous samples, and prints an AUC-ROC +comparison table. + +Usage: + python train.py +""" + +import numpy as np +import pandas as pd +from sklearn.metrics import roc_auc_score + +from ludwig.api import LudwigModel + +# --------------------------------------------------------------------------- +# 1. Generate synthetic sensor data +# --------------------------------------------------------------------------- + +RNG = np.random.default_rng(42) + +N_NORMAL = 800 +N_ANOMALY = 200 + +# Normal samples: Gaussian near origin +normal = pd.DataFrame( + { + "sensor_a": RNG.normal(0.0, 1.0, N_NORMAL), + "sensor_b": RNG.normal(0.0, 1.0, N_NORMAL), + "sensor_c": RNG.normal(0.0, 1.0, N_NORMAL), + "timestamp_hour": RNG.integers(0, 24, N_NORMAL).astype(float), + "anomaly": 0.0, + } +) + +# Anomalous samples: large offset from origin +anomalous = pd.DataFrame( + { + "sensor_a": RNG.normal(6.0, 1.0, N_ANOMALY), + "sensor_b": RNG.normal(6.0, 1.0, N_ANOMALY), + "sensor_c": RNG.normal(6.0, 1.0, N_ANOMALY), + "timestamp_hour": RNG.integers(0, 24, N_ANOMALY).astype(float), + "anomaly": 1.0, + } +) + +all_data = pd.concat([normal, anomalous], ignore_index=True) + +# Train split: ONLY normal samples (anomaly detection is unsupervised) +# Val split: mix of normal and anomalous for threshold selection +# Test split: mix for final evaluation + +normal_idx = all_data[all_data["anomaly"] == 0].index.tolist() +anomaly_idx = all_data[all_data["anomaly"] == 1].index.tolist() + +RNG.shuffle(normal_idx) +n_train = int(0.7 * len(normal_idx)) +n_val = int(0.15 * len(normal_idx)) + +train_idx = normal_idx[:n_train] +val_normal_idx = normal_idx[n_train : n_train + n_val] +test_normal_idx = normal_idx[n_train + n_val :] + +RNG.shuffle(anomaly_idx) +n_val_anom = len(anomaly_idx) // 2 +val_anom_idx = anomaly_idx[:n_val_anom] +test_anom_idx = anomaly_idx[n_val_anom:] + +# Assign split column: 0=train, 1=val, 2=test +all_data["split"] = -1 +all_data.loc[train_idx, "split"] = 0 +all_data.loc[val_normal_idx, "split"] = 1 +all_data.loc[val_anom_idx, "split"] = 1 +all_data.loc[test_normal_idx, "split"] = 2 +all_data.loc[test_anom_idx, "split"] = 2 + +train_df = all_data[all_data["split"] == 0].copy() +test_df = all_data[all_data["split"].isin([1, 2])].copy() + +train_df.to_csv("/tmp/sensors_train.csv", index=False) +test_df.to_csv("/tmp/sensors_test.csv", index=False) + +print(f"Train samples: {len(train_df)} (all normal)") +print(f"Test samples: {len(test_df)} ({(test_df['anomaly'] == 1).sum()} anomalous)") + +# --------------------------------------------------------------------------- +# 2. Helper: build Ludwig config dict +# --------------------------------------------------------------------------- + +INPUT_FEATURES = [ + {"name": "sensor_a", "type": "number", "preprocessing": {"normalization": "zscore"}}, + {"name": "sensor_b", "type": "number", "preprocessing": {"normalization": "zscore"}}, + {"name": "sensor_c", "type": "number", "preprocessing": {"normalization": "zscore"}}, + {"name": "timestamp_hour", "type": "number", "preprocessing": {"normalization": "zscore"}}, +] + +COMBINER = { + "type": "concat", + "fc_layers": [{"output_size": 64}, {"output_size": 32}], +} + +TRAINER = {"epochs": 20, "learning_rate": 0.001} + + +def make_config(loss: dict) -> dict: + return { + "model_type": "ecd", + "input_features": INPUT_FEATURES, + "output_features": [ + { + "name": "anomaly", + "type": "anomaly", + "loss": loss, + } + ], + "combiner": COMBINER, + "trainer": TRAINER, + } + + +CONFIGS = { + "Deep SVDD": make_config({"type": "deep_svdd", "nu": 0.1}), + "Deep SAD": make_config({"type": "deep_sad", "eta": 1.0}), + "DROCC": make_config({"type": "drocc", "perturbation_strength": 0.1, "num_perturbation_steps": 5}), +} + +# Deep SAD: inject ~10% labeled anomalies into the training set +N_LABELED = max(1, int(0.1 * len(train_df))) +labeled_anom = anomalous.sample(n=N_LABELED, random_state=0).copy() +labeled_anom["split"] = 0 +sad_train_df = pd.concat([train_df, labeled_anom], ignore_index=True) + +# --------------------------------------------------------------------------- +# 3. Train and evaluate each variant +# --------------------------------------------------------------------------- + +results_table = [] + +for method_name, config in CONFIGS.items(): + print(f"\n{'=' * 60}") + print(f"Training: {method_name}") + print("=" * 60) + + train_data = sad_train_df if method_name == "Deep SAD" else train_df + + model = LudwigModel(config, logging_level=30) # WARNING level + train_stats, _, _ = model.train(dataset=train_data) + + predictions, _ = model.predict(dataset=test_df) + + score_col = "anomaly_anomaly_score_predictions" + scores = predictions[score_col].values + true_labels = test_df["anomaly"].values + + auc = roc_auc_score(true_labels, scores) + + # Separation ratio: mean anomaly score / mean normal score + normal_scores = scores[true_labels == 0] + anom_scores = scores[true_labels == 1] + sep_ratio = anom_scores.mean() / (normal_scores.mean() + 1e-9) + + results_table.append( + { + "Method": method_name, + "AUC-ROC": round(auc, 4), + "Mean normal score": round(float(normal_scores.mean()), 4), + "Mean anomaly score": round(float(anom_scores.mean()), 4), + "Separation ratio": round(float(sep_ratio), 2), + } + ) + print(f" AUC-ROC: {auc:.4f}") + print(f" Mean normal score: {normal_scores.mean():.4f}") + print(f" Mean anomaly score:{anom_scores.mean():.4f}") + print(f" Separation ratio: {sep_ratio:.2f}x") + +# --------------------------------------------------------------------------- +# 4. Print summary table +# --------------------------------------------------------------------------- + +results_df = pd.DataFrame(results_table) + +print("\n") +print("=" * 70) +print("ANOMALY DETECTION — SUMMARY") +print("=" * 70) +print(results_df.to_string(index=False)) +print("=" * 70) +print("\nHigher AUC-ROC and separation ratio indicate better discrimination between normal and anomalous samples.") diff --git a/examples/calibration/README.md b/examples/calibration/README.md new file mode 100644 index 0000000..d8fca10 --- /dev/null +++ b/examples/calibration/README.md @@ -0,0 +1,22 @@ +# Calibration Examples + +Drawing on the methods in +On Calibration of Modern Neural Networks (Chuan Guo, Geoff Pleiss, Yu Sun, Kilian Q. Weinberger), Ludwig supports +temperature scaling for binary and category output features. Temperature scaling brings a model’s output probabilities +closer to the true likelihood while preserving the same accuracy and top k predictions. + +To enable calibration, add calibration: True to any binary or category feature: + +``` +output_features: + - name: Cover_Type + type: category + calibration: True +``` + +With calibration enabled, Ludwig will attempt to find a scale factor (temperature) which will bring the probabilities +closer to the true likelihoods using the validation set. This calibration phase is run after training is complete. If +no validation set is provided, the training set is used for calibration. + +To visualize the effects of calibration in Ludwig, you can use Calibration Plots, which bin the data based on model +probability and plot the model probability (X) versus the observed rate (Y) for each bin. diff --git a/examples/calibration/train_forest_cover_calibrated.py b/examples/calibration/train_forest_cover_calibrated.py new file mode 100644 index 0000000..4230af4 --- /dev/null +++ b/examples/calibration/train_forest_cover_calibrated.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +import copy +import logging +import shutil + +import numpy as np +import yaml + +import ludwig.visualize +from ludwig.api import LudwigModel +from ludwig.datasets import forest_cover + +# clean out prior results +shutil.rmtree("./results_forest_cover", ignore_errors=True) +shutil.rmtree("./visualizations_forest_cover", ignore_errors=True) + +# Download and prepare the dataset +dataset = forest_cover.load() + +config_yaml = """ +input_features: + - name: Elevation + type: number + - name: Aspect + type: number + - name: Slope + type: number + - name: Horizontal_Distance_To_Hydrology + type: number + - name: Vertical_Distance_To_Hydrology + type: number + - name: Horizontal_Distance_To_Roadways + type: number + - name: Hillshade_9am + type: number + - name: Hillshade_Noon + type: number + - name: Hillshade_3pm + type: number + - name: Horizontal_Distance_To_Fire_Points + type: number + - name: Wilderness_Area + type: category + - name: Soil_Type + type: category +output_features: + - name: Cover_Type + type: category +combiner: + type: transformer +trainer: + batch_size: 256 + learning_rate: .001 + epochs: 1 +""" + +uncalibrated_config = yaml.safe_load(config_yaml) + +scaled_config = copy.deepcopy(uncalibrated_config) +scaled_config["output_features"][0]["calibration"] = True + +uncalibrated_model = LudwigModel(config=uncalibrated_config, logging_level=logging.INFO) +uncalibrated_model.train( + dataset, + model_name="uncalibrated", + experiment_name="forest_cover_calibration", + output_directory="results_forest_cover", +) + +scaled_model = LudwigModel(config=scaled_config, logging_level=logging.INFO) +scaled_model.train( + dataset, model_name="scaled", experiment_name="forest_cover_calibration", output_directory="results_forest_cover" +) + +# Generates predictions and performance statistics for the test set. +uncalibrated_test_stats, uncalibrated_test_predictions, _ = uncalibrated_model.evaluate( + dataset, collect_predictions=True, collect_overall_stats=True +) + +scaled_test_stats, scaled_test_predictions, _ = scaled_model.evaluate( + dataset, collect_predictions=True, collect_overall_stats=True +) + +uncalibrated_probs = np.stack(uncalibrated_test_predictions["Cover_Type_probabilities"], axis=0) +scaled_probs = np.stack(scaled_test_predictions["Cover_Type_probabilities"], axis=0) + +ludwig.visualize.calibration_1_vs_all( + probabilities_per_model=[uncalibrated_probs, scaled_probs], + model_names=["Uncalibrated", "Calibrated"], + ground_truth=dataset["Cover_Type"], + metadata=uncalibrated_model.training_set_metadata, + output_feature_name="Cover_Type", + top_n_classes=[7, 7], + labels_limit=7, + output_directory="visualizations_forest_cover", + file_format="png", +) diff --git a/examples/calibration/train_mushroom_edibility_calibrated.py b/examples/calibration/train_mushroom_edibility_calibrated.py new file mode 100644 index 0000000..bd37177 --- /dev/null +++ b/examples/calibration/train_mushroom_edibility_calibrated.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python + +import copy +import logging +import shutil + +import numpy as np +import yaml + +import ludwig.visualize +from ludwig.api import LudwigModel +from ludwig.datasets import mushroom_edibility + +# clean out prior results +shutil.rmtree("./results_mushroom_edibility", ignore_errors=True) +shutil.rmtree("./visualizations_mushroom_edibility", ignore_errors=True) + +# Download and prepare the dataset +dataset = mushroom_edibility.load() + +# This dataset has no split, so add a split column +dataset.split = np.random.choice(3, len(dataset), p=(0.7, 0.1, 0.2)) + +config_yaml = """ +input_features: + - name: cap-shape + type: category + - name: cap-surface + type: category + - name: cap-color + type: category + - name: bruises? + type: category + - name: odor + type: category + - name: gill-attachment + type: category + - name: gill-spacing + type: category + - name: gill-size + type: category + - name: gill-color + type: category + - name: stalk-shape + type: category + - name: stalk-root + type: category + - name: stalk-surface-above-ring + type: category + - name: stalk-surface-below-ring + type: category + - name: stalk-color-above-ring + type: category + - name: stalk-color-below-ring + type: category + - name: veil-type + type: category + - name: veil-color + type: category + - name: ring-number + type: category + - name: ring-type + type: category + - name: spore-print-color + type: category + - name: population + type: category + - name: habitat + type: category +output_features: + - name: class + type: category +combiner: + type: concat +trainer: + batch_size: 256 + learning_rate: .0001 + epochs: 10 +""" + +uncalibrated_config = yaml.safe_load(config_yaml) + +scaled_config = copy.deepcopy(uncalibrated_config) +scaled_config["output_features"][0]["calibration"] = True + +uncalibrated_model = LudwigModel(config=uncalibrated_config, logging_level=logging.INFO) +uncalibrated_model.train( + dataset, + model_name="uncalibrated", + experiment_name="mushroom_edibility_calibration", + output_directory="results_mushroom_edibility", +) + +scaled_model = LudwigModel(config=scaled_config, logging_level=logging.INFO) +scaled_model.train( + dataset, + model_name="scaled", + experiment_name="mushroom_edibility_calibration", + output_directory="results_mushroom_edibility", +) + +# Generates predictions and performance statistics for the test set. +uncalibrated_test_stats, uncalibrated_test_predictions, _ = uncalibrated_model.evaluate( + dataset, collect_predictions=True, collect_overall_stats=True +) + +scaled_test_stats, scaled_test_predictions, _ = scaled_model.evaluate( + dataset, collect_predictions=True, collect_overall_stats=True +) + +uncalibrated_probs = np.stack(uncalibrated_test_predictions["class_probabilities"], axis=0) +scaled_probs = np.stack(scaled_test_predictions["class_probabilities"], axis=0) + +ludwig.visualize.calibration_1_vs_all( + probabilities_per_model=[uncalibrated_probs, scaled_probs], + model_names=["Uncalibrated", "Calibrated"], + ground_truth=dataset["class"], + metadata=uncalibrated_model.training_set_metadata, + output_feature_name="class", + top_n_classes=[3, 3], + labels_limit=3, + output_directory="visualizations_mushroom_edibility", + file_format="png", +) diff --git a/examples/class_imbalance/README.md b/examples/class_imbalance/README.md new file mode 100644 index 0000000..462ebfa --- /dev/null +++ b/examples/class_imbalance/README.md @@ -0,0 +1,50 @@ +# Credit Card Fraud Detection Example + +This API example is based on Kaggle's [Imbalanced Insurance](https://www.kaggle.com/arashnic/imbalanced-data-practice) dataset for detecting whether customers will buy vehicle insurance. + +### Preparatory Steps + +Create and download your [Kaggle API Credentials](https://github.com/Kaggle/kaggle-api#api-credentials). + +The Imbalanced Insurance dataset is hosted by Kaggle, and as such Ludwig will need to authenticate you through the Kaggle API to download the dataset. + +### Examples + +| File | Description | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------- | +| model_training.py | Demonstrates the use of oversampling by training two different models: one with no oversampling, and one with. | +| model_training_results.ipynb | Example for extracting training statistics and generating visualizations. | + +Enter `python model_training.py` will train a standard model with no class balancing and a balanced model with class balancing applied. Results of model training will be stored in this location. + +``` +./results/ + balance_example_standard_model/ + balance_example_balanced_model/ +``` + +The only difference between these two models is that the balanced model uses a small amount of oversampling in addition to the other configuration parameters. +The way this is done is by specifying the ratio that you want the minority class to have in relation to the majority class. +For instance, if you specify 0.5 for the `oversample_minority` preprocessing parameter, the minority class will be oversampled until it makes up 50% of the majority class. +In this example, we had an imbalance where the minority class was 19% of the majority class in size. We decided that we wanted to increase that to 26%. +Though this doesn't seem like much, it is enough to get some small performance improvements without experiencing performance degradation due to over-fitting. + +Here are the performance differences in the two models followed by some plots showing different metrics over training: + +| Metric | Standard Model | Balanced Model | +| :------: | :------------: | :------------: | +| Loss | 0.3649 | 0.2758 | +| Accuracy | 0.7732 | 0.8237 | +| ROC AUC | 0.8533 | 0.8660 | + +Here are the learning curve plots from both models: + +![](../images/balance_example_accuracy_curves.png) + +![](../images/balance_example_roc_auc_curves.png) + +Here is the comparison of model performances on ROC_AUC and Accuracy: + +![](../images/compare_performance_Response.png) + +The creation of the learning curves is demonstrated in the Jupyter notebook `model_training_results.ipynb`. The comparison plot was generated using the ludwig visualize [compare performance](https://ludwig-ai.github.io/ludwig-docs/0.4/user_guide/visualizations/#compare-performance) command. diff --git a/examples/class_imbalance/balanced_model_config.yaml b/examples/class_imbalance/balanced_model_config.yaml new file mode 100644 index 0000000..ce0550d --- /dev/null +++ b/examples/class_imbalance/balanced_model_config.yaml @@ -0,0 +1,34 @@ +input_features: + - name: Gender + type: category + - name: Age + type: number + - name: Driving_License + type: binary + - name: Region_Code + type: number + - name: Previously_Insured + type: binary + - name: Vehicle_Age + type: category + - name: Vehicle_Damage + type: category + - name: Annual_Premium + type: number + - name: Policy_Sales_Channel + type: number + - name: Vintage + type: number +output_features: + - name: Response + type: binary +preprocessing: + oversample_minority: 0.26 +trainer: + learning_rate: 0.0001 + learning_rate_scheduler: + decay: exponential + decay_rate: 0.9 + decay_steps: 30000 + staircase: True + epochs: 50 diff --git a/examples/class_imbalance/model_training.py b/examples/class_imbalance/model_training.py new file mode 100644 index 0000000..2dedf0e --- /dev/null +++ b/examples/class_imbalance/model_training.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# # Class Imbalance Model Training Example +# +# This example trains a model utilizing a standard config, and then a config using oversampling + +import logging +import shutil + +# Import required libraries +from ludwig.api import LudwigModel +from ludwig.datasets import imbalanced_insurance +from ludwig.visualize import compare_performance + +# clean out old results +shutil.rmtree("./results", ignore_errors=True) +shutil.rmtree("./visualizations", ignore_errors=True) + +# list models to train +list_of_model_ids = ["standard_model", "balanced_model"] +list_of_eval_stats = [] + +training_set, val_set, test_set = imbalanced_insurance.load() + +# Train models +for model_id in list_of_model_ids: + print(">>>> training: ", model_id) + + # Define Ludwig model object that drive model training + model = LudwigModel(config=model_id + "_config.yaml", logging_level=logging.WARN) + + # initiate model training + train_stats, _, _ = model.train( + training_set=training_set, + validation_set=val_set, + test_set=test_set, + experiment_name="balance_example", + model_name=model_id, + skip_save_model=True, + ) + + # evaluate model on test_set + eval_stats, _, _ = model.evaluate(test_set) + + # save eval stats for later use + list_of_eval_stats.append(eval_stats) + + print(">>>>>>> completed: ", model_id, "\n") + + +compare_performance( + list_of_eval_stats, + "Response", + model_names=list_of_model_ids, + output_directory="./visualizations", + file_format="png", +) diff --git a/examples/class_imbalance/model_training_results.ipynb b/examples/class_imbalance/model_training_results.ipynb new file mode 100644 index 0000000..a592e68 --- /dev/null +++ b/examples/class_imbalance/model_training_results.ipynb @@ -0,0 +1,246 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8c1e31e4-d8d4-4e83-8f4c-f868365d14d7", + "metadata": {}, + "source": [ + "# Model Analysis\n", + "\n", + "This notebook will analyze the training results of the standard and balanced model on the [imbalanced insurance](https://www.kaggle.com/arashnic/imbalanced-data-practice) dataset. In order for the cells in this notebook to run, you must first run the following command to train the models:\n", + "```\n", + "python model_training.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "a3b3b6c1-5d11-4a03-9dfa-b070e45b2adb", + "metadata": {}, + "source": [ + "## Import required libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 226, + "id": "6a6bbd43-1333-4a0e-b895-c3e393d5ee07", + "metadata": {}, + "outputs": [], + "source": [ + "import os.path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "from ludwig.utils.data_utils import load_json\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "b5e4d240-7607-4036-9573-6b452523c18f", + "metadata": {}, + "source": [ + "## Learning Curves" + ] + }, + { + "cell_type": "markdown", + "id": "725dde7d-f7a5-4836-8cf1-a3f50acafd30", + "metadata": {}, + "source": [ + "### Create Plotting Data Function " + ] + }, + { + "cell_type": "code", + "execution_count": 227, + "id": "d8c35325-cfab-4ead-8699-1e98e55a8c7b", + "metadata": {}, + "outputs": [], + "source": [ + "def create_plot_ready_data(list_of_train_stats, model_names, metric, target):\n", + " # List of splits to evaluate statistics for\n", + " list_of_splits = [\"training\", \"validation\", \"test\"]\n", + "\n", + " # Empty list to fill with dfs for each models' stats\n", + " list_of_train_stats_df = []\n", + "\n", + " # For each models' stats, create a df with columns of stats for each split listed above\n", + " for name, stats in zip(model_names, list_of_train_stats):\n", + " list_of_dfs = []\n", + " for split in list_of_splits:\n", + " df = pd.DataFrame(stats[split][target])\n", + " df.columns = [split + \"_\" + c for c in df.columns]\n", + " list_of_dfs.append(df)\n", + "\n", + " combined_df = pd.concat(list_of_dfs, axis=1)\n", + " combined_df.name = name\n", + " combined_df[\"epoch\"] = combined_df.index + 1\n", + " list_of_train_stats_df.append(combined_df)\n", + "\n", + " # holding ready for plot ready data\n", + " plot_ready_list = []\n", + "\n", + " # consolidate the multiple training statistics dataframes\n", + " for df in list_of_train_stats_df:\n", + " for col in [\"training\", \"validation\"]:\n", + " df2 = df[[\"epoch\", col + f\"_{metric}\"]].copy()\n", + " df2.columns = [\"epoch\", f\"{metric}\"]\n", + " df2[\"split\"] = col\n", + " df2[\"model\"] = df.name\n", + " plot_ready_list.append(df2)\n", + "\n", + " return pd.concat(plot_ready_list, axis=0, ignore_index=True)" + ] + }, + { + "cell_type": "markdown", + "id": "722f5f48-7026-427e-9bb7-388fec15a24f", + "metadata": { + "tags": [] + }, + "source": [ + "### Create Plotting Data" + ] + }, + { + "cell_type": "code", + "execution_count": 228, + "id": "6b48aef8-11a8-4bba-8d52-114adb9cb2f2", + "metadata": {}, + "outputs": [], + "source": [ + "standard_stats = load_json(os.path.join(\"results/balance_example_standard_model\", \"training_statistics.json\"))\n", + "balanced_stats = load_json(os.path.join(\"results/balance_example_balanced_model\", \"training_statistics.json\"))\n", + "\n", + "accuracy_learning_curves = create_plot_ready_data(\n", + " [standard_stats, balanced_stats], [\"standard_model\", \"balanced_model\"], \"accuracy\", \"Response\"\n", + ")\n", + "roc_auc_learning_curves = create_plot_ready_data(\n", + " [standard_stats, balanced_stats], [\"standard_model\", \"balanced_model\"], \"roc_auc\", \"Response\"\n", + ")\n", + "loss_learning_curves = create_plot_ready_data(\n", + " [standard_stats, balanced_stats], [\"standard_model\", \"balanced_model\"], \"loss\", \"Response\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 229, + "id": "a06aaa41-e692-4348-aab5-6bd78711f6f1", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(figsize=(10, 6))\n", + "sns.set_style(style=\"dark\")\n", + "ax = sns.lineplot(x=\"epoch\", y=\"accuracy\", style=\"split\", hue=\"model\", data=accuracy_learning_curves)\n", + "ax.set_title(\"Accuracy Learning Curves\", fontdict={\"fontsize\": 16})\n", + "ax.grid(visible=True, which=\"major\", color=\"black\", linewidth=0.075)\n", + "ax.grid(visible=True, which=\"minor\", color=\"black\", linewidth=0.075)\n", + "ax.set_xlabel(\"Epoch\", fontsize=15)\n", + "ax.set_ylabel(\"Accuracy\", fontsize=15);" + ] + }, + { + "cell_type": "code", + "execution_count": 230, + "id": "b6ef7fce-b49e-49a6-a4b0-436d078f72ed", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmMAAAGICAYAAAANo+ehAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAB3l0lEQVR4nO3dd3gUxRvA8e9eSy69J9RAAoTeBBGkFwsIwk/sYkMRFQuKBbuAiogI9oaKigKioKCIogiKiNI7CIQSIL1ecrm2+/vj4DQmgQDJXcr7eR4ekp0t704W7s3M7IyiaZqGEEIIIYTwCZ2vAxBCCCGEqMskGRNCCCGE8CFJxoQQQgghfEiSMSGEEEIIH5JkTAghhBDChyQZE0IIIYTwIUnGhKhmRo0aRVJSUok/rVu35oILLuDOO+9k//79pY7Jzc1l+vTpXHzxxbRr146ePXsyduxY1q5dW+51VqxYwejRo+nevTudOnVi+PDhzJ07F4fDUaE4n3vuOZKSknj//ffLLO/fvz+TJk0q99pJSUmkpKSU2H748GGeffZZBgwYQPv27RkwYABPPvkkx44dO2UsX331FUlJSWRnZ1codl949NFHueyyy7x2vV27dvHwww/Tt29f2rdvz8UXX8yLL75YretIiLrK4OsAhBClde7cmUceecTzvd1uZ/fu3bzxxhuMHj2a5cuX4+fnB8DBgwe55ZZbcDqd3HLLLbRp04bc3FwWL17MzTffzLhx47jnnntKnP/ZZ59l3rx5DB8+nGuvvZaAgAD+/PNPXnzxRf744w9mzpyJXq8vNz6n08m3335L8+bNWbhwIbfddts53/Pvv//OPffcQ6NGjbjzzjtp2LAhR48e5f3332fkyJF8+umnJCQknPN1fOWuu+6iqKjIK9f6+uuvefzxx+nUqRMPPPAAMTEx7N+/n3fffZeff/6ZuXPnEhUV5ZVYhBAVoAkhqpUbbrhBGzNmTJll8+fP11q0aKH98ssvmqZpmtPp1C677DJt0KBBWlZWVqn9Z86cqbVo0UL76aefPNsWLVqktWjRQps3b16p/b/99lutRYsW2qJFi04Z488//6y1bNlS+/3337UWLVpof/31V6l9+vXrpz377LNlHv/jjz9qLVq00I4cOaJpmqZlZWVpF1xwgXbDDTdoNputxL7Z2dlar169tBtvvLHceL788kutRYsWZdZBXbN//36tXbt22oMPPqipqlqi7NChQ1rHjh21iRMn+ig6IURZpJtSiBokKCioxPcrV65k7969TJgwgYiIiFL7jxs3jsaNG/P22297ts2ePZukpCSuvvrqUvsPHjyYW2+9lfDw8FPGsXjxYtq3b0/37t1JSEjgiy++OMs7+ud82dnZTJw4EZPJVKIsPDycRx55hO7du+N0Os/pOmvWrOHKK6+kffv29O7dm1mzZuFyuTzlDoeDV199lYsvvpi2bdvStWtXxo0bx/Hjxz379O/fn+nTp3PVVVfRpUsXPvroI1577TX+97//sXTpUk9X8RVXXMHGjRs9x/27mzIlJYWkpCR+/vlnRo8eTYcOHejVqxdvvfVWiXhTUlK488476dy5Mz179mT27NncfPPNPProo+Xe49y5c1FVlUcffRRFUUqUNW7cmAkTJtCqVasScXz//fcl9rv88ss911i3bh1JSUnMmzePnj170qdPHx599FE6d+6M3W4vcdy9997L9ddf7/l+6dKlDB06lHbt2jFw4EA++eSTEvtv2bKF66+/nk6dOnH++edz7733cvTo0XLvTYjaSpIxIaohTdNwOp2eP4WFhaxbt45XXnmF+vXr06VLF8CdXOh0Onr27FnmefR6PQMGDGDLli1kZ2eTnp7O3r176dOnT7nXfuSRR05ZXlBQwMqVKxk6dCjg/uD+/vvvKSgoOOv7XbNmDdHR0bRu3brM8iFDhjB27FgMhrMfWbF27Vpuv/12GjZsyOuvv87o0aP58MMPmTJlimefF154gU8//ZTbb7+dDz74gPvvv5+1a9fy/PPPlzjXhx9+SO/evXnppZfo3bs34O4ufvXVVxk3bhyvvfYaNpuN++6775QJ5MSJE+nQoQNvv/02/fr1Y+bMmaxatQoAm83GzTffTHJyMi+88AIPP/wwH3/8MRs2bDjlff7222+0adOm3G7I66+/nlGjRlWozv7tzTffZNKkSYwfP5477riDwsJCfv31V095UVERq1evZsiQIQAsWrSIBx98kK5du/LWW28xfPhwXnjhBc8YQ6vVypgxY4iNjeXNN99k8uTJ7Ny5kwceeOCMYxOippMxY0JUQ6tWraJNmzYltvn7+9O9e3cmTpxIYGAgAEePHiU8PJyAgIByz9WwYUMAjh8/7mkFql+//lnH9t1336GqKoMHDwbcydjMmTNZsmQJ11133VmdMzU19ZxiqoiZM2fSoUMHXnnlFQB69+5NaGgoEydOZPTo0TRs2JDs7GwefvhhRo4cCcD5559PcnIyS5YsKXGupk2bMm7cuBLbCgsL+eijj2jfvj0ALpeLu+66i927d9O2bdsyY7r00ku59957AejWrRvLly9n9erV9OnTh6+//ppjx46xbNky4uPjAUhISOCKK6445X2mpaWVm9Sei5tuuon+/ft7vm/Tpg3ff/89AwYMANyttA6Hg0suuQRVVZkxYwZDhw7lqaeeAqBnz54oisKbb77Jddddx759+8jNzWXUqFF06tQJcLeC/vHHH6iqik4nbQWi7pBkTIhq6LzzzmPixIkA/P3337z44ot0796dadOmlejG0zTtlAPtgRLlJ79WVfWsY1u8eDHdunXDYDCQn59PYGAgnTp1YuHChWecjJ3sRtPpdOcU0+lYrVa2bt3K+PHjS7RU9e7dG1VVWbduHQ0bNmTmzJmAO6E5cOAABw4cYOPGjaW64xITE0tdw2AwlEi64uLiPNcuT8eOHT1f63Q6YmJiPIP8161bR/PmzT2JGEDbtm09yXV5qqoumzVrVuL7oUOH8vrrr2O32zGZTCxbtowePXoQERHB/v37SU9Pp2/fvqXq+9VXX2Xr1q20bduWsLAwxo4dy5AhQ+jTpw/du3fn/PPPr/TYhajuJBkTohoKDg6mXbt2ALRr14569epxyy23YDKZmDZtmme/Bg0asHbtWmw2m+ftyv86OQanXr16nm3/HgP1X+np6URFRZXZMnHkyBHPOKiuXbuWKt+5c6enVcZsNpdKYk46ud1sNnvuY9u2beXGZLFY0DSN4ODgcvc5lfz8fFRV5eWXX+bll18uVZ6RkQHAxo0beeaZZ9izZw/BwcG0atWqzHqNjIwstc1kMpWos5Nfnyox8vf3L/G9TqdD0zTAPV1JWeMAT/cWZIMGDU75883NzcXPz89T9xX131gGDx7MtGnT+O233+jWrRu//vorzz77rOcaAA8++CAPPvhgqXNlZGQQFBTEp59+yhtvvMGiRYuYO3cuISEhjB8//qxbWIWoqSQZE6IG6N69OyNHjuSLL77gkksu8XQX9evXj88//5yVK1dyySWXlDpO0zR+/vln2rVr5/kwbd26Nb/++isTJkwo81q33HILUVFRzJkzp1TZ4sWL8ff35+233y6ReLhcLsaOHcsXX3zB008/DbgTlszMzDKvkZaWhtFoJCQkBIAePXqwcuVKdu3a5Rlc/m/z5s3jlVde4fvvv6dRo0anqqoynezWvfPOOz3dav8WExNDQUEBY8eOpXPnzrz22mueFqlp06axe/fuM77muYqJiWHnzp2ltmdnZ9O0adNyj+vRoweffvop2dnZZSZzs2bN4ptvvmH16tWelsn/JowVmYIjNjaWLl268MMPP3j2HzhwIIAnaX7qqac83bb/drJ1r3nz5sycORO73c6GDRuYM2cOzz77LG3atKFDhw6njUGI2kI65YWoIR544AGCg4OZOnWqp2WpZ8+etGvXjmnTpnlad/7tnXfeYf/+/YwZM8az7aabbmL37t1lvgH59ddfs2/fPs/g/P/65ptvPN1J3bp18/zp0aMHffv2ZenSpRQXFwPulrP169eXOcnoihUr6NSpk2dA/uWXX05YWBgvvvhiqda0zMxM5syZQ8eOHc8qEQP3W6gtW7bkyJEjtGvXzvPHaDQyY8YMUlNTOXDgAHl5edx0002eRExVVX7//XdPa5U3denShb///psjR454tu3du7fE92W57rrrUBSFF198sVTc+/fvZ/HixQwYMIDAwEDP27np6emefdLS0kpNxlueoUOHsnr1apYvX07fvn0950tISCAsLIy0tLQS9Z2bm8usWbOwWCysXr2a7t27k52djclkonv37jz55JMAp53kV4jaRlrGhKghIiIiuOOOO5g+fTqffPIJo0ePRq/XM2PGDEaPHs2IESO47bbbaN26Nfn5+SxdupRly5YxduxYLrroIs95Lr/8cn755Reeeuoptm7dyoABA1AUhd9++43PP/+cSy+9tMxB4uvXr+fw4cOMHz++zPiGDRvG8uXL+f777xk+fDg33HAD8+fPZ9SoUdx22200aNCA9PR0Fi1axNatW/nwww89x4aGhvLcc89x//33c80113DDDTdQv3599u/fz/vvv4/L5WLq1KmnraP58+eX6n5r2LAhAwcO5N577+Xuu+8mKCiIQYMGkZOTw8yZM9HpdLRo0QKn00lgYCBvvvkmqqpSXFzMZ599xu7du1EUBU3TSk0VUZWGDRvG22+/zdixY7n33ntxuVy88sorKIpyyjji4+OZOHEikydPJi0tjSuvvJKIiAh27NjB+++/T2xsLI89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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(figsize=(10, 6))\n", + "sns.set_style(style=\"dark\")\n", + "ax = sns.lineplot(x=\"epoch\", y=\"roc_auc\", style=\"split\", hue=\"model\", data=roc_auc_learning_curves)\n", + "ax.set_title(\"ROC AUC Learning Curves\", fontdict={\"fontsize\": 16})\n", + "ax.grid(visible=True, which=\"major\", color=\"black\", linewidth=0.075)\n", + "ax.grid(visible=True, which=\"minor\", color=\"black\", linewidth=0.075)\n", + "ax.set_xlabel(\"Epoch\", fontsize=15)\n", + "ax.set_ylabel(\"ROC AUC\", fontsize=15);" + ] + }, + { + "cell_type": "code", + "execution_count": 231, + "id": "9bfd8dc1-b0e0-451c-8f2b-de9e92e976c0", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmsAAAGICAYAAAAedKdVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABW70lEQVR4nO3dd3xUdf798dedml4JhA5BARUQkPJTARULNiyIuMQvFhTLigoqYqEoglhZFUEEWVFEkV1cu7sKsmBFBNEVQaUqJUBInUkymXJ/fwQiSAgDJLmT5Dwfj13IvXfmvuc9Fzh+bvkYpmmaiIiIiEhEslldgIiIiIgcmsKaiIiISARTWBMRERGJYAprIiIiIhFMYU1EREQkgimsiYiIiEQwhTWReq5v375MmDDB6jIOMmTIEG6++Wary6hUu3btmD17do3syzRN/vWvf3H11VfTo0cPunXrxlVXXcW7775bI/sXEes4rC5ARKS2evPNN2nSpEm17ycQCHD77bfz+eef85e//IUbb7wRu93OsmXLGD16NP/73/948MEHq70OEbGGwpqIyFHq3LlzjexnxowZLFmyhFmzZtG7d+/y5X369KFhw4Y8/fTT9OvXj27dutVIPSJSs3QaVEQOKycnhzFjxtCnTx9OPvlkrrnmGv73v/8dsM1LL73EueeeS8eOHTnnnHOYNm0aoVAo7PVHIxAI8Oyzz3LmmWfSsWNHBgwYwFdffXXANhs3buSOO+7g//2//0eHDh3o27cv06ZNY9/kLcuXL6ddu3bMnz+fXr16ccYZZ7B161b69u3LrFmzGD9+PD169KBr166MHj0aj8dT/t77nwadOnUqAwYM4P3336dfv3507NiRK664glWrVh1Qz0cffcTFF19Mp06dGDhwIIsWLaJdu3YsX768ws/o9/uZO3cuZ5111gFBbZ9rrrmGq6++GpvNVl5Hly5dDthm7dq1B+zjvvvu469//St33303Xbt2ZeTIkZx99tmMGzfugNfl5+fToUMH/vnPfwJQVFTEI488wmmnnUanTp0YMmQIP/300wGvqY7vWaS+U1gTkUp5vV4GDx7Ml19+yd13383f/vY3TNPk//7v//j5558B+PDDD3n22We57rrrmD17NldeeSVTp05lwYIFYa0/WmPHjuXll1/mmmuuYdq0aWRkZDBs2LDygOT1ernmmmvIy8vj8ccf58UXX6Rnz54899xzLFmy5ID3mj59OhMmTGDkyJE0a9YMgBdffJGCggKmTJnCiBEj+OCDD3jhhRcOWc/mzZt57rnnGD58OFOnTsXn83HnnXcSCAQAWLZsGSNHjqRjx45MmzaN0047jbvvvrvSz/jjjz+Sl5fHGWecUeH6qKgoxo0bR9euXcPuG8DSpUvx+XxMmzaNq666iosuuoiPP/6YYDBYvs0nn3wCwHnnnYdpmtx666188MEHjBgxgmeffRaXy8WQIUP47bffgOr7nkXqO50GFZFKvfXWW/z222+89957HHfccQD06tWL888/n+eff56pU6eyYsUKmjZtSmZmJoZh0KNHDxwOBw0bNgQ47PqjsWHDBt566y0mTpzIlVdeCZSdFty9ezfPPPMMr776Kps2baJFixY888wzpKSkAHDqqaeyaNEiVqxYQd++fcvf79prrz3gZ4D09HSmTJmCYRj06tWLb775hmXLljFq1KgKa/J6vcyZM4dOnToBEAwG+etf/8q6devo0KED06dPp3v37kyePBmA3r174/V6ee211w75ObOysgCq/Nq4QCDAhAkTyvuSmprKiy++yDfffMOpp54KlI0C9unTh4SEBD777DO+/vprXn75ZU477bTy+i+66CJeeOEFJk+eXC3fs4hoZE1EDmPFihUcd9xx5UENwOVycc455/DNN98A0KVLFzZt2sQVV1zBzJkz+eWXX7jhhhvKw8/h1h+Nffvu06cPgUCg/H9nnHEGq1atorS0lA4dOvD6668THx/P+vXrWbRoEc8//zyBQIDS0tID3m//z7dPx44dMQyj/Of09HSKiooOWZPD4aBDhw4HbA9QXFyMz+fj+++/5+yzzz7gNeeff36ln9NutwOUn7atKikpKeVBDeD444+nbdu2fPTRRwDk5eWxfPlyLr74YqDsdHF0dDTdu3cv7zWUBfevv/4aqJ7vWUQ0siYih1FQUECDBg0OWt6gQQO8Xi8Al1xyCcFgkHnz5jFlyhSefvpp2rdvz5QpU2jTps1h1x+NvLw8oCysVSQ3N5dGjRoxY8YMXnrpJQoLC2natCldunTB4XAcFH72Dy77REdHH/CzYRiVhiaXy1V+7RhQ/vtQKER+fj6hUOig/aSmph76Q/LHiNr27dsPuc3OnTtp1KhRpe/zZxXtt3///rz88suMHz+eTz75BKfTyVlnnQWU9bu4uPiAMLqP0+kEDn8ciMjRUVgTkUolJiaycePGg5bv3r2bpKSk8p8vv/xyLr/8cvbs2cOnn37KtGnTGD58ePlIzeHWH6n4+HgMw+CNN97A4Tj4r7Lk5GTefvttnnnmGcaPH8/FF19MfHw8QPlpvpqUmpqK0+kkJyfngOV//vnPTjzxRFJSUvjss88YPHjwQetLS0vp378/55xzDo8++iiGYRx0Qf++UH04F110EVOmTOHbb7/l3//+N2effXZ5YI2Pjy8/VVqZqv6eRUSnQUXkME455RTWr1/Phg0bypeVlpayaNGi8ovaH3zwQe644w6gLJRceeWVDBw4kB07doS1/mjrMk0Tr9dLx44dy//31VdfMWfOHBwOB9999x3p6ekMHjy4PKitWbOGnJycKj+teDh2u53OnTvz6aefHrB88eLFlb7OZrNx9dVX8+mnn/Lll18etP6ll14iPz+f/v37AxAXF0dJSQkFBQXl26xcuTKsGps2bUrnzp157733+Prrr8vfE8r6nZOTQ0xMzAH9fu+998ofzFsd37OIaGRNRIB169YxZ86cg5ZfdNFFDBgwgFdeeYVhw4YxYsQI4uPjmTNnDtnZ2dxyyy0AdO/endGjRzNlyhROO+00srKyeOONNzj33HPDWn8oW7durbCuM844gxNOOIF+/foxatQohg8fTps2bfjmm2944YUXuPHGG7HZbHTs2JH58+fz/PPP06NHDzZs2MC0adMwDIOSkpJj7tuRuu2227j++usZM2YM559/PqtXry6/uWD/06d/NmzYML7++mtuvvlm/u///o/TTjuN0tJSPvnkE95++22GDh1aPlrYu3dvJk+ezIMPPsjVV1/NunXreP3118OusX///kyaNIn4+PjyGwkAzjrrLDp27MhNN93E8OHDady4MR9//DHz5s3j4YcfBo7+exaRyimsiQgrV66scPSlc+fOdO7cmXnz5vH4448zYcIEgsFg+bITTzwRgMsuuwyPx8O8efOYM2cO8fHx9OvXr/yxFIdbfyjr168vv3Nyf2lpabRu3ZqnnnqKZ599lpkzZ7Jnzx6aNm3K3XffzQ033ADAgAED2LRpE/Pnz+ell16iadOm3HDDDWzYsCHs0aaqdOqpp/LEE08wbdo03n77bU488UTuvvtuJk+eTExMzCFf53a7mT17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7nsm/qGnnSpzSYd0Ljqp0RHV8corf+eKK646otfIoSmsHQNHTHLZb0ryrS1ERETEIr/9toVHH30Yh8OB3W6na9duFBTk89RTj3HrrcN57LGJeDyF5Ofn0b//5Vx++UCGD7+J449vx8aNGygq8vDII4+Tnt6YF1+cxvLlX9GoUSPy8/MA2LVrJ0899RilpT4KCvK57rph9OlzJkOGDKJ585Y4nU5uv/0uJkwYg2mapKSkVlrvqlXf8tprc3A6nezatZNLL72CVau+Zf36X7jyysFcfvlAVqz4mpkzX8DtdhMfn8Do0WOIj4/niScmsWnTRpo2bUZpadlZtZ07s3jiiUcpLfXhcrm5994HqrzHCmvHwIxKAsBVmmdpHSIiIgAXndQo7FGwqppGacWK5bRr157bb7+L77//juTkZBYuXMA999zHzz+v45xzzuOMM/qSnb2b4cNv4vLLBwJwwgknceedd/Pii9P45JP/cPrpvfj+++946aVXKS4u4i9/GQCUndb8y1+upmvXbvzvf98ze/aL9OlzJsXFxVx33Q20bdue559/hnPO6ccll1zO4sUf869//bPSmnft2sWcOa+zbt1axo27jzfffJvdu3fxwAOjuOyyK3jiiUeZPv0l0tIa8uab85g79+907dqd0tJSZs6cQ1ZWFv/972IApk17loEDr+LUU0/n22+/YcaM57nppr8eU0//TGHtGPib/D+uD45lg9HA6lJEREQscfHFlzJv3ivcffftxMbGcfPNt5WvS01NZcGC11m6dAkxMbEEAn88PaFt23YANGrUiD179rBp00batz8Bm81GbGwcGRnH7X2PBrzyymw++OAdwDjgPVq0aAXApk0b6dfvQgA6djz5sGEtI6MNDoeD+Ph4mjRpitPpJD4+gdJSH3l5ecTExJKW1hCATp26MGvWCyQnp3DCCScBkJ6eTsOGZaF448b1zJ37MvPmvQKAw1H10Uo3GBwDM6YB3zs7kRdwW12KiIiIJT7/fCknn9yFZ599gbPOOpt5814pvx7ujTfm0qFDJ8aNe4S+fc8pXw4HXyvXokVL1q5dQygUori4mM2bNwLw0kszOP/8ixg79hG6du12wGv2vUfLli1Zs+YHANau/emwNVd2mV5SUhJFRV6ys7MB+P77VTRv3pyWLVuV7yM7eze7d+/eW3crbr31dp5/fiajRj3A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RACSDA85mh9UREREjpLCWjUx3UkAJOLlf9sLrC1GREREai2FtWpiuuIJGXaSDA/ZGlkTERGRo6SwVl0Mg5ArgUS85Bb5ra5GREREaimFtWqUc95MZgcvIL9EYU1ERESOjsPqAuoyo8WpbDH9tPTpOWsiIiJydDSyVo1c699nqOM/FOvRHSIiInKUFNaqkXvjvxnm/pgGsS6rSxEREZFaSmGtGplRScSbHl2zJiIiIkdNYa0ahdxJRIc87CootroUERERqaUU1qqRGZWEDZPokJdgyLS6HBEREamFFNaqUWjvLAZJhocCnQoVERGRo6CwVo0CDTvxdtL1eMxoduSXWF2OiIiI1EIKa9UomNKWb5pcxx4S2VHos7ocERERqYUU1qpToIRTgqtpQjY7FdZERETkKCisVSOjtJArfr6TvvbviHXZrS5HREREaiGFtWpkuhMBSMKDtzRocTUiIiJSGymsVSe7i5AzliTDw9qsQqurERERkVpIYa2ame5EEvGyJVcPxhUREZEjp7BWzUx3EkmGF48vYHUpIiIiUguFHdZM0+Sdd94hKysLgNmzZ3PxxRfz4IMPUlRUVG0F1nalzU7nV5pTpGvWRERE5CiEHdaef/55HnroIbKysvj22295+umn6d69O9999x1PPvlkddZYq3l7jWe6MZiSQMjqUkRERKQWCjus/etf/+LJJ5+kc+fOfPTRR3Tu3Jnx48czadIkPvnkk+qssXYzQ6Q6SihVWBMREZGjEHZY2717Nx06dADg888/p3fv3gCkpaXh8Xiqp7o6IParyXwSuhGboYncRURE5Mg5wt2wefPm/Pjjj+Tk5LBlyxb69OkDwJIlS2jevHm1FVjbhaKScOHHCPoIBEM47LqnQ0RERMIXdli78cYbGTlyJDabje7du3PSSScxffp0pk2bxqOPPnrUBbz44ot8+umn+P1+Bg8eTI8ePbjvvvswDIPjjz+e8ePHY7PZWLBgAfPnz8fhcHDrrbdy1llnHfU+a9L+D8bNLfaTFue2uCIRERGpTcIOawMGDODEE09k69at5adAO3fuzJw5c+jevftR7Xz58uV89913vPHGGxQXF/P3v/+dyZMnM2LECHr27Mm4ceNYvHgxnTt3Zu7cuSxcuBCfz0dmZiann346LpfrqPZbk0LuJAASDS/b80sU1kREROSIhB3WANq3b0/7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lv+8Zs0aevToAUCfPn348ssvrSotovy5Tz/++CP//e9/ufrqq3nggQfweDwWVhc5zj//fO68887yn+12u46pQ6ioVzquKnbOOefwyCOPALB9+3YaNGig46oCFfXJ6mOqXoc1j8dDXFxc+c92u51AIGBhRZEpKiqKG264gdmzZ/Pwww9zzz33qE9/0q9fPxyOPwaqTdPEMAwAYmNjKSwstKq0iPLnPnXq1Il7772XefPm0bx5c6ZNm2ZhdZEjNjaWuLg4PB4Pd9xxByNGjNAxdQgV9UrH1aE5HA5Gjx7NI488Qr9+/XRcHcKf+2T1MVWvw1pcXBxer7f851AodMA/JFKmdevWXHLJJRiGQevWrUlKSmL37t1WlxXR9r/mw+v1kpCQYGE1kevcc8+lQ4cO5b//6aefLK4ocuzYsYNrrrmGSy+9lP79++uYqsSfe6XjqnKPP/44//nPfxg7dmz5ZS6g4+rP9u9Tr169LD2m6nVY69q1K8uWLQNg9erVtG3b1uKKItM///lPHnvsMQB27tyJx+MhLS3N4qoi24knnsjy5csBWLZsGd26dbO4osh0ww038MMPPwDw1VdfcdJJJ1lcUWTIzs5m6NChjBo1ioEDBwI6pg6lol7puKrY22+/zYsvvghAdHQ0hmHQoUMHHVd/UlGfhg8fbukxVa8nct93N+gvv/yCaZo8+uijtGnTxuqyIk5paSn3338/27dvxzAM7rnnHrp27Wp1WRFn69at3HXXXSxYsIBNmzYxduxY/H4/GRkZTJw4EbvdbnWJEWH/Pq1Zs4ZHHnkEp9NJgwYNeOSRRw64NKG+mjhxIh999BEZGRnlyx588EEmTpyoY+pPKurViBEjePLJJ3Vc/UlRURH3338/2dnZBAIBhg0bRps2bfR31Z9U1KfGjRtb+ndVvQ5rIiIiIpGuXp8GFREREYl0CmsiIiIiEUxhTURERCSCKayJiIiIRDCFNREREZEIpifAikid17dvX7Zt21bhuuOPP57333+/2mto164dTzzxBJdeemm170tE6haFNRGpF4YNG8a111570HLNWiIikU5/S4lIvRATE6OZN0SkVtI1ayJS723dupV27drx3nvvccEFF3DyySczZMgQfv755/JtAoEAs2bN4rzzzqNjx47079+fDz/88ID3Wbp0KVdeeSUnn3wyffv25aWXXjpg/YYNGxgyZAgdO3akb9++/POf/6yRzycitZvCmojIXo899hgjRozgn//8J/Hx8Vx//fUUFhaWr5s9ezZ33XUX7777LhdddBF33XUX//nPfwD47rvvuOWWWzj99NN5++23uf/++5k2bRoLFiwof/958+YxePBgPvzwQ/r27cvYsWP5/fffLfmsIlJ7aLopEanz+vbty65du3A6nQetu++++zj99NM5++yzGTNmDEOGDAGgsLCQPn36MHr0aC6++GJ69uzJuHHjuOqqq8pfO2LECH7//XcWLlzIXXfdxe7du5k7d275+rfffhu73U7//v1p164dt9xyCyNHjgQgPz+fHj16MHXqVM4777xq7oCI1Ga6Zk1E6oWrr76azMzMg5anpKSQn58PQPfu3cuXx8fH06ZNG3755Rc2btxIIBCga9euB7y2e/fufPrppwD88ssv9OnT54D1l1122QE/t2rVqvz3iYmJAJSUlBz1ZxKR+kFhTUTqhcTERFq2bFnhun1h7c8jb6FQCJvNhsvlqvB1wWCw/G7ScO4qtdkOvvJEJzdE5HB0zZqIyF4//vhj+e/z8/PZtGkTJ5xwAq1atcLpdLJy5coDtl+5ciXHHXccAG3atDng9QB/+9vf+Otf/1r9hYtInaaRNRGpF4qKiti9e3eF6/aNbk2ZMoXU1FQaNmzI008/TXJyMhdccAFRUVFcf/31PPPMMyQlJdG+fXs+/vhjPv74Y6ZMmQLA0KFDGThwINOnT+eiiy5i3bp1vPrqqzz44IM19hlFpG5SWBORemHWrFnMmjWrwnX7HqExaNAgJkyYwK5du+jRowevvPIKMTExANx5553YbDYeffRRcnNzadOmDVOmTOGCCy4A4KSTTmLq1Kk899xzTJ8+nfT0dEaOHMnAgQNr5gOKSJ2lu0FFpN7bunUrZ599NvPmzaNbt25WlyMicgBdsyYiIiISwRTWRERERCKYToOKiIiIRDCNrImIiIhEMIU1ERERkQimsCYiIiISwRTWRERERCKYwpqIiIhIBFNYExEREYlg/x/95ioavjW7OwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(figsize=(10, 6))\n", + "sns.set_style(style=\"dark\")\n", + "ax = sns.lineplot(x=\"epoch\", y=\"loss\", style=\"split\", hue=\"model\", data=loss_learning_curves)\n", + "ax.set_title(\"Loss Learning Curves\", fontdict={\"fontsize\": 16})\n", + "ax.grid(visible=True, which=\"major\", color=\"black\", linewidth=0.075)\n", + "ax.grid(visible=True, which=\"minor\", color=\"black\", linewidth=0.075)\n", + "ax.set_xlabel(\"Epoch\", fontsize=15)\n", + "ax.set_ylabel(\"Loss\", fontsize=15);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11855d06-552d-4bc2-ba74-668422aace0c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base39", + "language": "python", + "name": "base39" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/class_imbalance/standard_model_config.yaml b/examples/class_imbalance/standard_model_config.yaml new file mode 100644 index 0000000..a0d2028 --- /dev/null +++ b/examples/class_imbalance/standard_model_config.yaml @@ -0,0 +1,32 @@ +input_features: + - name: Gender + type: category + - name: Age + type: number + - name: Driving_License + type: binary + - name: Region_Code + type: number + - name: Previously_Insured + type: binary + - name: Vehicle_Age + type: category + - name: Vehicle_Damage + type: category + - name: Annual_Premium + type: number + - name: Policy_Sales_Channel + type: number + - name: Vintage + type: number +output_features: + - name: Response + type: binary +trainer: + learning_rate: 0.0001 + learning_rate_scheduler: + decay: exponential + decay_rate: 0.9 + decay_steps: 30000 + staircase: True + epochs: 50 diff --git a/examples/forecasting/README.md b/examples/forecasting/README.md new file mode 100644 index 0000000..c6e9337 --- /dev/null +++ b/examples/forecasting/README.md @@ -0,0 +1,3 @@ +- Download and unpack hourly weather data from https://www.kaggle.com/selfishgene/historical-hourly-weather-data +- `ludwig train --config config.yaml --dataset temperature.csv` +- `ludwig forecast -n 10 --model_path results/experiment_run/model --dataset temperature.csv` diff --git a/examples/forecasting/config.yaml b/examples/forecasting/config.yaml new file mode 100644 index 0000000..c02c5a0 --- /dev/null +++ b/examples/forecasting/config.yaml @@ -0,0 +1,20 @@ +input_features: + - name: Seattle + type: timeseries + preprocessing: + window_size: 10 + encoder: + type: passthrough +output_features: + - name: Seattle_next + type: timeseries + column: Seattle + preprocessing: + horizon: 2 +combiner: + type: concat + flatten_inputs: true +preprocessing: + split: + type: datetime + column: datetime diff --git a/examples/getting_started/rotten_tomatoes.yaml b/examples/getting_started/rotten_tomatoes.yaml new file mode 100644 index 0000000..9ca13ff --- /dev/null +++ b/examples/getting_started/rotten_tomatoes.yaml @@ -0,0 +1,20 @@ +input_features: + - name: genres + type: set + preprocessing: + tokenizer: comma + - name: content_rating + type: category + - name: top_critic + type: binary + - name: runtime + type: number + - name: review_content + type: text + encoder: + type: embed +output_features: + - name: recommended + type: binary +trainer: + epochs: 3 diff --git a/examples/getting_started/run.sh b/examples/getting_started/run.sh new file mode 100755 index 0000000..16c6921 --- /dev/null +++ b/examples/getting_started/run.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Fail fast if an error occurs +set -e + +# Get the directory of this script, which contains the config file +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Download the data +wget https://ludwig.ai/latest/data/rotten_tomatoes.csv +wget https://ludwig.ai/latest/data/rotten_tomatoes_test.csv + +# Check the first 5 rows +head -n 5 rotten_tomatoes.csv + +# Train +ludwig train --config ${SCRIPT_DIR}/rotten_tomatoes.yaml --dataset rotten_tomatoes.csv + +# Predict and Evaluate +ludwig predict --model_path results/experiment_run/model --dataset rotten_tomatoes_test.csv diff --git a/examples/hypernetwork/README.md b/examples/hypernetwork/README.md new file mode 100644 index 0000000..26eceee --- /dev/null +++ b/examples/hypernetwork/README.md @@ -0,0 +1,73 @@ +# HyperNetworkCombiner: Conditional Feature Processing + +> **Note:** This example requires PR #4092 to be merged into Ludwig, or `pip install ludwig` >= 0.14. + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/hypernetwork/hypernetwork.ipynb) + +## What the HyperNetworkCombiner does differently + +Most combiners — including the default `concat` combiner — treat all input features +symmetrically: they encode each feature independently and then merge the resulting +vectors (by concatenation, attention, or summation). The merged representation is +the same *kind* of computation regardless of what any individual feature says. + +The `hypernetwork` combiner breaks this symmetry. One feature, called the +**conditioning feature**, is fed through a small *hyper-network* that generates the +weight matrices and biases of the fully-connected layers that process all other features. +In other words, the conditioning feature does not just *contribute* to the prediction — +it *rewrites the transformation* applied to every other feature before prediction happens. + +This is based on **HyperFusion** (arXiv 2403.13319, 2024). + +``` +sensor_type ──► HyperNetwork ──► generates weights W, b + │ +sensor_a ─────────────────────► FC(W, b) ──► combined +sensor_b ─────────────────────► FC(W, b) ──► repr. +sensor_c ─────────────────────► FC(W, b) ──► +``` + +Contrast with concat: + +``` +sensor_type ──► encoder ──┐ +sensor_a ──► encoder ──┤ +sensor_b ──► encoder ──┼──► concat ──► FC ──► output +sensor_c ──► encoder ──┘ +``` + +With `concat`, the network learns *after* combining to react to different sensor types. +With `hypernetwork`, the combination itself is conditioned on sensor type. + +## When to use it + +Use the `hypernetwork` combiner when: + +- One feature is a **context** or **mode** that fundamentally changes how other + features should be interpreted (sensor type, device class, environment, language). +- The relationship between inputs and the target changes qualitatively across groups, + not just quantitatively. +- You have enough training data to learn the per-context transformations (at minimum a + few hundred samples per conditioning category). + +Stick with `concat` when: + +- All input features contribute on equal footing. +- The dataset is small (the hyper-network adds parameters). +- Interpretability of the encoding step is important and you want a fixed transformation. + +## Files + +| File | Description | +| -------------------------- | ------------------------------------------------------------------------- | +| `hypernetwork.ipynb` | End-to-end walkthrough with synthetic sensor data | +| `config_concat.yaml` | Baseline concat config | +| `config_hypernetwork.yaml` | HyperNetworkCombiner config | +| `train_hypernetwork.py` | Standalone script — generates data, trains both models, prints comparison | + +## Quick start + +```bash +pip install "ludwig>=0.14" +python train_hypernetwork.py +``` diff --git a/examples/hypernetwork/config_concat.yaml b/examples/hypernetwork/config_concat.yaml new file mode 100644 index 0000000..e4b4b64 --- /dev/null +++ b/examples/hypernetwork/config_concat.yaml @@ -0,0 +1,31 @@ +model_type: ecd + +input_features: + - name: sensor_a + type: number + preprocessing: + normalization: zscore + - name: sensor_b + type: number + preprocessing: + normalization: zscore + - name: sensor_c + type: number + preprocessing: + normalization: zscore + - name: sensor_type + type: category + +output_features: + - name: anomaly + type: binary + +combiner: + type: concat + fc_layers: + - output_size: 128 + - output_size: 64 + +trainer: + epochs: 30 + learning_rate: 0.001 diff --git a/examples/hypernetwork/config_hypernetwork.yaml b/examples/hypernetwork/config_hypernetwork.yaml new file mode 100644 index 0000000..02391f9 --- /dev/null +++ b/examples/hypernetwork/config_hypernetwork.yaml @@ -0,0 +1,31 @@ +model_type: ecd + +input_features: + - name: sensor_a + type: number + preprocessing: + normalization: zscore + - name: sensor_b + type: number + preprocessing: + normalization: zscore + - name: sensor_c + type: number + preprocessing: + normalization: zscore + - name: sensor_type + type: category + +output_features: + - name: anomaly + type: binary + +combiner: + type: hypernetwork + hidden_size: 128 + hyper_hidden_size: 64 + output_size: 128 + +trainer: + epochs: 30 + learning_rate: 0.001 diff --git a/examples/hypernetwork/hypernetwork.ipynb b/examples/hypernetwork/hypernetwork.ipynb new file mode 100644 index 0000000..7580020 --- /dev/null +++ b/examples/hypernetwork/hypernetwork.ipynb @@ -0,0 +1,427 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a1b2c3d4-0001-0000-0000-000000000001", + "metadata": {}, + "source": [ + "# HyperNetworkCombiner: Conditional Feature Processing\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/hypernetwork/hypernetwork.ipynb)\n", + "\n", + "> **Note:** This notebook requires **Ludwig >= 0.14** (PR #4092). The `hypernetwork`\n", + "> combiner type is not available in earlier releases. Install with:\n", + "> `pip install \"ludwig>=0.14\"`\n", + "\n", + "This notebook demonstrates the `HyperNetworkCombiner`, which lets one feature\n", + "(**the conditioning feature**) generate the weights of the layers that process all\n", + "other features — rather than simply concatenating everyone together.\n", + "\n", + "Based on **HyperFusion** ([arXiv 2403.13319](https://arxiv.org/abs/2403.13319), 2024).\n", + "\n", + "**What we cover:**\n", + "\n", + "1. Why concatenation is not always enough\n", + "2. Generating a synthetic multi-modal sensor dataset\n", + "3. Baseline: concat combiner\n", + "4. HyperNetworkCombiner\n", + "5. Comparing results and understanding why hypernetwork wins" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0002-0000-0000-000000000002", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install \"ludwig>=0.14\" --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0003-0000-0000-000000000003", + "metadata": {}, + "source": [ + "## The problem: context-dependent features\n", + "\n", + "Imagine a network of industrial sensors. Each sensor reports three readings\n", + "(`sensor_a`, `sensor_b`, `sensor_c`), but every sensor belongs to one of three\n", + "measurement types: **temperature**, **pressure**, or **humidity**.\n", + "\n", + "The catch: **the same numerical reading means something completely different**\n", + "depending on the sensor type.\n", + "\n", + "- For a **temperature** sensor, `sensor_a = 3.0` is an anomaly (overheating).\n", + "- For a **pressure** sensor, `sensor_a = 3.0` is perfectly normal.\n", + "- For a **humidity** sensor, the anomaly rule involves the *sum* of all three readings.\n", + "\n", + "A concat combiner encodes all four features independently and then stitches them\n", + "together. The network has to learn — **after** the concatenation — to undo the mixing\n", + "and apply type-specific logic. This is hard because the critical signal (`sensor_type`)\n", + "is buried in a shared representation alongside the numerical readings.\n", + "\n", + "The `hypernetwork` combiner solves this directly:\n", + "\n", + "```\n", + "sensor_type ──► HyperNetwork ──► generates weights W, b\n", + " │\n", + "sensor_a ────────────────────► FC(W, b) ──►\n", + "sensor_b ────────────────────► FC(W, b) ──► combined repr.\n", + "sensor_c ────────────────────► FC(W, b) ──►\n", + "```\n", + "\n", + "`sensor_type` does not contribute a feature vector — it **rewrites the entire\n", + "transformation** applied to the numerical sensors." + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0004-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "We generate a synthetic dataset with three sensor types. Each type has its own\n", + "normal operating range and its own anomaly rule, making the sensor type an\n", + "essential piece of context for correct classification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0005-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "RNG = np.random.default_rng(42)\n", + "\n", + "N_PER_TYPE = 600\n", + "SENSOR_TYPES = [\"temperature\", \"pressure\", \"humidity\"]\n", + "\n", + "\n", + "def make_samples(sensor_type: str, n: int, rng: np.random.Generator) -> pd.DataFrame:\n", + " \"\"\"Generate n samples for a single sensor type with type-specific anomaly rules.\"\"\"\n", + " if sensor_type == \"temperature\":\n", + " # Normal: readings near 0; anomaly: sensor_a > 2.5 (overheating)\n", + " sensor_a = rng.normal(0.0, 1.0, n)\n", + " sensor_b = rng.normal(0.0, 1.0, n)\n", + " sensor_c = rng.normal(0.0, 1.0, n)\n", + " anomaly = (sensor_a > 2.5).astype(int)\n", + " elif sensor_type == \"pressure\":\n", + " # Normal: readings near 1; anomaly: sensor_b drops below -0.5 (leak)\n", + " sensor_a = rng.normal(1.0, 0.8, n)\n", + " sensor_b = rng.normal(1.0, 0.8, n)\n", + " sensor_c = rng.normal(1.0, 0.8, n)\n", + " anomaly = (sensor_b < -0.5).astype(int)\n", + " else: # humidity\n", + " # Normal: readings near -1; anomaly: combined level exceeds threshold\n", + " sensor_a = rng.normal(-1.0, 0.9, n)\n", + " sensor_b = rng.normal(-1.0, 0.9, n)\n", + " sensor_c = rng.normal(-1.0, 0.9, n)\n", + " anomaly = ((sensor_a + sensor_b + sensor_c) > 0).astype(int)\n", + "\n", + " return pd.DataFrame(\n", + " {\n", + " \"sensor_a\": sensor_a,\n", + " \"sensor_b\": sensor_b,\n", + " \"sensor_c\": sensor_c,\n", + " \"sensor_type\": sensor_type,\n", + " \"anomaly\": anomaly,\n", + " }\n", + " )\n", + "\n", + "\n", + "frames = [make_samples(t, N_PER_TYPE, RNG) for t in SENSOR_TYPES]\n", + "df = pd.concat(frames, ignore_index=True).sample(frac=1, random_state=42).reset_index(drop=True)\n", + "\n", + "# Train / validation / test split (70 / 15 / 15)\n", + "n = len(df)\n", + "split = np.full(n, 2, dtype=int) # default: test\n", + "idx = np.arange(n)\n", + "RNG.shuffle(idx)\n", + "split[idx[: int(0.70 * n)]] = 0\n", + "split[idx[int(0.70 * n) : int(0.85 * n)]] = 1\n", + "df[\"split\"] = split\n", + "\n", + "print(f\"Total rows: {n}\")\n", + "print(f\"Overall anomaly rate: {df['anomaly'].mean():.1%}\")\n", + "print()\n", + "print(\"Anomaly rate by sensor type:\")\n", + "print(df.groupby(\"sensor_type\")[\"anomaly\"].mean().rename(\"anomaly_rate\"))\n", + "print()\n", + "df.head(10)" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0006-0000-0000-000000000006", + "metadata": {}, + "source": [ + "Notice that the three sensor types have **different anomaly rates** and, more\n", + "importantly, the anomaly rules are structurally different. The same value of\n", + "`sensor_a = 3.0` triggers an anomaly for `temperature` but not for `pressure`.\n", + "A model that treats all features symmetrically will struggle with this." + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0007-0000-0000-000000000007", + "metadata": {}, + "source": [ + "## Baseline: concat combiner\n", + "\n", + "The default Ludwig combiner concatenates all encoder outputs and passes them\n", + "through fully-connected layers. `sensor_type` is just another input — its\n", + "embedding is concatenated alongside the numerical sensor values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0008-0000-0000-000000000008", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "from ludwig.api import LudwigModel\n", + "\n", + "config_concat_str = \"\"\"\n", + "model_type: ecd\n", + "\n", + "input_features:\n", + " - name: sensor_a\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_b\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_c\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_type\n", + " type: category\n", + "\n", + "output_features:\n", + " - name: anomaly\n", + " type: binary\n", + "\n", + "combiner:\n", + " type: concat\n", + " fc_layers:\n", + " - output_size: 128\n", + " - output_size: 64\n", + "\n", + "trainer:\n", + " epochs: 30\n", + " learning_rate: 0.001\n", + "\"\"\"\n", + "\n", + "config_concat = yaml.safe_load(config_concat_str)\n", + "\n", + "model_concat = LudwigModel(config_concat, logging_level=30)\n", + "model_concat.train(dataset=df)\n", + "\n", + "test_df = df[df[\"split\"] == 2].copy()\n", + "preds_concat, _ = model_concat.predict(dataset=test_df)\n", + "\n", + "acc_concat = (preds_concat[\"anomaly_predictions\"].values == test_df[\"anomaly\"].values).mean()\n", + "print(f\"Concat combiner — test accuracy: {acc_concat:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0009-0000-0000-000000000009", + "metadata": {}, + "source": [ + "## HyperNetworkCombiner\n", + "\n", + "Now we switch to `type: hypernetwork`. The combiner reads `sensor_type` (the last\n", + "feature listed in `input_features`) through a hyper-network and uses the output to\n", + "generate the weight matrix and bias of the layer that processes `sensor_a`,\n", + "`sensor_b`, and `sensor_c`.\n", + "\n", + "Key parameters:\n", + "\n", + "| Parameter | Role |\n", + "|---|---|\n", + "| `hidden_size` | Size of the main processing layer |\n", + "| `hyper_hidden_size` | Hidden size of the hyper-network itself |\n", + "| `output_size` | Dimension of the combined representation passed to decoders |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0010-0000-0000-000000000010", + "metadata": {}, + "outputs": [], + "source": [ + "config_hypernetwork_str = \"\"\"\n", + "model_type: ecd\n", + "\n", + "input_features:\n", + " - name: sensor_a\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_b\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_c\n", + " type: number\n", + " preprocessing:\n", + " normalization: zscore\n", + " - name: sensor_type\n", + " type: category\n", + "\n", + "output_features:\n", + " - name: anomaly\n", + " type: binary\n", + "\n", + "combiner:\n", + " type: hypernetwork\n", + " hidden_size: 128\n", + " hyper_hidden_size: 64\n", + " output_size: 128\n", + "\n", + "trainer:\n", + " epochs: 30\n", + " learning_rate: 0.001\n", + "\"\"\"\n", + "\n", + "config_hypernetwork = yaml.safe_load(config_hypernetwork_str)\n", + "\n", + "model_hypernetwork = LudwigModel(config_hypernetwork, logging_level=30)\n", + "model_hypernetwork.train(dataset=df)\n", + "\n", + "preds_hyper, _ = model_hypernetwork.predict(dataset=test_df)\n", + "\n", + "acc_hyper = (preds_hyper[\"anomaly_predictions\"].values == test_df[\"anomaly\"].values).mean()\n", + "print(f\"HyperNetworkCombiner — test accuracy: {acc_hyper:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0011-0000-0000-000000000011", + "metadata": {}, + "source": [ + "## Comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0012-0000-0000-000000000012", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "# Per-type breakdown\n", + "rows = []\n", + "for stype in SENSOR_TYPES:\n", + " mask = test_df[\"sensor_type\"] == stype\n", + " true = test_df.loc[mask, \"anomaly\"].values\n", + "\n", + " acc_c = (preds_concat.loc[mask, \"anomaly_predictions\"].values == true).mean()\n", + " acc_h = (preds_hyper.loc[mask, \"anomaly_predictions\"].values == true).mean()\n", + " rows.append({\"sensor_type\": stype, \"concat\": round(acc_c, 4), \"hypernetwork\": round(acc_h, 4)})\n", + "\n", + "rows.append({\"sensor_type\": \"OVERALL\", \"concat\": round(acc_concat, 4), \"hypernetwork\": round(acc_hyper, 4)})\n", + "\n", + "results_df = pd.DataFrame(rows)\n", + "print(results_df.to_string(index=False))\n", + "\n", + "# Bar chart\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "x = np.arange(len(rows))\n", + "width = 0.35\n", + "ax.bar(x - width / 2, results_df[\"concat\"], width, label=\"Concat\", color=\"steelblue\")\n", + "ax.bar(x + width / 2, results_df[\"hypernetwork\"], width, label=\"HyperNetwork\", color=\"darkorange\")\n", + "ax.set_xticks(x)\n", + "ax.set_xticklabels(results_df[\"sensor_type\"])\n", + "ax.set_ylabel(\"Test accuracy\")\n", + "ax.set_ylim(0.5, 1.0)\n", + "ax.set_title(\"Sensor anomaly detection: concat vs HyperNetworkCombiner\")\n", + "ax.legend()\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0013-0000-0000-000000000013", + "metadata": {}, + "source": [ + "## Why hypernetwork wins\n", + "\n", + "### The problem with concatenation\n", + "\n", + "When we use `concat`, the model receives a vector like:\n", + "\n", + "```\n", + "[enc(sensor_a), enc(sensor_b), enc(sensor_c), enc(sensor_type)]\n", + "```\n", + "\n", + "The fully-connected layers after the concat have to learn — from scratch — that\n", + "`enc(sensor_type)` should *gate* the interpretation of the numerical sensors.\n", + "Effectively the network must implement a conditional logic as a series of\n", + "multiplications and additions over the entire concatenated vector. This is\n", + "theoretically possible but inefficient: many capacity-bearing parameters in the\n", + "FC layers end up implementing the routing rather than the actual anomaly detection.\n", + "\n", + "### What hypernetwork does instead\n", + "\n", + "The `HyperNetworkCombiner` separates the two roles explicitly:\n", + "\n", + "1. **Hyper-network** — a small MLP that reads `sensor_type` and emits a vector\n", + " of weights `W` and biases `b`.\n", + "2. **Main network** — a linear layer `FC(W, b)` applied to the concatenation of\n", + " `[sensor_a, sensor_b, sensor_c]` using the *dynamically generated* `W` and `b`.\n", + "\n", + "Because `W` and `b` are different for each sensor type, the transformation\n", + "applied to the numerical sensors is literally different per context. For\n", + "`temperature`, the generated `W` learns to make `sensor_a` highly predictive;\n", + "for `pressure`, the generated `W` shifts attention to `sensor_b`; for\n", + "`humidity`, it learns to combine all three.\n", + "\n", + "### When to use it\n", + "\n", + "- The conditioning feature is a **type, class, mode, or context** that changes\n", + " the semantics of other features qualitatively.\n", + "- You have enough samples per conditioning category (roughly 200+ per class) to\n", + " learn meaningful per-context transformations.\n", + "- The target signal requires different logic for different contexts, not just\n", + " different magnitudes.\n", + "\n", + "### When to stick with concat\n", + "\n", + "- All features contribute on equal footing with no hierarchical conditioning.\n", + "- The dataset is very small — the hyper-network adds parameters.\n", + "- The extra complexity is not warranted (always start simple)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/hypernetwork/train_hypernetwork.py b/examples/hypernetwork/train_hypernetwork.py new file mode 100644 index 0000000..d0bdc67 --- /dev/null +++ b/examples/hypernetwork/train_hypernetwork.py @@ -0,0 +1,160 @@ +# Colab: !pip install "ludwig>=0.14" +"""HyperNetworkCombiner vs concat — sensor anomaly detection. + +Generates a synthetic multi-modal sensor dataset where the correct interpretation +of numerical sensor readings depends entirely on the sensor type (temperature, +pressure, or humidity). Trains a baseline concat model and a HyperNetworkCombiner +model, then prints an accuracy comparison. + +NOTE: Requires ludwig >= 0.14 (PR #4092). The hypernetwork combiner is not +available in earlier versions. + +Usage: + python train_hypernetwork.py +""" + +import numpy as np +import pandas as pd + +from ludwig.api import LudwigModel + +# --------------------------------------------------------------------------- +# 1. Generate synthetic sensor data +# --------------------------------------------------------------------------- + +RNG = np.random.default_rng(42) + +N_PER_TYPE = 600 +SENSOR_TYPES = ["temperature", "pressure", "humidity"] + + +def make_samples(sensor_type: str, n: int, rng: np.random.Generator) -> pd.DataFrame: + """Generate n samples for a single sensor type. + + Each type has its own 'normal' operating range and anomaly rule so that the same raw reading can mean very different + things depending on the type. + """ + if sensor_type == "temperature": + # Normal: sensors cluster near (0, 0, 0); anomaly: sensor_a > 2.5 + sensor_a = rng.normal(0.0, 1.0, n) + sensor_b = rng.normal(0.0, 1.0, n) + sensor_c = rng.normal(0.0, 1.0, n) + anomaly = (sensor_a > 2.5).astype(int) + elif sensor_type == "pressure": + # Normal: sensors cluster near (1, 1, 1); anomaly: sensor_b < -1.5 + sensor_a = rng.normal(1.0, 0.8, n) + sensor_b = rng.normal(1.0, 0.8, n) + sensor_c = rng.normal(1.0, 0.8, n) + anomaly = (sensor_b < -0.5).astype(int) + else: # humidity + # Normal: sensors cluster near (-1, -1, -1); anomaly: sum > 0 + sensor_a = rng.normal(-1.0, 0.9, n) + sensor_b = rng.normal(-1.0, 0.9, n) + sensor_c = rng.normal(-1.0, 0.9, n) + anomaly = ((sensor_a + sensor_b + sensor_c) > 0).astype(int) + + return pd.DataFrame( + { + "sensor_a": sensor_a, + "sensor_b": sensor_b, + "sensor_c": sensor_c, + "sensor_type": sensor_type, + "anomaly": anomaly, + } + ) + + +frames = [make_samples(t, N_PER_TYPE, RNG) for t in SENSOR_TYPES] +df = pd.concat(frames, ignore_index=True).sample(frac=1, random_state=42).reset_index(drop=True) + +# Train / validation / test split (70 / 15 / 15) +n = len(df) +split = np.full(n, 2, dtype=int) # default: test +idx = np.arange(n) +RNG.shuffle(idx) +split[idx[: int(0.70 * n)]] = 0 +split[idx[int(0.70 * n) : int(0.85 * n)]] = 1 +df["split"] = split + +print(f"Dataset: {n} rows ({df['anomaly'].mean():.1%} anomalies)") +print(df.groupby("sensor_type")["anomaly"].mean().rename("anomaly_rate").to_string()) +print() + +# --------------------------------------------------------------------------- +# 2. Model configs +# --------------------------------------------------------------------------- + +INPUT_FEATURES = [ + {"name": "sensor_a", "type": "number", "preprocessing": {"normalization": "zscore"}}, + {"name": "sensor_b", "type": "number", "preprocessing": {"normalization": "zscore"}}, + {"name": "sensor_c", "type": "number", "preprocessing": {"normalization": "zscore"}}, + {"name": "sensor_type", "type": "category"}, +] + +OUTPUT_FEATURES = [{"name": "anomaly", "type": "binary"}] + +TRAINER = {"epochs": 30, "learning_rate": 0.001} + +config_concat = { + "model_type": "ecd", + "input_features": INPUT_FEATURES, + "output_features": OUTPUT_FEATURES, + "combiner": { + "type": "concat", + "fc_layers": [{"output_size": 128}, {"output_size": 64}], + }, + "trainer": TRAINER, +} + +config_hypernetwork = { + "model_type": "ecd", + "input_features": INPUT_FEATURES, + "output_features": OUTPUT_FEATURES, + "combiner": { + "type": "hypernetwork", + "hidden_size": 128, + "hyper_hidden_size": 64, + "output_size": 128, + }, + "trainer": TRAINER, +} + +# --------------------------------------------------------------------------- +# 3. Train and evaluate +# --------------------------------------------------------------------------- + +results = [] + +for label, config in [("Concat (baseline)", config_concat), ("HyperNetwork", config_hypernetwork)]: + print(f"{'=' * 60}") + print(f"Training: {label}") + print("=" * 60) + + model = LudwigModel(config, logging_level=30) + results_obj = model.train(dataset=df) + print(f" Saved to: {results_obj.output_directory}") + + test_df = df[df["split"] == 2].copy() + predictions, _ = model.predict(dataset=test_df) + + pred_col = "anomaly_predictions" + correct = (predictions[pred_col].values == test_df["anomaly"].values).mean() + + results.append({"Model": label, "Test accuracy": round(float(correct), 4)}) + print(f" Test accuracy: {correct:.4f}\n") + +# --------------------------------------------------------------------------- +# 4. Print summary +# --------------------------------------------------------------------------- + +results_df = pd.DataFrame(results) + +print("=" * 50) +print("SENSOR ANOMALY DETECTION — SUMMARY") +print("=" * 50) +print(results_df.to_string(index=False)) +print("=" * 50) +print() +print("The HyperNetworkCombiner lets sensor_type rewrite the") +print("transformation applied to sensor_a/b/c rather than") +print("just concatenating all features together.") diff --git a/examples/hyperopt/README.md b/examples/hyperopt/README.md new file mode 100644 index 0000000..3ff201c --- /dev/null +++ b/examples/hyperopt/README.md @@ -0,0 +1,28 @@ +# Hyperparameter Optimization + +Demonstrates hyperparameter optimization using Ludwig's in-built capabilities. + +### Preparatory Steps + +- Create `data` directory +- Download [Kaggle wine quality data set](https://www.kaggle.com/rajyellow46/wine-quality) into the `data` directory. Directory should + appear as follows: + +``` +hyperopt/ + data/ + winequalityN.csv +``` + +### Description + +Jupyter notebook `model_hyperopt_example.ipynb` demonstrates several hyperparameter optimization capabilities. Key features demonstrated in the notebook: + +- Training data is prepared for use +- Programmatically create Ludwig config dictionary from the training data dataframe +- Setup parameter space for hyperparameter optimization +- Perform two hyperparameter runs + - Parallel workers using random search strategy + - Serial processing using random search strategy + - Parallel workers using grid search strategy (Note: takes about 35 minutes) +- Demonstrate various Ludwig visualizations for hyperparameter optimization diff --git a/examples/hyperopt/README_optuna.md b/examples/hyperopt/README_optuna.md new file mode 100644 index 0000000..2cf3c7d --- /dev/null +++ b/examples/hyperopt/README_optuna.md @@ -0,0 +1,105 @@ +# Native Optuna Hyperparameter Optimization + +> **Requires Ludwig 0.15 / PR #4090 (data-pipeline-hyperopt-modernization branch).** + +Ludwig 0.15 adds a native Optuna executor that runs HPO trials directly without requiring +Ray Tune. This is the right choice for single-machine HPO: you get AutoSampler, GPSampler +(Bayesian optimization), TPE, CMA-ES, median / Hyperband pruning, SQLite-backed resumable +studies, and the optional Optuna dashboard — without the overhead of a Ray cluster. + +If you need distributed trials across many GPUs or nodes, keep using the `ray` executor +(it wraps `OptunaSearch` as its search algorithm). The native executor in this tutorial is +faster, simpler, and single-process. + +## Config + +```yaml +hyperopt: + executor: + type: optuna + num_samples: 50 # how many trials to run + sampler: auto # auto | gp | tpe | cmaes | random + pruner: null # null | median | hyperband (optional early stopping) + study_name: ludwig_wine_rmse + storage: null # or sqlite:///wine_hpo.db to persist and resume + time_budget_s: 1800 + + parameters: + trainer.learning_rate: + space: loguniform + lower: 1e-5 + upper: 1e-1 + trainer.batch_size: + space: int + lower: 32 + upper: 256 + combiner.num_fc_layers: + space: int + lower: 1 + upper: 4 + combiner.output_size: + space: choice + categories: [32, 64, 128, 256] + + output_feature: quality + metric: root_mean_squared_error + goal: minimize + split: validation +``` + +### Sampler options + +| `sampler` | Description | Rule of thumb | +| --------- | -------------------------------------------------------- | -------------------------------- | +| `auto` | Optuna AutoSampler (falls back to TPE on older versions) | Default choice | +| `gp` | Gaussian-Process Bayesian optimization | Continuous spaces, \<100 trials | +| `tpe` | Tree-structured Parzen Estimator | Mixed spaces, 50–500 trials | +| `cmaes` | Covariance Matrix Adaptation Evolution Strategy | Purely-continuous, medium budget | +| `random` | Random search (sanity-check baseline) | Sanity check | + +### Persistence and resuming + +Set `storage: sqlite:///wine_hpo.db` to persist trials to disk. Re-running with the same +`study_name` continues the study — failed trials are retried, successful trials are kept. + +### Pruning + +Set `pruner: median` or `pruner: hyperband` to stop clearly-losing trials early. Requires +the model code to report intermediate values back (Ludwig's Optuna integration reports the +validation metric at each epoch so this works out of the box). + +## Running + +```bash +pip install 'ludwig[hyperopt]' # pulls in optuna +python optuna_executor.py +``` + +Expected output (numbers are illustrative): + +``` +[Optuna] Best trial: + value: 0.6184 + params: + trainer.learning_rate: 0.0032 + trainer.batch_size: 64 + combiner.num_fc_layers: 2 + combiner.output_size: 128 + completed in: 412.8s +``` + +## Files + +| File | Description | +| -------------------- | ----------------------------------------------------- | +| `config_optuna.yaml` | Full hyperopt config using the native Optuna executor | +| `optuna_executor.py` | Runs `ludwig.hyperopt` with the above config | +| `README_optuna.md` | This file | + +## References + +- Optuna — Akiba et al., "Optuna: A Next-generation Hyperparameter Optimization Framework", + KDD 2019. +- AutoSampler — Optuna v4 AutoSampler documentation. +- Hyperband — Li et al., "Hyperband: A Novel Bandit-Based Approach to Hyperparameter + Optimization", JMLR 2018. diff --git a/examples/hyperopt/config_optuna.yaml b/examples/hyperopt/config_optuna.yaml new file mode 100644 index 0000000..baed353 --- /dev/null +++ b/examples/hyperopt/config_optuna.yaml @@ -0,0 +1,83 @@ +model_type: ecd + +input_features: + - name: fixed acidity + type: number + preprocessing: + normalization: zscore + - name: volatile acidity + type: number + preprocessing: + normalization: zscore + - name: citric acid + type: number + preprocessing: + normalization: zscore + - name: residual sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free sulfur dioxide + type: number + preprocessing: + normalization: zscore + - name: total sulfur dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality + type: binary + +trainer: + epochs: 20 + +# NOTE: The Optuna executor requires PR #4090 to be merged, or Ludwig >= 0.14. +# Install: pip install ludwig optuna +hyperopt: + executor: + type: optuna + num_samples: 20 + sampler: auto # auto selects the best sampler; also: tpe, gp, cmaes, random + pruner: hyperband # stop unpromising trials early (MedianPruner also supported) + storage: sqlite:///optuna_results.db # enables resumability across runs + parameters: + trainer.learning_rate: + space: loguniform + lower: 1.0e-5 + upper: 1.0e-2 + trainer.batch_size: + space: int + lower: 16 + upper: 256 + trainer.optimizer.type: + space: choice + categories: + - adam + - adamw + - radam + - schedule_free_adamw + goal: minimize + metric: validation.combined.loss + split: validation diff --git a/examples/hyperopt/model_hyperopt_example.ipynb b/examples/hyperopt/model_hyperopt_example.ipynb new file mode 100644 index 0000000..b7ed905 --- /dev/null +++ b/examples/hyperopt/model_hyperopt_example.ipynb @@ -0,0 +1,1124 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hyperparameter Optimization In Ludwig\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Demonstrates hyper-parameter tuning capabilities of Ludwig. The following steps occur in this notebook:\n", + "* Training data is prepared for use\n", + "* Programmatically create Ludwig config dictionary from the training data dataframe\n", + "* Setup parameter space for hyperparameter optimization\n", + "* Perform two hyperparameter runs\n", + " * Parallel workers using random search strategy\n", + " * Serial processing using random search strategy\n", + " * Parallel workers using grid search strategy\n", + "* Demonstrate various Ludwig visualizations for hyperparameter optimization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import required libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "warnings.simplefilter(\"ignore\")\n", + "\n", + "import datetime\n", + "import shutil\n", + "\n", + "import pandas as pd\n", + "\n", + "from ludwig.hyperopt.run import hyperopt\n", + "from ludwig.visualize import hyperopt_hiplot_cli, hyperopt_report_cli, hyperopt_results_to_dataframe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Retrieve data for training" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(6497, 13)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_df = pd.read_csv(\"./data/winequalityN.csv\")\n", + "train_df.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Standardize column names to replace spaces(\" \") with underscore(\"_\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "new_col = []\n", + "for i in range(len(train_df.columns)):\n", + " new_col.append(train_df.columns[i].replace(\" \", \"_\"))\n", + "\n", + "train_df.columns = new_col" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data Set Overview" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "type object\n", + "fixed_acidity float64\n", + "volatile_acidity float64\n", + "citric_acid float64\n", + "residual_sugar float64\n", + "chlorides float64\n", + "free_sulfur_dioxide float64\n", + "total_sulfur_dioxide float64\n", + "density float64\n", + "pH float64\n", + "sulphates float64\n", + "alcohol float64\n", + "quality int64\n", + "dtype: object" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_df.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create training and test data sets" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3 30\n", + "4 216\n", + "5 2138\n", + "6 2836\n", + "7 1079\n", + "8 193\n", + "9 5\n", + "Name: quality, dtype: int64" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_df[\"quality\"].value_counts().sort_index()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "categorical variables: ['type'] \n", + "\n", + "numerical variables: ['sulphates', 'fixed_acidity', 'total_sulfur_dioxide', 'density', 'pH', 'residual_sugar', 'free_sulfur_dioxide', 'alcohol', 'citric_acid', 'volatile_acidity', 'chlorides'] \n", + "\n" + ] + } + ], + "source": [ + "# isolate the predictor variables only\n", + "predictor_vars = list(set(train_df.columns) - set([\"quality\"]))\n", + "\n", + "# extract categorical variables\n", + "categorical_vars = []\n", + "for p in predictor_vars:\n", + " if train_df[p].dtype == \"object\":\n", + " categorical_vars.append(p)\n", + "\n", + "print(\"categorical variables:\", categorical_vars, \"\\n\")\n", + "\n", + "# get numerical variables\n", + "numerical_vars = list(set(predictor_vars) - set(categorical_vars))\n", + "\n", + "print(\"numerical variables:\", numerical_vars, \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
countmeanstdmin25%50%75%max
fixed_acidity6487.07.2165791.2967503.800006.400007.000007.7000015.90000
volatile_acidity6489.00.3396910.1646490.080000.230000.290000.400001.58000
citric_acid6494.00.3187220.1452650.000000.250000.310000.390001.66000
residual_sugar6495.05.4443264.7581250.600001.800003.000008.1000065.80000
chlorides6495.00.0560420.0350360.009000.038000.047000.065000.61100
free_sulfur_dioxide6497.030.52531917.7494001.0000017.0000029.0000041.00000289.00000
total_sulfur_dioxide6497.0115.74457456.5218556.0000077.00000118.00000156.00000440.00000
density6497.00.9946970.0029990.987110.992340.994890.996991.03898
pH6488.03.2183950.1607482.720003.110003.210003.320004.01000
sulphates6493.00.5312150.1488140.220000.430000.510000.600002.00000
alcohol6497.010.4918011.1927128.000009.5000010.3000011.3000014.90000
quality6497.05.8183780.8732553.000005.000006.000006.000009.00000
\n", + "
" + ], + "text/plain": [ + " count mean std min 25% \\\n", + "fixed_acidity 6487.0 7.216579 1.296750 3.80000 6.40000 \n", + "volatile_acidity 6489.0 0.339691 0.164649 0.08000 0.23000 \n", + "citric_acid 6494.0 0.318722 0.145265 0.00000 0.25000 \n", + "residual_sugar 6495.0 5.444326 4.758125 0.60000 1.80000 \n", + "chlorides 6495.0 0.056042 0.035036 0.00900 0.03800 \n", + "free_sulfur_dioxide 6497.0 30.525319 17.749400 1.00000 17.00000 \n", + "total_sulfur_dioxide 6497.0 115.744574 56.521855 6.00000 77.00000 \n", + "density 6497.0 0.994697 0.002999 0.98711 0.99234 \n", + "pH 6488.0 3.218395 0.160748 2.72000 3.11000 \n", + "sulphates 6493.0 0.531215 0.148814 0.22000 0.43000 \n", + "alcohol 6497.0 10.491801 1.192712 8.00000 9.50000 \n", + "quality 6497.0 5.818378 0.873255 3.00000 5.00000 \n", + "\n", + " 50% 75% max \n", + "fixed_acidity 7.00000 7.70000 15.90000 \n", + "volatile_acidity 0.29000 0.40000 1.58000 \n", + "citric_acid 0.31000 0.39000 1.66000 \n", + "residual_sugar 3.00000 8.10000 65.80000 \n", + "chlorides 0.04700 0.06500 0.61100 \n", + "free_sulfur_dioxide 29.00000 41.00000 289.00000 \n", + "total_sulfur_dioxide 118.00000 156.00000 440.00000 \n", + "density 0.99489 0.99699 1.03898 \n", + "pH 3.21000 3.32000 4.01000 \n", + "sulphates 0.51000 0.60000 2.00000 \n", + "alcohol 10.30000 11.30000 14.90000 \n", + "quality 6.00000 6.00000 9.00000 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_df.describe().T" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "unique values for type is 2\n" + ] + } + ], + "source": [ + "for p in categorical_vars:\n", + " print(\"unique values for\", p, \"is\", train_df[p].nunique())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create config" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# template for config\n", + "config = {\"input_features\": [], \"output_features\": [], \"trainer\": {}}\n", + "\n", + "# setup input features for categorical variables\n", + "for p in categorical_vars:\n", + " a_feature = {\"name\": p.replace(\" \", \"_\"), \"type\": \"category\", \"representation\": \"sparse\"}\n", + " config[\"input_features\"].append(a_feature)\n", + "\n", + "\n", + "# setup input features for numerical variables\n", + "for p in numerical_vars:\n", + " a_feature = {\n", + " \"name\": p.replace(\" \", \"_\"),\n", + " \"type\": \"number\",\n", + " \"preprocessing\": {\"missing_value_strategy\": \"fill_with_mean\", \"normalization\": \"zscore\"},\n", + " }\n", + " config[\"input_features\"].append(a_feature)\n", + "\n", + "# set up output variable\n", + "config[\"output_features\"].append({\"name\": \"quality\", \"type\": \"category\"})\n", + "\n", + "# set up trainer\n", + "config[\"trainer\"] = {\"epochs\": 20}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "config:\n" + ] + }, + { + "data": { + "text/plain": [ + "{'input_features': [{'name': 'type',\n", + " 'type': 'category',\n", + " 'representation': 'sparse'},\n", + " {'name': 'sulphates',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'fixed_acidity',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'total_sulfur_dioxide',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'density',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'pH',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'residual_sugar',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'free_sulfur_dioxide',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'alcohol',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'citric_acid',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'volatile_acidity',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'chlorides',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}}],\n", + " 'output_features': [{'name': 'quality', 'type': 'category'}],\n", + " 'trainer': {'epochs': 20}}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# View the config\n", + "print(\"config:\")\n", + "config" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define hyperparameter search space" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "SEED = 13\n", + "\n", + "hyperopt_configs = {\n", + " \"parameters\": {\n", + " \"trainer.learning_rate\": {\n", + " \"type\": \"float\",\n", + " \"space\": \"loguniform\",\n", + " \"lower\": 0.0001,\n", + " \"upper\": 0.01,\n", + " \"q\": 3,\n", + " },\n", + " \"trainer.batch_size\": {\n", + " \"type\": \"int\",\n", + " \"space\": \"qlograndint\",\n", + " \"base\": 2,\n", + " \"lower\": 32,\n", + " \"upper\": 256,\n", + " \"q\": 5,\n", + " },\n", + " \"quality.fc_size\": {\n", + " \"type\": \"int\",\n", + " \"space\": \"qrandint\",\n", + " \"lower\": 32,\n", + " \"upper\": 256,\n", + " \"q\": 5,\n", + " },\n", + " \"quality.num_fc_layers\": {\n", + " \"type\": \"int\",\n", + " \"space\": \"qrandint\",\n", + " \"lower\": 1,\n", + " \"upper\": 5,\n", + " \"q\": 4,\n", + " },\n", + " },\n", + " \"goal\": \"minimize\",\n", + " \"output_feature\": \"quality\",\n", + " \"validation_metrics\": \"loss\",\n", + "}\n", + "\n", + "# add hyperopt parameter space to the config\n", + "config[\"hyperopt\"] = hyperopt_configs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train with optimal hyperparameters on the whole data set" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# clean out old results\n", + "shutil.rmtree(\"./results_ray\", ignore_errors=True)\n", + "shutil.rmtree(\"./results_random_serial\", ignore_errors=True)\n", + "shutil.rmtree(\"./visualizations\", ignore_errors=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Random Search with ray executors\n", + "\n", + "This executor will use a local run cluster with 3 samples (should take less than 30 seconds)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{ 'executor': {'time_budget_s': 1000, 'type': 'ray'},\n", + " 'goal': 'minimize',\n", + " 'metric': 'loss',\n", + " 'output_feature': 'quality',\n", + " 'parameters': { 'quality.fc_size': { 'lower': 32,\n", + " 'q': 5,\n", + " 'space': 'qrandint',\n", + " 'type': 'int',\n", + " 'upper': 256},\n", + " 'quality.num_fc_layers': { 'lower': 1,\n", + " 'q': 4,\n", + " 'space': 'qrandint',\n", + " 'type': 'int',\n", + " 'upper': 5},\n", + " 'trainer.batch_size': { 'base': 2,\n", + " 'lower': 32,\n", + " 'q': 5,\n", + " 'space': 'qlograndint',\n", + " 'type': 'int',\n", + " 'upper': 256},\n", + " 'trainer.learning_rate': { 'lower': 0.0001,\n", + " 'q': 3,\n", + " 'space': 'loguniform',\n", + " 'type': 'float',\n", + " 'upper': 0.01}},\n", + " 'sampler': {'num_samples': 3, 'type': 'ray'},\n", + " 'split': 'validation',\n", + " 'validation_metrics': 'loss'}\n", + "\n", + "\n", + "Initializing new Ray cluster...\n", + "CPU times: user 492 ms, sys: 207 ms, total: 699 ms\n", + "Wall time: 12 s\n" + ] + } + ], + "source": [ + "%%time\n", + "%%capture\n", + "print(\"starting:\", datetime.datetime.now())\n", + "config[\"hyperopt\"][\"executor\"] = {\"type\": \"ray\", \"time_budget_s\": 1000}\n", + "config[\"hyperopt\"][\"sampler\"] = {\"type\": \"ray\", \"num_samples\": 3}\n", + "results_ray = hyperopt(\n", + " config,\n", + " dataset=train_df.sample(4000, random_state=42), # limit number records for demonstration purposes\n", + " output_directory=\"results_ray\", # location to place results\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Random Search with serial executor\n", + "\n", + "Run the serialize executor with 2 samples (should take less then 3 minutes)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "hyperopt_configs = {\n", + " \"parameters\": {\n", + " \"trainer.learning_rate\": {\n", + " \"type\": \"float\",\n", + " \"low\": 0.0001,\n", + " \"high\": 0.01,\n", + " \"space\": \"log\",\n", + " \"steps\": 3,\n", + " },\n", + " \"trainer.batch_size\": {\"type\": \"int\", \"low\": 32, \"high\": 256, \"space\": \"log\", \"steps\": 5, \"base\": 2},\n", + " \"quality.fc_size\": {\"type\": \"int\", \"low\": 32, \"high\": 256, \"steps\": 5},\n", + " \"quality.num_fc_layers\": {\"type\": \"int\", \"low\": 1, \"high\": 5, \"space\": \"linear\", \"steps\": 4},\n", + " },\n", + " \"goal\": \"minimize\",\n", + " \"output_feature\": \"quality\",\n", + " \"validation_metrics\": \"loss\",\n", + "}\n", + "\n", + "# add hyperopt parameter space to the config\n", + "config[\"hyperopt\"] = hyperopt_configs" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "starting: 2022-04-09 15:09:04.768901\n", + "Note: steps_per_checkpoint (was 40) is now set to the number of steps per epoch: 40.\n", + "\n", + "Note: steps_per_checkpoint (was 52) is now set to the number of steps per epoch: 52.\n", + "\n", + "CPU times: user 17min 20s, sys: 48 s, total: 18min 8s\n", + "Wall time: 2min 42s\n" + ] + } + ], + "source": [ + "%%time\n", + "print(\"starting:\", datetime.datetime.now())\n", + "config[\"hyperopt\"][\"executor\"] = {\"type\": \"serial\"}\n", + "config[\"hyperopt\"][\"sampler\"] = {\"type\": \"random\", \"num_samples\": 2}\n", + "results_random_serial = hyperopt(\n", + " config,\n", + " dataset=train_df.sample(4000, random_state=42), # limit number records for demonstration purposes\n", + " output_directory=\"hyperopt_results\",\n", + " experiment_name=\"random_serial\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Note:\n", + "`results_ray`, `results_random_serial` are `HyperoptResults` object with the ordered_trials, so will convert to dictionary to visualize." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "def hyperopt_results_dict(results):\n", + " return [{\"metric_score\": t.metric_score, \"parameters\": t.parameters} for t in results.ordered_trials]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Convert hyperparameter optimization results to dataframe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Results For Ray executor" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
losstrainer.learning_ratetrainer.batch_sizequality.fc_sizequality.num_fc_layers
01.2911790.001393170604
11.3003320.000203451004
21.9562020.00052775450
\n", + "
" + ], + "text/plain": [ + " loss trainer.learning_rate trainer.batch_size quality.fc_size \\\n", + "0 1.291179 0.001393 170 60 \n", + "1 1.300332 0.000203 45 100 \n", + "2 1.956202 0.000527 75 45 \n", + "\n", + " quality.num_fc_layers \n", + "0 4 \n", + "1 4 \n", + "2 0 " + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df1 = hyperopt_results_to_dataframe(\n", + " hyperopt_results_dict(results_ray), hyperopt_configs[\"parameters\"], hyperopt_configs[\"validation_metrics\"]\n", + ")\n", + "df1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Results for Random Search with serial executor" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
lossquality.fc_sizequality.num_fc_layerstrainer.batch_sizetrainer.learning_rate
01.300466754550.005918
11.362126451720.002479
\n", + "
" + ], + "text/plain": [ + " loss quality.fc_size quality.num_fc_layers trainer.batch_size \\\n", + "0 1.300466 75 4 55 \n", + "1 1.362126 45 1 72 \n", + "\n", + " trainer.learning_rate \n", + "0 0.005918 \n", + "1 0.002479 " + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df2 = hyperopt_results_to_dataframe(\n", + " hyperopt_results_dict(results_random_serial), hyperopt_configs[\"parameters\"], hyperopt_configs[\"validation_metrics\"]\n", + ")\n", + "df2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example Hyperopt Visualizations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Report results of the a hyperparameter optimization run" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABVIAAAVfCAYAAABY3ROgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3hUdfr+8XtCQghJ6IqFIiCEoiCIbWmCIAqCUgNIENsqLhZAQaQjCCgoGlAUcQX8AgFhEWwrTYpiASWIFClRiSAttCRASGZ+f3jt/IwkmYnM5JzPmffruuZa5szknGdkuXl4zueccXk8Ho8AAAAAAAAAAPkKs7oAAAAAAAAAALA7BqkAAAAAAAAA4AODVAAAAAAAAADwgUEqAAAAAAAAAPjAIBUAAAAAAAAAfGCQCgAAAAAAAAA+MEgFAACwSFxcnNLS0nJtW7JkiR555BGLKvJt2rRpWrlyZZ6vxcXFqUOHDrr77rt1zz33qG3bturSpYt++OGHIqlt//79evzxx4Oy70OHDqlHjx4XtY/ExESNHTvW5/seeOCBC/5/AQAAAOuFW10AAAAAzPH111/r6quvzvf12bNnq1y5ct7ns2bN0rhx45SUlBT02g4cOKCUlJSg7LtixYpasGBBUPb9V1988UWRHAcAAACFwyAVAADAhs6cOaPmzZtr4cKFqlatmiTp/vvv17333quVK1fK5XJp7969SktLU5MmTTR8+HBFRERo7969Gj9+vE6cOKGcnBwlJCSoa9eu+vrrrzV+/HiVLFlSmZmZev/99/Wf//xHc+fOVVhYmCpUqKARI0aoWrVqevbZZ/Pc/8KFC7Vt2za9+OKLKlasmNq0aVPgZ8jOztbBgwdVunRp77Y33nhDn332mdxut6688kqNGjVKFStWVEJCgmrUqKFt27bp+PHjuvvuu/XEE09IklauXKlp06YpJydHMTExGjp0qOrXr6/ExERt2bJFhw8fVs2aNfXDDz/o0KFDevDBBzVr1iwNGzZM11xzjXr27JmrriVLluijjz6S2+3WoUOHVLFiRU2cOFEVK1bUli1b9NJLLykrK0tHjhzRP/7xD73wwgtKTU1Vhw4d9P333+c6blxcnCZPnuzdd2pqqhISEnTjjTdq586d8ng8GjlypBo3bpyrht27d2vs2LE6ceKEXC6XHnjgAd1zzz0aOnSoJOm+++7TW2+9pcsvv/zv/58IAAAAAcUgFQAAwEL33XefwsL+/92WTp48qbi4OEVFRemee+7RokWLNHjwYP36669KSUlRy5YttXLlSu3cuVPvvfeeIiIi9MADDygpKUk9evTQE088oRdffFH16tXT6dOnFR8f711Bunv3bq1cuVJXXnmlNm7cqLfffltJSUkqV66clixZon/961/66KOPJCnP/ffu3Vuffvqp7r333nyHqPfdd59cLpfS0tIUGRmpli1basKECZKkpUuX6qefftKiRYsUHh6upKQkDR8+XDNnzpT0x4rS+fPn68yZM+revbuuvfZaValSRaNGjdKCBQtUuXJlbdy4UY899pg+/fRTSdJvv/2mDz/8UOHh4fr666/1/PPPa9asWZKk8ePH5/vf/bvvvtOSJUtUrVo1TZ48WePHj9drr72mOXPm6IknntBNN92kjIwM3Xbbbdq2bZvKlCmT6+f/fNy/OnDggJo2bapJkyZp7dq1euqpp7RmzRrv69nZ2erXr58GDx6s22+/XYcOHVK3bt1UtWpVTZgwQUuWLLlgZS8AAACsxyAVAADAQn8dmC1ZskT//e9/JUm9evVS7969NWDAACUlJalr164qVqyYJKlTp06Kjo6WJN19991atWqVbr75Zv3666967rnnvPs7e/astm/frho1aujyyy/XlVdeKUlav3692rVr5z12586dNX78eKWmpua7/969e/v9ebZv366HH35YDRs2VPny5SVJa9as0Q8//KAuXbpIktxut86cOeP92fj4eEVERCgiIkJ33HGHNmzYoOrVq+vmm29W5cqVJUm33HKLypUrp23btkmSrrvuujyHmb40adLEu9K3e/fuuvvuuyVJEydO1Lp16zRjxgzt27dPZ8+eVWZm5gWD1IKOW7p0aXXo0EGS1KJFCxUrVky7du3yvv7zzz/r3Llzuv322yX9cduA22+/XevXr1fDhg0L/VkAAABQNBikAgAA2FS1atUUFxenVatWafny5Vq0aJH3tf8NVCXJ4/EoLCxMOTk5KlWqlD744APva0ePHlVsbKy2bNmikiVL5vqZv/J4PMrOzs53/4VRt25dDR06VMOHD1eDBg1UqVIlud1uPfTQQ+rVq5ckKSsrSydPnvT+zJ8Hk/87pq86//yZCuPPn8/tdnuf33vvvapdu7aaNWumO++8U8nJyXnWUNBx/7zvv+7/f8//6s+fCQAAAPZUuI4YAAAARapXr1568cUX1aBBA1WsWNG7/ZNPPlFWVpbOnTun//znP2rZsqWqVaumyMhI7yD14MGDuuuuu7yrN/+sadOm+vjjj73fDr948WKVKVNGVatWzXf/0h9DQn8HfnfddZeuu+46vfDCC95jvv/++0pPT5ckvfrqqxo8eLD3/cuWLZPb7dbJkyf1ySefqFWrVrr55pv1xRdfaP/+/ZKkjRs36uDBg2rQoMEFxytWrJjOnz/vV21fffWVDh06JElasGCBWrZsqZMnT2rbtm16+umnvZfc//rrr3kOPguSlpamdevWSZJWr16tiIgI1apVy/t6tWrVFBERoc8++0ySdOjQIf33v//VP/7xD+/nYKgKAABgP6xIBQAAsLGWLVtq+PDh6tGjR67tJUqUUK9evXTq1Cm1bdtWXbp0UVhYmF5//XWNHz9eb7/9trKzs/Xkk0/q+uuv19dff53r55s0aaK+ffvqvvvuk9vtVrly5fTmm296V57mtf//1TNp0iSdP39enTp18ln/iBEj1LFjR61fv17dunXToUOH1L17d7lcLl1++eWaOHGi971nz55V165dlZGRoV69eumWW26RJI0aNUr9+/dXTk6OSpQooRkzZig2NvaCY9WsWVPFihVT165dtWjRIg0fPjzPL5uS/ric/plnntGRI0d09dVXa+zYsSpdurT++c9/qlOnTipTpozKli2rRo0a6ZdffvHeWsAf/xtmT548WSVKlND06dNzrUiNiIjQ66+/rnHjxikxMVE5OTn617/+pZtvvlmS1KZNG/Xq1Uuvv/56rgEsAAAArOXy5HWtEgAAAGzhu+++04gRI/Thhx/K5XJJkp599lnVrFlTDz74YFCOGez95yUhIUH33nuv7rjjjqAf63/3oX3zzTcDvu/U1FR16NBB33//fcD3DQAAAGuxIhUAAMCmhgwZom+++UaTJk3yDlEBAAAAWIMVqQAAAAAAAADgA182BQAAAAAAAAA+MEgFAAAAAAAAAB8YpAIAAAAAAACADwxSAQAAAAAAAMAHBqkAAAAAAAAA4AODVAAAAAAAAADwgUEqAAAAAAAAAPjAIBUAAAAAAAAAfGCQCgAAAAAAAAA+MEgFAAAAAAAAAB8YpAIAAAAAAACADwxSAQAAAAAAAMAHBqkAAAAA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UV/z5s29zxs1aqQNGzbI7XbrwIEDcrvdrEYFHObVV1/VqFGjNHjwYD3++OM6duyYypYtq2effdbq0vI1efJk3X///fr555+tLqVQZsyYofnz5ysmJkbbt2/XwYMH9corr+j//u//rC7NaKG7NAUAAAdr06aNvvjiC/Xo0UMej0cvvPCC1SUBgFFGjx6t559/XhEREapQoYJtV6SmpKSoUqVKkiSXy6Vx48apf//+KlGihGrUqKHu3btbXGH+UlJSVLlyZe/za665Ro0bN1Z8fLzcbrdGjhxpYXUAgmHjxo1asGCBsrKydNdddykxMVGStGrVKosry1/t2rX11FNPadCgQapVq5a6d++uhg0bWl2WT+vXr/cOTbt3765HH31UM2fOVI8ePXTvvfdaXJ25XJ787joLAAAAAAAABEiXLl300ksv6fjx43r00Uf18ccfKyoqSg888IAWLlxodXl56tOnj+bMmSNJWr16tZYtW6Zt27YpNjZW//nPfyyuLn+dOnXStGnTdOWVVyolJUWjRo3SO++8o65du2rp0qVWl2csBqkAAAAAAAAIui+//FIvvfSS6tatq5o1a+qtt95SdHS0hgwZotatW1tdXp4SEhI0d+7cC7anpaXZ+vYjGzZs0IgRI1SqVCmdPXtWL774otavX6+KFSuqW7duVpdnLAapAAAAAAAAKHKnT59WZGSkihcvbnUp+Tp69KgqVKhgdRl/i8fj0fHjx2098DUNXzYFAAAAAACAoFu7dq3mzJmj/fv3q3fv3rrzzjvVu3dv7dixw+rS8pWenq7HH39cTz/9dK4vnBo1apR1Rfnh8OHDmjBhgubNm6edO3eqTZs2uuOOO7RlyxarSzMag1QAAAAAAAAEXWJiotq2batx48bpySef1IYNGzR27FiNHj3a6tLyNWLECMXHx+uuu+7Sv/71L23fvl2StG/fPosrK9izzz6rOnXqyOVy6YEHHtCbb76pd999V5MnT7a6NKOFW10AAAAAAAAAnK948eKqWLGiJOmGG26QJNWuXdvKkvzStGlTSVKVKlX0+OOP6+2335bL5bK4qoJlZWWpU6dOkqRvvvlG1atXlyTb1213rEgFACBEJCYmav78+dqxY4emTZsmSVqxYoUOHTpkcWV/WLFihW6//Xbvt6L6Y8mSJZxVBwAAMES9evU0duxYNWzYUM8995xW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H3/0+X5/ValSRfv27fN+vieeeELDhg3zfsNrXvL7zCVLltRNN92k8ePHq2PHjpKk6tWrq2PHjt7m8n/3EPzzpftJSUl6/PHHVb58eY0cOVIrVqzwfuspAPxZtWrVJPl3EqtWrVoKDw9XeHi492TRTz/9pDfffFNvv/22PB6PwsP/+CdapUqV8lwx9L9vua5Zs6aOHDmisLAwRUREeI/7+++/Kzs7O9fPpKSkqGvXrpL+OBHXqFEjJSYmek96VahQQWfPns33M3bt2lUzZ87UQw89pNjYWA0YMCDX69nZ2RowYIA6duyoFi1aaOvWrUpLS/NeQpqRkaFff/2VQSqAXD3pkCFDNGjQIL399tu2/ib5jIwMnThxQpdccok3o8PDw30uVLDa/Pnz9eCDD+rdd99V6dKlrS7HMbjZFwAAhujWrZuOHTume++9V2+//bYkqU+fPhozZowefPBB5eTkSJLq16+v/fv3695779UTTzyhypUr6/Dhw3nus2HDhho8eLBOnDihBg0a6JdfflGHDh0kSf/+978LPFt95513au3atUpISND27dsD9jnLlSunhx9+WL1791Z8fLzq1q1b4BBVKvgzd+/eXatWrfJ+rkcffVSffPKJEhIS9NBDD6lmzZp57u+RRx7RfffdpyNHjujWW28N2OcDYL6wsDC53W7vr6X/f0Ln5Zdf1sCBA3X27NkLTmLlNSioXr26nn76ac2dO1djxozRHXfckWu/f/W/1aS7du3SFVdcoZ07d2rlypWaOnWqRowYIbfbLY/Hk6vGGjVq6IcffpAkffvtt3rppZfyrScvq1at0vXXX6/Zs2frjjvu8P4dJP0xFBk2bJgaNmyoe+65R9IfQ+DLL79c77zzjubOnavevXvruuuu8+tYAJytXbt26tq1q06cOCFJmjBhgjZu3Kjk5GRrCytAo0aN9Nhjj2nz5s3697//rYyMDN19991q166d1aUVqFy5cho0aFBA+3RILo8J66cBAEAu586d05133qnVq1cHbJ9ut1s9e/bUrFmzvJfxO8HWrVv13nvv6cUXX7S6FAAOce7cOXXv3l1NmzZVpUqV1LNnTx05ckSPPvqoSpQoIZfLpbNnz2ro0KH68ssvvfdIXbBggV555RVJUpMmTfTFF19o//79Gj16tM6dO6ezZ89q2LBhuuSSSzRw4EAtXLhQkvTAAw9oxowZevPNN7V9+3ZlZGQoKytLo0ePVtWqVfXII49474VXvHhxde3aVW3btvXW+OCDD+q5555TRkaGJOmFF17Q0qVL8/zCwLz8+uuvGjJkiCIiIuR2uzV06FClp6drwYIFuv322/Xcc8+pQYMG3hN6o0aN0o8//qj58+crJydHV155pSZMmKCoqKhg/9YAMMD+/ft1xRVXqFixYt5tK1eutP2tlDwej86cOaOoqCjt27fPe/sqhBYGqQAAGCjQg9T9+/erf//+6ty58wXf7hlIW7du9a6E+rM777xTvXr1yvfnRo8erb17916wfebMmd7LY/Py3nvv6f3339fUqVN11VVX/a2aAQAAEBjnzp3TggULtHHjRp0+fVqxsbFq3LixevfuXWBPZyUTa5b+qHv+/Pn66quvjKrb7hikAgAAAAD+9kkrAPDXwIEDVbt2bTVv3lzR0dHKyMjQunXrlJycrOnTp1tdXp5MrFkyt26748umAAAAAAAaPXq01SUAcLjDhw/r5ZdfzrWtdu3aBV6ZZDUTa5bMrdvu+LIpAAAAAAAABF1kZKSWLl2qY8eOKSsrS2lpafrPf/6jkiVLWl1avkysWTK3brvj0n4AAAAAAAAE3fHjxzV9+nR99913ysjIUHR0tBo1aqR+/fqpfPnyVpeXJxNrlsyt2+4YpAIAAAAAAKBInD9/Xjt37lR6erpKlSqlmjVrqnjx4laXVSATa5bMrdvOuEcqAAAAAAAAgu7zzz/XlClTdNVVVyk6Olrp6enat2+fBg4cqNatW1tdXp5MrFkyt267Y5AKAAAAAACAoJsxY4bmz5+vmJgY77bTp0+rb9++th3umVizZG7ddseXTQEAAAAAACDozp8/rxIlSuTaFhkZKZfLZVFFvplYs2Ru3XbHilQAAAAAAAAEXXx8vDp16qTrr79esbGxSk9P1+bNm5WQkGB1afkysWbJ3Lrtji+bAgAAAAAAQJE4evSotm7dqoyMDMXExOjaa69VhQoVrC6rQCbWLP3/utPT0xUTE6P69esbUbedMUgFAAAAAABA0J07d04LFizQl19+qdOnT6tUqVJq3LixevfufcFl6HZhYs0FWbNmjVq2bGl1GcZikAoAAAAAAICgGzhwoGrXrq3mzZsrOjpaGRkZWrdunZKTkzV9+nSry8uTiTUX5N1331Xfvn2tLsNY3CMVAAAAAAAAQXf48GG9/PLLubbVrl1bvXr1sqgi30ys+a/cbrfCwv74vnmGqBeHQSoAAAAAAACCLjIyUkuXLlWzZs28X4C0bt06lSxZ0urS8mVizZK0f/9+TZgwQdu2bVN4eLjcbrdq1aqloUOHqlq1alaXZywu7QcAAAAAAEDQHT9+XNOnT9d3332njIwMRUdHq1GjRurXr5/Kly9vdXl5MrFmSerTp48GDRqkBg0aeLdt2bJFEydO1IIFCyyszGwMUgEAAAAAAAAH6dGjR54D0/y2wz9hVhcAAAAAAACA0PXEE09YXUKh2b3muLg4DR06VB9//LHWr1+vTz/9VEOHDlVcXJzVpRmNFakAAAAAAACwzMmTJ1W6dGmryygUu9fs8Xi0cuVKbd68Wenp6YqJiVGjRo3Upk0buVwuq8szFoNUAAAAAAAAFImdO3fqyy+/1OnTp1WqVCldf/31ql+/vtVlFcjEmhEcDFIBAAAAAAAQdNOmTdPWrVvVtGlTRUdHKyMjQxs2bFDdunX11FNPWV1enkysGcHDIBUAAAAAAABB16tXL82bNy/XNo/Ho+7du2vRokUWVVUwE2tG8PBlUwAAAAAAAAi67Oxspaam5tqWmpqqsDD7jqdMrLkgGzZs0Ndff211GcYKt7oAAAAAAAAAON+wYcPUv39/nT9/XjExMUpPT1fx4sU1ZswYq0vLl4k1F2T79u2qWbOmfv/9d1122WVWl2McLu0HAAAAAABAkUlPT1dGRoaio6MVExNjdTl+MbFmBJ6Z65ABAAAAAABgpJiYGFWsWNGogaRpNW/ZskWdO3dWz549tWnTJu/2f/3rXxZWZT4u7QcAAAAAAAAcZOLEiZoyZYqys7M1ePBgDRo0SE2bNtWpU6esLs1oDFIBAAAAAAAAB4mIiFC1atUkSW+99ZYeeOABXXLJJXK5XBZXZjYu7QcAAAAAAIBlBg4cqEmTJunYsWNWl+I3u9ccHR2tOXPmKCsrS5dccokmT56sp556Sr/99pvVpRmNL5sCAAAAAACAZY4ePaqyZcvK4/EoPNyMi6ftXnN6err+/e9/6/777/fe13XPnj16+eWX9frrr1tcnbkYpAIAAAAAACDoXn75ZfXr109RUVFWl1IoJ06cUEREhEqWLKmlS5fK5XLp7rvvtv1l8j/99JMiIyNVtWpV77bk5GQ1aNDAwqrMxiAVAAAAAAAAQde0aVNddtllevrpp3XzzTdbXY5f5syZo3nz5snj8ejGG29UVlaWoqKiFBYWppEjR1pdXr6mT5+uDRs2KDs7W3Xr1tXo0aPlcrnUp08fzZkzx+ryjMU9UgEAAAAAABB01apV0yuvvKLZs2erT58++vDDD3Xy5EmryyrQhx9+qI8//ljz5s3TmjVrNGnSJI0ePVq7du2yurQCrVu3TvPnz9eiRYtUsmRJjRkzRpLEesqLwyAVAAAAAAAAQedyuVS5cmW98cYbGjZsmHbs2KH7779fLVq0sLq0fLndbp05c0bly5fXqFGjJElZWVk6f/68xZUV7M8D0yFDhuj06dN6++23bX87ArtjkAoAAAAAAICg+/NwLy4uTs8884yWLFmitWvXWlhVwR5++GF17txZbrdbbdq0kSQ9+OCD6tatm8WVFaxdu3bq2rWrTpw4IUmaMGGCNm7cqOTkZGsLMxz3SAUAAAAAAECR8ng8xqyOdLvdCgv7/2sR09PTFRMTY2FF/tm/f78uv/xyhYeHe7etXLlSrVu3trAqszFIBQAAAAAAQND9+uuvGjNmjPbt26fDhw+rXr16qly5sp599lldcsklVpeXpx9++EEpKSlq2rSpJk2apB9//FFXX321Bg8erCuuuMLq8lDEGKQCAAAAAAAg6B588EENHz5c1apV05YtW7Rq1Sq1bdtWr732mt566y2ry8tTfHy8xo4dqzfeeEO33nqrWrVqpW+++UazZ8/W3LlzrS4vX0lJSfm+Fh8fX4SVOAv3SAUAAAAAAEDQpaenq1q1apKk6667Tt99952uueYanTp1yuLK8hcREaG4uDidPn1a99xzj0qVKqXWrVvb/sum9u3bp1mzZunIkSMXPPD3hft+CwAAAAAAAHBxKlWqpJEjR6p58+b6/PPPdc011+jzzz9XVFSU1aXl68orr9SsWbPUokULTZs2Ta1atdLatWtteyuC/xk6dKj27dun5s2bq379+laX4xhc2g8AAAAAAICgy8rK0qJFi7Rnzx7VqVNHXbp00Q8//KCqVauqbNmyVpeXpzNnzmjWrFnasGGDjh8/rrJly6pRo0Z65JFHVLp0aavLK1BaWpoyMzNVqVIlq0txDAapAAAAAAAACLqXX35Z/fr1s/UKVF927typ2rVrW12G344fP6709HTFxsaqTJkyVpdjPAapAAAAAAAACLqmTZvqsssu0zPPPKObbrrJ6nL8smHDhlzPX3rpJT3zzDOS/vg8drV161aNHTtWbrdbJUuWVEZGhjwej0aOHKlGjRpZXZ6xGKQCAAAAAAAg6BISEvTCCy/ohRdeUEZGhrp3765mzZrZ+hL5e+65R2FhYYqLi5MkrV+/Xs2aNZMkTZgwwcrSCtSzZ0+9/PLLuvzyy73bDhw4oCeffFKLFi2ysDKz8WVTAAAAAAAACDqXy6XKlSvrjTfe0K5du7Rs2TK98847OnbsmNauXWt1eXmaP3++xo4dq0aNGqlbt25KSEiw9QD1f7Kzs3MNUSXp8ssvl8vlsqgiZ2CQCgAAAAAAgKD780XRcXFx3kvk7SwqKkoTJkzQO++8o5EjRyonJ8fqkvzSokUL9e3bV02aNFFsbKzS09P1xRdfqHnz5laXZjQu7QcAAAAAAECRcrvdCgsLs7qMQtm4caMWL16syZMnW12KX7Zv367NmzcrIyNDMTExatiwoerVq2d1WUZjRSoAAAAAAACCbv/+/ZowYYK2bdum8PBwud1u1apVS0OHDlW1atWsLi9fK1eu1MaNG3X69GmVLl1an3zyie644w7bXyZ/4MABpaSkeOsuX7686tata/u67YwVqQAAAAAAAAi6Pn36aNCgQWrQoIF325YtWzRx4kQtWLDAwsryN2bMGLndbjVv3lzR0dHKyMjQunXrlJ2drfHjx1tdXr5MrdvuWJEKAAAAAACAoMvKyso1RJWk6667zppi/LR792699957ubbddttt6tGjh0UV+cfUuu2OQSoAAAAAAACCLi4uTkOHDlWzZs0UGxurjIwMrV27VnFxcVaXli+3261NmzapcePG3m3ffvutIiIiLKzKN1Prtjsu7QcAAAAAAEDQeTwerVy58oIvQGrTpo1t79v566+/asKECfrxxx8lSWFhYapTp46GDBmiq666ytriCmBq3XbHIBUAAAAAAABFYufOnfriiy+8X4B0/fXXq379+laX5VNaWprS09MVGxursmXLWl0OLMIgFQAAAAAAAEE3bdo0bd26VU2bNvV+AdKGDRtUt25dPfXUU1aXl6etW7dq7Nixcrvd3prdbrdGjRqlhg0bWl1eoY0dO1YjR460ugxjMUgFAAAAAABA0PXq1Uvz5s3Ltc3j8ah79+5atGiRRVUVrGfPnnr55Zd1+eWXe7cdOHBATz75pG1rLsjevXtVo0YNq8swFl82BQAAAAAAgKDLzs5WamqqKlWq5N2WmpqqsLAwC6sqWHZ2dq4hqiRdfvnltr2n65+lpaXp22+/1enTp1WqVCldd911DFEvEoNUAAAAAAAABN1zzz2n/v376/z584qJiVF6erqKFy+u0aNHW11avlq0aKG+ffuqSZMmio2NVXp6ur744gs1b97c6tIKtGjRIiUlJen6669XdHS0du/erRkzZqhbt27q2bOn1eUZi0v7AQAAAAAAUGTS09OVkZGhmJgYRUdHW12OT9u3b9fmzZu9NTds2FD16tWzuqwC9ejRQ3PnzlVERIR3W1ZWlnr27KnFixdbWJnZWJEKAAAAAACAoNu/f78mTJigH3/8UcWKFZPb7VatWrU0dOhQVatWzery8nXgwAGlpKTo9OnTKl26tMqXL6+6deva+vL+7OxsnTt3Ltcg9ezZs7au2QSsSAUAAAAAAEDQ9enTR4MGDVKDBg2827Zs2aKJEydqwYIFFlaWvzFjxsjtdqt58+aKjo5WRkaG1q1bp+zsbI0fP97q8vK1evVqTZw4UVWrVvXekuCXX37R0KFDdeutt1pdnrFYkQoAAAAAAICgy8rKyjVElaTrrrvOmmL8tHv3br333nu5tt12223q0aOHRRX5p1WrVmrevLn27t2r9PR0xcTEqEaNGgoPZxR4MfivBwAAAAAAgKCLi4vT0KFD1axZM8XGxiojI0Nr165VXFyc1aXly+12a9OmTWrcuLF327fffpvrknk7GjlypBISEvL8b7tjxw7Nnz9fY8eOtaAys3FpPwAAAAAAAILO4/Fo5cqV2rx5s3eVZKNGjdSmTRvb3rvz119/9d7XVZLCwsJUp04dDRkyRFdddZW1xRXgxIkTmjp1qrZt26Zq1aqpQoUKOnXqlHbs2KH69evriSeeULly5awu0zgMUgEAAAAAAGCZ33//XZdddpnVZThSenq6kpOTdfz4cZUvX14NGjRQyZIlrS7LWGFWFwAAAAAAAIDQ9corr1hdQqGZcll8TEyMmjRporvuuku33HILQ9SLxIpUAAAAAAAAoBD27t2rGjVqWF0GihiDVAAAAAAAAATduXPnNH/+fH311Vc6ffq0YmNj1bhxY/Xu3VslSpSwurx8paWl6dtvv9Xp06dVqlQpXXfddbr00kutLgsWYJAKAAAAAACAoBs4cKBq166t5s2bKzo6WhkZGVq3bp2Sk5M1ffp0q8vL06JFi5SUlKTrr7/eW/O3336rbt26qWfPnlaXhyIWbnUBAAAAAAAAcL7Dhw/r5ZdfzrWtdu3a6tWrl0UV+bZ48WLNnz9fERER3m1ZWVnq2bMng9QQxJdNAQAAAAAAIOgiIyO1dOlSHTt2TFlZWUpLS9PSpUtt/QVI2dnZOnfuXK5tZ8+elcvlsqgiWIlL+wEAAAAAABB0x48f1/Tp0/Xdd98pIyND0dHRatSokfr166fy5ctbXV6eVq9erYkTJ6pq1aqKjY1Venq6fvnlFw0dOlS33nqr1eWhiDFIBQAAAAAAQJFbu3atWrRoYXUZPmVnZ2vv3r1KT09XTEyMatSoofBw7pYZiri0HwAAAAAAAEVu1qxZVpfg08iRI5WSkqK4uDhdf/31iouL8w5Rd+zYoZEjR1pcIYoS43MAAAAAAAAUORMukh44cKCmTp2qbdu2qVq1aqpQoYJOnTqlHTt2qH79+nrqqaesLhFFiEv7AQAAAAAAUOQ2b96s66+/3uoy/JKenq7k5GQdP35c5cuXV4MGDWz9JVkIDgapAAAAAAAACLqRI0eqd+/eqlWr1gWv7dixQ/Pnz9fYsWMtqAzwD4NUAAAAAAAABN2JEyfyvEx+586duvbaa/XEE0+oXLlyVpcJ5ItBKgAAAAAAAIoMl8nDVAxSAQAAAAAAAMCHMKsLAAAAAAAAAAC7Y5AKAAAAAAAAAD4wSAUAAAAAAIBlzp07p0WLFvn13iVLlmjVqlUBOW5iYqLmz58fkH392ZEjRzR69OiA77cgK1as0KFDh4r0mKGIQSoAAAAAAAAsc+TIEb8HqZ07d9Ztt90W5IouziWXXFLkg9Q5c+YoPT29SI8ZisKtLgAAAAAAAACha8aMGdqzZ49q166tf/zjH8rMzNT48eO1dOlSbdu2TSdOnFDt2rU1YcIEJSYmqkKFCqpevbpmzpypiIgIpaamql27durXr58OHjyoESNG6Ny5c4qMjNTzzz+vnJwc9evXT2XKlFHz5s318MMPX1DDlClTtGnTJrndbvXt21d33nmnvvnmG02bNk0ej0cZGRmaMmWKIiIicu1r3bp1ql27tnbv3q309HS9+uqr8ng8GjhwoBYuXKgOHTroxhtv1K5du+RyufT6668rJiZGY8aM0bZt21ShQgX99ttveuONN1SpUqU8//u0bNlS1atXV40aNdS1a1dNnDhROTk5On78uEaPHq1Tp05px44dGjJkiObNm6ekpCR9+OGHcrlcateunfr06RPs38KQwSAVAAAAAAAAlnn00Uf1008/qVmzZjp58qSGDx+u9PR0lSpVSv/+97/ldrvVvn37Cy5dP3DggJYtW6asrCw1a9ZM/fr106RJk5SQkKAWLVpo48aNmjx5sgYMGKAjR45o8eLFKl68+AXHX7t2rVJTUzV//nydO3dO3bt3V5MmTbR792699NJLqlixombMmKFPP/1UHTp0yLWvdevWqX79+ho2bJheeeUVffTRR2rXrp133xkZGWrfvr1GjBihQYMGad26dYqMjNSJEyf0/vvvKy0tTbfffnuB/30OHjyoJUuWqGzZsvr44481ZMgQxcXFafny5VqyZInGjRunOnXqaPTo0fr111/18ccfa968eZKk+++/X02bNlX16tUD8DsFBqkAAAAAAACwhWrVqkmSIiMjlZaWpoEDB6pkyZLKzMzU+fPnc723Vq1aCg8PV3h4uEqUKCFJ+umnn/Tmm2/q7bfflsfjUXj4H6OvSpUq5TlE/d/P/Pjjj0pISJAkZWdn67ffflPFihU1fvx4lSxZUocOHVKjRo3y3FfdunUlSZdddpmOHj16wf7/9/rll1+uc+fO6bffftN1110nSSpXrpzPIWfZsmVVtmxZSdKll16q119/XSVKlFBGRoZiYmIu+CwHDhxQ3759JUknT57UL7/8wiA1QBikAgAAAAAAwDJhYWFyu93eX0vSunXrdPDgQU2dOlVpaWlasWKFPB5Prp9zuVwX7Kt69ep64IEH1KhRI+3du1fffvttrv3mpXr16rrpppv0/PPPy+126/XXX1flypX1wAMPaMWKFYqJidGQIUO8xy9oX3n5a501a9bUBx98IOmPQefPP/9c4M//+Xjjx4/X5MmTVaNGDb322mv67bffvMfweDyqXr26rr76ar399ttyuVx69913FRcXV6h6kT8GqQAAAAAAALBM+fLldf78eZ09e9a7rX79+nr99dd17733yuVyqXLlyjp8+LDPfQ0ZMkSjR4/WuXPndPbsWQ0bNuyC9zzwwAOaMWOG93mrVq30zTffqFevXsrMzFTr1q0VExOjjh076t5771VU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UgF4hXfeeUdnzpxRTEyM+vbtqwceeEC+vr6KjIzU3XffbXQ8AAAAwG30tgBQNaq63tqcTmfl3DYLAAAAAAAAACyKawoAAAAAAAAAwAUGqQAAAAAAAADgAoNUAAAAAAAAAHCBQSoAAAAAAAAAuMAgFQAAAAAAAABc8DU6AMztxIkT6tevn5YsWaL8/Hw98sgjuuqqqyRJsbGx6tOnj7EBy/Dqq6/q448/VmFhoWJjY3X99dfrmWeekc1mU4sWLTRx4kT5+HjHOYasrCzNmDFDy5cv18GDB8vMOXfuXH366afy9fXVuHHj1LZtW6NjAwAAmJLZetvVq1frX//6lyQpPz9fu3bt0qxZszRt2jQ1atRIkjRq1Chdf/31RsYs8cfe9sknn9Tx48clST/++KPatWun2bNn09sCALyazel0Oo0OAXMqLCzU6NGjtXfvXs2fP1/ffPONcnNz9dBDDxkdrVxffvmlXn/9dc2fP19nz57VkiVLtGPHDj344IO64YYblJSUpK5du6pXr15GR9XChQu1du1aBQYG6q233tLw4cPPy3nFFVdo2rRpWrp0qX766SeNGjVKq1atMjo6AACA6Zixt/2jSZMmKSoqSkeOHFHr1q3Vu3dvoyOV8ufe9ne//vqrhg4dqoULF+rYsWP0toDFZWZmunxNly5dqiCJ+woKCly+xt/fvwqSVMyBAwdcviY8PLwKklgLK1Jx0aZNm6ZBgwbptddekyRt375dBw4c0Pr169WsWTONGzdOISEhBqcsLTMzUy1bttRjjz0mu92usWPH6q233io5S9+tWzd99tlnXjFIbdq0qebMmaOxY8dKknbs2HFezvDwcHXp0kU2m01XXHGFiouLdfLkSdWtW9fI6AAAAKZjxt72d99++6327t2riRMn6q9//at27dqlpUuXqm3btnrqqafk62v8r31/7m1/N2fOHA0ZMkSXXXaZPvjgA3pbwOKeeeYZde3atdznN27c6NawtSp16tRJDRo0kNPplM1mk6SSf3Y6nTp58qS2bt1qbMgyDBw4UK1atVJ56yf37Nmjr776qopTmZ/xf6PClFavXq26deuqa9euJc1m27ZtNWDAAF1zzTV65ZVXNG/ePCUkJBictLScnBwdOXJECxYsUHZ2tkaMGFGqGAYHBys3N9fglL/p3bu3srOzS74uK6fdblft2rVLXvP7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5ql+/fqqX7++li1bJkn6+eef1bBhQy1fvlwzZ87U119/LYfDoWHDhumOO+6okj9bAAAAVK5jx44pPT1dAwYMcPnafv36VUGi81Uk48Vo1aqVWrVq5ZH3BmBdPj4+atOmjaKjo9WsWbOS41lZWWrXrp2BycoXFhamqVOnSpK+++477d69W23atNG///1vg5NdWGBgoP7v//6v1LG///3vBqWxDgap8AorVqxQZGSknnzySX3zzTfKzMxU7dq1z3ud3W5XWFiYXn/9dTkcDt15551lXr5/yy23lJzVuvzyy9WvXz899dRT2rhxo2644YaSpewjR47U6tWrtWTJEmVkZCgnJ0dvv/22fv31V73++uvlDlLfffddPfzww7r99tu1Zs2aUitfe/XqpV69eunw4cMaPXq0pk6dqg0bNig7O1upqanKz8/XwIED1blzZ4WFhVXOHyAAAACqzIIFC7R3715FRUXp5ptv1pkzZzRlyhStWbPmvBP+c+bMUf369RUREaGFCxfKz89P2dnZ6tOnj0aMGKGffvpJEyZMUH5+vgICAvTcc8+puLhYI0aMUO3atdWtWzf97W9/K/nsdevW6f3339e5c+f07LPPqm3btnrzzTf14Ycf6uzZs6pTp47mzp1bknHu3LkaPHiwEhISlJubK6fTWbIaaf369frggw906tQpPfHEE+rRo0eZP++BAweUmJgoX19fORwOzZw5U4cOHdLKlSs1ZswYjRs3TtJviyP279+vTZs26dNPP9Ubb7whHx8fdezYUU899ZTn/8UA8Hrz5s1TZmamiouL1bp1a02cOFE2m00zZ84sWZDkbR599FEtW7ZMq1at0ooVK3TjjTdqxYoV6tevnwYOHGh0vHIVFBSU+5y/v38VJrEWBqnwCtnZ2erataskKTo6+rz/UzudTklSQECATp48qTFjxigoKEhnzpxRYWHhBd87JCRE1113nTIzM7V69Wo9+uijZb7uwIEDat++vSSpVq1aGj16dLnvmZiYqFdffVVvvvmmIiIizhu4Hjt2TE888YRSUlJ05ZVX6r333tOOHTsUFxcnSSoqKtKPP/7IIBUAAMCEhg8fru+++05du3bVr7/+qmeffdatE/5HjhzR2rVrVVBQoK5du2rEiBGaNm2a4uLi1L17d23atEkzZszQk08+qWPHjmnVqlXn9cVXXnmlJk+e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gfbu3auoqCjdfPPNOnPmjKZMmaI1a9Zo+/btOnXqlKKiopSSkqI5c+aofv36ioiI0MKFC+Xn56fs7Gz16dNHI0aM0E8//aQJEyYoPz9fAQEBeu6551RcXKwRI0aodu3a6tatm/72t7+dl2HmzJn6+uuv5XA4NGzYMN1xxx366quvNHfuXDmdTuXl5WnmzJny8/Mr9V4ZGRmKiorS999/L7vdrpdeeklOp1NjxozRW2+9pb59++r666/Xnj17ZLPZNH/+fIWEhGjSpEnavn276tevrx9//FGvvPKKGjduXOafz6233qqIiAg1b95c/fv319SpU1VcXKycnBwlJyfr9OnT2rVrlxISErRixQqlpaXp3Xfflc1mU58+fTR06FBP/yusNhikAgAAAAAAwDDDhw/Xd999p65du+rXX3/Vs88+K7vdrrCwML3++utyOBy68847z7t0/ciRI1q7dq0KCgrUtWtXjRgxQtOmTVNcXJy6d++uTZs2acaMGXryySd17NgxrVq1Sv7+/ud9/oYNG5Sdna3U1FTl5+dr4MCB6ty5s77//ntNnz5dl19+uRYsWKAPPvhAffv2LfVeGRkZatu2rcaPH6/Zs2frP//5j/r06VPy3nl5ebrzzjs1YcIExcfHKyMjQwEBATp16pTefvttnTx5Un/5y18u+Ofz008/afXq1apTp47ee+89JSQkKDIyUu+8845Wr16t559/Xq1atVJycrIOHTqk9957TytWrJAkPfjgg+rSpYsiIiIq4d8UGKQCAAAAAADAK4SHh0uSAgICdPLkSY0ZM0ZBQUE6c+aMCgsLS722ZcuW8vX1la+vr2rWrClJ+u677/Tqq69q0aJFcjqd8vX9bfTVuHHjMoeov3/Pjh07FBcXJ0kqKirSjz/+qMsvv1xTpkxRUFCQfvnlF0VHR5f5Xq1bt5YkNWzYUMePHz/v/X9/vlGjRsrPz9ePP/6o9u3bS5Lq1q3rcshZp04d1alTR5J02WWXaf78+apZs6by8vIUEhJy3s9y5MgRDRs2TJL066+/6uDBgwxSKwmDVAAAAAAAABjGx8dHDoej5J8lKSMjQz/99JNefPFFnTx5Uh999JGcTmep77PZbOe9V0REhB566CFFR0dr37592rx5c6n3LUtERIRuuOEGPffcc3I4HJo/f76aNGmihx56SB999JFCQkKUkJBQ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ysjIkCRt3bpVLVu2NDiRscr6e6tBgwZGxwIAAKhWXnrpJU2cOFFjx47VqFGjdOLECdWpU0fPPPOM0dHKFRUVpdGjRys+Pl6JiYn63//+Z3Qkt2zcuFHTp09XbGys5s2bp++//15z587VO++8Y3Q0U6u+S1NQ6ZKTk/Xcc8/Jz89P9evX13PPPWd0pPPceuut2rx5s/r37y+n06mkpCQ1btxYEyZM0KxZsxQREaHevXsbHbNMCQkJ5+WsUaOGOnXqpJiYGDkcDiUlJRkdE4CX6NWrlz777DMNGjRITqdT//jHP4yOBAAAgGpu06ZNWrlypQoKCnTXXXdpzpw5kqT169cbnKx8NptN7du316pVq/Txxx9r6dKlevrppxUaGqp//etfRscr15kzZ/Tjjz/qyiuv1KFDh5Sfn6+ioiKdO3fO6GimZnOWd/suAAAAAAAAoJLcf//9mj59unJycjR8+HC99957CgwM1EMPPaS33nrL6HhliouL0/Lly8877u03es7MzNSECRMUFhamc+fO6YUXXtDGjRt1+eWXa8CAAUbHMy0GqQAAAAAAAPC4zz//XNOnT1fr1q3VokULvfbaawoODlZCQoJ69uxpdLwyHT9+XPXr1zc6xkVxOp3Kycnx6oGv2TBIBQAAAAAAQJXLzc1VQECA/P39jY5Srh9++EEzZ85UQECARo4cqauuukqSNHHiRE2aNMnYcBdw9OhRLVq0SGFhYerZs6dGjRqlGjVqaOrUqWrfvr3R8UyLm00BAAAAAADA4zZs2KBly5bp8OHDGjJkiO644w4NGTJEu3btMjpauSZMmKCYmBjdddddeuyxx7Rz505J0v79+w1OdmHPPPOMWrVqJZvNpoceekivvvqq3njjDc2YMcPoaKbGzaYAAAAAAADgcXPmzNG8efOUlJSkJ554Qtddd512796tiRMnKi0tzeh45erSpYskqWnTpho1apQWLVokm81mcKoLKygo0H333SdJ+uqrrxQRESFJXp/b27EiFQAAAAAAAB7n7++vyy+/XJJ03XXXSZKioqKMjOSSr6+vPv74YxUXFysiIkITJkzQI488ouPHjxsd7YLCwsI0f/58OZ1OLV26VJL073//WwEBAQYnMzcGqbCEOXPmKDU1Vbt27dLcuXMlSR999JF++eUXl987ffp09e3bV19++eUlZVi9erXWr19/Se8BAJ50KbWyKnz00Uf6y1/+omXLlrn9PatXr+byJAAAAJNo06aNJk+erA4dOmjcuHH66KOP9Oyzz6p58+ZGRyvXlClT9OGHHyo3N1eSdOONN2rcuHHy8/MzONmFzZw5U8HBwaVWoP7yyy+aNm2aganMj5tNwRLmzJmj+vXrKzY2tuRYXFyckpOTXRbk2267Tf/+978VEhLi6ZgAYKhLqZVVITExUb169VKPHj3c/p7Vq1dr//79euqppzyYDAAAAJXB4XDo3//+tzIzM5WTk6PatWurY8eOGjBggNfecMput5fMC7777jvt3r1bbdq08Yr+GVWPPVLhFfLy8hQfH6/Tp0/r6quv1v/+9z/Vrl275Jf71NRUHT9+XKNGjdLMmTO1fft2nTp1SlFRUUpJSSl5ny+//FIrV67UPffco127dikhIUEDBgzQDz/8oISEBBUXF+vee+/V22+/rYCAAM2dO1dHjx7VI488osWLF+uFF17Qtm3bVFhYqFGjRqlnz55l5v3www+1cOFC+fr66rLLLtPs2bM1b9481a9fX/Xr1y9ZTfXzzz+rYcOGWr58uWbOnKmvv/5aDodDw4YN0x133FElf7YArMOoWpmdna34+Hg1bNhQhw8f1rXXXqtJkyaVGszu27dPycnJWr58ufr27atOnTppz549ioiIUL169fT111/L399fr732Wpln79evX6+MjAxt375dderU0d69e5WamiqHw6EePXro8ccfd/nnU9bPPGjQID333HNq0aKFNmzYoE8++UTx8fEaP368cnJyJEnPPvusIiMjdeuttyoiIkLNmzdXp06dzqvzPj5cyAMAAHApfHx81KZNG0VHR6tZs2Ylx7OystSuXTsDk5Xv0Ucf1bJly7Rq1SqtWLFCN954o1asWKF+/fpp4MCBRscrV0FBQbnPeevQ2gwYpMIrrFixQpGRkXryySf1zTffKDMzU7Vr1z7vdXa7XWFhYXr99dflcDh05513lnlJ6i233KJWrVopOTlZl19+ufr166ennnpKGzdu1A033FCyJ8jIkSO1evVqLVmyRBkZGcrJydHbb7+tX3/9Va+//nq5g9R3331XDz/8sG6//XatWbNGdru95LlevXqpV69eOnz4sEaPHq2pU6dqw4YNys7OVmpqqvLz8zVw4EB17txZYWFhlfMHCKBaMKpWStIPP/yg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qKC3335bffv21fnz56850JSuPGjjn//8p/r376/58+drz549zn4ir0DeAsjHRVekuipvyVoApZYXZa1UevIWAEojw0aqxWJRcnKy0tPTlZGR4fRZewClj8VmvMTHx6tbt272JT4+Pt8+OnbsKD+/whfFp6am6rvvvlPfvn21bNkyff3119qxY8c168nJyZHVapUkWa3WEvugCvIWQF5mstaTeUvWAiitvClrpdKTtwBQGhlO7R8yZIg2btyorl27qn379uratasn6gLgxSxW43UiIyMVGRnp9L4rVKigO++8U7Vq1ZIktWrVSgcOHFDz5s2L3KZz586KiopS48aNtX//fnXq1Mnp43oD8hZAXmayVvJc3pK1AEorxrYAALMMG6l//etfVb9+fZ0+fVobN25U2bJlPVEXAG9m8ob716NGjRpKT0/XiRMndOedd2r37t3q0aOHw3Xnzp1rP0NfpUoVbdmyRfXr11dKSor7CnQj8hZAPm7MWsl83pK1AEo9xrYAAJMMG6n/+c9/9K9//Uu5ubl66KGHZLFY9Mwzz3iiNgDeyg2DzQ8//FAZGRmKjIzU9OnTNWrUKNlsNjVp0kT333+/w23y3l+qZs2aatu2resL8yDyFkA+bvqHvbN5S9YCKPUY2wIATLLYbLZrfm307t1b7777rgYOHKh3331X3bt319q1az1VH9zE+mud4i7BKT5VjxR3Ccij7rR5huv8OHGEByopXcjb0qmk5a07keXOMZO1EnnrLLIW3qIkfj+U1hxnbAsAMMvwilRfX18FBATIYrHIYrEoKCjIE3UB8GZunm56syJvAeRD1roFWQugEPIWAGCSYSO1adOmGjlypM6ePatJkybprrvu8kRdALyYhcGmW5C3APIia92DrAVQEHkLADDLsJE6cuRIJSYmqkGDBqpVqxb3aQEgmXySNJxD3gLIh6x1C7IWQCHkLQDApCIbqfHx8fl+LleunM6dO6f4+HhFRka6vTAA3ouz9q5F3gJwhKx1LbIWQFHIWwCAWUU2UpOTkz1ZB4ASxMJZe5cibwE4Qta6FlkLoCjkLQDArCIbqUOGDHH4ek5OjtuKAVBCcNbepchbAA6RtS5F1gIoEnkLADDJx9kNBg0a5I46AJQkNhMLbhh5C9zkzGQteXvDyFoAZC0AwCzDRurSpUuv+TOAm4/FarzAeeQtgLzMZC156zyyFkBBZC0AwCzDRuqXX36p3NxcT9QCoKTgrL1bkLcA8uGKVLcgawEUQtYCAEwq8h6pV6WmpqpVq1aqXr26LBaLLBaLVq5c6YnaAHgpnmzqHuQtgLzIWvcgawEURN4CAMwybKS+8cYbnqgDQAnC9Cb3IG8B5EXWugdZC6Ag8hYAYJZhI9XPz0+vvPKKUlJS9NBDD6lu3bq64447PFEbAG/FWXu3IG8B5EPWugVZC6AQ8hYAYJLhPVInTpyo7t27Kzs7W82aNdP06dM9URcAL2axGS9wHnkLIC8zWUveOo+sBVAQWQsAMMuwkXr58mU1b95cFotF4eHhCgwM9ERdALwZN+R3C/IWQD48bMotyFoAhZC1AACTDKf2BwYGauvWrbJardq7d68CAgI8URcAL8ZZefcgbwHkRda6B1kLoCDyFgBgluEVqdOmTdPatWuVmpqqt956S7GxsR4oC4BXs5pY4DTyFkA+ZrKWvHUaWQugELIWAGCS4RWpVatW1bx58zxRC4ASgrP27kHeAsiLrHUPshZAQeQtAMAsw0bqG2+8oSVLlqhMmTL217Zt2+bWogB4OQabbkHeAsiHrHULshZAIeQtAMAkw0bqhg0btHXrVgUFBXmiHgAlgIXpTW5B3gLIi6x1D7IWQEHkLQDALMNGavXq1fOdsQcApj+5B3kLIC+y1j3IWgAFkbcAALMMG6nZ2dnq0qWL6tSpI0myWCyaO3eu2wuDe/lUPVLcJaAkc9FZ+3379mnOnDmKi4vL9/rbb7+tVatWqWLFipKkKVOmKDw83DUH9WLkbelE3uK6ufAKKfL2f8haeAu+H7wIY1sAgEmGjdQnn3zSE3UAhjr49CzuEpy20bqquEtwC1ectV+8eLHWr1/vcGrlgQMHNGvWLP3lL3+58QOVIOQtbgYlMcvdxeg7wlVXSJG3+ZG1wI0paTluZjzO2BYAYJZPUW9s2bJFknT8+PFCC4CbnM3EYiAsLEzz5893+N7Bgwf15ptvKioqSosWLXJR0d6LvAXgkJmsJW9NI2sBFImsBQCYVOQVqefPn5ckJScne6oWACWExWo8moyPj1d8fLz958jISEVGRtp/7tixo06fPu1w286dOys6OlohISEaMmSItmzZorZt29544V6KvAXgiJmslchbs8haAEVhbAsAMKvIRurjjz8uSRoyZIjOnTunnJwc2Ww2nTt3zmPFAfBOZqY/FRxcmmWz2fT3v/9d5cqVkyS1adNGhw4dKtWDTfIWgCNmp5qSt+aQtQCKwtgWAGCW4T1SX3zxRe3du1eXLl3S5cuXVaNGDSUkJHiiNgDeyo1PNk1LS9MjjzyiDRs2KDg4WN988426d+/uvgN6EfIWQD5ufor0zZq3ZC2AQhjbAgBMKvIeqVcdPnxYH3/8sVq2bKmPP/5YgYGBnqgLgBezWI0XZ3344YeKj49XuXLlNGLECPXr10/R0dH685//rDZt2rj+Q3gh8hZAXmaylrx1HlkLoCCyFgBgluEVqaGhobJYLMrIyFDFihU9URMAL+eqJ0lXr17dfhVQly5d7K8/9thjeuyxx1xzkBKEvAWQl6uyViJv8yJrARTE2BYAYJZhI7Vhw4ZaunSpKleurBEjRujy5cueqAuAN3PzdNObFXkLIB+y1i3IWgCFkLcAAJMMG6mPPfaYKleurDJlyigxMVGNGjXyRF0AvJjZJ0nDOeQtgLzIWvcgawEURN4CAMwyvEfq+PHjFRISIj8/P7Vr106VKlXyRF0AvJjFZrzAeeQtgLzMZC156zyyFkBBZC0AwCzDK1KDg4M1Y8YM1axZUz4+V/qukZGRbi8MgPey5BZ3BaUTeQsgL7LWPchaAAWRtwAAswwbqdu3b1eTJk30+++/S5IyMzPdXhQAL8dZebcgbwHkQ9a6BVkLoBDyFgBgUpGN1FWrVmn16tUKDg7W1q1bJUlWq1U5OTkaNWqUxwoE4H2Y3uRa5C0AR8ha1yJrARSFvAUAmFVkI7Vr165q3ry5Fi1apKefflqS5OPjo1tvvdVjxQHwTtyQ37XIWwCOkLWuRdYCKAp5CwAwq8hGakBAgKpXr65p06Z5sh4AJQFjTZcibwE4RNa6FFkLoEjkLQDAJMN7pAJAQUx/AgD3I2sBwDPIWwCAWTRSATiN6U8A4H5kLQB4BnkLADCLRioA5zHWBAD3I2sBwDPIWwCASTRSATjNkstoEwDcjawFAM8gbwEAZtFIBeA8xpoA4H5kLQB4BnkLADCJRioAp3FDfgBwP7IWADyDvAUAmEUjFYDTuCE/ALgfWQsAnkHeAgDMopEKwHmMNQHA/chaAPAM8hYAYBKNVABOs9gYbQKAu5G1AOAZ5C0AwCwaqQCcxpNNAcD9yFoA8AzyFgBgFo1UAM5jrAkA7kfWAoBnkLcAAJN8iruAvFJTUzVp0iRJ0ueff67u3bsrMjJSCQkJhdY9ceKEoqKiFB0drcmTJ8tqtUqSFixYoB49eqh3797av39/vm1mzJihFStW2H9+++231bNnT/Xs2VMLFiyQJF28eFFPP/20+vbtq8jISH333XdF1mu1WjVp0iRFRkYqJiZGJ06cKLROQkKCunXrpl69emnLli2SpJSUFA0YMEDR0dEaPny4Ll26ZF8/JSVFHTt2VGZmpiQpIyNDgwcPVp8+fdS/f3+dPXtWkrRt2zY99thjioqK0sKFCyVJ48ePV7NmzZSUlGTwmwZujMVqM1zg3chb8hbez0zWkrfejawla1EykLUAALO8qpH62muvKTo6WtnZ2Zo5c6beeustxcXFKT4+Xr/99lu+dWfOnKnhw4fr/fffl81m0+bNm3Xw4EHt3LlTq1at0quvvqopU6ZIujKA+7//+z99/vnn9u1PnTql9evXa+XKlUpISNC2bdt0+PBhLVu2TPfdd5+WL1+umTNnaurUqUXWu2nTJmVlZSk+Pl6jRo3Syy+/nO/95ORkxcXFaeXKlVq6dKleffVVZWVlaeHChXrkkUf0/vvvq0GDBoqPj5ckbd26VQMGDFBycrJ9HwkJCWrYsKHee+89Pfroo1q8eLGsVqsmTJig+fPna8WKFTp27Jh2796t6dOnq379+jf8dwAM2WzGiwn79u1TTExMke9PnDhRc+bMcVXVyIO8JW9RApjJWvLWq5G1ZC1KCLIWAGCS1zRS09LS9P3336tevXpKSkpSWFiYbrnlFgUEBKhp06batWtXvvUPHjyoe+65R5LUunVrbd++XXv27FHLli1lsVhUrVo15ebmKiUlRenp6Ro6dKi6du1q375q1apasmSJfH19ZbFYlJOTo8DAQPXv31+9e/eWJOXm5iowMLDImvfs2aNWrVpJku6++24dOHAg3/v79+9XkyZNFBAQoHLlyiksLEyHDx/Ot93V2iXJx8dHy5YtU4UKFez76N+/vwYPHixJOnPmjMqXL6/U1FSVL19eNWrUkCRFRETo22+/dfp3Dlwvi9V4MbJ48WJNmDDBfoVKQStXrtSRI0dcXDkk8lYib1EymMla8tZ7kbVkLUoOshYAYJbXNFL37t2rmjVrSroy8CxXrpz9vbJlyyotLS3f+jabTRaLxf7+xYsXlZaWppCQkHzbXbx4UTVq1FDjxo3zbe/v76+KFSvKZrNp1qxZatCggWrWrKny5curTJkySk5O1vPPP6+RI0cWWXPB4/n6+ionJyff+44+R97Xr9YoSS1atFBoaGih4/j6+qpfv35avny5OnTooIoVK+ry5ctKSkpSbm6uEhMTlZGRUWSdgMtZbcaLgbCwMM2fP9/he99++6327dunyMhIV1cOkbcSeYsSwkzWkrdei6wla1GCkLUAAJO85mFTqampqlSpkiQpJCRE6enp9vfS09PzDdqkK2e4875fvnx5U9vllZmZqRdffFFly5bV5MmT7a//+OOPGjlypF544QX7lQGOFDye1WqVn59fke9frefq62XKlLHXbuTdd99VUlKSBg0apE2bNmn27NmKjY1VQECA6tSp43CQCriLxcT0pvj4ePvUPkmKjIzMN3js2LGjTp8+XWi7c+fO6fXXX9eCBQv0ySefuKZg5EPeXht5C29hJmsl8tZbkbXXRtbCmzC2BQCY5TVXpN566626cOGCJKlWrVo6ceKEzp8/r6ysLO3evVtNmjTJt36DBg30zTffSJISExPVrFkzRUREaNu2bbJarTpz5oysVqsqVqzo8Hg2m03PPPOM6tatq6lTp8rX11eSdPToUQ0bNkxz585VmzZtrllzRESEEhMTJV256qBOnTr53m/UqJH27NmjzMxMXbx4UUlJSapTp44iIiL05Zdf2mtv2rRpkcdYtGiR1q1bJ+nKGf6rdW7btk1Lly7VkiVLdPLkSf3tb3+7Zq2AS5m4j1RkZKTWrl1rX8yegf/000+Vmpqqp556Sm+++aY++ugjrV271s0f6OZC3jpG3sLrmLxHKnnrnchax8haeCWyFgBgktdckdq4cWP7jbf9/f01duxYDRw4UDabTd27d1eVKlV09OhRLV++XLGxsRozZowmTpyoV199VeHh4erYsaN8fX3VrFkzRUZG2p86WpRNmzZp586dysrK0tatWyVJI0eO1JtvvqmsrCxNnz5d0pUz7//617/05ptvql69emrdurV9Hx06dNBXX32l3r17y2azacaMGZKkZcuWKSwsTA888IBiYmIUHR0tm82mESNGKDAwUIMHD9aYMWOUkJCg0NBQzZ07t8g6u3fvrjFjxmjNmjXKzc21H6Ny5crq2bOnypQpoy5duqh27do39gcAnGDJ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AVz1xhtv6OjRo6pXr57+9re/KSMjQ9OnT9e6desKndC5etIpPDxcixcvlr+/v06fPq1OnTpp8ODB+uWXXzRx4kRlZmYqMDBQ06ZNU25urgYPHqwKFSqodevWevLJJ+3H3rRpkz755BNdvnxZEyZMUKNGjbR8+XJ99tlnunTpkkJDQ7VgwQJ7jQsWLFB0dLTGjBmjixcvymazadasWZKunMT69NNPdf78eQ0bNkzt2rVz+HmPHz+ucePGyc/PT1arVXPnztXJkye1cuVKjRw5Ui+++KKkK/l/7Ngx7dixQ1988YXefvtt+fj4qGnTpho9erT7/zAAALgZjVR4hdOnT6tVq1aSpIiICAUEBOR732azSZICAwOVkpKikSNHKjg4WBkZGcrOzr7mvkNCQvTXv/5V27Zt09q1a/XMM884XO/48eO6++67JUm33HKLhg8fXuQ+x40bp0WLFmn58uUKDw8v1HBNTk7WsGHDNHPmTN1xxx3asGGDDh48qJiYGElSTk6Ofv75Z/5RDsApxZmVYWFhCgkJkSTddtttyszMvOb+GjZsKEkqX768atWqZf9vo+0k6dSpU6pdu7bKlCkjSab+8V3UZ3744YfVrVs3DRw4UGfPnlXDhg312muv6euvv9Ynn3wiSfrjjz8kSaGhoQoNDZVknPMAbm5PP/20jhw5olatWumPP/7QhAkTTJ3EOnPmjNavX6+srCy1atVKgwcP1qxZsxQTE6M2bdpox44dmjNnjkaMGKHk5GStWbOmUNbfcccdmjp1qv773//aTxKdP3/e3rQcOHCgvv/+e3uNQ4YM0UsvvaR27dopKipK3377rfbv3y9JqlKliqZPn65vvvlGS5YsKbKRun37djVq1EjPP/+8du/erYsXL9rfq1GjhuLi4pSVlaWnn35a//jHP5SZman58+drzZo1CgoK0vPPP6+vvvpKLVq0cPFfAgAAz2KOGrxC3bp1tWfPHknSjz/+qKysLAUEBNiveDp06JAkKTExUb/88oteffVVjRw5UpcvX7Y3DgqyWCz293r16qVVq1bp999/V7169RyuHx4eru+//16SdPHiRQ0cOLDIeuPj4zV06FAtX75c0pUHpFx14cIFPfvssxo3bpzq1q1r3/e9996ruLg4vfPOO3r44YdVo0YN078fAJCKNystFkuhbQMDA+3HPnjwoOH6ZoWFhenYsWPKysqSJD333HOGD8Qq6jMHBwfr3nvv1fTp0/Xoo49KupLJ/fv3V1xcnF577TX763mn7l8r5wEgr5o1a0rKf0Jn0qRJDk9i1alTR35+fgoODrafLDpy5IgWLVqkmJgYvf766/r9998lSdWrVy/URJWkv/71r5Kk2rVrKzk5WT4+PvL397dfGfrrr78qJycn3zbHjx9XkyZNJF05EXc1966e9KpUqZIuX75c5Gfs0aOHypcvr//7v//Te++9J19f33zv5+TkaMSIEXr00UfVpk0bnTx5UikpKXrqqacUExOjpKQknTx50twvFAAAL8YVqfAKPXv21Pjx49WnTx9Vq1ZNktSvXz9NmTJF1apVU+XKlSVJjRo10sKFC9WnTx9ZLBbVqFFD586dc7jPJk2a6IUXXtBbb72lxo0b68SJE+rTp48kadmyZQoLC8v3cKgHHnhAO3bsUFRUlHJzc/Xss88WWW+jRo00aNAglS1bVsHBwbr//vvt/9ieN2+ezp07pwULFshqtcrf319Lly7Vzp07FR0drYyMDLVv395+ZRcAmFVcWXn1pFBBDz/8sIYPH65du3bZ/zHuChUrVtSTTz6pvn37ymKxqG3btqpSpco1tynqM9eoUUO9evVSdHS0YmNjJV25kmz8+PFKSEhQWlqa/cFbBfdXMOcB4CofHx9ZrVb7f0v/O6Hz2muvKSUlRRs3bix0EsvRSabw8HANGDBAERERSkpK0q5du/Ltt6D9+/erS5cu+vHHH1WtWjUdPnxYmzZt0qpVq3Tp0iV169ZNNpstX421atXS999/r3r16mnXrl364osvVKZMGdMnvTZv3qymTZtqyJAh+uijj7RkyRI99thjkq7Mhhg/fryaNGlif6169eq6/fbb9dZbb8nf319r167lfqoAgFLBYivqEhWgmGRmZurhhx/W559/7rJ9Wq1WRUVFaenSpTQwAZQKZKV5+/fv1/LlyzV79uziLgVAKXH1nvctW7ZU9erVFRUVpeTkZD399NP2BuXly5c1btw4bd++3X6P1JUrV2revHmSpBYtWuirr77SqVOnFBsbq8zMTF2+fFnjx4/XbbfdppEjRyohIUGSNGDAAL3xxhtatGiRDh06pPT0dGVlZSk2NlZ33nmnBg0aZL+KPyAgQD169FDHjh3tNQ4cOFAvvvii0tPTJUkzZszQunXrHD4w0JGTJ09qzJgx8vf3l9Vq1bhx45SWlqaVK1fqwQcf1IsvvqjGjRsrNzdXkjR58mQdPHhQK1asUG5uru644w7NnDlTQUFB7v7TAADgVjRS4XVc3Rw4deqUhgwZom7duunvf/+76e2ysrIcTu+vWbOmpk6d6pLaAOB6eUtWOmv//v165ZVXCr3+8MMPKzo6usjtYmNjlZSUVOj1xYsX26fHOrJ8+XKtXr1ar732mv70pz9dV80AAAAAINFIBQAAAADo+k9aAQBws6CRCgAAAAAAAAAGHN/BHAAAAAAAAABgRyMVAAAAAAAAAAzQSAUAAAAAAAAAAzRSAQAAAAAAAMAAjVQAAAAAAAAAMPD/ABx0Bm1a9q54AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "hyperopt_report_cli(\"hyperopt_results/random_serial/hyperopt_statistics.json\", output_directory=\"./visualizations\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Generate parallel coordinates plot on hyperparameter optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "hyperopt_hiplot_cli(\"hyperopt_results/random_serial/hyperopt_statistics.json\", output_directory=\"./visualizations\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To view parallel coordinates plot, using your browser open html page in the `visualizations` directory, i.e, `visualizations/hyperopt_hiplot.html`. The browser should display something similar to the image below." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAACrIAAAXgCAYAAAAXW0u3AAAMY2lDQ1BJQ0MgUHJvZmlsZQAASImVlwdYU8kWgOeWVBJaIAJSQm+iSA0gJYQWQUCqICohCSSUGBKCih1dVsG1iyhWdFVE0dUVkLUgYmdR7K5lsaCysi4WbKi8CQnouq98b75v7vw5c+bMOScz984AoNPBl8lyUV0A8qQF8rjwYNaElFQW6REgg+HAANgBFl+gkHFiY6MALIPt38ub6wBRtVdcVLb+2f9fi75QpBAAgKRBzhAqBHmQmwDAiwUyeQEAxBAot55WIFOxGLKBHDoIeZaKs9S8XMUZat4+oJMQx4XcAACZxufLswDQboFyVqEgC9rRfgTZVSqUSAHQMYAcIBDzhZATII/Iy5uq4nmQHaC+DPIuyOyMr2xm/c1+xpB9Pj9riNVxDRRyiEQhy+XP+D9T879LXq5ycA47WGlieUScKn6Yw5s5UyNVTIPcLc2IjlHlGvI7iVCddwBQqlgZkajWR00FCi7MH2BCdhXyQyIhm0IOk+ZGR2nkGZmSMB5kuFrQ6ZICXoJm7CKRIjReY3ODfGpczCBnyrkczdhavnxgXpV+izInkaOxf1Ms4g3af10kTkiGTAUAoxZKkqIha0M2UOTER6p1MKsiMTd6UEeujFP5bwOZLZKGB6vtY2mZ8rA4jb4sTzEYL1YilvCiNVxRIE6IUOcH2y3gD/hvBLlOJOUkDtoRKSZEDcYiFIWEqmPH2kTSRE282D1ZQXCcZmyPLDdWo4+TRbnhKrkVZBNFYbxmLD6mAC5OtX08SlYQm6D2E0/P5o+NVfuDF4IowAUhgAWUsGaAqSAbSNq667vhL3VPGOADOcgCIuCikQyOSB7okcJnPCgCf0ISAcXQuOCBXhEohPJPQ1L10wVkDvQWDozIAY8h54FIkAt/KwdGSYdmSwKPoETyj9kF0NdcWFV9/5RxoCRKI1EO2mXpDGoSQ4khxAhiGNERN8EDcD88Cj6DYHXD2bjPoLdf9AmPCe2EB4RrhA7CrSmSYvk3vowDHdB+mCbijK8jxu2gTU88GPeH1qFlnImbABfcA87DwQPhzJ5QytX4rYqd9W/iHIrgq5xr9CiuFJQyjBJEcfh2pLaTtueQFVVGv86P2teMoaxyh3q+nZ/7VZ6FsI38VhNbhB3EzmAnsHPYEawesLDjWAPWih1V8dAaejSwhgZnixvwJwfakfxjPr5mTlUmFa41rl2uHzV9oEA0vUC1wbhTZTPkkixxAYsDvwIiFk8qGDmC5ebq5gqA6puifk29Yg58KxDm+S+y/CYAfEqhMOuLjG8NwOHHADDefJFZv4TbA77rj14SKOWFahmuehDg20AH7ihjYA6sgQOMyA14AT8QBELBWBADEkAKmAzzLIbrWQ6mgVlgPigBZWA5WAPWg81gG9gF9oIDoB4cASfAaXABXALXwG24fjrBM9AD3oA+BEFICB1hIMaIBWKLOCNuCBsJQEKRKCQOSUHSkSxEiiiRWcgCpAxZiaxHtiLVyE/IYeQEcg5pR24h95Eu5CXyAcVQGmqAmqF26CiUjXLQSDQBnYRmofloEboQXYpWoFXoHrQOPYFeQK+hHegztBcDmBbGxCwxF4yNcbEYLBXLxOTYHKwUK8eqsFqsEf7TV7AOrBt7jxNxBs7CXeAajsATcQGej8/Bl+Dr8V14Hd6CX8Hv4z34ZwKdYEpwJvgSeIQJhCzCNEIJoZywg3CIcArupk7CGyKRyCTaE73hbkwhZhNnEpcQNxL3EZuI7cSHxF4SiWRMcib5k2JIfFIBqYS0jrSHdJx0mdRJekfWIluQ3chh5FSylFxMLifvJh8jXyY/IfdRdCm2FF9KDEVImUFZRtlOaaRcpHRS+qh6VHuqPzWBmk2dT62g1lJPUe9QX2lpaVlp+WiN15JozdOq0NqvdVbrvtZ7mj7NicalpdGUtKW0nbQm2i3aKzqdbkcPoqfSC+hL6dX0k/R79HfaDO2R2jxtofZc7UrtOu3L2s91KDq2OhydyTpFOuU6B3Uu6nTrUnTtdLm6fN05upW6h3Vv6PbqMfRG68Xo5ekt0dutd07vqT5J304/VF+ov1B/m/5J/YcMjGHN4DIEjAWM7YxTjE4DooG9Ac8g26DMYK9Bm0GPob6hh2GS4XTDSsOjhh1MjGnH5DFzmcuYB5jXmR+GmQ3jDBMNWzysdtjlYW+NhhsFGYmMSo32GV0z+mDMMg41zjFeYVxvfNcEN3EyGW8yzWSTySmT7uEGw/2GC4aXDj8w/DdT1NTJNM50puk201bTXjNzs3Azmdk6s5Nm3eZM8yDzbPPV5sfMuywYFgEWEovVFsct/mAZsjisXFYFq4XVY2lqGWGptNxq2WbZZ2VvlWhVbLXP6q411ZptnWm92rrZusfGwmaczSybGpvfbCm2bFux7VrbM7Zv7eztku2+t6u3e2pvZM+zL7Kvsb/jQHcIdMh3qHK46kh0ZDvmOG50vOSEOnk6iZ0qnS46o85ezhLnjc7tIwgjfEZIR1SNuOFCc+G4FLrUuNwfyRwZNbJ4ZP3I56NsRqWOWjHqzKjPrp6uua7bXW+P1h89dnTx6MbRL92c3ARulW5X3enuYe5z3RvcX3g4e4g8Nnnc9GR4jvP83rPZ85OXt5fcq9ary9vGO917g/cNtgE7lr2EfdaH4BPsM9fniM97Xy/fAt8Dvn/5ufjl+O32ezrGfoxozPYxD/2t/Pn+W/07AlgB6QFbAjoCLQP5gVWBD4Ksg4RBO4KecBw52Zw9nOfBrsHy4EPBb7m+3NncphAsJDykNKQtVD80MXR96L0wq7CssJqwnnDP8JnhTRGEiMiIFRE3eGY8Aa+a1zPWe+zssS2RtMj4yPWRD6KcouRRjePQcWPHrRp3J9o2WhpdHwNieDGrYu7G2sfmx/4ynjg+dnzl+Mdxo+NmxZ2JZ8RPid8d/yYhOGFZwu1Eh0RlYnOSTlJaUnXS2+SQ5JXJHRNGTZg94UKKSYokpSGVlJqUuiO1d2LoxDUTO9M800rSrk+ynzR90rnJJpNzJx+dojOFP+VgOiE9OX13+kd+DL+K35vBy9iQ0SPgCtYKngmDhKuFXSJ/0UrRk0z/zJWZT7P8s1ZldYkDxeXibglXsl7yIjsie3P225yYnJ05/bnJufvyyHnpeYel+tIcactU86nTp7bLnGUlso583/w1+T3ySPkOBaKYpGgoMICH91alg/I75f3CgMLKwnfTkqYdnK43XTq9dYbTjMUznhSFFf04E58pmNk8y3LW/Fn3Z3Nmb52DzMmY0zzXeu7CuZ3zwuftmk+dnzP/12LX4pXFrxckL2hcaLZw3sKH34V/V1OiXSIvufG93/ebF+GLJIvaFrsvXrf4c6mw9HyZa1l52cclgiXnfxj9Q8UP/Uszl7Yt81q2aTlxuXT59RWBK3at1FtZtPLhqnGr6lazVpeufr1myppz5R7lm9dS1yrXdlREVTSss1m3fN3H9eL11yqDK/dtMN2weMPbjcKNlzcFbardbLa5bPOHLZItN7eGb62rsqsq30bcVrjt8fak7Wd+ZP9YvcNkR9mOTzulOzt2xe1qqfaurt5tuntZDVqjrOnak7bn0t6QvQ21LrVb9zH3le0H+5X7//gp/afrByIPNB9kH6z92fbnDYcYh0rrkLoZdT314vqOhpSG9sNjDzc3+jUe+mXkLzuPWB6pPGp4dNkx6rGFx/qPFx3vbZI1dZ/IOvGweUrz7ZMTTl5tGd/Sdiry1NnTYadPnuGcOX7W/+yRc77nDp9nn6+/4HWhrtWz9dCvnr8eavNqq7vofbHhks+lxvYx7ccuB14+cSXkyumrvKsXrkVfa7+eeP3mjbQbHTeFN5/eyr314rfC3/puz7tDuFN6V/du+T3Te1W/O/6+r8Or4+j9kPutD+If3H4oePjskeLRx86Fj+mPy59YPKl+6vb0SFdY16U/Jv7R+Uz2rK+75E+9Pzc8d3j+819Bf7X2TOjpfCF/0f9yySvjVztfe7xu7o3tvfcm703f29J3xu92vWe/P/Mh+cOTvmkfSR8rPjl+avwc+flOf15/v4wv5w8cBTBY0cxMAF7uBICeAs8Ol+A1YaL6zjdQEPU9dYDAf2L1vXCgeAGwMwiAxHkARMEzyiZYbSHTYKs6qicEAdTdfahqiiLT3U1tiwZvPIR3/f2vzAAgNQLwSd7f37exv/8TvKNitwBoylffNVWFCO8GW4xV1HpDF3xb1PfQr2L8tgUqDzzAt+2/AFoGh9r+AwoRAAAAimVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA5KGAAcAAAASAAAAeKACAAQAAAABAAAKsqADAAQAAAABAAAF4AAAAABBU0NJSQAAAFNjcmVlbnNob3Q+xNfQAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB2GlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNzM4PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6VXNlckNvbW1lbnQ+U2NyZWVuc2hvdDwvZXhpZjpVc2VyQ29tbWVudD4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjE1MDQ8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KAhYPZAAAABxpRE9UAAAAAgAAAAAAAALwAAAAKAAAAvAAAALwAAbZj15up6kAAEAASURBVHgB7J0HfBRFG4ff9B6SkITee5EO0lRUioCKqDRBBKV8VDsWLCCIBUEQC9IUG1ZA6SBSpEjvvRNKEiCkkUqSb9455jJ3ueQul0uD//gjOzttZ5+d3Qvy3LtOK1asyHByciJOGRkZpPKywMIP1UbfcjNL/VQbNYy+z+3T09Nlleqr16s+5lvVRt9yGzWG3l61UWX6vjp+dHS07FujRo078vx1djofxcx8q9roW30Mvb1qo8r0fcVf76vXqz7mW9VG3+pj6O1VG1Wm7+P4ReP+06+dfn3UNTPfqjb6Vh9Db6/aqDJ9H9cf178ofP7oa1dfn2rNmm9VG32rj6G3V21Umb6P9Y/1j/Vf+L9/6veufn+qe9Z8q9roW30Mvb1qo8r0fdz/uP9x/+P+158d+vNBPTPMt6qNvtXH0NurNqpM38fzB88fPH/w/NGfHfrzQT0zzLeqjb7Vx9DbqzaqTN/H8wfPHzx/8PzRnx3680E9M8y3qo2+1cfQ26s2qkzfx/MHzx88f/D80Z8d+vNBPTPMt6qNvtXH0NurNqpM38fzB88fPH/w/NGfHfrzQT0zzLeqjb7Vx9DbqzaqTN/H8wfPHzx/8PzRnx3680E9M8y3qo2+1cfQ26s2qkzfx/MHzx88f2x//pw4cUIIokRe3l7qdjLxJfne4uTs7CzLeV/luV8G/+AkVFMn/nErGe5J7pOexeFUY/K9KhMPkdnV5PhOK1euFO0zBVaV1wfRy3hQtX9rLsaNLBd78niaHMsN1GQs9dXLVJ63qp9e5ujjs8jK49esWVMeTz9WQRyfj6GOqee5jJN+vqqd2soG2g9ZLvaLE3+evn4+Ks9bTjj/zPtNZ6PuJwnp1g9ZL/K4/qbPG8ajeCmGt5DJjV6m8rzlhPWH9aevCX09yAWi/ZDtxD7uP9P7jRHh/jP8BqbWkrZs8PknnrXm6wPPX3z+qOeGumfMt/o9xHlZz1vxR19PXKfvqzyXc1Lj6nku48RtVb35VjbQfsh6sY/jm/JmRIq5YqhhM/LlMlXPW07gj/Wnrwl9PcgFov2Q7cQ+7j/T+40R4f7D71+8DtS9xHmV9DKV5y0n/X7T69R6UmPwVtbzVvxR9fo4xjbqf0xygUhqXD2v91P15lvZWfsh63k88QfHz1zvjEjnofJczklx1fNcxonbqnrzrWyg/ZD1Yh/8TXkzIsVcMdSwGflymarnLSfwx/rT14S+HuQC0X7IdmIf95/p/caIcP9lfh4oFmrpqPXF+yrPW076etPrzMfgtrKet7f6qTLeqvZqDC5TSS9Ted5ywvHx/NPXhL4e1PpRW9lO7GD9md5vzAf3H55/vA7UvcR5lfQylectJ/1+0+vUelJj8FbW81b8UfX6OMY2+PsfozAmxZULVF7nppfp18M4wK2MbMdjiD/gn3m/Mx6dh8pzOSfFV8+DP+5/Xg/6/abWidpyvZ5kuSjA/Wd6vzEjdc9ZYqeXqTxvVT+9TL8esoH2Q7YT++Bvyltx5K1iyXmV9DKV5y0nxVuKrGLf29tbjqHXqTZcppJexnmVLI3PdXq5aV+uU70z58Mlqh1vndauXSvGMIgEqiKzW2ZjVaa3UXne6onH46Tq9To9r+pxfPDntaDWg6U1osr0NirPWz1h/eH+4/Wg1oe+NvS8qsfzB88fXgtqPVhaI6pMb6PyvNUTnj94/vB6UOtDXxt6XtXj+YPnD68FtR4srRFVprdRed7qCc8fPH94Paj1oa8NPa/q8fzB84fXgloPltaIKtPbqDxv9YTnD54/vB7U+tDXhp5X9Xj+4PnDa0GtB0trRJXpbVSet3rC8wfPH14Pan3oa0PPq3o8f/D84bWg1oOlNaLK9DYqz1s94fmD5w+vB7U+9LWh51U9nj94/vBaUOvB0hpRZXobleetnvD8wfOH14NaH/ra0POqHs8fPH94Laj1YGmNqDK9jcrzVk94/uD5w+tBrQ99beh5VY/nD54/vBbUerC0RlSZ3kbleasnPH/w/OH1oNaHvjb0vKrH86f4P3+cFixYkKEuqLrIlva5Tj0gVDvzLdcbw8mqSn7GiPvKfExVzVvzOkv73A7HNzygmIWlBP5Yf7j/DKGtjfcHnj94/uLzJ8tnrPH+EBlLn7f6Zy3Xc9LLZIHZD3z+4PMHnz/4/DF5TuDzF5+/+PzN8hmrf3Ti89cQdUgxscSD60yeK6qxtsXvH/j9A79/4PcPk+cEfv/A7x/4/QO/f4j/h2HyXNB+b7D0+4beFv//A///h5eLvia05WPMcj1+/8DvHybrBL9/4PcP/P6B3z/w+0e2n5/4/cv0d1NLPPD7B37/Mvm9wvhbZ2YGv3/i92/8/QN//zB5Ttwhf/9wenDOTfFrNhIIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIOIZAte3D5UBNmzaV227dulFYWBjFxMRQcnKyLPPw8CCIrI7hjVFAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARuEdBF1ipVqlBqaqpFNhBZLWJBIQiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAgL0ElMjao0cPSklJyXYYp/Pnz2dkW4sKEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABELCDwIULF+j69es59oTImiMeVIIACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACOSWQHx8PJ05c8ZqN4isVhGhAQiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAQG4I2BKNlceDyJobqmgLAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiBglcCxY8coJSXFajuIrFYRoQEIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgEBuCBw4cMCm5hBZbcKERiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAArYSgMhqKym0AwEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQcCgBiKwOxYnBQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEbCUAkdVWUmgHAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiDgUAIQWR2KE4OBAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAjYSgAiq62k0A4EQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQMChBCCyOhTnnTvY5cQU2n49nk7FJVFCWvqdC6KInrm3izNV8/OkFoG+VMbL3WSW4UlXaff1Q3T2xgVx7ZJM6rCTlYC3iydV9ilPTQLrUWnPYJMGGVeuUfreQ5R+9gJRQqJJHXaKEAFvL3KuXJ6cG9Ujp5CSJhM7ffo0zZo1i/bu3UvHjx+nmzdvmtRjx3EEXF1dqWbNmtSoUSMaMmQIVa1a1WTws2fPymuxZ88eOnbsGK6FRofZ1apVixo3bizZVa5cWatFFgRAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAASKFwGIrMXrehXJ2e69foOWXr5eJOeGSWUl8HCZQGoU6CMrDsQco1Xhm7I2QolNBDqVbkt3lagl26YfPkFpazba1A+Nig4Blw73knPdGnJCy5cvp9GjR1NKSkrRmeAdMhN3d3f67LPPqEuXLvKMV61aRaNGjaKkJMj11paAp6cnzZgxgzp16mStKepBAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAoEgSgMhaJC9L8ZnUJRGJdd6ZyOIzYcxUEni2Sig5OcXQj+f+ApE8Euhb6VEqHeNEN3/+M48joXthEXDt3Y3OJcZLETAxMZGaN28uolwOpSd79CAvL6/CmtZtf1xm/ftvv4moq1/Tjh07JGsWWJ2cnOS1SEhIoKZNm9LgwUOoZ69e5O3tfdszsfUEmc2vv/xCs2fPol27dkk2zK5SpUq2DoF2IAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIFBkCEBkLTKXonhOZPHFKDoYk1A8J38Hz7p+CW9yc95PR2JP3cEUHHPqdfyrUad9RBnHwNIxRAt+FKda1eiVlYtp4cKF1EsIk/O/+77gJ3GHH/GZ/k/TL0LMfPzxx6XI+scff1APIRJ//8OPdzgZ66ff/+l+9Ouvv0p206ZNs94BLUAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABECgiBGAyFrELkhxm87U45co4WZ6cZv2HT9fb1dn8nReRwlpeG13XheDt4snDVpL5IRXoOcVZaH1zxCvZm/x1WSKioqiffsPUK1atQptLnfqgY8dO0YNG9xFQUFBEgGuhe0rQWe3d+9e2zuiJQiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAgUEQIQWYvIhSiu05h4+EJxnfodP29Pl1V3PANHAXh+paNGwjiFRaDalPHy0EnJKYU1hTv+uJ4e7iYMcC1McOS4o9idP38+x3aoBAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAIGiSAAia1G8KsVoThBZi9HFMpsqRFYzIHnYhciaB3hFpCtE1sK/EErGVDOByKpIWN8qdhBZrbNCCxAAARAAgYIl4Be5p2APmIejxYU2zkNvdAUBEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEMgLAYiseaGHvgSRtfguAoisjrt2EFkdx7KwRoLIWljkM4+rZExVApFVkbC+VewgslpnhRYgAAIgAAIFSwAia8HyxtFAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAoLgSgMhaXK9cEZk3RNYiciHsmAZEVjugZdMFIms2YIpRMUTWwr9YSsZUM4HIqkhY3yp2EFmts0ILEAABEACBgiUAkbVgeeNoIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIFBcCUBkLa5XrojMGyJrEbkQdkwDIqsd0LLpApE1GzDFqBgia+FfLCVjqplAZFUkrG8VO4is1lmhBQiAAAiAQMESgMhasLxxNBAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAorgQgshbXK1dE5g2RtYhcCDumAZHVDmjZdIHImg2YYlQMkbXwL5aSMdVMILIqEta3ih1EVuus0AIEQIDo5s2b5OrqChQgUCAEILIWCGYcBARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAASKPQGIrMX+EhbuCUBkLVz+eTk6RNa80DPtC5HVlEdx3IPIWvhXTcmYaiYQWRUJ61vFDiKrdVZoAQJ3OoHPP/+c+C+AkydPJn9//zsdB86/AAhAZC0AyDgECIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACNwGBCCyFvOLeGzvRQo7eY2SElLIx9+TKtUMoap1SxXYWUFkLTDUDj8QRFbHIYXI6jiWhTUSRNbCIp95XCVjqhKIrIqE9a1iV9xF1vXr19PJkydNTrhSpUrUoUMHkzLsZBK4cuUKLV++nFJTU2Vh+fLl6aGHHspsgFyRIhAeHk7Hjh0jDw8PatmypU1z4+ip586dowsXLlBAQABVrVqV/Pz8bOpr3ujEiRM0YcIEcnd3p+nTp5OPj495E+N+SkoKHT9+XB63WbNmFBwcbKzLLuPIuWZ3DJQXPwKWRNb0jAw6dSGSrkXHUcu7qtt8UonJKXT49EWKjk+g+tXKU6mgEjb1tbVfXGhjk/HS09MpKSmJvL29TcqxAwIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIg4HgCEFkdz7RARvxn0QFauWAPRV6MyXK8ijWCqfNTTahVx1pZ6hxdAJHV0UQLbjyIrI5jDZHVcSwLaySIrIVFPvO4SsZUJRBZFQnrW8UuP0TWU6dO0enTpykyMpK8vLyoYsWKVLZsWdqzZw+1aNGCQkJCrE/QxhYvvvgiXbt2zaR1hQoV6P333zcpKyo7LCUePnyY2rRpI8XEwpgXy7/z5s0zHppF1kmTJhn3kSl8AmfPnqV169bRwYMHicVjlb766qscRVJut3HjRpo/f75RVFZ9mzdvTkOGDMn1uvv000/lvdu+fXvq37+/Gs64TUxMpA0bNtC+ffukcMtiKqeOHTtSv379jO0sZRw9V0vHQJntBPbu3UsJCQnUunVr2zvlU0slsqaK9bRu5xHae+wcHTgVRvEJSfKI3703jPy8Pa0efdPeYzR9wUq6mZZubNu8blUa88wj5OribCwzz+SmnxJZY2Nj6YcffqBdu3bJ+4/l8UceecTmLwrwZ+aWLVvkvQMJ1vyKYB8EQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAELBMoViLroR1hQt7cbflMsil9/mPxD1uu2f/DVjbd7Co+eTCc/py3LVd9h77biXxLWP+HO33Qr8evpq2rj+lFFvMdnmxIfV+812KdowohsjqKZMGPA5HVccwhsjqOZWGNBJG1sMhnHlfJmKoEIqsiYX2r2DlSZL1+/boUJFloyy517dqVevXqlV11rssvXbpE/Ieji16+fJkWL15MRVlk/frrr2nz5s3EAm7jxqZR/HJ98nZ24KiZR44ckbLxokWLCCKrnSDzqRuLoSNGjCAlhOqHYam0ZMmSepFJntf/woULTcr0HY5W/MYbb9gcKZLvLW7v5OREkydPtiih//LLL7Rs2TL9MDJ/77330qBBg7KUqwJHz1WNi619BFhGnjt3Lj344IP0zDPP2DeIA3spkXXdzsP02c+rsoz8zbtDKcAv54inu4+eoUnz/qI0ESG1eoVSFOjvQ/uOn6eU1JvUpmFNeuXprlnG5YLc9lMiK3+BgqMnlyhRQj5XOc/38bPPPkvt2rWzeCy9kL8EMn78eKpZsya9/vrr4v9HuOrVyIMACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACFggUKxE1s0rjtLsiWssnEb2RbPXDSc3d5fsGziwZu/mMzRtzNJcjfjp4oEUGOJrc5857/9Nm5Yfsbl9135Nqcew/IvEA5HV5ktR5BpCZHXcJYHI6jiWhTUSRNbCIp95XCVjqhKIrIqE9a1i5yiRNTk5md577z0KCwsjT09Puuuuu+SfuLg42rRpk5RMeVbW5DbrM8++xcmTJ+UcirLI+tlnn9HOnTtp4MCBdP/992d/MgVQo3hBZC0A2Lk4BL+W/OWXX6aoqCgpZev3aE4i69WrV+nVV1+ltLQ0eTSOBtmwYUMpenOEZJW6d+9O/MeWNHv2bPr333+pVatWNGzYMItdOHLst99+K+VYFu9iYgxvfsjpXs+PuVqcHAptIsBfAnj77beJJfd3332XqlWrZlO//GykRNY4EYF1vZBZQ4P8qWq5UBry/lx5WFtE1tc+W0DHz4dTi3rV6I2Bj8p+u46coffn/UkZGRk07eWnqVKZ4Cynkdt+LLJy5GS+b0NDQ+njjz8mZ2dn4sjK77zzDtWqVYvGjh2b5TiWClhgZYGcv/DBX/xAAgEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQyJkARNac+eSqNr9F1v/WHKeZ47JGsbE2yTe+eJxqNSpnrZld9RBZ7cJWJDpBZHXcZYDI6jiWhTUSRNbCIp95XCVjqhKIrIqE9a1ip0ty1ntl34KjjHK0UR8fH5o4caJJ1EiOMPnhhx/SmTNnqGnTpvT8889bHIgFPhbcWOLhVzSXKlWKypUrR76+tn15R4mZuRFZ83JMPq8LFy4Qvw6aI/CVLVuWgoKCKD4+XgphnDdPeRFZ8zJXngfPlyVHxVPxgshqfpUKf5+vE0dy9PDwoP79+xsnlJPIOn/+fFq7dq1s6+7uLsVEjsDKY3E01cOHD8s6vkd5HBbOc0os0rKYx/0nTJhAPFZ2idcWj8dRVjnKL6ecRFZHzzW7eaHcNgKzZs2SXzioUaOGXDe29crfVkpk1Y+SnJJKvd/8XBZZE1nPXb5KL0z5XradNfY5Cgn0Nw713uyFtOfYOeratjENeqydsZwz9vRjkZW/xMGyar169ei1116TY/J9MXz4cCm3fvTRRybHyW6HxfBvvvmG/P39acqUKfIZkF1blIMACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACBBBZHXgKshvkXXi0N/p5MHLuZ5xiwdq0PAJD+W6ny0dHCGyuosoN+ItpyYpXUTWSU3PMCnDjmMJOEJkdXN2JWcnZ/GaTyFpZBiihpnP0tXJhVycDVGRc2pn3q847TtEZBU3gZObja8dFYJYxk3LvB3FzfeBNuRZt5aMcnX9218o/UaCo4bOMk7Akw+Ta+lQSk9Kpqg5P2apL4gCiKwFQTnnYygZU7UqbiLrF1/OlJJY2zatqUmTgn3NvGLnKJFViWmNGzemF198UV0S45YlOo5Sx1FILb22+tChQ8RjhIeHG/twhqPatW/fnp544gny8vIyqTPfUWKmrSKrvcdMSEigBQsW0MaNG+XzTs2DX79eu3Zt4tdDs/w3Z84ccnFxkXIun/+NGzeIX9/Nom6zZs2IpTE9BQQEyMiXepnK2ztXliH5VfPbt2+XEQM5CiG/mr5Ro0bUokUL+uCDD+QrsCdNmqQOlWXLghbPmyXj5s2byznydUEqGAK2iqwszLFEzck8gurRo0dJv8ajR4+WazCnM+A1vmLFChlZmSO92pJYYrVFZHX0XG2ZG9pYJhAdHS2f2fzMevrpp6lDhw6WGxZwaV5F1j837KJvl2ykCqVK0mevZsrgfBrLNu2hOYvXU9mQQPritQEmZ2ZPPxZZU1NTaeTIkcTRyR9//HEpfvNnxI4dO+iee+6hwYMHmxwnux2OisvjJCUlISprdpBQDgIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIaAYisGoy8ZvNTZL0WHkcvP/GtXVN0dXOhOeuH29XXWqe8iqyuzk70Yeu7yMXcZBUHZkHjenIq7b4STVvDr1FUUoq16TisvqKfN1Ur4SPH+y88ihLzWRp02MRzMVBeRVYXIaiOb/mCuHYudC3pOn2ye3aWowd5BtCwu/qRr5u3rDsSdZK+O7owS7viXuAIkdX3/jbEf2xJGckpFPH+NFua2t0moOej5Fm/tux/ZdosSouKtnssax2DRz5LrqHBlCHEi4jxU6w1z5f6vIqs24RYNmv2PJvmVrFSBXr3LdteS2vTgIXQKEFEJuMIfyz7lRORKx2RlIypxsqLyPrmW+9QREQk3VW/Hr3w/Cg1ZJbtn38tob+WLJPnMX3aFPLxNjyrsjS0oaBO/Yay1fOjR9L/htgmudgwrE1NFDtHiawciZGFSY7KOHXqVGPUT30yHJ3Okoy6atUq+vHHH2VTjkLHMixHDeXXMrPAyYmjho4bN4440mR2KTciq73H5Eix/KpoXsuc+HzKlCkj1k6EFFX1uanomd9//z2tWbNGr8o2//nnn8tIfHoDe+fKr3hnUZXFWU5877m5uclosbzPr4Fn0TWniKzqNffXrl3jLjK99NJLUoRV+9jmLwFbRFaW3oYMGWKcyLBhw0ykaL6OXMb3IKennnqKHnoo+y+ssazNQjq351ed161b1zh2ThlbRFZHzzWn+aDOOoHff/+d/vrrL8Nn2vTpxEJ9UUh5FVm/+WsD/bVxN93XtA690Md0rR85c5He/OJX8nB3o58njTQ5XXv6scjKib8wwJHJWWpVqXTp0jRw4EDavXs37dy5k3r27Glyb6p2+nbGjBlSgOXPQ/4c4ec2EgiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAgGUCEFktc7GrND9F1gP/naMpL/9l17y406Qf+1LZyllfi2v3gLc6OkJkndymgdVpJKSm0RcHTtGlG4Z/tLfaIY8NulcrR/eWDZajzNh/kk7H3MjjiEWve15FVo60OqHVy/LEYpLj6MNdX5mcpI+QV4fd1ZdKegbK8ujkWJp54EeKSYkzaXc77DhCZPXrcC/53NPSJhwFIXxCZLXpUhgbsRD5+ptvGfdzytSqWZMWL/wtpyZFvu6XX3+nce9NkPPcummDQ2QZJWOqk8+LyNqhUxe6cPEitW7ViubOnqmGzLKdNn0GfT17jkPO43YSWU+cOEETJ06UXyjhV5dz5M4GDRrIVyqXKlUq29cjX7hwQYqhLFTWr1+fnn/+eZO2e/bsoelCrmIR78EHH7QYzVVdJFtF1rwckyXdvXv3SgmUhaSOHTvKqLE8v3379hGLqEpi4rbBwcF0+fJlKTDxl202b94s9zkiavXq1dXU5ZYjpbZpY/rlhLzMlbnt2rVL8uzduze1bdtWisAXxTrn6LfHjh2Tx81JZGUJlkVGPVm7Dnpb5PNOwBaR1fw68SvOa9WqZXJwft05r0VOHHWTo29ml5YsWUK//fYbVa1aVQrk2bUzL7dFZHX0XM3ngP3cERgzZoyMhM3rhddNUUl5FVmn/LCcNu09Rl3bNqJBj91vcloXIqJo1OT5suzHicPJ29PDWG9PPyWy8iBXrlyh/fv3ywjW/EWRuLg4+aUMruMvPbBwXq1aNd7NNm3atIlmzZol69977z2qXLlytm1RAQIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAJ3OgGIrA5cAfkpsu5Yd5K+eGuF3bMdN7cXVa4danf/7Do6UmTNEAdZfyFSRhHydnWhMj5eVME389XDcak3afz2w+I19twyfxNEVut8cxJZ3V3caFC93uL6lZEDxacm0NdCYr0qIrfejsnRImvq5Ui6ecn0ldw6twzxqtPYlev0IofnIbLmDqkust4rBLOQUiHZDlBGRPQaMex/2dYXhwqIrFmv0u0ksvLZrVy5Ur5WXEV9VGfs4uIixZ2uXbvKaKuqnLfz5s2j9evXS8GSo4eGhGS9D3744QdavXo1eYvot1999ZX8zNfHUHlbRVZ7j6kLeH369KHOnTurQxu3LLlyBD2WVlkkDQw0fDFDNfjss8+k1MoR+u6/31SuUm30rb1zZbF4wgSDOD506NAsgiyLwyytsdiYk8jK58GiG0ecVSk3ETpVH2ztJ2CLyHrkyBEZfVcdhaXyihUrql25HT9+PJ06dUrmW7ZsScOHW37zAovYHI2Vow+PGjVKSukmA+WwY4vI6si55jAVVNlAgAX85557jtJEdPsuXboQC+9FJeVVZB0/ayHtPX6OnnywBfXtbPoFgavRcTR4ouELKV+/+RyFBvkbT9uefrrIGhYWJj/T+EsLHNmYI1/zFzv4eV+7dm3jcXLKsAz78suGL/6ZR1fOqR/qQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQOBOJACR1YFXPT9F1qN7LtKHI+1/Jfsnfwyg4NJ+Djxbw1COFFmjk1OlqKpPsklIAPWrXYmcbhVO23uCzsUl6E1M8u7OznRTiBrp4k9ekj0iq7uLM6WkpeflsFn6OotXB7uKPyniH6cdnfIrIquzkzM9Xbs71Q40RChKSkum2QcXiGi6kdmegpuzeCVyRpqUhbJtpFXwK5VZpE1Nv6mVWs86iZXk6pz7ftZGdrTIGvPnSkrctd/aYfO1Pl9FVnFDOwkZIEPI6ZyCRz5LrqHBZC3SrJMQ3LkN5e32tsit2pTxstzeKKC6yPrT9/OF4NfI4nFul0KIrFmv5O0msvIZsrizbt06OnjwIHHkT369PcuQKvXt25c6deqkdknJdfzq5OxeXx4ZGWmMJPnJJ5/IKK/GAbSMrSKrvcdUUfJYzJ09e7YUlLTDG7Pbtm2ja9euSTHMWHgrk1uR1d65rlmzhr7//nsp0rJQaynxdfrmm29yFFm5H0tVLGVFRUVRs2bNZKRdS+OhLH8I2CKyspDMEVdVevPNN7NIcyryJrfhe5DvRUtJrQt+JfqHH34oIw5bamepzBaR1ZFztTQHlNlO4OrVq/TSSy/JDubPZttHyZ+WeRVZp/64nP7dwxFZG4uIrO1MJhkWcY1GT/5Oli14fyR5ergZ6+3pxyIry8AfffQRHT16VI5VtmxZ+cWM0NBQ8vT0lNGNmzRpku0XMYwTEBn+osGzzz4ri7p37078BwkEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQMAyAYislrnYVZqfImtyYioNbZ/965FzmnBQqC9NXTQwpyZ21+W3yMoTG35XNaoR4CvnuOjURdp46arJfKv4e1O7cqFUXkRvDfJ0F3JjBl1JTKb9V2Po7wsRJhFcPYRsym3vKulPwV7uxOJrTMpNupaUTIeiYmnv1Wh6pHJZeTxfN1d5nIvxiXRdSLYqLT17iSISkuVuYyHa3l0qiMqJY3P7hNQ0unAjkXZFXqft4lWXempXLoSqlfCltIx0+ul4GAV5uMt5lPf1pqjkFPrz9CXZ3NXZiTpWLEXVS/hROR9PYkE2NiWVeB7LzoXLrT6uvfn8ElmfqPYQNSvVQE6LRdNvDv9KZ2IvZJlmJb9y1LZscyrrU0pctxJ0U7S9mhRFB68dF5F5twlOQljUEour91doLRhWpDLeoYKLG8WlxNPlhEhadW6jiSjr7+5L3ap2kL0PXTshROBUahZanyqKY3q5esrIsCeiz9CKs+tzLcNqUzJmC1pkdRLiVYknugoZ1EXO4ca23ZRy6pxxPpxxLR1Kfg8YolZlJKVQ9MJlxnrXMqXIp2UT2YYFUhJC6c1rUZR6/iIlCIH2ZuRVsiSyuoYEkV+H++Q43C75mCEanBrY2c+X/B/uIP5hnyjx0DFK2ndYVcmtZ92a5NW8EbmXL0tOYv1nJCVRStgl8qhRVdZbElndKpQlnzYtyK1MKLkEBlCGEAJ4rsmHjtONjf9RhoMk74IWWc+cPSteV3tAnnfFCuWzRLbkiqPHjhtfF96sWVMqLV7pvmzZCuHxZlBDfs17qVD65591tGfPPgqPuEx16tSh+9u1o3p168hxzX/sP3CQtm3fTkePHBWyf7oUo5o3bUpNmjQ2acoCh36cypUryder83wvXQ4Xr4VPoy1b/6O/liyV/d4a+zr5+hie0VzQqWMHKXqYDGrDjqdYE3qyVyrmMTp06kIXhHjZulUrmjs7+8/OadNn0NezDZHctm7aQAEBAfoU6Pz5MNq0ZTMdOnyEoq5dp8qVKlHDhg3EObbPIrBYElkPC9YcTdPDw5Me6tSBeH/zli3y2vOxGjdsSF27dpavizc5cC53FDt+9XF+pmQRDXq7WENLly6VMqq7uzvNnDnTKIGOGDFCvnrZljlw3xkzZpCXV2bkdb2frSKrvcdUkl7JkiVl1FX92Lbmcyuy2jvX7777jv7++2+qUaMGvf322xand+jQISle5RSR1WJHFBYoAVtEVr7PBg8ebJyXeRRefkZzZMck8RnKKTtpkSN0shDLEXhZpGsnPh9yk9Q9wn3uvfdeGjRoUJbujpprloFRkGsChw8flrIydxw5ciS1aNEi12PkV4e8iqzfLtlIf27YRfc0rkUv9e1iMs3Dpy/S2C9/JS/xO8RP748wqbOnH4usLJ9OmjSJSonfuzj66noRaZy//KCnBuL3MBaHncXfJa0lvl9v3LhBrcTvJJxHAgEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQsEwAIqtlLnaV5qfIyhOaOW4V/bfmeK7n1qVvU+o5vHWu+9nSoSBE1mFCZK15S2RdcuYy/XMhM7Jn+wqh1LlSaeLIpZZSmJA/Z+w7IeVWX3dXeqFhdSrp6WGpqSzbIeTT5kJMzSnNPnSGjonXWPaoXl5KrNm13XMlmhYcPy+PzW0G16tCdW+97nLDxStC4gwml1vzDk9Ioo92HaMQLw8aUKeykDs9LQ6bJiLh8Zi7IqMt1uemMD9E1vYV2tCD4g8nFuW+P7qIjl43lR25rl25ltShYltx3Sz/4+/F+AiaefBHKbdy+2DPQHqqdjcpsPK+eWLp9fcTK4SIbBAny/uKV7c36G/eLMt+WNwlmitE2+S0lCx1uSkoaJGV5xb4TE/yqFZZTjMtJpauzphLGUJ45uQkZOigoc9I+ZP3k/YfpujfDdKhb7tW5HNfa2IZ1lLiKKkRE6ZaFFk9alWjwL5PyG7x6zYT/9GTW7kyVHLo07IoYcceil2yxljt3/kB8m7VzLhvKWMusvreezf5PHCPOB/L6yT1UgRFzflByK1plobLVVlBi6wXL12ibt2flGIDv2J96Z+LqEyZ0sY5x8XF0cPdHieOXsmi3ZJFv5OHEP6aNm8p23Ts0J6OiEhhYWGmkji/9nbSxAn0yMOZogcLGSxszvt2vkk0TXWwPn160WuvvGyUKRMSE43HeeP1MeTr7UOTPvpYzlX1yWm7ZtVyKl+uXE5NLNYpGVNVFrbIylFnP/x4slEWU/Pibcu776Ypkz+goKCSxmJLIuvkT6ZK7hyhtHfPHvTDTwuyXAMWib+c8RmVKOFvHCu3GcXOUSIrC5oc3Y9fV+/hkfUzk6PUseTDSX/t+TvvvENnhaTNwlxOUec4Ciqve16v2SVbRVZ7j8mRVr/44guyFpE1u/lxuRJZ+/XrRx07dsypqayzd65//vkn/fHHHxQcHExTp061eBwVedMWkTU6OlpGZK1SpUoWIdvi4Ch0GAFbRFY+mC4986vMR40aZZwDR0j++OOPjfsvvPCC+EJCE+O+yrB0/vnnn4tnSwkpa+d0v6k++tYWkZXbO2Ku+nGRt4+ALrKOHj1aRly2byTH98qryPrXxt30zV8bqFxIIH3+2gCTCRrrQoPo8zHPWK7LRT8WWfWk7jeWWnv16iU/ExcuXEinTp2Swvk999yjN7eYVyKr+b1ssTEKQQAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQOAOJgCR1YEXP79F1vMnrtI7Axbkasbevh704c9Pk3+g5YhnuRrMQuP8FllZUH2tSS0K9TaINN8cEVEMRaRVTvVFVNXn6laReX7R8T4RTfWSEFf9xCslWwoZ1e2W/LbyXAStOh9OvWtWMIqnyWnpdComXkRjTaVQIY9W8feRMuzWy9eohOhfWex734p2GSmir0bfEgT5YItF5J8aIrJq92qZotaJ6HgKT0yiUCHJ1gr0k3PiH8vPXqY1YQbxVhdZjQ1uZVhk/ViIrC82rkkVRHRXTlEiiibLsBxhtrYYs7KIPMuJo76+v+uI3MoCO384WmT958IWwaSTnA1HjPz1xDLae8U0IidX1gmqTv1rP25sd+DqMQpPuEJ+bj4ykqubs0Fs+jtsE60N20JO4j+WUsv5lpJ9rifHiDVwVEZSrRlQRURZLSvLE1KTaMqe2ZRwM1FE580qsrLsGpFwVVxXLwrwyBTGVp7bQBsubpNj2PvD0SJrwpYdlLj/SLbTuXnlKjl5e1HIiGfJ6ZaYnSCissYu+1v2YVnVVwignKTk+sU3IvppMnFE1IDej8lyWRcdS6lhF0mYXCIqqhCahGyXHyKrZ71aFNCrm+lxL4XLiLLuVSrK43KlLrJ61KoupFnDOhHmHyWJCK83wyOJo756NWkg+hjWSfw/myh+/Rbj2PZmClpk5Xn+/sdCevvd8XLK97ZtS1/P/MI4/bHvvEsLFy6W+19/9TndK0QJXTBVDVkGrCsisEaJKLWnz5yRxU7iublEiLHVqhqej+Mnvk8///yrrKsgor+2bdtGSPTOtGnzFjp77pwsf+SRrvTxBwYxUT+Oj49PFoG1Vs2alJScROfOnZd9GzduTB4iuqZK5oKnKre2VTKmaleYIitLvyyhcqpbp7aIyNZORg7dsWMXbdi4UZazLPzxhx/IPP/ISWQ1NhKZSpUqUqnQUiLK62Ej2y4PdaIpn2SKaXp7W/KKnaNEVo68mJKSQizdtW/fPssUOLKciio3YcIEcU6VZJvZs2fTv//+S/waZhZcsxPnOKIkj59dNFYezFaR1d5jhoeHE7+enVOPHj3okUcekXn9B0e03Lp1K50+fVpGvTSPvPf111/T5s2bZV8ew1qyd667d++madOmyeFZaGQZSk8sq7N0zK95tyayfvvtt8TSa4Z4rrIk/9Zbb8mtPh7y+UfAVpH1p59+opUrV8qJsGzN17d69erGSJF8f3Dy8/OTkipHODZP7777Lp0Rnws9e/akhx9+2Lza6r6tIqsj5mp1MmhglcC1a9foxRdflO2eeeYZevDBB632KagGeRVZL0ZG0ciP58vpfvn6QCoTHGCc+jszf6cDJ8Oo231NacAj9xrLOWNPP3ORddmyZfTLL79Qnz59qHPnznL8ffv20ZQpUyRjZp1TSk1Npeeee0426dq1q5Rhc2qPOhAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARC4kwlAZHXQ1XeNjqIzW4/R2xN35mrETxcPpMCQzFcyW+u85rd99OM0g0RjrS3Xj5jYmZrfX92Wpna1yU+RlSXWhyqVog4VDAJjshBf3tthEDhdRMTJ15rWohAh8bHEOu/wGfFK+ljjOVTw8xLRV2tIOZX7vbHlII27uy75u7vJNt8eOSvEV4MQywWlhCj7cJWydDb2hpAnI6Wkeq+ImMppxv6TdDrmhszzDy8huI5tVpt8bol0i05dpI2XrhrrW5UuST1rlJf7SeLYE3ccpRsiyqW5yHpSiLS7RWTV8IREERE0Q8iXXtRHyLacWLLlyK8s3HJiFs/UrkQNgkvI/b/OXKJ1F67IvL0/HCmy8hxYimGBjtOSM3/Tlsu7ZV7/4eLkQi80flZGWGXZlSO2HokyyBjcrhwLqHc9LcfhKKnjt02nxiH1qEeNLnKYM7FhNP/IH8YIqny8p2p2E1JzTVm//Ox6+vfSdhORlee16fJOYjE2JS1VirGPVetILUo1lH1iU+JFNNyZMoKsLLDjh6NFVmtTiPlzJSXu2k9ejepRice7GpqL84ya+xOli1cSl/zfM4aIq1z27c+UciZMSqPBoweTS4BB4mUxNGbhMimu8gAsxPrd35a8GteniEnTHRaRlSO/Bo9+jlwCDdJB4u79FPvXasoQgpg8rngVbPCIgWJeJYwiK0dgDR4l+pQM5IVF139aRMnHMteJa9lSFDxURNwV1z8jOYUi3jcIXnJAO384UmQNCAwkJRZams7nn02nekI+5fS/4aOMYiRLkSxHbhJi3OChw2V979496d23xsq8Lph6eXnS86NGCrnuKXIVjDnN/+57EUH0E5l/4onuNHH8OCm3PvrYE8TiYJvWrWn6tCnkI+RXTvxa6pdeGUPr1m+Q99zvv/4spU39ONwuJDiE+vXrTQ3Fa3Tdhexcp04d+vOvpTTuvQlcTVs3baCAgEypRBba8cOcmSNEVhYpg4Mzo6aaTytWRDPm8+WkziMq6hp16vIoxcfHU/fHutF74981MuZ2n06bQbPmzOEsLfz9V6pTu5bMWxNZOfLqBDFWVREFkxNHPO0/4Dk6c/ascMmdadWKZVROCKD2JMXOUSKrirDI0Vj51cl8zVViaZJfdb9+/Xop0XFUUhbtOJ0TYvS4cePkertbRK1laY9FO07cj4XQnTt3StmVRdbp06eTr6/l34FsFVnzcsxZs2bJ10Xz/Fn269Spk/E10cePHxcC+M9SqOXPGp6r+TpnuYklp9KlS9PLL78sX0PNr1o/ceIE7dixQ54rS6cDBgyQDOydK4tQLDKyfOvp6Sml2jZt2khR+JKI7sxyKkfJ5ZSTyMqRWJ9//nn5eS0bix+PPfYYPf74rS8NqEJsHUaA1/xff/1FHGWbE68NlVgAZwGVI+2yCM1bla5fvy7vPX52c2Lpu169enTx4kUpLKt22UmqR44coQ8++ECuF167OUnjaize8prme48l7itXrhCvGU58n5YpU0bm+ZX1fK+olNe5qnGwzRsB/n138ODB8ksCRe2+zqvIymTGfvkrHRZfJmxcqxK9Pai7/L1ly/4TNPm7pRIcR2rliK3mKbf9zEXWjeLLK3PEZz4/c4cOHSqH54isixcvlpHHc4o+zo35PuLPB04DBw4UX4y5X+bxAwRAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAICsBiKxZmdhU4pyYQB5hZ8nz/GlyP3eKXGNjKDKgPPWdkyk82jJQbkVWHnPd4oP03SccTSv7I/j4e9KAMffnq8TKR3ekyJouTmj5uXByFZEC/dxdRNRTP2MkVj7WkjOX6Z8LkZyl6iIi6ogG1WR+R0QU/XQ8TOb1HxytlaO2chq37TCNEeKrirI6Zc9xuiCit2aXONpqdiKrHgn2ohjjEzGWeRotJNoqtyKofnf0nIysqousfC7rxLnol3CkOJ9q4ry47H0hv14TETT1VE6Irq+IiK2ctoZfExFPL+jVuc47WmTVJzDn0M9CxjVEa9TLq/pXoMH1+8ii3ZEH6beTy/VqmX+6dneqG1RD5j/Y+aWIpPuIYFlBcMmgT3bPFpFqDVKF6ljWJ5RGNRwgd3dE7KOFp1aZiKw7I/bTH6cMUc1UHzdnNxrbfAR5uLjLosm7Z2UZV7W1ZVvQImusEFkThMjKKaBPd/KsY+CVdu06pQtZzK1UiKy7sXk7xa1aL/PulcpT0HNPyXy6kKevTJ1JGVqkYVkhfjgJ2ZvLA3o+Sp71a8viK9NmUVpUNHnUqiaipD4hy+LXbSb+oye3cmWo5NCnZVHCjj0Uu2QNuVcWx33WcNy02Hi6+qk47i1BW/UNHvksuYYGG0VW98oVRB/DOkncc5BiFmVdJ4FPdSeP2obzjpz8JaXHxavh7No6UmS1NoGfvv+WOIoppytXr9Cj3Z6g6JgYYgH21wU/0DMDnxOiUjhVqVyZ/vjtFyEgecq2umA65tWXaKCFKGCPPPa4kJBOSXlw+9ZNpKKxsoi3bMliOaYc7NaPi0KC69S5qxAP06W0OWnieyaRX1l+/WTyhxQgXk2tp19+/b1YiKz6nK3llcg6/bPPaeas2fJ6/L1quVH8Vf1vJCRQy9b3SDHzvXHvUI8nDfdETiIrS2B8PZTsr8ZatXoNvfDSK3L39ddepWee7qeqcrV1tMj66quvUkREhJwDS541atSQUVcjIyOJX1/NsianZ599ltq1ayfz6seSJUvot99+k7sswlasWFHKoSz0sZCpEkt8LL0qCXb//v00d+5cKWFxGxZf1XE4MrBK3P6VV16hyuL+UMneYyYKifm9996TciCPxZIovz46RtyPSuDja8aiZ7dumVGl1XHDwsKII9KyFM4pJCSEoqKipMir2rC0xPKSSvbOlUVVfp08c+HE0WFZglTHZi5KevT396fhw4eLaM111WHlNjY2Voqsqh0X8quyOUogUv4QYAGORThrSY/2qNquWrWKONopC4qWUk0RHZvvBV635onXCr8SPTcRIHXhznw8fZ/XHZ+XnvIyV30c5PNGgCMs8xca6tevb4w4nbcRHdNbiayr/ztAC//ZQTfE33FYlk4Qb5/g5CW+1MRf6PAUv4M+3aUt3dvE8PunfnSOujphziJKvZlG5cWbN0IC/GQk1pvi95cHm9ejkb066s2N+dz2MxdZWULnLxLwZwI/493EF3r4CwT8+cbPf/4iQ06Jv9QwY8YM2eSNN94w+WJITv1QBwIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAJ3IgGIrDZedSchDrhfCiMPIa16sLx6RQgeZv+wXFAiK0854kIMcXTWnetOUvS1THk2uIw/tXigOnXu04T8Ar1sPDv7mzlSZM1uFnEimulvQto8cC0zgmprEfW0x62opzHJqXT4emY0VjVO45AA8hRiB6fP95+SYqqKaMqRUv8Lj6JDIorr+fgEEanTEB1S9c1JZG1XLoS6VS0rm+pyrerL23tENNfHhQzLafnZy7RGRHnVRdZ3hVgbayYRjtcixrKoap5chUzTXPzDLacT0fH05YFT5k1ytZ+fIuuN1ATB/DuKTja9LhwFtXs1QxQvjoR67HrWc7gruLa4bh7yXGYdXCCi1D4ixGZDxD4WVc0TR3ltElpfFrM8yxJteY7s2kBE7BRpW8ReWnxqtczrP3RhNjvxVm+fU97RImvyidOUcjqrCKzmkHT4GKVdN9wPzj7eInrps+R8K8qmapMaEUlRM78zSqPeTRuQf7eHZLUuuKr25ltHiaz6ceM3bKX4tf+aH4rMRVbvZg3J/1HDOmH5NflE1nXiVa+2jCLLg0XN+4lSzuZN7HakyNqrVw+qWKFClvNUBY8+3NUk8t6yFSvplVdfk9UsJLGYxpFEF/z4HdUXEfhU0kXWN14fQ/379VVVxq0eLXTHf5tp5OgXadv27SKSX136/ZcFxnZ6pm//AbR79x5q1LgRLfh+vonImt1xiovIWqFCeerdq6d+uib59Rs2isiZO2WZEllHjBpN/6zbQDWFvPnKyy+atFc7r455nWKEFDjo2YH08ksvyOKcRFZ/fz/atmWT6m7csqjZpHlLKfQ80/9pen2MQWo1NrAx42iR9cMPP5TCKr96nuUdXXzkKbHMwxEkW7ZsaXGGLND98MMPUvbRG7Bs2UBE9uXIdk2aNJHrXNXzq5o5uqsuu6o6fcsS3ZgxY4glPj3Zc0zuz2IoR8zk17grKZTLWRRt2LAhdenShWrVqsVFFtOpU6ek1MdikxIO+T7m82Q+fJ48lp7snSuLsxxFliO7qsSRNh944AGqWrWqUZZizvyKcZ6DeeIogsuXL5eSMAvKL7zwQrZRcc37Yj/3BLaL5+/8+fONEVktjcD32ahRo+Q1NK9nCW7evHl040bm7/p8ffkeGjBggMk9pPry+nj77belJD516lQKFF+SsCWxWMhyOfdXa9m8H382cbRlFZlSr7dnrnp/5PNOgNfa2rVr5bX/8ssvbY7Em/cj5zyCEllXbtlHsxb9Y/5XaGNnVyGzDu7+AHVseZexTM/sPHyapvy4nJLE3/048RcN7mtSh0b17ijfXqG31fO56WcusvI4/HxnqZwjKvNnRmXxRQqWz6tXr64fxmKer8N///0nv2D06aefyi8gWGyIQhAAARAAARAAARAAARAAARAAARAAARCLi5pgAABAAElEQVQAARAAARAAARAAARAAAYLImt0iEJIqy6ocbVVGXRUSK8usOaWCFFn1eURFxFNiQgr5+HlQQHBm1DK9TX7lC0JkXRMWIWTQcJNTeLRqGbq/XKhJWU47cw6fIQ8hkjxdu1KWZhwJ9qAQWpcK4fRKoiHKXE4i6xPVy1HbMsFynO9FtNXdV0wjhHIFC7MD61SWbVT01JxEVg/xD7cftrb8j7ZyELMfYSIS7FQLkWDNmuW460iRlaOlbg/fS81KNSAWSzldjA+nmQd+pJsZacZ5dKl8v5B8mxv3rWUWHF8iRVZr7VT9xfgIIdDOt0lkfaLaQ3K+3Hfe4d+EHHxGDZPrraNF1hgRcTXxVsRVWybjWbcmBfR+LLOpWNNXv/yGbkZcNZb5dbqffNoY2OsRXY0NzDKOEln9Ot5HPm3vlqNzZFWOsGqezEVWv07txFxbmDfLdv/6j39Q8rGssmu2HSxUOFJk/UnIoI2FFJqb9NLLr9KKVZnC9fOjR9L/hgw2GcIWkXXBgl/ovfcnyX4cgXXwkGF06fJlav/gAzRj+qcm46mdV4SUuWz5CmKZatOGf24rkbV1q1Y0d/ZMdapZttOmz6CvZxsiCyqR9eFHH6NTp217Hjz6yMP00Qfvy3HtEVm5Y+t776frIopnl4c60ZRPPs4yR1sKHC2yssjGEVJZkGSJ9ezZs8SvD+fIqCyx8uvrWaazlhJE9FoWgFJSUmS/oKAgm/pZGzenenuPyeLe1atXZSRaPz8RET40NFcSGEvJHMWWGbE4aC6vWpqzvXPlCIEstfJxOBqgeaRfS8fSy1jE4mNz5Fakok+A1+Zl8Rzn+5Kf0xzNmKNBZpf+/fdfmj17NrVv35769zd8qSe7to4uz+1cHX38O308foZxRG1+brMc3by57b9z5yc7JbI64hgcgfX4ucsUn5hEtSqVoRK+3jYNa2s/SyKrfgBe47Y+c/mLGSNGjJBfknjsscdkdG99LORBAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARMCUBk1Xi4xsbcEleF9Bh2hpwTE7Ra69lLPmXpmfnZv67e0gifLh5IgSGGaJOW6ot6mSNF1ngReXXSzqPylPvVqiheL28QLFg0nXXotIjeGW/E0aN6eWpdpqTc58imZ2NzvlbLz4VTREIScTTVDhVKkbdbVgEnTRxn/pFzMvJrTiJr75oV6O5bkVG/OXKW9l81RMY0Tk5kmpUKpL41K8qijZeu0qJTF3OMyOrn7krv3Z0ZedHSmPr4l8W5rBTnlJfkSJGVo6t+sPNLalW6CT1atb1xWjsj99MfJ1ca9x+r1lGwMwh+caLP+fjLxjpLmS2XdtLg+oZXzHP9oagTlpoZy8JvRNLfYZttEll7i0ivDYPryL4/HF1kdWzjQSxkCltkdS4hpKuXhxlnliGiql2Z+jWlx8YZy/wf6UDezRvL/eg/llLSvsPGOksZqyLr+s0U/89mk65u5cpQyaFPy7KEHXsodska8n+koziu4ZpH//InJR06ZtKHd8xFVr1PWlw8pYZdytJHL4hfu5FuXonSi3KdL2yRdauI1vXsoKHGef/2y08m0Vi5whaR9eeff6XxEw1i5fKlf9KTPftIYe2xbo/SB+9PMI6vZ8a+8y4tXLhYvp56z85tNh2nuERktUdkbdXmXooWr5ZnYaxxw6wRLXV2d7dsQf2eekoW2SuythEiK7+Ovmvnh+iTyR/pw9ucd7TIavOB0RAEQKDIEWBR+ejRozJqMUcwRrqzCMydO5c2bNhA9evXl9Gri8LZO1Jkze/zsSay5ub4HKV4xowZMnLytGnT8OWB3MBDWxAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAgTuSwB0tsjonJ5H7+TMi4qoQV8+fJtfovIlQ591L0XM/peRqIUFkdaLJbQyiULR4TeT47Qa5zstVvBa3cQ0K8TREnEq4mUbT9p4wRkx9oHwoPVKljGTNkijLorYmN2cnqlrCl6r6+1CdID+qoEXyORgVS3MPnSFdZP18/yk6FZMp0XasWIo6VyotD/eHOPYmC8e+u3QQ9a5RQbb5OyySlp29nKPI6iRackRWdxGZlc917NasUSttPT9b2zlSZI1JjqMPd30lD92zRldqHJIp5S4+tZq2ReyVdfeVu5seqnSfzC85s5a2XN6V43SdyInGt3yB3JzdKPFmEr23/bMc26vK8r6laUQDQxQyPjbPwTz1r/24uP6GV4LO2PctXRISrL2psEXWoIG9yb2KQZxW55B88gxd/+43tSuiorYgv47t5H783xsofuM2Y52ljDWR9ca/2yhuzQaTrpZEVt92rcn3gbayXeyS1ZSww7AW9I7mIqs+19jlaynhv5zXiT6WvfnCFFk5kuOTPXvTyVOnjdPn19r//usCcnNzM5bZIrJ+/uVX9MWXM2W0MJZSn+zVh06ePEVtxWuoZ3/9pXEsPTNy9PO09p/1VKFCeVq9YlnuRdZ/11OAja+u1o9rnlcypipPSs7d56nqx9sOnbrQhYsXyR6Rla/FocNHqE3r1jRnluG5po+dXd4ekZWj5jVs0kxEz0unwYOeo5deGJ3d8DmWK3bnz5/PsR0qQQAEQAAEbm8CV65ckQIrf75MmjRJRtAu7DO+U0XWCRMm0IkTJ6hjx47Ur1+/wr4MOD4IgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIFHkCd5TI6pSeRm4Xwwziathpco8Q0SBF5EJHJYisuSfpKqRSSyIrj1TGx5Oeb1iDPITcyYkjqk7bd5KShOh5V8kS9GzdyrKcJVOWTe1NdQL9aEj9qrJ7anoGvbn1gJBky9K9ZYNl2VcHTtHx6EyRtWloAPWrVUnWHbgWI15Lf1bm9R8cUbZpaKAs+vlEGG0Lj8pRZOWGrzapRWXFOXP6QpzPSU2elYUO/pFfIqubsysNa9CPyniHyhmnZaTRrIML6HzcJaoXVIP61e4uy8/Ehslya6c1utEA41izxTinRT9ryZrI6uXqSa83HSbEYYMkOG7bdEpOS7Y2bLb1hSmy+rRuTn4P3W+Ym4gqLAxG4zx1cdSjdnUKfOpxWZd6KYKuzZxvbGcpY0lkda9emYL695TNE3ftp5g/M6PtcqElkdWrcX0q0b2L7JN04AhF/7ZE5vUf5iKrZ50aFNDHsE5SzoZR1LwFevN8yRemyPrBhx/Tdz/8KOXTB+6/T0qlfJLmYqMtIuvgocNp0+bNFCpe/75h3d80YtRo+mfdBipVKpTWrl6Z5bXu/Irch7o+QufPhxnFTVuOo0dk/Xf9WgoONjwv83JxlIypxigskfWVMa/TsuUrZETWtatX5PgKbzVX3tojsu7bt5969zVEMX5v3DvU48kn9CFtzit2EFltRoaGIAACIHDbEli6dCn9+uuv9MADD9CAAQMK/TzvRJE1LCyMxo4dK74kVIHGjRtn8sWkQr8gmAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIFFECt73IOv/3HuRz6Sx5srh64Tw5pdof4c3aNYTIao1Q1vqcRFZu3TC4BA2oU9nY8TBHTBXiqL+7K41tVoe4P6fsIqPWDPClByuUklFWhzeoRmvCIujQtVjjeJxxFuLfh63ri6ifzpQmpK7Xthyg9uVLicihpWS7xacv0YaLV4x9gr086I2mtWQ/lsA+3XeCwuISjfWlvT3plSY1yUWMK7RC+mT3cRHtM9GqyPpk9XLUpoxBBgsX0u6XQqCNS7lpHJczHKn2IRENNvxGEm0Nv2ZSl9ud/BJZeR5BngE0qsEz5OlqiKgbmyJk433zyVkwfqXJEHJ1cpHT/fP0GvovfE+WqVcrUYnalWtJ84/8Tl2rPEAtSzeWbSISrtKcQ79QfOoNkz6eLh7UvmJbihT12yP2kS6y8v6iU6uM7Z3EdXmsakdqUaqhLGMxlgXZvKTCElldS4VQyf/1JycXwVOsxai5P5F71YoiAuo98nQyUlLp6hffUNr1aHIu4UchLw4lJ3ENOMUsXkGJuw/IvPrhWacmeTVrSNe//40siayuJQMp+PnBsnladCxdnT6LMkQkSU7O/n5CWO1MHtUqy/2EHXsodskaIbeWppJD+8uyDPG642tfzaebVzLXrmeDuhTw5MOGehE9LGL8FDlWyItDDOclamKXrqGE7VnXCZ+rzz0tKfqHP8Q80uQY9v4oLJF1y9atNGjIMHH5Mujpvk+JKGqvUE8RRfXI0WNCOnWmH+Z/S40aGdaqNcH00KHDMgIrM+jduye9+9ZYmjPvG5oydZrEMm3qJ9SpYwcTRBs3baKh/xshy4YNHUKjR42wKSLripWr6aVXXpX9/vjtF6pbp7bJuPbsKBlT9S0skfWnn3+mCRM/kNMYNXI4Df/fUDUlky1Huk0XX4apWbOGLM+tyMrXnKPhsmjMr/9etXwplS5t+NwxOZANO4odRFYbYKEJCIAACNwBBFavXi1/t+jUqVOhn+2dKLJeunSJ1qxZQ926daOAgIBCvwaYAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAgUBwK3vci6ulccOaWZyoD5dWEKQmTdtOwIHdx+3uZTaNmxFjVqU9nm9rltOPHwhdx2MWlvTWTlxl0rl6H2FQzRPXl/bVgkLT17mbpULk0dhKSq0iEhuR4UEVLjU29SSU8PKcFW8feR1W9uPUiTWtWX+TOxCbQzMopYFvUUAmDrMiVFpFB/WXcuLoGm7T1BzUoFUt+aFWVZkhDkVp2LoBQh7FUt4SNl2HblQ6llqSBZnyzKV5wLp8sJiRTq5UldhGjKwimn3Vei6fuj52R+cL0qVPfWcd7ddphihWSoJ18h577RtDZ53+qbkJpGGy5dkZFoWbatXsKXGoUEyPr1Qqz9Uwi2eUn5KbLyvGoHVqP+dR4nJ/EfpzOxF4RQ/As9UKE1PVC+lSzjH0eiTtKR6yfFdUugII8AEW23FlXyLyfrx22bJmRlV3q58SDB1BCtNiE1iTaH7xTS6jUhEzuLa1KBGpSsLev/vbSDlp9dZyKypmek02FxDI4Ay9FiawVWpSr+FYzHn3nwRzoXe9G4b0/G0SIri543IzLlafM5pYtX0cct/ZuChMTqJmRWTglbd1HsirVSVC05bAC5ljJI0SnnLoiIpj8J0ZXIv8uD5N2yqXG4hF37iCOeOvt4SwHVo0ZVyhD3T8SEqRZFVichVoa++QI5ubnKMbhv8onT5BpSkjiKqpOHR+bYt0RWLig5uB+5VSgr69LFfZIojpshBECPqpVEueFacyXLqCyycvJ9sC353tda5vlH8rGTlHz0JKXFJ5BLUAB51qtF7hUNfSMmfkos7eYlOVJkHTl8GFWpWiXb6QSU8JevvI+OiaFujz1BkeI1wBXKl6M/Fy0kL/EMYSG111N95evmK1WqSIt+/02W6yIry63/GzKI6terR7zG//tvB70/6QOKiY2VEURXr1wmo7ImJiZRx4e60tVrV8lfyMaTP/qA7mnbVs5t+44d9OIrr9H1qCgKKFGCVq1YJtvox3nj9THUv1/fLOdy8NAh6tHrKVneqWN7Gvf2W+Th6UW7du+ipk2ayvlm6WSlQMmYqllhiaw3hXDd/cmexKIqi+8sBT874BkqV7asFFcPHT5CCxctoj8WLqZ33x5LTz5hiHSck8jqIj5rhg4eRJ0f6iSj454Pu0Bz5s6llavWyNPt91QfGvvm6+rUc71V7CCy5hodOoAACIAACOQzgTtRZM1npBgeBEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABG5LAhBZHXhZC0Jk/X7KBlq7cL/Ns+45vDV16Zsprtnc0caGBSGyssTJEmjtQD/jrFgOPSgiq/7vLpYSDbKqsdJCRhdZLVTLonQRHe/z/Sw8JkgR9c1mtcn3lrCn95l96AxdjE+k0Q2ri8ij7nqVSZ5F1en7TlJUUoostyaycqPGQlTtU7OCjA5rMpjZTnEQWXnK7Su0ERFx2xhnv/XybiH9rqfn6vYyyqrGSgsZFlmT01KoQXAderJ6ZymiWmhmLLIkshorLWRUewtVuSpytMhq7eAsfCZs200+rZvLpmlR0SLy6jwpoXKBW/kyUh4VFp6sj1u5jm5s2UFOQvAuOeRpcg02SNiy0uxHTiIrN/V9QAim7TIFU7Puxl0VkZUL3KtXocB+TxijwRobmWV0kdVJCN2BA3obZVWzpia7RU1kNZmchZ1aNWvS4oW/0YsvvyJFRpYl58+bQ82bNzO2nvzJVJr37Xy5ryRHXTA1NjTLcBTXt8WrbHv1fNJYw9FTX3vjTUpNNci+Pj4+MjpyXFycbMN9JojX3nbv3k3u68fJTmTlSKQ9ROTYw0eOGo+jMmtWLafy5cqpXZu3SsZUHQpLZOXj7927j4aNep6ir19X05GCMEuuaeL+U2nC+HdtEllVe0vbOrVr0dxZMykwKPv70lI/vUyxg8iqU0EeBEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABECguBCAyOrAKwWRNfcwXZyd6AMRKdVNvO48MjGZPtiZVYriUTlK6YuNa1CwEPE4cdRVjmrKiSOqdhARW/3d3eS++iECUFKYiN64M+I6bb58TbZrVboklfUxRPZU7XjLYupiEeH0ZEy8sbiaiIDap2Z5Gd1VFfKYMw+eouPX46Xs2qtGeRENtISM2qe34eiwPx8Poxtinio9U6cSNQoOoDQhzL6z7RBxxFVLKdjLg56oVo5qBPiSyy0RUbVLvJlG+0XU2Q0iIuvlG0mq2K5tXiOyuji50Lt3Py/l0iuJUTR1z5ws82BBb0CdJ6hmQFVj3eTdX9P15FgR0bYx3V++Jfm5+xrrOJMh/rsYH057rhyireF75GtRuTzIM4C6Ve1A1UpUFFwMEW+5nFPSzWQhNh8T13mniLR71SQiK9enpKWSu0vm+ohNiae/wzbRjgjbpXAeJ7vkCJHV975WIgrpPdkdwrRcrCGOaOokojwKQBT1zc8ysqreyL+ziL7ayiCxp99IoMiPPpfVLIj63t+WvO9uTE7ideZ6YiE2YedeurFpO5V47CHyatJAjh85ZSalxxqkR+7v17UDeTcVdVpKjYiU/QIe70os0N7YvJ3iVq03tnCvXpkCejxKziLiqDGJuXNUWPeK5ck1NJjSRfTQyA8+M1ZzMF/v5o3JR7Bx8TNdJ3zeqZfCKXGvuJe275HzzOyY+1xeI7IuWbKMxghZ1JbE4uKwYf+j0c+/KJv36dOL3hlr2jcpKYke7f4EhYnInXwf/fzTD1S9ejVq2ryl7MNjhEdekdFU1TE5euvbY9+gNq2zisYHDh6kV197g86dM43ozcLpRx+8T02aNFbDEEdxbdqipbz3xr75GvV76iljnZ45feYMvf7GWDpw8JCxmCOPrlm5nMqUKW0sszWjZEzVPi8ia8fOXSW7tm3a0Oyvv1RDZtl+NuML+urrWbL8v83/UgkRLVclllg/nDyFVotXAzMTPYWGhFCXzp1pwDP9RIRVQ2Tweg0ay4itL74wioYMGiSb60IyX5fNW7YYh/H28qIOHdqLqK5v2RXB1jiQyCh2EFl1KsiDAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAgUFwK3vci6qmccOadnyoT5eWEgsuYnXetje7u5UCkhgaYJ29RVCLIRCUlCJM0qi3oKES9YRFJlSZRF2muJKcTRU1lSNU8cDZajrnJbDxG18IqQbVkm1ZO7KGc5tqSQbK8np9AlIZgmmbXR29ua52MHe7nL47LIG5siXvmekCxeI25ppraOmtkuryJr5kh5y3m7elGIV5C4buniurkIofmakHwTsx3U2cmZgjwCyMPVXQjOnhQnpNQI0SdD41LetzSNaNBfjrEtYi8tPfOP6FNCRNj1pqjkGIoWIq0jkyNEVkfOx9axXAL8RZRWTyGXivsmOpbSrsfY2pW4r2upUCHTOlPKxcuUHmMQXXMcQKxp19Ih5BpSkjJEpOJU7ickW1sSC7AuHEn2lsB782oUpSdkv05sGVNvk1eRVR8rv/LmkVKf6tObLl26TFevXKGqVYQoHBho9dBXrl6hI0ePUbqIKlqndm2jhGm1Yw4NIiIiKDw8gtw9PKhK5UrkKdaUPUnJmKpvXkRWNYYjtvxsuXw5nC5cvCBF1TKlyxBLw7YkJbL6+/vRti2bKDomhi5cuECurm5UQ4jJLP46Iil2EFkdQRNjgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIFDSB215kXdkjllwyTMVDR0O+6eJKMSXK0FGPcjRuxrFcDf/p4oEUGGIWaTCHEb6fsoHWLrQ9imTP4a2pS19DVMYchrW7auLhC3b3RcfCJVBURNb8oGAusi4+tTo/DmMcs7iKrMYTQIaKo8jav1/f2+rKKRlTnVRREVnVfOzZmous9oxhSx/FDiKrLbTQBgRAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAoKgRuO1F1lVPRpEzv5/agSlDjBfjF0w3SlektGo1yLlGVXJydaW9m8/QtDFLc3UkiKy5woXGDiQAkdVxMCGyOo5lYY0EkbWwyGceV8mYqgQiqyJhfavYQWS1zgotQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEih4BiKw2XpMbnv4UF1qBkitXI+faQl718cnSEyJrFiQoKMIEILI67uJAZHUcy8IaCSJrYZHPPK6SMVUJRFZFwvpWsYPIap0VWoAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACBQ9AhBZs7kmya6eFBtcjhIrVCGqVZNcQoKzaZlZDJE1kwVyRZ/A7SyyBnmWoKdrPy5iJzvRrsiD9O+l7fl6QSCy5iveAhm8OIisycnJ1LN3X8oQ/w0dPIi6dulcIGwK6iBKxlTHux1E1vnffU9/LFpMfn5+9ON336pTc/hWsYPI6nC0GBAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQKAACEBkvQU53SmdYlxS6KoHUUJoCHnVb0he5RuTs2egzZcBIqvNqNCwCBC4nUXWgsYLkbWgiTv+eMVBZHX8WRetEZWMqWZ1O4is6lzye6vYQWTNb9IYHwRAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAID8I3LEia4agGSfE1etuSXTdNUlIrMmU7sSlmcnJmcgjIJh8S9cg7/INyaNUfXISkVqzSxBZsyOD8qJIACKr464KRFbHsSyskSCyFhb5zOMqGVOVQGRVJKxvFTuIrNZZoQUIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgAAIgEDRI3BHiayJzjeluBolxNVolyRKdU636Yq4umZQCb9kKhmYRkEVq1GKXyNK8W1MKR5ViNh2vZUgsioS2BYHAhBZHXeVILI6jmVhjQSRtbDIZx5XyZiqBCKrImF9q9hBZLXOCi1AAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAASKHoHbXmT94amLFC3EVY66yiKrLclJRGb190mhwBIpFOSfTH6+qeRsFq2Vx0l3LUHJPo0oSfxhsXXn9hs0bcxSWw5hbPPp4oEUGOJr3LeW+X7KBlq7cL+1Zsb6nsNbU5e+TY37js5MPHzB0UNivAIiAJHVcaAhsjqOZWGNBJG1sMhnHlfJmKoEIqsiYX2r2EFktc4KLUAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABEAABIoegdteZH1ziJA+0zOskvf2vCnE1SQK9BcCq5BXXV2s9zEf9EJKSxo4MtC8OMd9iKw54kFlPhKAyOo4uBBZHceysEaCyFpY5DOPq2RMVQKRVZGwvlXsILJaZ4UWIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACIAACRY/AHSuyurulUYCQVoOkvJpMnu7peb46kal3Ud8R5XM1DkTWXOFCYwcSgMjqOJgQWR3HsrBGgshaWOQzj6tkTFUCkVWRsL5V7CCyWmeFFiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAiAAAkWPwB0jsrq4pFMJX0O01aCAFPL1SnX41YDI6nCkGDAfCUBkdRxciKyOY1lYI0FkLSzymcdVMqYqgciqSFjfKnYQWa2zQgsQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAAEQAIGiR+C2F1lnvvMfBfndENFXU8nJKSNfr8CdKLJOPX6JEm7mPZptvl4YDJ6FgLerM3k6r6OEtKQsdSjIHQFvF08atPb/7N0HmFXluTfuFxCkqNgrGnvvilEjxpiiUfNZsRMl9gJq7L1r7LEbNXaJFVskfz3xiJpY0Ch2QUXsigI2qgL/ed5z1pzNsAf2zOzBYbjf65JZe62137XWvfb1fdd58lvPSqnNeJYNk2s5e0/p2DFtcPUFadSoUenlV15NK620Uss5udnkTIYMGZLWWnONNP/88+crdi8qv/GldoMHD678i/YkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLQQgVYfZL3j6nfT/G3fqQmxNn/Y8oNvVk77HLVMg27tJff3TvMtNFfF37n1oifSY/1fqXj/nQ/eOG21x3oV79/QHe//eFR67euxDf2a/X9kgdW7dk7t276S3vzm3R/5TGb9w68yz3Jpi5dTmjKE5ax6N9ustFw66v+7P/Xv3z/tsssu6eZbbp1VL2WWPe+9ft8r3XnnnWmHHXao+f+v26R777039ezZM9162+2z7DXNrBPvtece6e6770477rhjuuSSS2bWYR2HAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFA1gVYfZL3u8YPTHG3GpUmfv5A6fPtimvv719Jc7T6rGmDpRB98XRNkPXr2CrJ+Mm5iuuG9EaUMlmcBgT8ss3Bq2/abdNvwB2aBs23Zp7jHT/5fWvTrNumHO1i27DtV/9nNseu26f1x36UtttgijRs3LnXv3j3tv/8BaaeaIGWnTp3q/6ItTRII63tqApjXXvuX9Pzzz2frRx55JAdZ416MHTs2rbfeemm//fZPO9cEjDt37tyk47WmL4fNXTXB3+uuuzb95z//yTZh95Of/KQ1XaZrIUCAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBGYTgdkiyNq+Q7upbuek72qCrF8+nzqOeSl1nfx6mrPtt1Ntb+yHl75ePh1z9AoN+vqs3pE1Lnbw6DHp75+ObtB12/nHE9hmsfnS2vN1ySfw6tdD0iOf/evHO5lZ/MhbLLpJWqPr/7yGfvIbb6dJ//XkLH5Fs9/pt/vNpqntKv/z/24PGDAg9e3bN02cOHH2g/iRr7hDhw7psssuS1tttVU+kwhl9unTJ40fP/5HPrOWf/iOHTumyy+/PAexW/7ZOkMCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAwLQCs2WQdWqGKWnSqKGpzaiajmbjBqeubYbWdHBtXIjpha+XS8cfveLU08/gU2sIssYlflrTmXXQ6O/Su9+OT2MnTZ7BVds8swU6t2ublpu7Y9pgvrnSYp06THX4z8Z/mV4c/XoaPuajmnsnNDYVTpkPndt1TEt36ZbWnW+1tGjHBafaY8oXI9Pkwa+nycM/SmnsuKm2+dCCBDp3Sm2X7pbarr1aarPQAlOd2LBhw2o6hF6bBg8enIYOHZp++OGHqbb7UD2BOeaYI6244opp7bXXrumAu39adtllp5p8+PDh+V689NJLaciQIe5FiU7YrbTSSmmdddbJdksvvXTJVosECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgVlLQJC17v2aNDFN+vKV1O6rF9JcE15N87QbntqkKXX3Kvt5dg6ylgWxkgABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAwHQFB1jo4EVl967uv03MjR6VXR49OI78ekdZu93766Zw1/3V4Py05x+g63/i/j4Ks/2dhiQABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECAwIwFB1hqhj8ePTc99NSq9PGp0GjryqzRmwvf1ui3e7uu0QYRa5/wgbdDhgzRv27G1+wqy1lJYIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAjMUGC2DLKObTM5vZzGpBdr/vtP+i59NGZMavvlhNR21MTU7qvvU5sfoi/rjEfbNCWt1P6LmmDr8NytddLYjunUY5ed8RdL9rjk/t5pvoXmKlkz/cVbL3oiPdb/lenvVLJ154M3TlvtsV7JGosECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZYhMFsEWdt2aJfeajO2JrT6P8HVt9LYNKlN+RtQk3FNbb+a+D+h1pE1f7+p6c5aWa41LfvZ5PTV1W+Wn7ietYKs9cBYTYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLR6gVYfZF36if+XXmw/NkUX1kaNmu6s7UZOSO2iW2vNf23GTKp3mmU+m5K+vvqNereX2yDIWk7FOgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGB2EGj1QdYvnvhFmtChnvarjbjDbcZNyoHWtjXdWnOwdeL/BWQFWRsB6isECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAbCsgyNqUWz9lSmrz3aQ0x5cTUtuabq3LvzEhfX2VjqxNIfVdAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYPYREGSt4r1e+6kx6ctjnmvQjJfc3zvNt9BcFX/n1oueSI/1f6Xi/Xc+eOO01R7rVby/HQkQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECM0tAkLWK0t2fGps+PebZBs0oyNogLjsTIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECrUhAkLUJN3PO1CatMaVLWid1SevV/P3235+lS4/5e4NmFGRtEJedCRAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgVYkIMjagJvZtia4uvyUjmndCK6mudJqUzqnDjXrijH43++lPwuyFhz+EiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgSmKyDIOl2elBariaquW9NtNYKra9f8nSe1q/cbgqz10thAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEJhGQJC1DsncNUHVCKxGcHW9mr8RZK10CLJWKmU/AgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEBKs32QtX1qk1ab0jkHV6Pz6vKpY02UtU2jfhuCrI1i8yUCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgNhWY7YKsEVFdLnVKEVqN/1ZPnWuiq22rcvsFWavCaBICBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIEBgNhGYLYKsXTt0qAmtzpXWTTXh1TRXmndKu2a5vYKszcJqUgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKCVCrT6IOvpj++bftKh00y5fYKsM4XZQQgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAIFWItDqg6zXPX5wat+heTqw1v0NCLLWFfGZAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFC/gCBr/TYN3iLI2mAyXyBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRmYwFB1irefEHWKmKaigABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEGj1AoKsVbzFgqxVxDQVAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0OoFBFmreIsFWauIaSoCBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECg1QsIslbxFguyVhHTVAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgECrFxBkreItFmStIqapCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgVYvIMhaxVssyFpFTFMRIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECrV5AkLWKt1iQtYqYpiJAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRavcAsFWQd9sbn6elH3mrQTdmtb4/Url3bBn2nsTt/NGxkGvjAaw36+o77b5Q6delQ8Xeef/ydNGTwxxXvv26P5dKq63ereH87EiBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRmlsAsFWSdWSiOQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0PwCgqzNb+wIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQQEWcugWEWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIND8AoKszW/sCAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUEBFnLoFhFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ/AKCrM1v7AgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlBARZy6BYRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0PwCgqzNb+wIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQQEWcugWEWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIND8AoKszW/sCAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUEBFnLoFhFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ/AKCrM1v7AgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlBARZy6BYRYAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAg0PwCgqzNb+wIBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECZQQEWcugWEWAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIND8AoKszW/sCAQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAmUEBFnLoFhFgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECDQ/AKCrM1v7AgECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQJlBCoOsr799ttTynzfKgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQKNEhg6dGhF32szYMAAQdaKqOxEgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQicCUKZXFU9s89thjle1ZyVHtQ4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgMNsLjB8/viKDNuPGjRNkrYjKTgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABApUI/Pd//3cluyVB1oqY7ESAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiqgCBrVTlNRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUKmAIGulUvYjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoqoAga1U5TUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiqgCBrVTlNRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUKmAIGulUvYjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoqoAga1U5TUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiqgCBrVTlNRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUKmAIGulUvYjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoqoAga1U5TUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiqgCBrVTlNRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUKmAIGulUvYjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoqoAga1U5TUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiqgCBrVTlNRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUKmAIGulUvYjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoqoAga1U5TUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIEKiqgCBrVTlNRoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgUKmAIGulUvYjQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBCoqoAga1U5TUaAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIFCpgCBrpVL2I0CAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQqKqAIGtVOU1GgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBQqYAga6VS9iNAgAABAgQIECBAgACBLDBy5Mj8d/75509t2rRJU6ZMSaNGjcrrFlhggZmiNGLEiPTBBx/kY3fv3n2mHLOSg/wYFpWcl30IECBAgAABAgQIECBAgAABAgRmT4ExY8ak8ePHp44dO6YuXbo0CqEaczTqwPV8aezYsWncuHFNuqZ6pp5pq1ua6Uy7cAciQIAAAQL1CAiy1gNjNQECBAgQIECAAAECBAhMK/Dxxx+nfffdN2/o379/6tSpU3r77bdT375987oHH3wwtW/fPgdMJ0yYkIOuc84557QTNWHNTTfdlO688848Q4Rpb7/99ibMVt2vlrOo7hHMRoAAAQIECBAgQIAAAQIECBAg0FoEvv/++zRp0qRcT2vXrl2zXNapp56aBg0alHbeeefUu3fvRh2jGnM06sD1fOm8885LAwcOTDvssEPab7/96tmrZa9uaaYzS2tm/OZn1rU4DgECBAhUV0CQtbqeZiNAgAABAgQIECBAgECrFqg0yPrss8+m008/Pc0333ypX79+VTOJQueOO+6Y4m+EaKMb6/HHH1+1+Zs6kSBrUwV9nwABAgQIECBAgAABAgQIECAw+wgceuih6d133019+vRJW221VbNceDUCk9WYo5oXJ8haTc2ZO9fM+M3P3CtyNAIECBColoAga7UkzUOAAAECBAgQIECAAIHZQKBckDVeg/X888/nq990001T27Zt0zPPPJPOOOOMqgdZo7Afxc4Y0Zl1kUUWycst5Z9yFi3l3JwHAQIECBAgQIAAAQIECBAgQIBAyxI45JBD0rBhw5o1yPrmm2+mzz//PC211FJp2WWXbRRANeZo1IHr+VJrCLK2NNN6qKu+emb85qt+0iYkQIAAgZkiIMg6U5gdhAABAgQIECBAgAABAk0XmDx5cp4kgqLNNcaOHZtfZda+ffuyhygXZC23Y3MFWYcMGZIOP/zwfMiHH344h2bLHf/bb79N4dW1a9dym5u0LuYOn44dOzZpnkq/HN1n67sflc5hPwIECBAgQIAAAQIECBAgQIAAgZYnUGmoL+pDUeuac845p7mIGdXzpvlCPSsmTZqUj9GUOlQ15qjn9KZaXUmQdcqUKSnOZ4455pjqu/V9aIzj9O5LHGf8+PFNriHOLNP6XMqtj4f5O3funNq0aTPV5koMG/Kbb8pvcaoT84EAAQIEZgkBQdZZ4jY5SQIECBAgQIAAAQIEZmeBwYMHp379+qWhQ4emdu3apbXXXjv96le/Su+880569tlnU8+ePdNmm22Wia644ooUT/NvvfXW07yOLMKlt912W+68cOSRR9aSjhw5Mv3tb39LTz31VPrmm29yaHLFFVdMq622WooOq8stt1ztvuWCrB999FG69NJL8/fOOuusFK8a+/DDD3Onh/hidHpYYYUV8lx//etf81zHH3986tatW+28sTBq1Kh08skn53UHHHBAWnPNNafaftFFF+UOFdGlIsbqq6+eFlxwwXTsscfmzxMnTkw33HBD+te//pXimmJ06dIlbbnllmm33XbLy3llI/756quv0i233JKefPLJFIXaKNJGN9hNNtkk7bXXXrUF8boWETq+995704z+j++NN9447bHHHrVnFmHZsHrttdfSJ598kuaZZ558P+K+rrvuurX7WSBAgAABAgQIECBAgAABAgQIEJj1BIo6XdSSoqY199xzp4UWWig/wB0PZp9++uk5tHraaaflutugQYNyrezss8/OF9uQel7Uy6JeGDWyX/7yl/n711xzTXr11VdzPSrqXFG/ilpjhCaXXnrptP3226fNN9+8FrYacxSTvfHGG7kW+dZbb+V6YtS64ljvv/9+ijpoHDvqn9Mb0wuyRo3zwQcfzHXECRMmpGWWWSbPt/vuu6dOnTpNNW1DHEeMGFHvfYlaZpxT3LsTTzwxXXfddflaohPu/PPPn+t6ffv2TXPNNVft8atlGmHaW2+9Nb8xK+qIUctdf/31cy02zmPeeedNRxxxRO1xG7pw4403phdeeCHtsMMOuQYac4bbxRdfnFZZZZW8XElteXq/+agdx1ATbejdsT8BAgRal8CM/re04mrbjBs3bkrxwV8CBAgQIECAAAECBAgQmDkCjz32WPrzn/+cfvjhh3zACEZG94UoMEcHhniqf++990677LJL3h6hzldeeSX16tUrRXG2dDzyyCN5rgiWXnnllXlTzBuh1gjJxlhggQVSzf/9l+Lp+RhR3L3wwgtrXztWLsj69ttvpyjExogi8UEHHZRiv9Kxxhpr5IBrnFMU58ud30MPPZSuuuqqztK+mAAAQABJREFUXMCO4meEUEtHzDt8+PDSVbkQfPvtt+dw6THHHJML1LFDPK0f1xadF2J07949F5rrdgnIG2fwT1hEF9gI58aI4m8U9aOwGmOdddZJ55xzTl6uaxHnce2116b77rsvb6/vn5///OfpuOOOy5sjqBth4C+//DJ/jvscRfcYcf5RGN92223zZ/8QIECAAAECBAgQIECAAAECBAjMegIDBw7Mwce6Zx5hyKjP7bvvvrm+tfLKK+fAaewXgc8Isja0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kENqP1sgQIAAAQIECBAgQIAAAQIECBAgQKD1CUyePDk/ZP7MM8+UvbhVV101b+/UqVPZ7VZOK/D666+nE088MU2YMGGajW3btk3HHXdc6tGjxzTbrCBAgAABAi1RQJC1Jd4V50SAAAECBAgQIECAAIEKBO677770wQcf5I6h0a1gVhjRYeH2229PUZBeffXVUwRZo6hqECBAgAABAgQIECBAgAABAgQIECDQugUmTZqUnnzyyfTEE0+kTz/9NHcMXWGFFVL8t/nmm6fOnTu3boBmuLpPPvkkPfTQQ2nIkCG5q+3iiy+eVlpppbT++uunCAcbBAgQIEBgVhEQZJ1V7pTzJECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAi0MgFB1lZ2Q10OAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQGBWERBknVXulPMkQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECLQyAUHWVnZDXQ4BAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAYFYREGSdVe6U8yRAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQItDIBQdZWdkNdDgECBAi0DoGxY8emcePGpY4dO6YuXbq0jotyFQQIECBAgAABAgQIECBAgAABAgRaocCUKVPSqFGj8pUtsMACjbrCaszRqAPX86URI0ak8ePHp4UWWih16tSpnr1+/NXh/t1336V55503zTPPPD/qCbW0e/ijYjg4AQIECBAgQIAAAQIEGiggyNpAMLsTIECAAIGZIXDeeeelgQMHph122CHtt99+M+OQjkGAAAECBAgQIECAAAECBAgQIECg1Ql8//33adKkSal9+/apXbt2zXJ9b7/9durbt2+e+8EHH8zHauiBqjFHQ485vf333Xff9PHHH6czzjgjde/efXq7NnpbBD8nTJiQ2rRpk+acc85GzXPqqaemQYMGpV69eqXdd9+9UXNU60st7R5W67rMQ4AAAQIECBAgQIAAgZkhIMg6M5QdgwABAgQINFBAkLWBYHYnQIAAAQIECBAgQIAAAQIECBAgUEbg0EMPTe+++27q06dP2mqrrcrs0fRV1QgwVmOOpl/J/80wM4Kszz77bDr99NPTfPPNl/r16/d/B2/AkiBrA7DsSoAAAQIECBAgQIAAgRYsIMjagm+OUyNAgACB2VdAkHX2vfeunAABAgQIECBAgAABAgQIECBAoHoChxxySBo2bFizBlnHjBmTnn/++XzSm266aWrbtm2DL6AaczT4oNP5wswIsj7zzDO542trCbK2tHs4ndtrEwECBAgQIECAAAECBFqcgCBri7slTogAAQIECKRUSZA1CqPxarR55523IrJvv/02v9asY8eOM9y/IfvOcDI7ECBAgAABAgQIECBAgAABAgQIEPiRBCoNskadbfLkyWVfcT927NhcV2vfvn2Tr2L8+PGpkvrc9A5UjTmmN39sKxdk/frrr9Pcc89dUVA3LMNtrrnmqvdQlQZZo1YZ83Xt2nWaucp1ZJ04cWJq06ZNvmfTfKGJK2Z23TR+l9X43TXxsn2dAAECBAgQIECAAAECzS4gyNrsxA5AgAABAgQaLlBfkPWHH35It956a3ryySfTZ599lieOAu6GG26Y9tlnn1xILj3aV199lW655Za8fwRfo4C7yCKLpE022STttddeaY455qjdvSH71n7JAgECBAgQIECAAAECBAgQIECAAIEWKBAhydtuuy199NFHKYKNEcBcaKGF0uGHH54DkfFK+znnnDOddtpp6dJLL02DBg1Ka665Zjr77LPz1YwcOTL97W9/S0899VT65ptvcphwxRVXTKuttlqKrqvLLbdc7VXHMWKOCByeddZZOej5wQcf5IfVo3Z34oknpuuuuy4NHjw4ff7552n++efP8/Tt27c26FmNOYoTivBj1BCjS+wnn3ySz3X99dfP5x3nEQ/GH3HEEcXuZf8WQdaTTz45vfLKK+lf//pXCpMwW2GFFdJBBx2Ull122Wm+O3DgwHTfffflLrhRywzzMFt99dXTFltsUVuPjHk//PDD7BGTxFwxb9yfGHHPbrjhhtrjxrouXbqkLbfcMu222255OdaVBlmjs2sc/80330xTpkxJSy+9dNp5551Tjx49YtdGj0rrpuXu4b333ptm9D/GbrzxxmmPPfaoPb8Iy/71r39Nr732Wr5/88wzTzbceuut07rrrlu7nwUCBAgQIECAAAECBAi0JoEZ/d9OxbW2GTdu3JTig78ECBAgQIBA8wqUC7JGEDWK3kOGDMkH79y5cw6mxvoYUYC+5JJL0qKLLpo/R8eDKPxGQbjYPmnSpBSF0BjrrLNOOuecc/JyQ/bNX/APAQIECBAgQIAAAQIECBAgQIAAgRYsEIHGqLHVHbFugQUWyB1HI3i68sorp1dffTXvFiHBCLJGAPPII49MQ4cOzetj/5r/nSx3GI0VnTp1ShdeeGFtkPPtt99OEUqN8eCDD+ZAa7EujrHYYoulCLbGiAfNI2QZI4KbUc9r165dKvaP9Y2dI74btcITTjih9txjXTEizBu1wQhG3nnnncXqsn+LIGvUIKN2GCNCrBMmTMjLcV0RAi4NVhYdVot94zhffPFF3j/+2XzzzdPRRx+dPxfz126sWVhjjTXS+eefn6/hmGOOyWHY2B7HintSuHXv3j1FEDksiyBr1EYjcFp3tG3bNu+zwQYb1N1U0eeG1E3L3cNrr702B3und7Cf//zn6bjjjsu7DBs2LJ/vl19+mT+Xmsf1HnDAAWnbbbed3nS2ESBAgAABAgQIECBAYJYUEGSdJW+bkyZAgACB1i5QLsh68803pzvuuCN16NAhFzZ/+tOf5mLtG2+8kYvG3333XVpvvfVy14fweeSRR9Kf//znXGC++OKLawvrzz33XDrjjDPy67iiA0O3bt0atG9rt3d9BAgQIECAAAECBAgQIECAAAECs75AhB4j/BgB0+HDh+cOor/97W9zR9DoUhpBymLE245+8YtfpKVrOngutdRS6YEHHkjXXHNN7pZ67rnnpuWXXz7X0iKoGAHL6Ba6yy67pL333jtPUS7AWLouwpQ77bRT7iQ6efLkdPvtt6f+/fvn7/7pT39Ka6211nSDrLFjJXPEftHJ85577snh2IMPPjhttNFG2SG60z766KOxS4OCrLH/Zpttlvbff//8IH2Ee6O2OGrUqLTkkktmpzi3uK5dd901B2Wja+ohhxySrSM4e/311+djx37RobRjx44pHrh/+umn84P2EUKNt0pFUDPeIBWdWO++++78+aijjsodVUePHp0eeuihfG1xTvGAfjyoXwRZY110fe3Tp0++h/Fw/ymnnJLfalUaoI39GjIaUmMtvedFGDmuvwilFseN32YEpuN3GCZnnnlmDgSHYfxe33333fxbjGuPzr9hfeONN6Z//vOfeYrLL788/yaL+fwlQIAAAQIECBAgQIBAaxAQZG0Nd9E1ECBAgECrE6gbZI2CZ69evXLHgygab7/99lNd87PPPpu7EMTKK6+8ModWr7rqqlzcjVeVRaEzArDFiEJ8FFbj1VoRiG3IvsUc/hIgQIAAAQIECBAgQIAAAQIECBBo6QIRqIwulxFw3GqrrfLpfvzxx7VB1tK3FhXX8pe//CU9/PDDef8DDzywWJ3/RoDyqaeeSptuumk6/vjj87pyAcbSdRHw3GuvvWrnicBi1PciEHvooYemeGV86f5FCLJ0XSVzRA1xzz33zPP+4Q9/SD179qw9ZixEN9R4XX1DOrJGkPKyyy7Lgctisrj+4k1PEciMrqxff/11+v3vf5/DpzfddFMOvRb7f/7557Wh3+hQGgHYGEUH1/nmmy/169cvryu9ht122y3PmTfU/BMB0KiNfvTRRzk0G6ZFkHWuuebKId64tmLEnLfeemtaZpllcv2zWN+Qvw2pm5ber+IeljtW0bAgtkWH1e222y7v9vjjj+eOtNGFNX6DiyyySO3XS0Ouv/vd71KElA0CBAgQIECAAAECBAi0JgFB1tZ0N10LAQIECLQagbpB1ni9WXR7iI4E0ZW1S5cuU11rvDIsCtNRzD322GPTZjVdEqJDQRRaY0QxOQqc0d0hXn1WdzRk37rf9ZkAAQIECBAgQIAAAQIECBAgQIBASxWYUZA1am7RjXV6Izq7RlfN6JQZAcMvvvgidwk94YQT8tfKBRhL10WwszSUGF+KAGvM17t37/yween+RQiydF0lc0RINcKqMf72t79NFSaNdU888USKDrANCbIedthhKTqslo4IrUbINGqR4bvNNtuUbq5djlDqiBEj0j/+8Y8cDI4N4Rddb2OUC7IWddDYHgHaFVZYIRZrx3vvvZc7mS6++OI5oFoEWTfZZJN04okn1u4XC//+97/z26siGHr//fdPta3SDw2pm5ber+Ie1j3OwIEDU9R+Y4Rr+BYjrjesokNwXFfdEV1r475GEDgCwQYBAgQIECBAgAABAgRak4Aga2u6m66FAAECBFqNQN0ga/EKq0UXXTR3Vy13odFl4dNPP82dW3ffffc0duzYdNJJJ6U333yzdvd27dqlVVZZJRdDo0Af3VpjNGTf2sksECBAgAABAgQIECBAgAABAgQIEGjhAjMKst51111p7rnnnuYqootrPFA+ZMiQHFyN0Gbp6NGjR6o0yPrAAw9M9bakmKfojrr33nunXXbZZYYdWSuZ49FHH02XXHJJfgj+nnvuKT3dvDx06NAcnGxIkDWCr/FwfN0RHUEjVLrTTjulffbZJ2/+/vvv88P1EZiNrqlRc6w7ZhRkLa6hTZs26b777ksRQp3eKIKsO+64Y22X3WL///znP7k+Gs0BIpDamNGQuumMgqzhH8HpCRMmpNVXXz2de+65uXFBcV7RoOCVV17JH8tdd3wvRnSfvfvuu/OyfwgQIECAAAECBAgQINBaBARZW8uddB0ECBAg0KoE6gZZo6B+4403pmWXXTZdeeWVZa81Xi8W3RBKXxsWxeMoHD/55JMpuhmMHz++9rudO3fOr6qK14PFaMi+tZNYIECAAAECBAgQIECAAAECBAgQINCCBWYUZO3fv/80bzCKelrU54rw6oILLpi6deuWO4BGQPP555+vuCNrBDIHDBgwjVBDgqyVzhHBz+jUGR1PIzBad7z88svpuOOOa1BH1jPPPDOtv/76dafKr7aPIGs8UN+rV680efLk3J11+PDhed/27dvn8wi3FVdcMV133XV5/YyCrEUdNMLFsTyjUQRZ4xziXEpHNYKsMV+lddPpBVlHjhyZ+vbtm0aNGpW781566aWpa9eupaebDjrooBR+EbxdbLHFptpW+iGCrBdffHHpKssECBAgQIAAAQIECBCY5QUEWWf5W+gCCBAgQKA1CtQNss7otV+fffZZfg1ZWMQrtOJVWnVHvAItnvqPV3ZFB4cowNb3mqqG7Fv3OD4TIECAAAECBAgQIECAAAECBAgQaCkCDQ2yRng1AoXvv/9+6t69ezrggAPSEkssUXs58ZD53//+9xYZZH366adTBE/r67gaNcFrrrmm3u21F1mzsO+++6aPP/44X/92221XuimNGTMm9ezZMwd9jzjiiPSb3/wmPfXUU+mcc87JHVQPO+ywtNFGG6WOHTvm78XD9/EQfowZBVkHDhyYQ8SxbzzYH2+oKh3vvvtu7vYaAdl4QH9mBFlLjz+9uml9QdaJEyfmDrxRm+3UqVO66KKLcii6dN5YPv3009Ozzz6bu7VecMEFdTf7TIAAAQIECBAgQIAAgVYtIMjaqm+viyNAgACBWVWgbpD1nXfeSX369MmXc+GFF6bVVlttqksrLfAWxeB4TVUEXKPwHoXj0vHXv/41xevFFl988RTLDdm3dB7LBAgQIECAAAECBAgQIECAAAECBFqyQEODrBE2jCBmjKuuumqawOHBBx+cohNpjx490gknnJD3KxdgLNZV2k212D8mfPDBB1N0NC3WVTrHsGHDclfUmCM6fkYn1GJEQPekk05KL774YoOCrD/72c/y94p54u+gQYNygDSWL7vssrTCCiukU045JXeqjTpkLJeOImAb64raZSzHA/dnnHFGmm+++VK/fv1iVYqg6qGHHpqXDz/88LTFFlvk5fhn0qRJaZ999kmff/557gIbHVibO8jakLppcb/iXIt7GMtFrTfu48knnzxNrTb2iXH99dene++9N7Vt2zbdeeedKTqvlo6o40bDgzXXXDMdddRRpZssEyBAgAABAgQIECBAYJYXEGSd5W+hCyBAgACB1ihQFDd32GGHtN9+++VXc0WQNYrRUYA+66yzUrxeK8a3336bC5cffPBBfs1XdF2IEUXgKAavt9566bTTTsuvpMobav655JJL0qOPPpq7SsR+Ddm3mMNfAgQIECBAgAABAgQIECBAgAABAi1dIEKREY7cZZdd0t57751PNzqNRsfRGP37989dMvOHmn8++eSTHJaMzwceeGDadttt86bJkyfnoOF9992XP8cbkeLNSDHKBRiLdZWGUIv9Y74iBFmsq3SOCHrG9cbr6aOGGDW/eH19vJnp9ttvz+HImL+0Y2tYxAPvMbbffvu01FJL5eWiI2t8KLquxnJ0Y40w5ptvvpkfto+H7mOcf/756fHHH0/zzz9/uummm3IQN9ZH6DfCpl988UV8zB1hf/KTn+Tl6D4aXUjnnHPOfH5dunTJ6+N4b731Vu64eu655+Y6aFxb3Ksbbrgh71MEaJs7yNqQumlxv+IEi3sYgdTwiBG/v/gd1jeiC3AEr+NaI6Tbq1ev2l0HDx6c3aMjbAStt9xyy9ptFggQIECAAAECBAgQINAaBARZW8NddA0ECBAg0OoE6gZZ4wJffvnldPzxx+dXdkVBOJ68jwL6a6+9lkaNGpVf1RUF4+iAUOx/3HHH5eUoDq+99tpp3LhxuQgcodcYUejdcMMN89yV7pu/6B8CBAgQIECAAAECBAgQIECAAAECs4DA5ZdfngYMGJCDlcsss0zq27dvrqPVF2SNzqXRdTXCoO3atUtrrbVWDrq+/vrr6auvvsrB0K+//jp3y/z1r3+d9t9//xYTZI3b8eqrr+YaYoQh4zX23bp1Sx9++GEaP3587d0qDbJGbfHoo4/O284+++y07rrr5uXSIGusWGyxxfJ/EQqO64+Oseecc05affXV8/5PPfVU/hwfFlpooVy7jOPG/h06dMh1zAkTJmTPPfbYI62xxhq5phnLMRZZZJH80H0EOSMke+yxx+YA7rzzzptWXnnlNGTIkDR69Oi87y9/+cvajqTNHWSNmmylddO6QdaJEyemnj175npunHgEksuNeGtWdGONUXRljeUIIy+33HK5A+1LL72U5wnvqB1H11aDAAECBAgQIECAAAECrUlAkLU13U3XQoAAAQKtRqDoYFB0ZC0u7JVXXsmFygiulo4lllgiv+Jr6aWXLl2d7r///tylILoulI54LVV0ANh6661rVzdk39ovWSBAgAABAgQIECBAgAABAgQIECDQggWis2cELouOoBECXHDBBWu7rtbtyBqXEg+BxxuRIohZjM6dO6ftttsud8I85ZRTctA11sWr4COsGZ1QYxSdOIt1c8wxR3rooYeKaWr/RjgyQpJRo4suncX+TZmjmDzmuvrqq3P4MwKtcZ4RHN18882zRQRNb7nllrz7G2+8kY488si8/Kc//SkHTeNDvCXqo48+yoHRO+64Iy/nnWr+iXBpGKyyyirFqvw3Oo/efffdObRabFh++eVzUHbQoEH5mFGnjPBvdH+NUXQ8jeU4x6iLxhg6dGiKYO2IESPy5/gnLLfZZpu011575TByrIuOrtHZtXCMdcX4z3/+k2um9d2DYr8Z/a20blr3HkaQdaeddprR9GnJJZdM1157bd4vgtRxvBtvvDEHeYsvxzX06NEjHXTQQbVv6iq2+UuAAAECBAgQIECAAIHWICDI2hruomsgQIAAgdlKIIq9UUyP13JFETo6SSy66KL1PtEfr/uKTgxRrI9OCdE9IQrIxau6SvEasm/p9ywTIECAAAECBAgQIECAAAECBAgQaKkCUU+L2lh0WF144YXrraOVnn98J8KUn3/+eYq3HUUNruiCGWHD6L4Z9bV4wLyljriGsWPH5i6ycY6PPfZYuvDCC3OXzyuuuKJBp/3ZZ5/law6L6PJaWNSdJIKn77zzTt6+6qqrpuj+Wox4OD8842H86BZbjJEjR6YIfca+dWuWn376aXr//ffzNUTgMx7Q/zHGzK6bxvEiGBvX37Vr1/wWrgUWWODHuHTHJECAAAECBAgQIECAwEwREGSdKcwOQoAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQKD5BCK0etRRR+UDHHDAAbXdVWNFhG+j+2l0L91yyy3TYYcd1nwn0kJnjiBvdOitdESwtlevXpXubj8CBAgQIECAAAECBAgQaIKAIGsT8HyVAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAQEsROPjgg/ObnKJzas+ePdN6662XohPqgAED0sMPP5y70l5wwQVplVVWaSmnPNPO46677kqDBg2q+HjR/fW0006reH87EiBAgAABAgQIECBAgEDjBQRZG2/nmwQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgRajMDrr7+eTjzxxDRhwoRpzqlt27bpuOOOSz169JhmmxUECBAgQIAAAQIECBAgQODHFBBk/TH1HZsAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIBAFQU++eST9NBDD6UhQ4ak0aNHp8UXXzyttNJKaf3110+rrrpqFY9kKgIECBAgQIAAAQIECBAgUB0BQdbqOJqFAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECggQKCrA0EszsBAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgEB1BARZq+NoFgIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgQYKCLI2EMzuBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAECBAgQIECAAAEC1REQZK2Oo1kIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQIECBAgAABAgQaKCDI2kAwuxMgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECFRHQJC1juOYMWPS+PHjU8eOHVOXLl3qbJ324/fff58+/fTT1KZNm7TkkktOu4M1BAgQIECAAAECBAgQIECAAAECBAikkSNHZoX5558/19KmTJmSRo0aldctsMAChJooMGHChPT++++nL774Iq2wwgpp4YUXbuKM0//62LFj07hx4yquo05/NlsJECBAgAABAgQIECBAgAABAgQIEJidBQRZ69z9U089NQ0aNCjtvPPOqXfv3nW2Tvvx448/Tvvuu2+aY4450kMPPTTtDtYQIECAAAECBAgQIECAAAECBAgQmM0FihpaMPTv3z916tQpvf3226lv375Z5sEHH0zt27fPy/Hg+KRJk/Lndu3a5XX+mb5A+J544onp888/zzuG629/+9vpf6mJW88777w0cODAtMMOO6T99tuvibP5OgECBAgQIECAAAECBAgQIECAAAECs7OAIGuduy/IWgfERwIECBAgQIAAAQIECBAgQIAAAQJNFGhIkPXQQw9N7777burTp0/aaqutmnjk2ePrt99+e7rtttvyxS6zzDJp7733ThtssEGzXrwga7PympwAAQIECBAgQIAAAQIECBAgQIDAbCUgyFrndr/55pu5c8FSSy2Vll122Tpbp/1YFOF1ZJ3WxhoCBAgQIECAAAECBAgQIECAAAECIVDU0GK56Mg6ZsyY9Pzzz8eqtOmmm6a2bdvm5UMOOSQNGzZMkDVrVPbP6aefnp599tn0q1/9Kh155JGVfamJewmyNhHQ1wkQIECAAAECBAgQIECAAAECBAgQqBUQZK2laNxCUYQXZG2cn28RIECAAAECBAgQIECAAAECBAg0r8DkyZPzAYqgaHMcLY4xduzYNNdcc5WdvqihxcYiyFp2x5qVMyvIOmnSpBTn3b59+/pOpcnrJ06cmDp06DDNPLE+6onVuicnn3xyeuGFF9Luu++eevXqNc3xYsUPP/yQvvrqq9S1a9eqXHMlQdb4TYRvcxhPmTIlxT0Mx0rH999/n+/5nHPOOdVXvv3223yOHTt2nGq9DwQIECBAgAABAgQIECBAgAABAgQIzByBVh9kveKKK1J0Wd16662neRXZM888k1+5FZ1Xi04FN9xwQ95/yy23TL/85S9r78KECRPSzTffnF5++eX00UcfpSWWWCKttdZauVvEH//4x1wwfeihh2r3t0CAAAECBAgQIECAAAECBAgQIEDgxxQYPHhw6tevXxo6dGhq165dWnvttXPHznfeeSd37+zZs2fabLPN8ik2tIZWXNfAgQPTfffdlzuoRlByoYUWSquttlpaffXV0xZbbFEbMiwXZI0a26WXXpoDhGeddVZ67rnncq0u1kfQc+65587zxTyPPPJIPuTxxx+funXrVhw+/x01alSKIGeMAw44IK255pp5ufSfa665Jr366qtpjz32SG3atEn33ntvCocIQi699NJp++23T5tvvnntV6Ij7EUXXZQ/X3nllbXri4WTTjopjR49OneNXXnlldMHH3yQItjZuXPnFOd45513ZuORI0emNdZYI7tHrfG//uu/UtQQY/4IuK644orp4IMPTvF2qMaMuKZbbrklDR8+PH333Xc5pLrkkkum3XbbLa277rp5ytdffz1dd911+ZgR5Izrj+P+/ve/r92nMceuL8ga1/y3v/0tPfXUU+mbb77J9zeOF7+L6Ly73HLL5cOdeeaZ6bPPPsv3K+5b3XHrrbdmw7jfYVqMmPfBBx/M1xM122WWWSb/tiPE26lTp2K3NGLEiBSdaiO0etppp+Xf2qBBg/Lxzj777BzqDbsnn3wyRXfgcFlkkUXSJptskvbaa6/a327thBYIECBAgAABAgQIECBAgAABAgQIEGg2gVYfZD322GPTK6+8kjsRRDGzdEQB/M9//nOKIGtRkD711FNTFDR33nnn1Lt377x7PJF/yimnpLfeeqv063k5ukxEkVhH1mlorCBAgAABAgQIECBAgAABAgQIEPiRBB577LFc94pwaYzo/BndRyOsF8G+8ePHp7333jvtsssueXtDa2jxpXhI/IwzzsjfjznnmWee9MUXX+TP8U8EQ48++uj8uVyQ9e233059+/bN2yOY+O9//zuHQfOKkn8ijBjBwwi3RrfRujW+CIZeddVVOTAZAcouXbqUfPt/FouaX9QB33vvvRTdPMMi/saI5RNPPDH97Gc/y58jIHrMMcfk9QMGDMjrSv+J8/jyyy9zzXCjjTZKpdcSD8DH9ZbOH9+NueMa6475558/n390Sm3oiPBvhDTrjnD97W9/mwPAl19+eQ7sxj5xnyL8GSPCzRFGjYBpY0a5IGv83qJhQISnYyywwAJp3LhxuVtvfI6g6YUXXpjrsTfddFMO/Ma6uG9xbsWI32qEjqOD7E477ZT22WefvOnqq6/OIdb4EL/p+K/4jce9jd/KggsumPctfnPRDTbCxnFPY0TAN+714Ycfnj788MO8bt55581GUQeOsc4666RzzjknL/uHAAECBAgQIECAAAECBAgQIECAAIHmFxBkrSDIeu211+bOEhFW3XfffXMHhSiQRoH1gQceyHdJkLX5f6yOQIAAAQIECBAgQIAAAQIECBAgMGOBCONFSDVe6x5dWONtQhHUiyBfhA+jS2aM2KexQdYIGu66664pjhVvNjrkkEPyg97x+frrr0+PPvpoDhlG59N4XXsRKozj9u/fPwcaS8OfEWSN+lrU3CKEObymw+hBBx2Uw5ix/txzz80dPqP7ZoRWS0cETuPaIiganVLLjSLIGtui++kRRxyRVlpppdzVMx5gj66u0UX2ggsuyF9vSpA1ApnRZXWzmm63Eew94YQTcnfQmDi6i0Yn3J/+9Ke5A+0ll1ySjxfnEIHYxozoshrX99JLL+V7usMOO+SQatznCIDG7yC61MY1R8fcF198MUW9MzrfrrrqqrWdZxt67HJB1qiVRvfbePg/7tnyyy+fA9Rxr+M+RRg5fnPx24sutkUn1uiou/HGG9eeQjQmiHB1jAivLl3TNbcITkdAeM8990w77rhjvs7YN44VzQbC9bT/DfaW/uZing033DD94he/yHPFG7yiwUHcq4svvjgHa2OfCAZHODt+39HFtm7339jHIECAAAECBAgQIECAAAECBAgQIECg+gKCrDMIskbxPV6zVbdLRXErosj8/PPP68hagPhLgAABAgQIECBAgAABAgQIECDwowrEK9n79euXO6TefPPNOUhanNDLL7+cjjvuuPyxKUHWr7/+OtfMIlQYnTUjKFuMzz//PAcV43MEJuNV96WhwvqCrNE5M0aEYocNG5b69OmTttpqq7zu2Wefzd0240MEZaPraYzRo0fnzp3RWbVuGDLv8L//FEHW6NZ64403prnnnrt28x133JHCKa4hHlyP0ZQga92usfEmqL///e953iKUmT/U/LP//vvnrqDRfTTCmY0dce0vvPBCDq5GB9MY4RRB4giVRiiz9B4NHDgwh5ojJHz33XdP9Rup9BzKBVn/8pe/pIcffjjftwMPPHCqqaLD6VNPPZU23XTTdPzxx+dthx56aHr33Xdz6LcIrsaGwqw0uBwNBuJ3tM022+TfSOnkRcg1OrTedtttab755pvqN1e3w2qEoaOTb3TDjd9Dhw4daqeLIG4Eb+ONXRGMNQgQIECAAAECBAgQIECAAAECBAgQaH4BQdYZBFkjpBph1SiCRlG3c+fOU92V4vVdOrJOxeIDAQIECBAgQIAAAQIECBAgQIDAjyQQQdUIrEbHygj/lY4IfEbQMbp0NiXIWjpnsRwP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hx/Gc889Zwj3l8iqy2prf61k1cpXTZqIi4szn9u8eTN+8pOfmK7aKLKO/MXKLZAACZAACZAACZAACZAACZAACZAACZAACRyaQGgPQ9/97ndx7rnnmg9pyqQKfNu2bTOvQ0XWwdahabfrK1asMHVtmoYaTPTUOjSVRuvr6802HnroIeTm5h5QKtyxYwduueUWs9zSpUv71qF1cSqSXn755SY11izw6T+hcmLwfa3nCyZ/6nyVELWo8JiTk2OmByuyKisVJzWpMzSlVtNgVdZsb2836z2WRda1a9fijjvuMPsZmm5aV1eHH/7whybZduHChVDhdShlX5G1qqrKJOXqur75zW/ioosuMqvdty725JNPxu23377XJoPrCr55+umnm16wgq91HFxGxVztQSsoF7vdbqiwrfJ2cnKySee1WCwHvOaC6/v5z39u0lfnzZsHFb+1bjdY7r//frzxxhsmVVaXYyEBEiABEiABEiABEiABEiABEiABEiABEiABEhh5Ase9yPrxxx+bilnt2ioqKsq0pNdK1Y6Ojj66/YmsutDGjRvx4x//GLqO2NhYzJgxw1Rir1u3zlSwd3V1mcrOF198sW+dnCABEiABEiABEiABEiABEiABEiABEiABEiCBo0FA67BuvfVWbN261XQdr42zU1NT8cknn+xVJxYqsg62Du3dd981Db71+FQenDlzJioqKoyAqumeKi9qwqZ2Af/lL3/ZCK/7dvN+MJFVG6O/8sorpt5N911lV03YDBYVSTX9U8uUKVNw3333BWdBG55rMqeWX/3qV5g7d66ZHqzIqh+67bbboFy0aP2hyq27d+82r5WxlmNZZNX9U0lTBc+wsDBMnjzZMNXrQPff6XTiD3/4A/Ly8nTRQZegWKrC79e+9jWzTk1d3bVrF6xWqzn3mpq7ZcsWtLS0mHrV1tZWU0d79tln75Wqqum+ocm+oecuuGONjY34xje+gc7OTrOOOXPmmCRZPUcqTusx/vKXv+w756HS87PPPmsSfIPr0iRflXm1qGg9e/Zsk/qqkrf24KVFr5lFixaZaf5DAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQwsgSOe5FV8b366qt48MEHTWVzEOf8+fNx5plnmpb8oSJrsMutL37xi7j++uuDi0OlVW3ZHyrAasqrtsr/9re/bSpnKbL24eIECZAACZAACZAACZAACZAACZAACZAACZDAUSSgsp/Wh7311lt9e6Gi33nnnWeSKlX+CxVZdaHB1KHp8prEqt3Aq7QaLOPHjzci6Zo1a/DYY4+Z+jjtBv6EE07oS+sMSoWauqrpq1pCE1lVJtRekYKpripMqigbLC+88AI06VVLaNKovlZ59//9v/+nk6YuT0VaLcE6v32PWecF5dd9e1xqa2sziaZBaVaX1YbyKm0uW7bMfC4osgaPZd916GeC+ztx4kT88Y9/1Lf6itY3vv322/jsZz/bl07bN3MQE0Fh9YYbbsBll13W90lNlP2f//kfaL1lUL7VmSqvampqkE/fBwYxEUzlDYqs+lGVQFUmVak5WCIjI3HxxReba095qeiq7/3nP/8JLoJAIGCEZxVdNWn1kUcegaaq7ls0EVe3G0wVDs7PyMgw1/Mpp5wSfAuhCbHBa65vpkw8//zz+Pvf/75XnbHO13Os18n5558fujinSYAESIAESIAESIAESIAESIAESIAESIAESIAERpDAmBBZlZ9W2paVlZnW/9rVWFpa2qCxaoqEJkXU1NSYFAat8D1QheqgV8wPkAAJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkMAIENAUSxUHVbBUydTlcuFHP/qR6YHoQFLnYOvQtJv6nTt3mjqyqVOnIiYmpu8ompqaUFtba6RJTeYcTNH0UxVZNdlTxUaVcIPlgw8+wC9+8QsjQz7++OMIDw8PzhqRsaaJlpSUGMExJydnxLc3EgfhdrtNWq72LKXSZ3p6+ojVa+q5U/lXz72mnWqqbrAOVWVarV/V61DraEOLprmWlpbi6quvxpe+9KXQWXtN6zo0bVXlYRVgtZ5X02aD29hr4UO8UOFbE2r1WrPb7YZL8L+TQ3yUs0mABEiABEiABEiABEiABEiABEiABEiABEiABIaRwJgRWYeRGVdFAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAsckgf5k9WNyh/fZqTvuuANr167FRRddZJKD95k9al+2t7ebpOrBHMDZZ58NTXM+nKLJ3PumGPe3Pm2QoEL5cJaioiJ897vfNY0qNK07Pj5+OFfPdZEACZAACZAACZAACZAACZAACZAACRwHBCiyHgcnkYdAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiRAAkpgNIqsmu6sKbsVFRV49NFHTQrzww8/bFJyj5ezqunOv/zlLwd1OEuWLMGJJ544qM/su/DTTz+NNWvW7Pv2QV9HRUXhrrvuOuj8wcxYvXo1NEX5iSeeMGnMKuZ+//vfH8wquCwJkAAJkAAJkAAJkAAJkAAJkAAJkMAYIUCRdYycaB4mCZAACZAACZAACZAACZAACZAACZAACZAACZAACZDA8U9gNIqsK1euxG9/+9u+k3PBBRfgpptu6nvNidFJ4Ktf/SoqKyvNzkdEROCBBx5AWlra6DwY7jUJkAAJkAAJkAAJkAAJkAAJkAAJkMCIEqDIOqJ4uXISIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESOHIEnnvuOZSXl5skzwULFhy5DR/GlrZt24bHH38cKjtOnz4dKrJaLJbDWCM/eiwQeOihh7B7925kZGSYc5qTk3Ms7Bb3gQRIgARIgARIgARIgARIgARIgARI4BgkQJH1GDwp3CUSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESGAsEKLKOhbPMYyQBEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiCBY5DAqBVZtVsh7W7o/fffx9atW5GWlobTTz8d8+fPN5h7e3uPQdzcJRIgARIgARIgARIgARIgARIgARIgARIgARIgARIgARIggaESaGtrw7Jly/r9+JIlSxAWFtbvMjpTl+ns7MS///1v7NixA1FRUVi8eDEWLlyIyMhI8DnDIRFyARIgARIgARIgARIgARIgARIgARIgARIYFgKjUmRVifUPf/gD7rnnHvT09OwFIiYmBq+99hry8vL2ep8vSIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAERjeBt956C1dffXW/B1FcXIyIiIh+l1GJdfXq1bjmmmugcmxoiY6OxgsvvIDJkyeHvs1pEiABEiABEiABEiABEiABEiABEiABEiCBESIwKkXWZ555BrfccotBoi2jzz//fHg8HvzrX/9CeXk5xo0bhzfffBNOp3OEsHG1JEACJEACJEACJEACJEACJEACJEACJEACJEACJEACJEACR5rAww8/jDvvvBM5OTnIzc3db/M2mw2PPvoo7Hb7fvNC32hsbDTpqx0dHabHt0svvdTIr0uXLsX27duRkJCAV199FdnZ2aEf4zQJkAAJkAAJkAAJkAAJkAAJkAAJkAAJkMAIEBiVIuvcuXNRU1ODCy+8EA899FBfF0Eqs5555pkoKSkxrah/85vfjAAyrpIESIAESIAESIAESIAESIAESIAESIAESIAESIAESIAESOBoEPjxj39sRNX7778fl19++ZB3QWVYlWJjY2NNL29BKba9vR1nnHEGqqqqcMUVV+C+++4b8jb4QRIgARIgARIgARIgARIgARIgARIgARIggYERGHUia1lZmWklrYen3f7s2xr6iSeewK233oqkpCRs3boVfr9/YCS4FAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQAAmQwDFN4Morr8Tbb78NTU6dP3/+kPbVYrGY5wylpaX49re/jR/96Ed7reeRRx7B7bffjujoaGzbtq0vTGOvhfiCBEiABEiABEiABEiABEiABEiABEiABEhg2AiMOpFVd/iqq66CVjRVV1cjEAjsBeP999/HF77wBfPemjVrkJWVtdd8viABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEhh9BKxWK+bNm4fdu3ejsLAQcXFx2LVrlxknJyfD5/Oht7f3kAemy+Tk5JjnCy+99BK0F7jQUlRUZFJZ9b1ly5Zh2rRpobM5TQIkQAIkQAIkQAIkQAIkQAIkQAIkQAIkMMwERp3Iun79elxwwQUGw4oVKzBp0qS9kDz77LO4+eabzXuvvvoqZs2atdd8viABEiABEiABEiABEiABEiABEiABEiABEiABEiABEiABEhh9BHp6eoyAquNTTjkFq1atMvKqHomKrN/97nfx1a9+db8AjH2PtLy8HIsWLTJv79ixAy6Xa69FNEAj2BvcU089hVNPPXWv+XxBAiRAAiRAAiRAAiRAAiRAAiRAAiRAAiQwvARGncja0dFh5FVtMX3bbbeZiqlgC2ubzYYlS5bgvffeM5SefPJJnHbaafsRq62thUquAyl5eXlISkrC9OnTB7I4lyEBEiABEiABEhjFBLZu3Yq6uroBH8Hs2bNN6suAP8AFSYAESIAESIAERh0Bt9ttJJmB7nhMTMx+qW4D/SyXIwESIAESIAES6J9AWVkZFi9e3LeQPhPIzMxEZWUl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8jwfAQQQQAABBBBAAAEEEEAAAQQQ6F2BARdktYap/fffX1asWCGnnXaaXHvttdLY2OhUfD6fXHDBBfLwww/LHnvsIQsWLJCmpqbeFWNvCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggg0OcCPQ2ycp2hz986TgABBBBAAAEEEEAAAQQQQAABBBBoITDggqx29rfccotcc801rurq008/LQcccIALrL766qty6qmnSk1NjVx33XVy8sknt3ixzCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggMDOIRBOkPX++++X5cuXy4QJE+T0008PvnCuMwQpmEAAAQQQQAABBBBAAAEEEEAAAQT6XGBABlmrqqrk+OOPl8WLFzvA3XbbTeLj4+WDDz6Q+vp6OeaYY+TOO++kGmuf/3pxAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIbB+BcIKs3/3ud+WNN95wPb3NmzcveCJcZwhSMIEAAggggAACCCCAAAIIIIAAAgj0ucCADLKaWmlpqZxyyiny3nvvBRFjY2PlkEMOkbvuuktsmgEBBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQR2TgGfzyezZ8+WvLw8uemmm+R73/temxd64oknyn//+1856KCD5LHHHmuxnusMLTiYQQABBBBAAAEEEEAAAQQQQAABBPpMYMAGWT2xwsJCef/9911F1r322suNvXWMEUAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIHOBLjO0JkO6xBAAAEEEEAAAQQQQAABBBBAAIHtLzDgg6zbn4gjIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAghsDwGCrNtDlX0igAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCHQpEHaQ9b333mvqcm9sgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAQJgCmzZtCmtLH0HWsJzYCAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAgTAGCrGFCsRkCCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAA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7gZZLSBr4W2r7tp66CjI2tPXZxUy7N+BDRaY/etf/+qmW/8wk5KSEsnKymq9inkEEEAAAQQQQAABBBBAAAEEEEAAgXYELLgZSa9Xtsue9sATGmS1tlfrncp6CSooKBCr7mrtbzbYzdN33nmnKxLgFuiP0LawznoOsnYlu+nZ64nIei7Kzc2V4cOHu6qs1v7rDY8++qgLu3pBVm+5jcPtxct7DkFWT4IxAggMVgGCrIP1ned1I4AAAggggAACCCCAAAJ9J9CnQVZ72R999JG7o711N+ehJNY4+cMf/tCFWr3loWFU6ybKC6x6621sjaXW7VNdXZ1rrJw/f75YSPb+++93VQZsm5NPPllOOeUUm2wzWNDWqmraMHPmTLFKlxrodZU0rTsrC8FaGNUbrBrnqlWrXDVN61pr48aNMnnyZLnlllvcJnY+xx13nFhlztAQo/f8jsZekDWqvlKsiqY1vHpjm7aHvS5v3NF+bLkFWW++4263iQVZx+WObbP58wtfkseeeNotv+2GP0tmRnqLbey1n/aTQED4W9/4qpxy4gkt1vfGzJtvvysPP/pYl7uy0GNaeroUawCyq+HA/feVbx56SJdBVgtUWgXf7jTUWBjbftdqa2tdd2N33x0wbu+czjzzTFm9erWrSGHdv1kDfGjjvXVdlq6vqfVgFYIffPBBt/jUU0+Vk046KbhJd4OsFrq2iwjtDe0FWSN5fZWVlWJBcK+a7I033timCkd758EyBBBAAAEEEEAAAQQQQAABBBBAAIHOBSLt9SqSHnheeOEFqaiokP33398FS0PP1NpF7WZtrzeuSy65RPbbb7/gJqFtYbawo56D/vjHP8pbb73lnmftgFZUwNpbvSEvL08swPrqq6+6sVVtDQ2ydrcXL2+/BFk9CcYIIDBYBbpzfWSwGvG6EUAAAQQQQAABBBBAAAEEelegz4Os9nIs4GZdp7/++uuuW/OtW7e2eZUW9vv1r38tX//61926cIKstqEF6KzKow3PPvusq7z6pz/9Sd544w23zLprt26m2huKi4vl+9//vluVk5PjuqeyGatyYNUObLBQrFcBwKs6aZVcbb0XUrTQoDW0rly5Un7+85+75x199NHys5/9zE139WNHB1n/piHWBRpmteGe22+QxMTENqf4f2ec4wLC++8zR8766Q/brI9kwfsffCh33ftAsGpD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AAIQgAAEIAABCEAAAt0ggJA1kHqWwb3f//73fSZjdtpppyZBgiZsrrvuuqara9nYzTffvLHv/PPPNx988EHjuza22GKLPh5Z9XB+yy23mIceesi89NJLduBekz1zzjmnWWihhcyVV17ZFEfaF1/M2kuiQHkr+/Of/2zFuRLo+mGaaaZp4uI4JTFUGdx3333mvffeM9NOO23TssIaaFb5PPnkk/Z6WtpSQWymm246M9tss9myihOZvPzyy+bqq6/2k2dmmGEGs/XWWxtNyOqa4qpryGvsYostZjbddFMryNRJyucdd9xhnnjiCTuhqEH5hRde2Pz2t79tm4fZXq+zSUJWMfMnN3upzipthO4RCJlwdKnTfaulwt5++207uCkxh0TrusdnnHFGs+yyy5pVVlnFHd7nc9SoUXZpcHmDlN12NkOiIsUh+77ccsvZFwK0zw+PPfaYue2226yt+fDDD427toS0U001lbURur7sCKG9BEIHtl0qnJhVE9fOo6bKadCgQe6QUj+z9EXcheP6CBKWqY+w8sorG01Y+UETYvvss09j16OPPmruvPPOxndtzDvvvGb99ddv2nfOOeeYESNGNO3bZpttzPTTT2/3PfLII+bBBx80r732mhUXavJcaXNBadJ9Is/za621VqwIPO2eTmp/XR/i9ddf79O3mnrqqc2ss87qkmLbZ32JCstc36Jx4JgNTSr97ne/s8J1f/9WW21lbYe/L7p91llnWRbqv0XDkksu2XhxRgzF0g/Kq/pEslny2OpEQLIbOn755Ze3f2ULOZSGPPYuqT8msalYZA177713H5HkRx99ZPu9Kmf1FR0XveA1xRRTmLnmmsssscQSjT5Xq2tWpV+hPqz6nC+88ILtz6rdUn1W+av+6n7zg178Ur9Toax7WxPpN954o+3byoZKDKfVEVzQBLts5XzzzWdWWmkl26d1v7nPkInzpDqk+iP78/DDD7soG596ES9NmBd3jymCe++919qtRmRjNmaaaSaz5ZZb+rtabrdKs14muOqqq2y78dZbb9l7Xc8iq6++ullvvfUa974iLquckuxjrz2fOHutezj0mc8vhG7Yx5A6nLcdlM0944wz/Cza7SJtktpfte1qf9SOyHZqn2yfbIhWgIlrx5LqkfpC99xzj7VHEtbr2XeOOeawaW3VbshOqZ3SNWUj9GzcSpTT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iF0uykzPJepr6/lCfX0CBCAAgXYQKMPOtXvsqci4Q5G0lTXWkrXcumn/u/nMnpe3ew7SGHzoWJZfJkXG84ryKjIO2a164njnfe4skueivP1y77VtZ4sRsvZayZAeCEAAAhCAAAQgAAEIQKBbBBCyBpJ3D5RZhKx6OL/pppvMSy+9ZF5//XUrZJ1++umNBJBDhgwxM888c+DV+/9hefh2g8oLL7xgjjjiCHvpZZZZxuy2226NZBx55JHm+eeft99PO+00M8000zR+Y6P/EShaZ//xj3+Ye+65x4oADzjggMyCtjiiZducV199tSHcnmqqqcxWW23V57JvvfWWefjhh43ujQ8++MCMHDnSHqNB5EGDBpk111zTLLnkkn3O+9Of/mSP7/NDwg7ZzYUWWsi88cYb5vrrr084su9PE044odl5552bfpDw/IorrjAvv/yyFQDpx3HHHdfeu6uvvrpZYYUVrCio6aT/ftGEyB133GHuvfdemw99V5h88snNPPPMY37961+bqaee+r9Hh32E8FZMEtRfe+21RserEdcA8rTTTmvblk022cQMGDAg9YISrj7yyCNG9VDxfPbZZ0b7JOJVee2xxx6xcTz22GPmoYcesu3axx9/bAXLs88+u5lvvvlsWSstISGk/pdVznHp0eC76uCHH35of15vvfXMbLPN1nSo7qdzzz23aV/Sl7XXXtuWQdwxKrO//e1vRvy0rUkahYknntgcfPDBfa4dF4f2FbU7yvfxxx9vRdtLLbWUWWWVVVpdKnZ/SLm5E/PaBnc+nxCoG4Es97cm8R599FEzfPhw884771i7oslMBdmVOeaYw8gmyTYTIAABCEAgH4EsdjnuCln7TcOGDbPPVOqfjho1yqjfpr61nsOWW245s+qqqwa9rFC0vxeXF/ZBAAL9k0BeO1f22FOUbpFxhyJpK2OsRXnJYof13FyW/de1s7Y9ecbzdJ0777zTPPvss9pMDXphfNttt+1zXB7eGnt78sknLTOxU5up5yCJWNVeajxO42KTTTZZn+u5HXnH88oY48g7Dll2PREL1dO0cbEynjuL5Dlv/XRlXZVPZ4sRslalxEgnBCAAAQhAAAIQgAAEINBuAghZAwm7B8pQIasmHo4++mg7uRx3CYmqJF7RRDOhuDinUwwRsnaKdO9fJ6tNiObomGOOMc8884zdfdFFFxX2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5L2/1SbI47eW0vzkk0/shLLaJGfzoysT9GeG5A0CEIBAmQTy2mWXhrz9JvU/9adnsddee83cdtttjT62niu0+oSesVqFov29VvGyHwIQ6H8Estg5/zm3HePdRcYdiqatyFhLXK3Ia4eL2n+lJW/b4/IR+syuF6I1zjH22GO7U+2nnkluueUWI7GoC5tssonZYIMN3FdTNm+92H3QQQc1BLRa8UYvWkeDno/yjOdF4/G/h/IqMg7pX0/bofWkrHExd/2sz51l5tmlIZS3O74qn84WI2StSomRTghAAAIQgAAEIAABCECg3QQQsgYSdg+UaULWe++914pbFO0yyyxjdtttt9graIL5rLPOsr8tscQS1tNe7IE12RnKVzhChKz33XefFbHpeISsokAom0CWOht37aKDyy7OvDZnq622aggrXVyhn62EkDrfF5HrexYBje/JMkmIqnijQSLYPffc03o40G9bbrmlWXvttaOHma+++srstNNOduBXAl55bPBFthJiyjZLqOiCBsDd0u/ywuoG5OXRVPbIDdzLi4iEoFrO3gmIZpllFis+9YWsLt7QT5/3PffcY6644orGAL0fh0S5mtT+7LPP7G6V8VprrWW3/UmhVl5Vv/vuO7PzzjsbechQOGKMAFkeX8VL4iiF0047zUwzzTR2O/rPr9Mnn3yyFfBGj3Hf/WMvuugiK5R2vyV9hpazP2CfFF/0N+VX+Q4J8ua79957m2+//dYeLq8YEsP6HsFXWWUVo5cn4oK8smoQXkHCYpVzWihqd/JOqLl05S03nV/ENrjr8wmB/kyg6P3ts7nzzjuNXkZQ0IsoernLtVX+cWxDAAIQgEBrAkXtcpF+k58qvfgkgY68mv0/9s4C3I7i/MNDg5dAggSXBHfX4lJCaCBAcA/uaYHiUNxdgxV3irtrKBR39wdNCBQI1v7//Kad07l795wzu7Pnnrt33+957j0ro+/Mfufst99+I/F/4/vp3Hbs7z1XDp8QgEDPJ5BFz+W1PemF1xCJsTsU0ba8tpa0vhWlh7Pqf7Ul5runyHv2c88914ipxF8tyB749V9RvPWCulYEcg+4tBLOYYcdZlencHW5z7z2PJc/+RnKK9YOmaw3uV9vnnSFXazefWcr+hzKO8mnDPtOF+PIWobRoo0QgAAEIAABCEAAAhCAQFcQcPf5zeoa59cb0P9r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dqxlNd544wXXmZZQ/CQTTjhh2ulOx2J5dyow8IDjLdZ59GRRvP79738blSVeecY9sLs2WWyfXV2xeqddY672t7Nux49PCHRnArHXd3fuG22DAAQgUEYC3UUvd+Vv1jKOE22GAATyEyhCz+W1w4S2WraWiSee2IwzzjihWWrpWt22WkXdaCOmz7G2rbxjVZS9pBsNQ5c0JS/v2MbFzDG/7q60YcbYqv02t2q7CF3cqrZRLgQgAAEIQAACEIAABCAAgXYQwJE1kHrsDeWDDz5obr/9dmt4O/TQQ4MdjQKbV/pksXy7CsCLL75o/vznP9vqVlxxRbPPPvvUqtb2Cy+8YPfPP/98M/3009fOsdHzCMTO2VbohH/+85/W0fqVV14xb731lnVknWWWWczcc89tBg0aZGadddZMA/HGG2+Yq6++2uaZeuqpzfbbb98p/zvvvGMeeeQRo2vjk08+MV999ZWREfo3v/mNmWaaacwqq6xiBg8enOqwqLyPPvqozasvo9GjR9vy5RA644wzmnXWWccst9xyner0D+Tpsxx8//GPf5iHH37YvPfee7bdMmrKmVFtnn/++c0WW2xh+vbt61fVYVt9lE5X38VaBuTJJpvMDBgwwAwcONAsv/zyHdInd+QUf/nll5vXX3/dfPTRR5bXTDPNZMdKdausZqIy7r77bvPEE08YbcspU/Lb3/7WHHnkkWbOOefsUEQM71tuucU899xzHcqrtzPllFOanXbaqXZaxunHH3/ctvP99983n332mRFvidqqlwLWW289s+CCC9byJDeK4CUngMcee8w88MADlvuYMWOMjumBmObZfvvtV6v27bffNldccUVtP2RDD9f23HNPm7SIPqfVmVXvxIz5N998Y0499dS0ZqQe0xhqLJ3E1O3K4BMCVSKQ9fquxybku7teXo5DAAIQgMD/CMTq5bz3e1l+s15wwQUm1KjnerbGGmuYxRZbzO3yCQEIVJhAXj2Xxw6TBfPIkSPNQw89ZF599VXz+eef25eGZdeSzUB2Itmb6klRbQvV4UXrYdmaDj74YGszkV3pD3/4Q72u1o7H9DnWtpVnrFplL8lzH5SHtwMfk1dlhM4xV58+8/COsee5umPmmCujK22YskfltVW79nblZ15d3JVtpC4IQAACEIAABCAAAQhAAAJdSSDU5j3Orw4y/1dkw8p2gxbb3gMOOMA888wzFuG1116bO3JfkWPQncqK5dtVfcGRtatId/96Yuds0TpBDqT777+/dcxMoycnuyOOOMI6SqadTx6Ts+Hw4cONnP0kM888sznnnHM6JJPjtu/M3eGktyOnTDnEyRnWiZxtndOfO5b2ueiiixo5/6dFzMzbZ599Wp06Jl777ruvWXzxxTslGTVqlDn++OPN888/3+mcO7DCCitYfmnRUe+8805z7rnn1hxPXR73KV577723Ud/ryVNPPWWOOeYY8/3336cmOeqoo8zCCy9cOxfL+4QTTjD33XdfrbxGG9NOO6258MILa0nk/C/d2UyGDh1qttlmm07JiuClByXHHnusdabtVMGvB+SAeeKJJ9ZO6eHAYYcdVtsP2ZBT7nXXXWeTxva5Xn1Z9E7smOshg6KLh4rm7Morr2yTx9YdWifpINCTCGS5vuv1O+S7u15ejkMAAhCAQEcCsXrZv+cItQFl/c26yy67GDlrZJEddtjBDBkyJEsW0kIAAj2UQB49l9cOE4rwpptuMiNGjLAvSKflkXOn7reLtBGl1ROqw4vWw3Le/dOf/mSbtPbaa5sdd9wxrXm1YzHjEWvbyjtWrbCX5L0Pysq7Bv7XjZi8Kid0jrk68/KOseep7pg55trelTbMGFu1a29Xf+bRxV3dRuqDAAQgAAEIQAACEIAABCDQlQRwZA2kneeGUpE0BFiGhltvvbVWU+hDjFqGCmzk4dsOLDiytoN696wzz5xtlU5QhFFFwPz4448tLC1Rv8ACC1iH+WeffdYosqJkookmMieddJJRlNZG8u2331on0w8++KCWLM2RVc75Mrw6UbnTTTedfaCgKKWK0OpE0UbPOuus2sMG/1pSmv79+5sZZpjBRpF96aWXjNrgZNNNNzWbbbaZ27WfMX32oyfLYXWuueYyk046qdXXiuDgRA6leoDiR0dVxAXld46Z448/vllyySVNv379zJtvvlmLyqwyNt98c7PJJpu44uynnF/lIOtEzDRWP/30k40WqigHEkWlveiii2zEUpfWfd54443mvPPOs9FEdWzcccc14isH0q+//tq89tpr1gnTd2SN5Z3F8K1o1IpK7UQPYWTglzje6p+i2IqHliVzIraKdu2kCF5ffvmlOeSQQzo85O/Tp4+Nnqvxe/fdd80UU0zRwZFVUW7lQJ1FfEfWmD43qjOL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To0uYd4+H3DAAa6IDp+KKKoIdx999JE9riinethSb7l4RQpVRINnnnmmQzluRw6TL7zwgt1VGddff707ZT/vuOMOc+GFF3aIZOoSyGlQzr5jxoyxhxwzP8q0ol8oCkmayAlYzoIStUNjVU9CedfL7x8/7bTTjPolcVFk/fONtm+55RajqLUSOUyrr77DbB5eKkuO185Btl6UEDm6aqlVF8lWjtMuQq7K8EUPozQebj5vu+22dvlDP03odrM+1ysnVu8UOeaffPKJdZJwDtwu8nC9thdZd706OA6BMhMIub79B7hZ+xr63Z21XNJDAAIQ6KkEQvRyo76H3u8V/ZvVj4IW+uJVo35wDgIQ6LkEsui5ou0wSapaTUsvu0rOP/98M/300yeT2H3dv+slXolsHzPPPLNpRdtCdbhtSOJfjB4OdY4sos95bVsxY5VAlbqbZi8ZNWpU7YW+1EwNDja6DwrlnVZ8TF6VFzrHWs27nj0vZo51ZxtmVlt12tgXfSyLLi66bsqDAAQgAAEIQAACEIAABCDQHQngyBo4KrE3lKHGicDm9LhkWfiGOLLed9991tFMoHBk7XHTpVt0KMucTWtwUTpBS6MfeuihtopGDoQ333yzOfvss206OUIqAutmm22W1rSmx+TEOe+88zZNpwSK0njBBRfYtMstt5zZf//9U/Np2XpF03CRShVFWddumuTtc5rzpyLIysH1tddes1VNM800VnfUi/7ht+eVV14xzz33nJFjkdo944wzmnnmmccuwS4nR0n//v1rTpp+XhlOH3zwQaMvYT08UH2KWqsxVORanZMcdNBB9pjvBLjyyiubvffe255P/nv11VeNlnmXyCn02muvTSax+1l4pxaQOPiPf/zDtlWHp5tuutqYJ5Kl7orduuuua6O0KoGipIqFL1l5Ka+Lvq1tOQ5PO+202uwkGn/nlKwItopkmyb+Q4o+ffoYLcXXLBprWjk6FtLntLwxeqfoMVf7FMX49ddft01VlOeVVloprdmmFXWnVsRBCJSYQMj1rQe4XfHdXWKMNB0CEIBAYQRC9HKjykLv94r8zaqXuPTilV44kriX4hq1k3MQgEB1CWTRc0XaYdKI+7beRi9JbrDBBrWVdNwLq61oW6gOT/YlVg/7dodGq+wU2eestq2YsUrySttPs5fope9W3AeF8k5rZ0xelRc6x1rN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DkmtnPWa1SGnVBn3pROVshwc68vncWSVk+q6665rVllllfGiKssB709/+lMtIvass85ql/RyDyZSytb32f8cMuaUtlPkfNttt5kXXnjBOjHOOeecfrftvh5E6AUM9xBHER5WXHHF8fJlHWjlyKq5pzmopOjkMu4rOqtLo0aNsvPDOZO6JeZTv08VtWHPPfc0ihCqJF0vne8nLcsqfa8088wzm1NPPbXpAyxXttW1ETtmV3+jbR69oyXx5MDtZJo1/kbtpLLX7yDNNyU53stJ1V1/rk3/hRNFbNXSiD179nSn2UKgcgTyXN8p3yWVA8uAIQABCEQSyKOXs5oIvd/TbzW93DN69GhbzR577GH0UplLchKRY86YMWPsoWOPPbb2YmNKWVc/WwhAoLoE8uq5FJvE559/bqNHOtobb7yxGThwoPto71tjdaEqie3bY489ZmRj0wufeqm2/r71s88+s6sLvf7667av9SvIpNgdUu7Z//Of/5jDDjusxm///fc3Sy65ZO2zdtQ32dDfffdde3zLLbc0v/nNbzrkyfrQyraV+t3TGfcyekFWTp3ODiHb6Mknn5w1vMxjrcas7+F+/frZeSK7Vn267777zDnnnFM7HGIDVeY8/ZadZvfddzeffvqpbcd/XlFruG4nZY6pqhRZxV6T/hBixpxyTfptd+W+08U4snYlddqCAAQgAAEIQAACEIAABCZkAjiyBkrH3VC2cmTVW7/77LNPrVa9Fa2le5qlueee22y44YbNsnT7c6F8BSLVkdV/G1316SHRXHPNZXQxyJnLOT3pnJJvGPKX9JOTl5y9XPKXbpezrJyiSN2XQJ45KwqvvvqqeeONN2pALr/88trDSC17Pumkk9pzmjduiTKXuZUjq4yYinTpoi4utNBC1glRdcqIe88997iqrGOfjPMhqZWznuqQsVfLsivioqIAvP/++2bYsGHmxRdfrDXRv39/s9NOO433QED9lbHXReT473//ax588EFbTvX5jpTq8+STT16rU8umyTgph0gZ65Vfulbt6xr/4IMPannro9SmlFWlKWNOaTtFzjfccIO54oorLBN954iZHHwVrVd67f7776/JQfPn8MMPt3lD/rRyZNVLAfvuu6957733bHUzzDCD2WqrrWyE3DfffNNGl5Izq9LCCy9cexhTxPep5pMeZrikpeuXX355O3d0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Vzbrr7/+eHncgZRrI2XMrv2sbareyTN+175e4Ljiiivs/H733XetzGeffXarV8R+qqmmclmDturDhRdeaGWpAltssYWZb775gsqSCQLdmUCe6ztFP3VnhowNAhCAQJEEulIv615q6NChQd2ffvrpza677hqUl0wQgAAEmhHIo+f8eoq2w/h1a//RRx81999/v/nPf/5jPvzwQ+twJ7uW7OiyW8gO0Sil9C2lrN+fmPtuVz6lrOrIYyNybbptatuqJ/Z+P2+/U2SVaodM4dUu+4rkIhu07HqyF8shVLacfv36mbXXXtvIPtYspfD2680rZ5WN0Qdl+10Vq4t9tuxDAAIQgAAEIAABCEAAAhDoTgRwZA2UZp4bSjk6ylGoVdp0003Njjvu2CpbJc7n4dtOIDiytpP+hNV26pw96KCDzNNPP20Hde2110a/Ce+oyLHwwAMPtM5+7pi/VQSLo48+2jqf+cdlRFUE4dD0f//3f2b11VevZR85cqR11nznnXdqx+p3FMFxyJAhZs011+xwKqWsKoodc4dOeB/00sFee+1lnVF1eI455jDnnnuul6PjrpwVTzjhBPvSQscz4z7J4ffkk0/ucKro74fQeSTn5n322adDX7I+/PKXvzRyrPajncpB33fazyqnYzKCyylaDsB+ShmzPz6/Tn9fc3v//fc3Sy+9tH84c18Pwv7whz/YcxtttJEZPHhwZj4dTLk2UsbcsENjT6TqnTzjVz/knH7++efbKMNZ/ZLMpRM0b0KTnFivu+66WnbJeMUVV6x9ZgcCVSWQ5/pO0U9V5cu4IQABCOQl0JV6+U9/+pO5++67g7qoFwQvuuiioLxkggAEINCMQB495+op2g7j6nXbG264wZx33nnWIdId87dyuNP9tm+zcOdT+pZS1rXvtnnvu105bVPKqrxvQ8lra0xtW+3H3u/n6XeKrFLtkBqjS3l5tcu+ojGfdNJJ5tlnn3VdH2+7yiqrWHuoVpeqTym86+vKI2eVjdUHZftdFaOL69nyGQIQgAAEIAABCEAAAhCAQHcigCNroDTz3FDKSUbGDCU52CywwALWSU1vu8po4KIV6rycgxTZs+opD992ssKRtZ30J6y2Y+asInVK6coQd/PNN9cGlNe4XCv4446iVioqz3vvvWePaNnvxRZbzOqdZ555xihym9Jkk01mTjnlFKMorS7l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AQhAAAJdSGDUqFHmiy++aNkijqxfjvP2xZG15VyJyuBu2Cd0vvL8vv766+0Y559/frPKKqvUxnvrrbead999134eOHCgmXLKKWvn2Ol+BCb0Oas3FPR/0kknTYL/1Vdf2fKTTDJJUj1dXbgr+/3DDz+Yzz//vLLX/Pfff2++/PJLoznSo0eP3KLOIyux/vTTT83PfvYz06tXr6i2vvnmm2hZpVxXn332mfn666/NtNNOayaaaKLcfVeBCUnv5JGbG6yuk8knnzx6/K4ethDojgRSru9U3dgdeTImCEAAAqkEUvRyatt++dTf2n5d7EMAAhDwCRSh51Lukf2+NNpPuYdM6VtKWX8seXV4qn1Jtpmf//zn9r/fj87e/+6778zHH39s7TRls0PGzjHZd1RWtikxz5Pc/ZueHcTY0VLnSZ6+dlbeGJuS60ve68qV0zalrF9PkftOF+PIWiRV6oIABCAAAQhAAAIQgAAEykxA99wjRoxoOQQcWQtyZH311VdrjpAzzjij2W677VrCr0IGd8M+oTuyvvTSS+bwww+3IllxxRXNkCFDauI54ogjzIsvvmg/n3766WbmmWeunWOn+xFInbMPPfSQueuuu6xD1/7772+dAFMpyVHuhhtuMC+//LJ5/fXXrSNr3759jZyu11hjDTPXXHO1bELO2vfee6954oknjPZlBFeaYoopzEEHHWTmmWeepnXImHrcccdZh70VVljBrLXWWk3z+ydTyqb22/UjVEerr5Lfo48+alnrjZDevXubOeaYw/Tv399o7PXpjTfeMH//+9/rDzf9PNlkk5ndd9/d5pF8zzvvvKb5/ZMbbLCBlb1/zN+P5S2j77///W+jOSxeo0ePtoZgOWcuv/zyZq+99vKbGW8/j6xk2H788cfNk08+ad555x07J/XDRUlzcr755jMa5yKLLDJeOzrw1ltvmccee8xId7///vvmk09+jAY/9mHBLLPMYtZbbz3b58zCPx5Mua70csOVV15phg0bZh1wVWXPnj3t98M666xjVlttNeuU26x9/1yq3omVufqQR25+n6VLHnnkEauX9INTD2rmnXdeKzPxl1MyCQIQyOeonqob4Q0BCEAAAq0J5PndVeTv9NTf2q1HRg4IQAAC4wjk0XM+s5R7ZL+eRvsp95ApfUsp68YSo8Nj7EuuPW11r37ttdda+8z/+3//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", + "text/plain": [ + "" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "\n", + "Image(filename=\"./images/parallel_coordinates_plot.png\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/hyperopt/optuna_executor.ipynb b/examples/hyperopt/optuna_executor.ipynb new file mode 100644 index 0000000..8411888 --- /dev/null +++ b/examples/hyperopt/optuna_executor.ipynb @@ -0,0 +1,453 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "# Hyperparameter Optimization with Native Optuna\n", + "\n", + "This notebook shows how to run Ludwig hyperparameter optimization using the\n", + "**native Optuna executor** introduced in PR #4090.\n", + "\n", + "> **Note:** Requires PR #4090 to be merged, or `pip install ludwig` >= 0.14.\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/hyperopt/optuna_executor.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acae54e37e7d407bbb7b55eff062a284", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig optuna --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "9a63283cbaf04dbcab1f6479b197f3a8", + "metadata": {}, + "source": [ + "> **Dependency note:** The `optuna` executor type (`hyperopt.executor.type: optuna`) is\n", + "> available from **Ludwig >= 0.14** (merged in PR #4090). Earlier versions only ship the\n", + "> Ray Tune executor. To use this notebook with the development branch:\n", + ">\n", + "> ```bash\n", + "> pip install git+https://github.com/ludwig-ai/ludwig.git@data-pipeline-hyperopt-modernization\n", + "> ```" + ] + }, + { + "cell_type": "markdown", + "id": "8dd0d8092fe74a7c96281538738b07e2", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "We use the [UCI Wine Quality dataset](https://archive.ics.uci.edu/ml/datasets/wine+quality).\n", + "It contains physicochemical measurements for red and white wines. We combine both files\n", + "and create a **binary classification** target: `quality >= 7` → `1` (high quality),\n", + "otherwise `0`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72eea5119410473aa328ad9291626812", + "metadata": {}, + "outputs": [], + "source": [ + "import pathlib\n", + "import urllib.request\n", + "\n", + "import pandas as pd\n", + "\n", + "DATA_DIR = pathlib.Path(\"data\")\n", + "DATA_DIR.mkdir(exist_ok=True)\n", + "\n", + "WHITE_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv\"\n", + "RED_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\"\n", + "\n", + "combined_path = DATA_DIR / \"wine_quality.csv\"\n", + "\n", + "if not combined_path.exists():\n", + " print(\"Downloading …\")\n", + " urllib.request.urlretrieve(WHITE_URL, DATA_DIR / \"winequality-white.csv\")\n", + " urllib.request.urlretrieve(RED_URL, DATA_DIR / \"winequality-red.csv\")\n", + "\n", + " white = pd.read_csv(DATA_DIR / \"winequality-white.csv\", sep=\";\")\n", + " red = pd.read_csv(DATA_DIR / \"winequality-red.csv\", sep=\";\")\n", + " df = pd.concat([white, red], ignore_index=True)\n", + " df[\"quality\"] = (df[\"quality\"] >= 7).astype(int)\n", + " df.to_csv(combined_path, index=False)\n", + "else:\n", + " df = pd.read_csv(combined_path)\n", + "\n", + "print(f\"{len(df)} rows | {df['quality'].mean():.1%} positive (quality >= 7)\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "8edb47106e1a46a883d545849b8ab81b", + "metadata": {}, + "source": [ + "## Define search space\n", + "\n", + "The `hyperopt` section of the Ludwig config specifies:\n", + "\n", + "- **executor** — which HPO backend to use and how many trials to run\n", + "- **parameters** — the search space for each hyperparameter\n", + "- **goal / metric** — what to optimise\n", + "\n", + "The Optuna executor supports the following `space` types:\n", + "\n", + "| Space | Ludwig key | Description |\n", + "|---|---|---|\n", + "| Log-uniform float | `loguniform` | Continuous on log scale — ideal for learning rates |\n", + "| Uniform float | `float` | Continuous on linear scale — ideal for dropout |\n", + "| Integer | `int` | Integer range, linear scale |\n", + "| Categorical | `choice` | Discrete set of values |\n", + "| Grid | `grid_search` | Exhaustive grid over a list of values |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10185d26023b46108eb7d9f57d49d2b3", + "metadata": {}, + "outputs": [], + "source": [ + "# Build feature list dynamically from the dataframe\n", + "feature_cols = [c for c in df.columns if c != \"quality\"]\n", + "\n", + "config = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [\n", + " {\"name\": col, \"type\": \"number\", \"preprocessing\": {\"normalization\": \"zscore\"}} for col in feature_cols\n", + " ],\n", + " \"output_features\": [\n", + " {\"name\": \"quality\", \"type\": \"binary\"},\n", + " ],\n", + " \"trainer\": {\n", + " \"epochs\": 20,\n", + " },\n", + " # NOTE: type: optuna requires Ludwig >= 0.14 (PR #4090)\n", + " \"hyperopt\": {\n", + " \"executor\": {\n", + " \"type\": \"optuna\",\n", + " \"num_samples\": 20,\n", + " \"sampler\": \"auto\", # auto, tpe, gp, cmaes, random\n", + " \"pruner\": \"hyperband\", # stop bad trials early\n", + " },\n", + " \"parameters\": {\n", + " \"trainer.learning_rate\": {\n", + " \"space\": \"loguniform\",\n", + " \"lower\": 1e-5,\n", + " \"upper\": 1e-2,\n", + " },\n", + " \"trainer.batch_size\": {\n", + " \"space\": \"int\",\n", + " \"lower\": 16,\n", + " \"upper\": 256,\n", + " },\n", + " \"trainer.optimizer.type\": {\n", + " \"space\": \"choice\",\n", + " \"categories\": [\"adam\", \"adamw\", \"radam\", \"schedule_free_adamw\"],\n", + " },\n", + " \"combiner.dropout\": {\n", + " \"space\": \"float\",\n", + " \"lower\": 0.0,\n", + " \"upper\": 0.5,\n", + " },\n", + " },\n", + " \"goal\": \"minimize\",\n", + " \"metric\": \"validation.combined.loss\",\n", + " \"split\": \"validation\",\n", + " },\n", + "}\n", + "\n", + "import json\n", + "\n", + "print(json.dumps(config[\"hyperopt\"], indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "8763a12b2bbd4a93a75aff182afb95dc", + "metadata": {}, + "source": [ + "## Run HPO\n", + "\n", + "`LudwigModel.hyperopt()` runs the full HPO loop and returns a list of trial results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7623eae2785240b9bd12b16a66d81610", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.api import LudwigModel\n", + "\n", + "model = LudwigModel(config=config, logging_level=20)\n", + "\n", + "hyperopt_results, output_dir, _ = model.hyperopt(\n", + " dataset=str(combined_path),\n", + " output_directory=\"hyperopt_output\",\n", + ")\n", + "\n", + "print(f\"\\nCompleted {len(hyperopt_results)} trials. Output: {output_dir}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cdc8c89c7104fffa095e18ddfef8986", + "metadata": {}, + "outputs": [], + "source": [ + "# Show top-5 trials sorted by metric\n", + "sorted_results = sorted(hyperopt_results, key=lambda r: r.get(\"metric_score\", float(\"inf\")))\n", + "\n", + "rows = []\n", + "for i, r in enumerate(sorted_results[:5]):\n", + " row = {\"rank\": i + 1, \"loss\": round(r.get(\"metric_score\", float(\"nan\")), 5)}\n", + " row.update(r.get(\"parameters\", {}))\n", + " rows.append(row)\n", + "\n", + "pd.DataFrame(rows)" + ] + }, + { + "cell_type": "markdown", + "id": "b118ea5561624da68c537baed56e602f", + "metadata": {}, + "source": [ + "## Sampler comparison\n", + "\n", + "Ludwig's Optuna executor exposes all of Optuna's built-in samplers via the `sampler` key.\n", + "\n", + "| Sampler | Key | Best for |\n", + "|---|---|---|\n", + "| **Auto** | `auto` | Default — Optuna selects the best sampler based on search space type |\n", + "| **TPE** | `tpe` | General purpose; efficient with < 100 trials; the classic Optuna default |\n", + "| **CMA-ES** | `cmaes` | Continuous spaces with many parameters; covariance matrix adaptation |\n", + "| **GP (BoTorch)** | `gp` | Sample-efficient Bayesian optimisation; requires `pip install botorch` |\n", + "| **Random** | `random` | Baseline; useful for ablations or very large search spaces |\n", + "\n", + "Change the sampler by editing the executor block:\n", + "\n", + "```python\n", + "\"executor\": {\n", + " \"type\": \"optuna\",\n", + " \"num_samples\": 50,\n", + " \"sampler\": \"tpe\", # <-- change this\n", + "}\n", + "```\n", + "\n", + "For GP, install the optional dependency first:\n", + "\n", + "```bash\n", + "pip install botorch\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "938c804e27f84196a10c8828c723f798", + "metadata": {}, + "source": [ + "## Resumable HPO with SQLite\n", + "\n", + "If your HPO run is interrupted (Colab runtime reset, preempted spot instance, etc.) you can\n", + "resume from where you left off by pointing Optuna at a **persistent storage** backend.\n", + "\n", + "Add a `storage` key to the executor:\n", + "\n", + "```python\n", + "\"executor\": {\n", + " \"type\": \"optuna\",\n", + " \"num_samples\": 50,\n", + " \"sampler\": \"auto\",\n", + " \"storage\": \"sqlite:///optuna_results.db\", # <-- persist to disk\n", + "}\n", + "```\n", + "\n", + "Re-running `model.hyperopt()` with the same storage path will **continue the existing\n", + "study** rather than starting a new one. Optuna automatically detects how many trials\n", + "have already been completed and runs only the remaining ones.\n", + "\n", + "For distributed or cloud setups you can also use a PostgreSQL URL:\n", + "\n", + "```python\n", + "\"storage\": \"postgresql://user:pass@host/dbname\"\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "504fb2a444614c0babb325280ed9130a", + "metadata": {}, + "outputs": [], + "source": [ + "# Example: run with SQLite storage for resumability\n", + "import copy\n", + "\n", + "config_resumable = copy.deepcopy(config)\n", + "config_resumable[\"hyperopt\"][\"executor\"][\"storage\"] = \"sqlite:///optuna_results.db\"\n", + "config_resumable[\"hyperopt\"][\"executor\"][\"num_samples\"] = 10 # fewer trials for demo\n", + "\n", + "print(\"Executor config:\")\n", + "print(json.dumps(config_resumable[\"hyperopt\"][\"executor\"], indent=2))\n", + "print(\"\\nRe-running with storage enabled — existing trials will be reused.\")\n", + "\n", + "model2 = LudwigModel(config=config_resumable, logging_level=20)\n", + "results2, _, _ = model2.hyperopt(\n", + " dataset=str(combined_path),\n", + " output_directory=\"hyperopt_output_resumable\",\n", + ")\n", + "print(f\"Done. {len(results2)} trials.\")" + ] + }, + { + "cell_type": "markdown", + "id": "59bbdb311c014d738909a11f9e486628", + "metadata": {}, + "source": [ + "## Pruner: stop bad trials early\n", + "\n", + "A **pruner** monitors intermediate results reported during training and stops trials that\n", + "are unlikely to beat the current best. This can dramatically reduce total compute when\n", + "combined with epoch-level reporting.\n", + "\n", + "Ludwig's Optuna executor supports:\n", + "\n", + "| Pruner | Key | Description |\n", + "|---|---|---|\n", + "| **Hyperband** | `hyperband` | Successive halving over training steps; efficient for deep learning |\n", + "| **Median** | `median` | Stops trials below the median performance at a given step |\n", + "| **None** | *(omit key)* | No pruning; every trial runs to completion |\n", + "\n", + "```python\n", + "\"executor\": {\n", + " \"type\": \"optuna\",\n", + " \"num_samples\": 50,\n", + " \"sampler\": \"auto\",\n", + " \"pruner\": \"hyperband\", # <-- add this\n", + "}\n", + "```\n", + "\n", + "Hyperband is the recommended default for neural network HPO. It requires at least\n", + "`min_resource` epochs (default 1) to have completed before making pruning decisions,\n", + "so short-running models (< 5 epochs) may see limited benefit." + ] + }, + { + "cell_type": "markdown", + "id": "b43b363d81ae4b689946ece5c682cd59", + "metadata": {}, + "source": [ + "## Results\n", + "\n", + "The cells below plot a **parallel coordinates** chart — each line is one trial,\n", + "colour-coded by the validation loss. Narrow bundles indicate which regions of\n", + "the search space consistently produce good results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a65eabff63a45729fe45fb5ade58bdc", + "metadata": {}, + "outputs": [], + "source": [ + "# Build a dataframe of all trial results\n", + "records = []\n", + "for r in hyperopt_results:\n", + " row = {\"loss\": r.get(\"metric_score\", float(\"nan\"))}\n", + " row.update(r.get(\"parameters\", {}))\n", + " records.append(row)\n", + "\n", + "results_df = pd.DataFrame(records)\n", + "print(f\"{len(results_df)} trials\")\n", + "results_df.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3933fab20d04ec698c2621248eb3be0", + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "\n", + "# Map categorical optimizer to numeric for colour scale\n", + "opt_map = {v: i for i, v in enumerate(results_df[\"trainer.optimizer.type\"].unique())}\n", + "results_df[\"optimizer_idx\"] = results_df[\"trainer.optimizer.type\"].map(opt_map)\n", + "\n", + "dims = [\n", + " dict(label=\"learning_rate\", values=results_df[\"trainer.learning_rate\"], type=\"log\"),\n", + " dict(label=\"batch_size\", values=results_df[\"trainer.batch_size\"]),\n", + " dict(\n", + " label=\"optimizer\",\n", + " values=results_df[\"optimizer_idx\"],\n", + " tickvals=list(opt_map.values()),\n", + " ticktext=list(opt_map.keys()),\n", + " ),\n", + " dict(label=\"dropout\", values=results_df[\"combiner.dropout\"]),\n", + " dict(label=\"val loss\", values=results_df[\"loss\"]),\n", + "]\n", + "\n", + "fig = px.parallel_coordinates(\n", + " results_df,\n", + " dimensions=[\"trainer.learning_rate\", \"trainer.batch_size\", \"optimizer_idx\", \"combiner.dropout\", \"loss\"],\n", + " color=\"loss\",\n", + " color_continuous_scale=px.colors.sequential.Viridis_r,\n", + " labels={\n", + " \"trainer.learning_rate\": \"learning rate\",\n", + " \"trainer.batch_size\": \"batch size\",\n", + " \"optimizer_idx\": \"optimizer\",\n", + " \"combiner.dropout\": \"dropout\",\n", + " \"loss\": \"val loss\",\n", + " },\n", + " title=\"HPO trials — parallel coordinates (lower loss is better)\",\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dd4641cc4064e0191573fe9c69df29b", + "metadata": {}, + "outputs": [], + "source": [ + "# Print best configuration\n", + "best = sorted_results[0]\n", + "print(f\"Best validation loss : {best['metric_score']:.5f}\")\n", + "print(\"\\nBest hyperparameters:\")\n", + "for k, v in best[\"parameters\"].items():\n", + " print(f\" {k:35s} = {v}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/hyperopt/optuna_executor.py b/examples/hyperopt/optuna_executor.py new file mode 100644 index 0000000..55bb2ff --- /dev/null +++ b/examples/hyperopt/optuna_executor.py @@ -0,0 +1,145 @@ +""" +Hyperparameter Optimization with Native Optuna Executor +======================================================== + +NOTE: Requires PR #4090 to be merged, or Ludwig >= 0.14. + Install dependencies: pip install ludwig optuna + +Usage: + python optuna_executor.py + +The script downloads the UCI Wine Quality dataset, binarises the target +(quality >= 7), and runs Ludwig HPO using the native Optuna executor. +Results are persisted in `optuna_results.db` so interrupted runs can +be resumed by simply re-running the script. +""" + +# Colab: !pip install ludwig optuna --quiet + +import pathlib +import urllib.request + +import pandas as pd + +# --------------------------------------------------------------------------- +# 1. Download dataset +# --------------------------------------------------------------------------- +DATA_DIR = pathlib.Path("data") +DATA_DIR.mkdir(exist_ok=True) + +WHITE_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv" +RED_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv" + +white_path = DATA_DIR / "winequality-white.csv" +red_path = DATA_DIR / "winequality-red.csv" +combined_path = DATA_DIR / "wine_quality.csv" + +if not combined_path.exists(): + print("Downloading Wine Quality dataset from UCI …") + urllib.request.urlretrieve(WHITE_URL, white_path) + urllib.request.urlretrieve(RED_URL, red_path) + + white = pd.read_csv(white_path, sep=";") + red = pd.read_csv(red_path, sep=";") + df = pd.concat([white, red], ignore_index=True) + + # Binary target: 1 if quality >= 7, else 0 + df["quality"] = (df["quality"] >= 7).astype(int) + + df.to_csv(combined_path, index=False) + print(f"Dataset saved to {combined_path} ({len(df)} rows)") +else: + print(f"Dataset already present at {combined_path}") + df = pd.read_csv(combined_path) + print(f" {len(df)} rows, class balance: {df['quality'].mean():.1%} positive") + +# --------------------------------------------------------------------------- +# 2. Ludwig config +# --------------------------------------------------------------------------- +config = { + "model_type": "ecd", + "input_features": [ + {"name": col, "type": "number", "preprocessing": {"normalization": "zscore"}} + for col in df.columns + if col != "quality" + ], + "output_features": [ + {"name": "quality", "type": "binary"}, + ], + "trainer": { + "epochs": 20, + }, + # NOTE: Optuna executor is available from Ludwig >= 0.14 (PR #4090). + "hyperopt": { + "executor": { + "type": "optuna", + "num_samples": 20, + # 'auto' lets Optuna pick the best sampler given the search space. + # Alternatives: 'tpe', 'gp', 'cmaes', 'random' + "sampler": "auto", + # Hyperband pruner stops unpromising trials early. + "pruner": "hyperband", + # SQLite storage makes runs resumable: re-run the script and + # Optuna will continue from where it left off. + "storage": "sqlite:///optuna_results.db", + }, + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 1e-5, + "upper": 1e-2, + }, + "trainer.batch_size": { + "space": "int", + "lower": 16, + "upper": 256, + }, + "trainer.optimizer.type": { + "space": "choice", + "categories": ["adam", "adamw", "radam", "schedule_free_adamw"], + }, + "combiner.dropout": { + "space": "float", + "lower": 0.0, + "upper": 0.5, + }, + }, + "goal": "minimize", + "metric": "validation.combined.loss", + "split": "validation", + }, +} + +# --------------------------------------------------------------------------- +# 3. Run HPO +# --------------------------------------------------------------------------- +try: + from ludwig.api import LudwigModel +except ImportError: + raise SystemExit("Ludwig is not installed. Run: pip install ludwig optuna") + +print("\nStarting hyperparameter optimisation with Optuna …") +print("Results are persisted in optuna_results.db — re-run to resume.\n") + +model = LudwigModel(config=config, logging_level=20) # INFO + +hyperopt_results, _, _ = model.hyperopt( + dataset=str(combined_path), + output_directory="hyperopt_output", +) + +# --------------------------------------------------------------------------- +# 4. Report results +# --------------------------------------------------------------------------- +print("\n" + "=" * 60) +print("HPO complete") +print("=" * 60) + +if hyperopt_results: + best = min(hyperopt_results, key=lambda r: r.get("metric_score", float("inf"))) + print(f"\nBest metric (validation.combined.loss): {best.get('metric_score', 'n/a'):.4f}") + print("\nBest hyperparameters:") + for param, value in best.get("parameters", {}).items(): + print(f" {param}: {value}") +else: + print("No results returned — check logs above for errors.") diff --git a/examples/image_encoders/README.md b/examples/image_encoders/README.md new file mode 100644 index 0000000..522ab21 --- /dev/null +++ b/examples/image_encoders/README.md @@ -0,0 +1,78 @@ +# Pretrained Image Encoders: CLIP, DINOv2, and SigLIP + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/image_encoders/image_encoders.ipynb) + +## Overview + +Pretrained image encoders are neural networks trained on large datasets (ImageNet-21k, LAION-5B, or proprietary corpora) whose learned weights can be directly transferred to new tasks. Instead of training a convolutional network from scratch on your small dataset, you can use a pretrained encoder as a frozen feature extractor and only train a lightweight classification head on top—this is called **linear probing**. + +### Why pretrained encoders matter for few-shot learning + +When you have limited labeled data (e.g., 5–100 examples per class), training from scratch typically leads to overfitting. Pretrained encoders solve this by: + +- Providing rich, general-purpose visual features learned from millions of images +- Allowing the model to converge in far fewer epochs +- Requiring only a small head to be trained, which needs very little data + +Ludwig supports three HuggingFace-backed pretrained image encoders alongside the traditional `stacked_cnn` approach. + +## Encoder comparison + +| Encoder | Pretrained | Trainable by default | Best for | +| ------------- | ---------- | -------------------- | --------------------------------------------------------------------------- | +| `stacked_cnn` | No | Yes | Full control, small images, custom architectures | +| `dinov2` | Yes | Yes | General image classification, dense prediction, linear probing | +| `clip` | Yes | Yes | Image-text tasks, zero-shot classification, multimodal fusion | +| `siglip` | Yes | Yes | CLIP-like tasks with better scaling, Google's improved contrastive training | + +All three pretrained encoders (`dinov2`, `clip`, `siglip`) support: + +- `use_pretrained: true` — load weights from HuggingFace Hub +- `trainable: false` — freeze the encoder for fast linear probing +- `trainable: true` — fine-tune the full encoder end-to-end + +## Quick start + +Install Ludwig with vision support: + +```bash +pip install ludwig[vision] +``` + +Train with a pretrained DINOv2 encoder (linear probe — fast, works well with limited data): + +```bash +ludwig train \ + --config examples/image_encoders/config_dinov2_linear_probe.yaml \ + --dataset my_images.csv +``` + +Your CSV needs two columns: `image_path` (absolute or relative paths to image files) and `label` (the class name). + +## Available configs + +| Config file | Description | +| --------------------------------- | --------------------------------------------- | +| `config_stacked_cnn.yaml` | CNN trained from scratch (20 epochs) | +| `config_dinov2_linear_probe.yaml` | DINOv2 frozen backbone, head only (10 epochs) | +| `config_dinov2_finetuned.yaml` | DINOv2 full fine-tune (5 epochs, lower LR) | +| `config_clip.yaml` | CLIP frozen backbone (10 epochs) | +| `config_siglip.yaml` | SigLIP frozen backbone (10 epochs) | + +## Running all configs and comparing results + +```bash +python examples/image_encoders/compare_encoders.py --dataset my_images.csv +``` + +## Full walkthrough + +See the [notebook](image_encoders.ipynb) for a complete step-by-step example using the `beans` plant disease dataset (3 classes, ~1000 images) from HuggingFace Datasets, including a few-shot experiment with only 15 training examples. + +## Hardware requirements + +- `stacked_cnn`: CPU or GPU +- `dinov2` (linear probe): GPU recommended, runs on CPU for small datasets +- `dinov2` (fine-tune), `clip`, `siglip`: GPU required (T4 or better) + +The linear probe is especially well-suited for Google Colab free tier (T4 GPU). diff --git a/examples/image_encoders/compare_encoders.py b/examples/image_encoders/compare_encoders.py new file mode 100644 index 0000000..ed23adf --- /dev/null +++ b/examples/image_encoders/compare_encoders.py @@ -0,0 +1,183 @@ +"""Compare pretrained image encoders: stacked_cnn, DINOv2, CLIP, and SigLIP. + +Runs all encoder configs on a dataset and prints a comparison table of +accuracy, training time, and approximate GPU memory usage. + +Usage: + python compare_encoders.py --dataset /path/to/data.csv + +The dataset CSV must have columns: image_path, label + +Example (using the beans dataset — see the notebook for download instructions): + python compare_encoders.py --dataset /tmp/beans/train.csv \ + --val_dataset /tmp/beans/validation.csv \ + --test_dataset /tmp/beans/test.csv +""" + +import argparse +import os +import time +from pathlib import Path + +SCRIPT_DIR = Path(__file__).parent + + +def get_gpu_memory_mb() -> float: + """Return current GPU memory usage in MB, or 0 if no GPU available.""" + try: + import torch + + if torch.cuda.is_available(): + return torch.cuda.max_memory_allocated() / 1024**2 + except ImportError: + pass + return 0.0 + + +def reset_gpu_memory_stats() -> None: + try: + import torch + + if torch.cuda.is_available(): + torch.cuda.reset_peak_memory_stats() + except ImportError: + pass + + +def run_experiment( + config_path: str, + dataset: str, + val_dataset: str | None, + test_dataset: str | None, + output_dir: str, +) -> dict: + """Train and evaluate one config. + + Returns a dict with result metrics. + """ + from ludwig.api import LudwigModel + + reset_gpu_memory_stats() + start = time.time() + + model = LudwigModel(config=config_path, logging_level=30) # WARNING level + + train_kwargs = dict(dataset=dataset, output_directory=output_dir, skip_save_processed_input=True) + if val_dataset: + train_kwargs["validation_set"] = val_dataset + + _, _, output_directory = model.train(**train_kwargs) + train_time = time.time() - start + peak_mem = get_gpu_memory_mb() + + # Evaluate on test set + eval_dataset = test_dataset or dataset + eval_stats, _, _ = model.evaluate(dataset=eval_dataset, collect_overall_stats=True) + + accuracy = 0.0 + if "label" in eval_stats: + accuracy = eval_stats["label"].get("accuracy", 0.0) + + return { + "train_time_s": train_time, + "peak_gpu_mb": peak_mem, + "accuracy": accuracy, + "output_directory": output_directory, + } + + +CONFIGS = [ + ("stacked_cnn", "config_stacked_cnn.yaml"), + ("dinov2_linear_probe", "config_dinov2_linear_probe.yaml"), + ("dinov2_finetuned", "config_dinov2_finetuned.yaml"), + ("clip", "config_clip.yaml"), + ("siglip", "config_siglip.yaml"), +] + + +def print_table(results: list[dict]) -> None: + header = f"{'Encoder':<25} {'Accuracy':>10} {'Train time':>12} {'Peak GPU (MB)':>15}" + sep = "-" * len(header) + print("\n" + sep) + print(header) + print(sep) + for r in results: + name = r["name"] + acc = r.get("accuracy", float("nan")) + t = r.get("train_time_s", float("nan")) + mem = r.get("peak_gpu_mb", 0.0) + mem_str = f"{mem:>15.0f}" if mem > 0 else f"{'N/A':>15}" + print(f"{name:<25} {acc:>10.4f} {t:>11.1f}s {mem_str}") + print(sep + "\n") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Compare Ludwig image encoder configs.") + parser.add_argument("--dataset", required=True, help="Path to training CSV (image_path, label columns).") + parser.add_argument("--val_dataset", default=None, help="Path to validation CSV.") + parser.add_argument("--test_dataset", default=None, help="Path to test CSV (used for evaluation).") + parser.add_argument( + "--output_dir", + default="/tmp/image_encoder_results", + help="Base output directory for Ludwig experiment results.", + ) + parser.add_argument( + "--encoders", + nargs="+", + default=None, + help="Subset of encoders to run (e.g. --encoders stacked_cnn dinov2_linear_probe). Defaults to all encoders.", + ) + args = parser.parse_args() + + configs_to_run = CONFIGS + if args.encoders: + valid = {name for name, _ in CONFIGS} + for e in args.encoders: + if e not in valid: + parser.error(f"Unknown encoder '{e}'. Valid: {sorted(valid)}") + configs_to_run = [(name, cfg) for name, cfg in CONFIGS if name in args.encoders] + + results = [] + for name, config_file in configs_to_run: + config_path = str(SCRIPT_DIR / config_file) + if not os.path.exists(config_path): + print(f"[SKIP] Config not found: {config_path}") + continue + + output_dir = os.path.join(args.output_dir, name) + os.makedirs(output_dir, exist_ok=True) + + print(f"\n{'=' * 60}") + print(f"Running: {name}") + print(f"Config: {config_path}") + print(f"{'=' * 60}") + + try: + metrics = run_experiment( + config_path=config_path, + dataset=args.dataset, + val_dataset=args.val_dataset, + test_dataset=args.test_dataset, + output_dir=output_dir, + ) + results.append({"name": name, **metrics}) + print( + f"Done: accuracy={metrics['accuracy']:.4f}, " + f"time={metrics['train_time_s']:.1f}s, " + f"peak_gpu={metrics['peak_gpu_mb']:.0f}MB" + ) + except Exception as exc: + print(f"[ERROR] {name} failed: {exc}") + results.append({"name": name, "error": str(exc)}) + + print_table([r for r in results if "error" not in r]) + + failed = [r for r in results if "error" in r] + if failed: + print("Failed experiments:") + for r in failed: + print(f" {r['name']}: {r['error']}") + + +if __name__ == "__main__": + main() diff --git a/examples/image_encoders/config_clip.yaml b/examples/image_encoders/config_clip.yaml new file mode 100644 index 0000000..ae41296 --- /dev/null +++ b/examples/image_encoders/config_clip.yaml @@ -0,0 +1,14 @@ +model_type: ecd +input_features: + - name: image_path + type: image + encoder: + type: clip + use_pretrained: true + trainable: false +output_features: + - name: label + type: category +trainer: + epochs: 10 + learning_rate: 0.001 diff --git a/examples/image_encoders/config_dinov2_finetuned.yaml b/examples/image_encoders/config_dinov2_finetuned.yaml new file mode 100644 index 0000000..9daad4c --- /dev/null +++ b/examples/image_encoders/config_dinov2_finetuned.yaml @@ -0,0 +1,14 @@ +model_type: ecd +input_features: + - name: image_path + type: image + encoder: + type: dinov2 + use_pretrained: true + trainable: true +output_features: + - name: label + type: category +trainer: + epochs: 5 + learning_rate: 0.0001 diff --git a/examples/image_encoders/config_dinov2_linear_probe.yaml b/examples/image_encoders/config_dinov2_linear_probe.yaml new file mode 100644 index 0000000..1577604 --- /dev/null +++ b/examples/image_encoders/config_dinov2_linear_probe.yaml @@ -0,0 +1,14 @@ +model_type: ecd +input_features: + - name: image_path + type: image + encoder: + type: dinov2 + use_pretrained: true + trainable: false +output_features: + - name: label + type: category +trainer: + epochs: 10 + learning_rate: 0.001 diff --git a/examples/image_encoders/config_siglip.yaml b/examples/image_encoders/config_siglip.yaml new file mode 100644 index 0000000..ce86190 --- /dev/null +++ b/examples/image_encoders/config_siglip.yaml @@ -0,0 +1,14 @@ +model_type: ecd +input_features: + - name: image_path + type: image + encoder: + type: siglip + use_pretrained: true + trainable: false +output_features: + - name: label + type: category +trainer: + epochs: 10 + learning_rate: 0.001 diff --git a/examples/image_encoders/config_stacked_cnn.yaml b/examples/image_encoders/config_stacked_cnn.yaml new file mode 100644 index 0000000..75dade9 --- /dev/null +++ b/examples/image_encoders/config_stacked_cnn.yaml @@ -0,0 +1,13 @@ +model_type: ecd +input_features: + - name: image_path + type: image + encoder: + type: stacked_cnn + use_pretrained: false +output_features: + - name: label + type: category +trainer: + epochs: 20 + learning_rate: 0.001 diff --git a/examples/image_encoders/image_encoders.ipynb b/examples/image_encoders/image_encoders.ipynb new file mode 100644 index 0000000..7005231 --- /dev/null +++ b/examples/image_encoders/image_encoders.ipynb @@ -0,0 +1,741 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a1b2c3d4", + "metadata": {}, + "source": [ + "# Pretrained Image Encoders: CLIP, DINOv2, and SigLIP\n", + "\n", + "This notebook compares four approaches to image classification in Ludwig:\n", + "\n", + "1. **stacked_cnn** — a convolutional network trained from scratch\n", + "2. **DINOv2 linear probe** — pretrained DINOv2 backbone, frozen; only the head is trained\n", + "3. **DINOv2 fine-tuned** — pretrained DINOv2 backbone, full fine-tuning\n", + "4. **CLIP** — pretrained CLIP vision encoder, frozen\n", + "5. **SigLIP** — pretrained SigLIP vision encoder, frozen\n", + "\n", + "We use the [`beans`](https://huggingface.co/datasets/beans) dataset: a plant disease dataset with 3 classes (~1000 images total), which makes it a realistic small-data scenario where pretrained encoders shine.\n", + "\n", + "**Runtime**: GPU is required for DINOv2/CLIP/SigLIP. In Colab, go to **Runtime → Change runtime type → T4 GPU**." + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig[vision] datasets --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import warnings\n", + "\n", + "import torch\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "print(f\"PyTorch version: {torch.__version__}\")\n", + "print(f\"CUDA available: {torch.cuda.is_available()}\")\n", + "if torch.cuda.is_available():\n", + " print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n", + " print(f\"GPU memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5f6a7b8", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "The `beans` dataset contains RGB images of bean leaves classified into three categories:\n", + "- `angular_leaf_spot` — fungal disease\n", + "- `bean_rust` — rust disease \n", + "- `healthy` — healthy leaf\n", + "\n", + "It has ~1034 training images, 133 validation images, and 128 test images. This makes it a realistic small-data scenario." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6a7b8c9", + "metadata": {}, + "outputs": [], + "source": [ + "import csv\n", + "from pathlib import Path\n", + "\n", + "from datasets import load_dataset\n", + "\n", + "# Download the beans dataset from HuggingFace\n", + "ds = load_dataset(\"beans\")\n", + "print(ds)\n", + "\n", + "# Save images to disk and create CSV files\n", + "DATA_DIR = Path(\"/tmp/beans\")\n", + "DATA_DIR.mkdir(parents=True, exist_ok=True)\n", + "\n", + "label_names = ds[\"train\"].features[\"labels\"].names\n", + "print(f\"Classes: {label_names}\")\n", + "\n", + "\n", + "def save_split(split_name: str) -> str:\n", + " \"\"\"Save images to disk and return path to a CSV with image_path and label columns.\"\"\"\n", + " split_dir = DATA_DIR / split_name\n", + " split_dir.mkdir(parents=True, exist_ok=True)\n", + " csv_path = DATA_DIR / f\"{split_name}.csv\"\n", + "\n", + " rows = []\n", + " for i, example in enumerate(ds[split_name]):\n", + " img = example[\"image\"]\n", + " label_idx = example[\"labels\"]\n", + " label = label_names[label_idx]\n", + "\n", + " img_path = split_dir / f\"{i:04d}_{label}.jpg\"\n", + " if not img_path.exists():\n", + " img.save(str(img_path))\n", + "\n", + " rows.append({\"image_path\": str(img_path), \"label\": label})\n", + "\n", + " with open(csv_path, \"w\", newline=\"\") as f:\n", + " writer = csv.DictWriter(f, fieldnames=[\"image_path\", \"label\"])\n", + " writer.writeheader()\n", + " writer.writerows(rows)\n", + "\n", + " print(f\"Saved {len(rows)} examples to {csv_path}\")\n", + " return str(csv_path)\n", + "\n", + "\n", + "train_csv = save_split(\"train\")\n", + "val_csv = save_split(\"validation\")\n", + "test_csv = save_split(\"test\")\n", + "\n", + "print(f\"\\nTrain: {train_csv}\")\n", + "print(f\"Val: {val_csv}\")\n", + "print(f\"Test: {test_csv}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7b8c9d0", + "metadata": {}, + "outputs": [], + "source": [ + "# Preview a few examples\n", + "import pandas as pd\n", + "\n", + "train_df = pd.read_csv(train_csv)\n", + "print(train_df.head())\n", + "print(f\"\\nClass distribution:\\n{train_df['label'].value_counts()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8c9d0e1", + "metadata": {}, + "outputs": [], + "source": [ + "# Visualize a few images\n", + "import matplotlib.pyplot as plt\n", + "from PIL import Image\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(12, 4))\n", + "for i, (label, group) in enumerate(train_df.groupby(\"label\")):\n", + " sample = group.sample(1).iloc[0]\n", + " img = Image.open(sample[\"image_path\"])\n", + " axes[i].imshow(img)\n", + " axes[i].set_title(label)\n", + " axes[i].axis(\"off\")\n", + "plt.suptitle(\"Beans dataset — one example per class\", fontsize=13)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "c9d0e1f2", + "metadata": {}, + "source": [ + "## Helper: timing and memory tracking" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0e1f2a3", + "metadata": {}, + "outputs": [], + "source": [ + "import gc\n", + "\n", + "results = {} # Will accumulate {encoder_name: {accuracy, train_time_s, peak_gpu_mb}}\n", + "\n", + "\n", + "def reset_gpu():\n", + " gc.collect()\n", + " if torch.cuda.is_available():\n", + " torch.cuda.empty_cache()\n", + " torch.cuda.reset_peak_memory_stats()\n", + "\n", + "\n", + "def peak_gpu_mb() -> float:\n", + " if torch.cuda.is_available():\n", + " return torch.cuda.max_memory_allocated() / 1024**2\n", + " return 0.0\n", + "\n", + "\n", + "def evaluate_model(model, test_csv: str) -> float:\n", + " \"\"\"Return accuracy on the test set.\"\"\"\n", + " eval_stats, _, _ = model.evaluate(dataset=test_csv, collect_overall_stats=True)\n", + " return eval_stats.get(\"label\", {}).get(\"accuracy\", float(\"nan\"))" + ] + }, + { + "cell_type": "markdown", + "id": "e1f2a3b4", + "metadata": {}, + "source": [ + "## Approach 1: Stacked CNN (trained from scratch)\n", + "\n", + "The `stacked_cnn` encoder is Ludwig's default: a stack of 2D convolutional layers followed by fully connected layers. There are no pretrained weights — the model learns everything from the 1034 training images.\n", + "\n", + "This is the baseline. Expect lower accuracy and longer training time compared to pretrained encoders on this small dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2a3b4c5", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.api import LudwigModel\n", + "\n", + "config_stacked_cnn = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"encoder\": {\n", + " \"type\": \"stacked_cnn\",\n", + " \"use_pretrained\": False,\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\"}],\n", + " \"trainer\": {\"epochs\": 20, \"learning_rate\": 0.001},\n", + "}\n", + "\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "\n", + "model_cnn = LudwigModel(config=config_stacked_cnn, logging_level=30)\n", + "_, _, output_dir = model_cnn.train(\n", + " dataset=train_csv,\n", + " validation_set=val_csv,\n", + " output_directory=\"/tmp/results/stacked_cnn\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "\n", + "train_time = time.time() - t0\n", + "gpu_mem = peak_gpu_mb()\n", + "accuracy = evaluate_model(model_cnn, test_csv)\n", + "\n", + "results[\"stacked_cnn\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time, \"peak_gpu_mb\": gpu_mem}\n", + "print(f\"stacked_cnn — accuracy: {accuracy:.4f}, time: {train_time:.1f}s, peak GPU: {gpu_mem:.0f}MB\")" + ] + }, + { + "cell_type": "markdown", + "id": "a3b4c5d6", + "metadata": {}, + "source": [ + "## Approach 2: DINOv2 linear probe\n", + "\n", + "DINOv2 (Oquab et al., TMLR 2024) is a self-supervised vision transformer from Meta trained on 142M images. It produces rich general-purpose features without requiring image-text pairs.\n", + "\n", + "In **linear probe** mode (`trainable: false`), the DINOv2 backbone is frozen — only the Ludwig output head (a small linear layer) is trained. This is:\n", + "- **Fast**: no backprop through the large backbone\n", + "- **Data-efficient**: works well with very few labeled examples\n", + "- **Memory-efficient**: no gradients stored for the backbone\n", + "\n", + "Default model: `facebook/dinov2-base` (86M parameters, 768-dim output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4c5d6e7", + "metadata": {}, + "outputs": [], + "source": [ + "config_dinov2_probe = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"encoder\": {\n", + " \"type\": \"dinov2\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": False, # freeze the backbone\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\"}],\n", + " \"trainer\": {\"epochs\": 10, \"learning_rate\": 0.001},\n", + "}\n", + "\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "\n", + "model_dinov2_probe = LudwigModel(config=config_dinov2_probe, logging_level=30)\n", + "_, _, output_dir = model_dinov2_probe.train(\n", + " dataset=train_csv,\n", + " validation_set=val_csv,\n", + " output_directory=\"/tmp/results/dinov2_linear_probe\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "\n", + "train_time = time.time() - t0\n", + "gpu_mem = peak_gpu_mb()\n", + "accuracy = evaluate_model(model_dinov2_probe, test_csv)\n", + "\n", + "results[\"dinov2_linear_probe\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time, \"peak_gpu_mb\": gpu_mem}\n", + "print(f\"DINOv2 linear probe — accuracy: {accuracy:.4f}, time: {train_time:.1f}s, peak GPU: {gpu_mem:.0f}MB\")" + ] + }, + { + "cell_type": "markdown", + "id": "c5d6e7f8", + "metadata": {}, + "source": [ + "## Approach 3: DINOv2 fine-tuned\n", + "\n", + "In **fine-tuning** mode (`trainable: true`), gradients flow through the entire DINOv2 backbone. This can improve accuracy over linear probing but requires:\n", + "- More GPU memory (stores activations for backprop through 86M parameters)\n", + "- A lower learning rate (to avoid destroying pretrained features)\n", + "- Fewer epochs (the model has a strong starting point)\n", + "\n", + "Use fine-tuning when you have enough data and GPU memory and want maximum accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6e7f8a9", + "metadata": {}, + "outputs": [], + "source": [ + "config_dinov2_finetune = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"encoder\": {\n", + " \"type\": \"dinov2\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": True, # full fine-tuning\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\"}],\n", + " \"trainer\": {\n", + " \"epochs\": 5,\n", + " \"learning_rate\": 0.0001, # lower LR for fine-tuning\n", + " },\n", + "}\n", + "\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "\n", + "model_dinov2_ft = LudwigModel(config=config_dinov2_finetune, logging_level=30)\n", + "_, _, output_dir = model_dinov2_ft.train(\n", + " dataset=train_csv,\n", + " validation_set=val_csv,\n", + " output_directory=\"/tmp/results/dinov2_finetuned\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "\n", + "train_time = time.time() - t0\n", + "gpu_mem = peak_gpu_mb()\n", + "accuracy = evaluate_model(model_dinov2_ft, test_csv)\n", + "\n", + "results[\"dinov2_finetuned\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time, \"peak_gpu_mb\": gpu_mem}\n", + "print(f\"DINOv2 fine-tuned — accuracy: {accuracy:.4f}, time: {train_time:.1f}s, peak GPU: {gpu_mem:.0f}MB\")" + ] + }, + { + "cell_type": "markdown", + "id": "e7f8a9b0", + "metadata": {}, + "source": [ + "## Approach 4: CLIP\n", + "\n", + "CLIP (Radford et al., ICML 2021) from OpenAI is trained on 400M image-text pairs using contrastive learning. Its visual encoder produces embeddings that are aligned with text in a shared latent space.\n", + "\n", + "CLIP features are particularly useful for:\n", + "- Zero-shot image classification (not shown here)\n", + "- Tasks where visual-semantic alignment matters\n", + "- Multimodal applications\n", + "\n", + "Default model: `openai/clip-vit-base-patch32` (86M parameters, 768-dim output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8a9b0c1", + "metadata": {}, + "outputs": [], + "source": [ + "config_clip = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"encoder\": {\n", + " \"type\": \"clip\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": False,\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\"}],\n", + " \"trainer\": {\"epochs\": 10, \"learning_rate\": 0.001},\n", + "}\n", + "\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "\n", + "model_clip = LudwigModel(config=config_clip, logging_level=30)\n", + "_, _, output_dir = model_clip.train(\n", + " dataset=train_csv,\n", + " validation_set=val_csv,\n", + " output_directory=\"/tmp/results/clip\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "\n", + "train_time = time.time() - t0\n", + "gpu_mem = peak_gpu_mb()\n", + "accuracy = evaluate_model(model_clip, test_csv)\n", + "\n", + "results[\"clip\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time, \"peak_gpu_mb\": gpu_mem}\n", + "print(f\"CLIP linear probe — accuracy: {accuracy:.4f}, time: {train_time:.1f}s, peak GPU: {gpu_mem:.0f}MB\")" + ] + }, + { + "cell_type": "markdown", + "id": "a9b0c1d2", + "metadata": {}, + "source": [ + "## Approach 5: SigLIP\n", + "\n", + "SigLIP (Zhai et al., ICCV 2023) from Google uses sigmoid loss instead of softmax contrastive loss for image-text pre-training. This removes the dependency on global batch statistics and enables better scaling.\n", + "\n", + "SigLIP features are similar to CLIP in nature but often outperform CLIP on downstream classification tasks, especially with smaller models.\n", + "\n", + "Default model: `google/siglip-base-patch16-224` (86M parameters, 768-dim output)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0c1d2e3", + "metadata": {}, + "outputs": [], + "source": [ + "config_siglip = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"encoder\": {\n", + " \"type\": \"siglip\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": False,\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\"}],\n", + " \"trainer\": {\"epochs\": 10, \"learning_rate\": 0.001},\n", + "}\n", + "\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "\n", + "model_siglip = LudwigModel(config=config_siglip, logging_level=30)\n", + "_, _, output_dir = model_siglip.train(\n", + " dataset=train_csv,\n", + " validation_set=val_csv,\n", + " output_directory=\"/tmp/results/siglip\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "\n", + "train_time = time.time() - t0\n", + "gpu_mem = peak_gpu_mb()\n", + "accuracy = evaluate_model(model_siglip, test_csv)\n", + "\n", + "results[\"siglip\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time, \"peak_gpu_mb\": gpu_mem}\n", + "print(f\"SigLIP linear probe — accuracy: {accuracy:.4f}, time: {train_time:.1f}s, peak GPU: {gpu_mem:.0f}MB\")" + ] + }, + { + "cell_type": "markdown", + "id": "c1d2e3f4", + "metadata": {}, + "source": [ + "## Results comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2e3f4a5", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "rows = []\n", + "for name, metrics in results.items():\n", + " rows.append(\n", + " {\n", + " \"Encoder\": name,\n", + " \"Accuracy\": f\"{metrics['accuracy']:.4f}\",\n", + " \"Train time (s)\": f\"{metrics['train_time_s']:.1f}\",\n", + " \"Peak GPU (MB)\": f\"{metrics['peak_gpu_mb']:.0f}\" if metrics[\"peak_gpu_mb\"] > 0 else \"N/A\",\n", + " }\n", + " )\n", + "\n", + "df_results = pd.DataFrame(rows)\n", + "print(df_results.to_string(index=False))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3f4a5b6", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot accuracy comparison\n", + "import matplotlib.pyplot as plt\n", + "\n", + "names = list(results.keys())\n", + "accuracies = [results[n][\"accuracy\"] for n in names]\n", + "times = [results[n][\"train_time_s\"] for n in names]\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "colors = [\"#4e79a7\", \"#f28e2b\", \"#e15759\", \"#76b7b2\", \"#59a14f\"]\n", + "\n", + "bars = ax1.bar(names, accuracies, color=colors[: len(names)])\n", + "ax1.set_ylim(0, 1.05)\n", + "ax1.set_ylabel(\"Test Accuracy\")\n", + "ax1.set_title(\"Accuracy by encoder\")\n", + "ax1.tick_params(axis=\"x\", rotation=20)\n", + "for bar, acc in zip(bars, accuracies):\n", + " ax1.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01, f\"{acc:.3f}\", ha=\"center\", va=\"bottom\")\n", + "\n", + "bars2 = ax2.bar(names, times, color=colors[: len(names)])\n", + "ax2.set_ylabel(\"Training time (s)\")\n", + "ax2.set_title(\"Training time by encoder\")\n", + "ax2.tick_params(axis=\"x\", rotation=20)\n", + "for bar, t in zip(bars2, times):\n", + " ax2.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 1, f\"{t:.0f}s\", ha=\"center\", va=\"bottom\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f4a5b6c7", + "metadata": {}, + "source": [ + "## Few-shot: 5 examples per class\n", + "\n", + "One of the biggest advantages of pretrained encoders is their ability to generalize from very few labeled examples. In this section, we compare `stacked_cnn` vs `dinov2` (linear probe) when trained on only **5 examples per class** (15 examples total).\n", + "\n", + "This simulates a realistic scenario where annotation is expensive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5b6c7d8", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import pandas as pd\n", + "\n", + "# Build a 5-shot training set (5 examples per class)\n", + "train_df = pd.read_csv(train_csv)\n", + "fewshot_df = train_df.groupby(\"label\").sample(n=5, random_state=42).reset_index(drop=True)\n", + "\n", + "fewshot_csv = \"/tmp/beans/fewshot_5shot.csv\"\n", + "fewshot_df.to_csv(fewshot_csv, index=False)\n", + "\n", + "print(f\"Few-shot training set: {len(fewshot_df)} examples\")\n", + "print(fewshot_df[\"label\"].value_counts())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6c7d8e9", + "metadata": {}, + "outputs": [], + "source": [ + "fewshot_results = {}\n", + "\n", + "# Few-shot with stacked_cnn\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "model_cnn_fs = LudwigModel(config=config_stacked_cnn, logging_level=30)\n", + "model_cnn_fs.train(\n", + " dataset=fewshot_csv,\n", + " output_directory=\"/tmp/results/fewshot_cnn\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "train_time = time.time() - t0\n", + "accuracy = evaluate_model(model_cnn_fs, test_csv)\n", + "fewshot_results[\"stacked_cnn (5-shot)\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time}\n", + "print(f\"stacked_cnn (5-shot) — accuracy: {accuracy:.4f}, time: {train_time:.1f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7d8e9f0", + "metadata": {}, + "outputs": [], + "source": [ + "# Few-shot with DINOv2 linear probe\n", + "reset_gpu()\n", + "t0 = time.time()\n", + "model_dinov2_fs = LudwigModel(config=config_dinov2_probe, logging_level=30)\n", + "model_dinov2_fs.train(\n", + " dataset=fewshot_csv,\n", + " output_directory=\"/tmp/results/fewshot_dinov2\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "train_time = time.time() - t0\n", + "accuracy = evaluate_model(model_dinov2_fs, test_csv)\n", + "fewshot_results[\"dinov2_linear_probe (5-shot)\"] = {\"accuracy\": accuracy, \"train_time_s\": train_time}\n", + "print(f\"DINOv2 linear probe (5-shot) — accuracy: {accuracy:.4f}, time: {train_time:.1f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8e9f0a1", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\nFew-shot results (5 examples per class):\")\n", + "print(\"-\" * 55)\n", + "print(f\"{'Encoder':<35} {'Accuracy':>10} {'Time':>8}\")\n", + "print(\"-\" * 55)\n", + "for name, m in fewshot_results.items():\n", + " print(f\"{name:<35} {m['accuracy']:>10.4f} {m['train_time_s']:>7.1f}s\")\n", + "print(\"-\" * 55)\n", + "\n", + "print(\"\\nFull dataset results (for reference):\")\n", + "print(\"-\" * 55)\n", + "print(f\"{'Encoder':<35} {'Accuracy':>10} {'Time':>8}\")\n", + "print(\"-\" * 55)\n", + "for name in [\"stacked_cnn\", \"dinov2_linear_probe\"]:\n", + " m = results[name]\n", + " print(f\"{name:<35} {m['accuracy']:>10.4f} {m['train_time_s']:>7.1f}s\")\n", + "print(\"-\" * 55)" + ] + }, + { + "cell_type": "markdown", + "id": "e9f0a1b2", + "metadata": {}, + "source": [ + "## Summary and recommendations\n", + "\n", + "### When to use each encoder\n", + "\n", + "| Scenario | Recommended encoder |\n", + "|---|---|\n", + "| Large dataset (>10k images), custom domain | `stacked_cnn` or fine-tuned pretrained |\n", + "| Small dataset (<1k images), natural images | `dinov2` linear probe |\n", + "| Few-shot (<50 examples total) | `dinov2` or `siglip` linear probe |\n", + "| Multimodal or image-text retrieval | `clip` or `siglip` |\n", + "| Best possible accuracy, GPU available | `dinov2` fine-tuned |\n", + "\n", + "### Key takeaways\n", + "\n", + "- **Linear probing is surprisingly effective**: freezing the pretrained backbone and training only the head takes a fraction of the time and memory while achieving strong accuracy on small datasets.\n", + "- **DINOv2 is the most versatile**: its self-supervised training (no text labels needed) makes it robust across diverse image domains.\n", + "- **CLIP and SigLIP are better for semantically rich tasks**: their image-text alignment gives them an edge when classes have meaningful visual-semantic structure.\n", + "- **Few-shot gap is dramatic**: with only 5 examples per class, pretrained encoders maintain high accuracy while the CNN from scratch struggles.\n", + "\n", + "### Changing the pretrained model\n", + "\n", + "You can use any HuggingFace-compatible model by overriding `pretrained_model_name_or_path`:\n", + "\n", + "```python\n", + "{\n", + " \"type\": \"dinov2\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": False,\n", + " \"pretrained_model_name_or_path\": \"facebook/dinov2-large\", # larger model\n", + "}\n", + "```\n", + "\n", + "Available DINOv2 variants: `facebook/dinov2-small`, `facebook/dinov2-base`, `facebook/dinov2-large`, `facebook/dinov2-giant`\n", + "\n", + "Available CLIP variants: `openai/clip-vit-base-patch16`, `openai/clip-vit-large-patch14`\n", + "\n", + "Available SigLIP variants: `google/siglip-base-patch16-224`, `google/siglip-large-patch16-256`, `google/siglip-so400m-patch14-384`" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/insurance_lite/config.yaml b/examples/insurance_lite/config.yaml new file mode 100644 index 0000000..4216c9c --- /dev/null +++ b/examples/insurance_lite/config.yaml @@ -0,0 +1,64 @@ +input_features: + - name: image_path + type: image + preprocessing: + height: 224 + width: 224 + in_memory: true + num_channels: 3 + encoder: vit + img_height: 224 + in_channels: 3 + use_pretrained: true + hidden_size: 768 + num_hidden_layers: 12 + num_attention_heads: 12 + intermediate_size: 3072 + - name: insurance_company + type: category + preprocessing: + missing_value_strategy: fill_with_const + fill_value: UNKNOWN + - name: cost_of_vehicle + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: expiry_date + type: date + preprocessing: + missing_value_strategy: fill_with_const + fill_value: "" + datetime_format: "%Y-%m-%d" + - name: min_coverage + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: max_coverage + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: condition + type: category + preprocessing: + missing_value_strategy: fill_with_const + fill_value: UNKNOWN +combiner: + type: concat + num_fc_layers: 3 + output_size: 256 +output_features: + - name: amount + type: number + preprocessing: + normalization: zscore +trainer: + epochs: 10 + early_stop: 0 + batch_size: 8 +preprocessing: + split: + type: random + probabilities: [0.7, 0.1, 0.2] diff --git a/examples/insurance_lite/train.py b/examples/insurance_lite/train.py new file mode 100644 index 0000000..a97094a --- /dev/null +++ b/examples/insurance_lite/train.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +# # Simple Model Training Example on multi-modal data. + +# Import required libraries +import logging +import os +import shutil + +from ludwig.api import LudwigModel +from ludwig.datasets import insurance_lite + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +# Download and prepare the dataset +dataset = insurance_lite.load() + +# Define Ludwig model object that drive model training +model = LudwigModel(config="./config.yaml", logging_level=logging.INFO, backend="local") + + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train(dataset=dataset, experiment_name="simple_experiment", model_name="simple_model") + +# list contents of output directory +print("contents of output directory:", output_directory) +for item in os.listdir(output_directory): + print("\t", item) diff --git a/examples/kfold_cv/README.md b/examples/kfold_cv/README.md new file mode 100644 index 0000000..4fc0514 --- /dev/null +++ b/examples/kfold_cv/README.md @@ -0,0 +1,74 @@ +# K-Ffold Cross Validation Example + +This directory contains two examples of performing a k-fold cross validation analysis with Ludwig. + +## Classification Example + +This example illustrates running the k-fold cv with the `ludwig experiment` cli. + +To run this example execute this bash script: + +``` +./k-fold_cv_classification.sh +``` + +This bash script performs these steps: + +- Download and prepare data for training and create a Ludwig config file +- Execute `ludwig experiment` to run the 5-fold cross validation +- Display results from the 5-fold cross validation analysis + +Sample output: + +``` +Cleaning out old results +Downloading data set +Preparing data for training +Saving training and test data sets +Preparing Ludwig config +Completed data preparation +Training: 100%|████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 23.14it/s] +Evaluation train: 100%|████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 98.62it/s] +Evaluation test : 100%|█████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 321.03it/s] +Training: 100%|███████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 190.18it/s] +Evaluation train: 100%|███████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 331.68it/s] +Evaluation test : 100%|█████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 298.08it/s] +<<<< DELETED LINES >>>>> +Training: 100%|███████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 248.00it/s] +Evaluation train: 100%|███████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 400.31it/s] +Evaluation test : 100%|█████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 340.35it/s] +Evaluation: 100%|████████████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 27.87it/s] +retrieving results from results +# +# K-fold Cross Validation Results +# +{'combined': {'accuracy_mean': 0.9736263736263737, + 'accuracy_std': 0.011206636293610508, + 'loss_mean': 0.06359774886251807, + 'loss_std': 0.011785678840394689}, + 'diagnosis': {'accuracy_mean': 0.9736263736263737, + 'accuracy_std': 0.011206636293610508, + 'average_precision_macro_mean': 0.995842104045726, + 'average_precision_macro_std': 0.002339014329647542, + 'average_precision_micro_mean': 0.995842104045726, + 'average_precision_micro_std': 0.002339014329647542, + 'average_precision_samples_mean': 0.995842104045726, + 'average_precision_samples_std': 0.002339014329647542, + 'loss_mean': 0.06359774886251807, + 'loss_std': 0.011785678840394689, + 'roc_auc_macro_mean': 0.9973999160508542, + 'roc_auc_macro_std': 0.0011259319854886507, + 'roc_auc_micro_mean': 0.9973999160508542, + 'roc_auc_micro_std': 0.0011259319854886507}} +``` + +## Regression Example + +This illustrates using the Ludwig API to run the K-fold cross validation analysis. To run the example, open the jupyter notebook `regression_example.ipynb`. Following steps are performed: + +- Download and prepare data for training and create a Ludwig config data structure from a pandas dataframe structure +- Use `ludwig.api.kfold_cross_validate()` function to run the 5-fold cross validation +- Display results from the 5-fold cross validation analysis + +Expected output from running the example: +![](../images/regression_kfold_cv_example_results.png) diff --git a/examples/kfold_cv/display_kfold_cv_results.py b/examples/kfold_cv/display_kfold_cv_results.py new file mode 100644 index 0000000..bba04b4 --- /dev/null +++ b/examples/kfold_cv/display_kfold_cv_results.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + + +import argparse +import os.path +import pprint +import sys + +from ludwig.utils.data_utils import load_json + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Display K-fold cross validation results", + prog="display_kfold_cv_results", + usage="%(prog)s [options]", + ) + + # ---------------------------- + # Experiment naming parameters + # ---------------------------- + parser.add_argument( + "--results_directory", type=str, default="results", help="directory that contains the K-fold cv results" + ) + + args = parser.parse_args(sys.argv[1:]) + results_directory = args.results_directory + + print("Retrieving results from ", results_directory) + + kfold_cv_stats = load_json(os.path.join(results_directory, "kfold_training_statistics.json")) + + print("#\n# K-fold Cross Validation Results\n#") + pprint.pprint(kfold_cv_stats["overall"]) diff --git a/examples/kfold_cv/k-fold_cv_classification.sh b/examples/kfold_cv/k-fold_cv_classification.sh new file mode 100755 index 0000000..b290b81 --- /dev/null +++ b/examples/kfold_cv/k-fold_cv_classification.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# +# Download and prepare training data +# +python prepare_classification_data_set.py + +# +# Run 5-fold cross validation +# +ludwig experiment \ + --config config.yaml \ + --dataset data/train.csv \ + --output_directory results \ + --logging_level 'error' \ + -kf 5 + +# +# Display results from K-fold cv +# +python display_kfold_cv_results.py --results_directory results diff --git a/examples/kfold_cv/prepare_classification_data_set.py b/examples/kfold_cv/prepare_classification_data_set.py new file mode 100644 index 0000000..b13a277 --- /dev/null +++ b/examples/kfold_cv/prepare_classification_data_set.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + + +# Download and prepare training data set +# Create Ludwig config file +# +# Based on the +# [UCI Wisconsin Breast Cancer data set](https://archive.ics.uci.edu/ml/datasets/breast+cancer+wisconsin+(original)) +# +import os.path +import shutil + +import pandas as pd +import requests +import yaml +from sklearn.model_selection import train_test_split + +from ludwig.constants import TRAINER + +# Constants +DATA_SET_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data" +DATA_SET = "wdbc.data" +DATA_DIR = "./data" +RESULTS_DIR = "results" + +# Clean out previous results +print("Cleaning out old results") +if os.path.isfile(DATA_SET): + os.remove(DATA_SET) +if os.path.isfile("config.yaml"): + os.remove("config.yaml") + +shutil.rmtree(RESULTS_DIR, ignore_errors=True) +shutil.rmtree(DATA_DIR, ignore_errors=True) + +# Retrieve data from UCI Machine Learning Repository +# Download required data +print("Downloading data set") +r = requests.get(DATA_SET_URL) +if r.status_code == 200: + with open(DATA_SET, "w") as f: + f.write(r.content.decode("utf-8")) + +# create pandas dataframe from downloaded data +print("Preparing data for training") +raw_df = pd.read_csv(DATA_SET, header=None, sep=",", skipinitialspace=True) +raw_df.columns = ["ID", "diagnosis"] + ["X" + str(i) for i in range(1, 31)] + +# convert diagnosis attribute to binary format +raw_df["diagnosis"] = raw_df["diagnosis"].map({"M": 1, "B": 0}) + +# Create train/test split +print("Saving training and test data sets") +train_df, test_df = train_test_split(raw_df, train_size=0.8, random_state=17) +os.mkdir(DATA_DIR) +train_df.to_csv(os.path.join(DATA_DIR, "train.csv"), index=False) +test_df.to_csv(os.path.join(DATA_DIR, "test.csv"), index=False) + +print("Preparing Ludwig config") +# Create ludwig input_features +num_features = ["X" + str(i) for i in range(1, 31)] +input_features = [] + +# setup input features for number variables +for p in num_features: + a_feature = { + "name": p, + "type": "number", + "preprocessing": {"missing_value_strategy": "fill_with_mean", "normalization": "zscore"}, + } + input_features.append(a_feature) + +# Create ludwig output features +output_features = [{"name": "diagnosis", "type": "binary", "num_fc_layers": 2, "output_size": 64}] + +# setup ludwig config +config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 20, "batch_size": 32}, +} + +with open("config.yaml", "w") as f: + yaml.dump(config, f) + +print("Completed data preparation") diff --git a/examples/kfold_cv/regression_example.ipynb b/examples/kfold_cv/regression_example.ipynb new file mode 100644 index 0000000..970b560 --- /dev/null +++ b/examples/kfold_cv/regression_example.ipynb @@ -0,0 +1,686 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# K-fold cross validation - Regression Model\n", + "Based on the [Ludwig regression example](https://ludwig-ai.github.io/ludwig-docs/examples/#simple-regression-fuel-efficiency-prediction) \n", + "\n", + "[Data set](https://archive.ics.uci.edu/ml/datasets/auto+mpg)\n", + "\n", + "This example demonstrates teh following:\n", + "\n", + "- Download a data set and create a pandas dataframe\n", + "- Create a training and hold-out test data sets\n", + "- Create a Ludwig config data structure from the pandas dataframe\n", + "- Run a 5-fold cross validation analysis with the training data\n", + "- Use Ludwig APIs to train and assess model performance on hold-out test data set" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import os\n", + "import os.path\n", + "import shutil\n", + "import tempfile\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import requests\n", + "import seaborn as sns\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "from ludwig.api import kfold_cross_validate, LudwigModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Contstants" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "DATA_SET_URL = \"http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data\"\n", + "DATA_SET = \"auto_mpg.data\"\n", + "RESULTS_DIR = \"results\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Clean out previous results" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "if os.path.isfile(DATA_SET):\n", + " os.remove(DATA_SET)\n", + "\n", + "shutil.rmtree(RESULTS_DIR, ignore_errors=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Retrieve data from UCI Machine Learning Repository" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Download required data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.get(DATA_SET_URL)\n", + "if r.status_code == 200:\n", + " with open(DATA_SET, \"w\") as f:\n", + " f.write(r.content.decode(\"utf-8\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Pandas DataFrame from downloaded data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(398, 8)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "raw_df = pd.read_csv(DATA_SET, header=None, na_values=\"?\", comment=\"\\t\", sep=\" \", skipinitialspace=True)\n", + "\n", + "\n", + "raw_df.columns = [\"MPG\", \"Cylinders\", \"Displacement\", \"Horsepower\", \"Weight\", \"Acceleration\", \"ModelYear\", \"Origin\"]\n", + "raw_df.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MPGCylindersDisplacementHorsepowerWeightAccelerationModelYearOrigin
018.08307.0130.03504.012.0701
115.08350.0165.03693.011.5701
218.08318.0150.03436.011.0701
316.08304.0150.03433.012.0701
417.08302.0140.03449.010.5701
\n", + "
" + ], + "text/plain": [ + " MPG Cylinders Displacement Horsepower Weight Acceleration ModelYear \\\n", + "0 18.0 8 307.0 130.0 3504.0 12.0 70 \n", + "1 15.0 8 350.0 165.0 3693.0 11.5 70 \n", + "2 18.0 8 318.0 150.0 3436.0 11.0 70 \n", + "3 16.0 8 304.0 150.0 3433.0 12.0 70 \n", + "4 17.0 8 302.0 140.0 3449.0 10.5 70 \n", + "\n", + " Origin \n", + "0 1 \n", + "1 1 \n", + "2 1 \n", + "3 1 \n", + "4 1 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "raw_df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create train/test split" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(318, 8)\n", + "(80, 8)\n" + ] + } + ], + "source": [ + "train_df, test_df = train_test_split(raw_df, train_size=0.8, random_state=17)\n", + "print(train_df.shape)\n", + "print(test_df.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup Ludwig config" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "num_features = [\"Cylinders\", \"Displacement\", \"Horsepower\", \"Weight\", \"Acceleration\", \"ModelYear\"]\n", + "cat_features = [\"Origin\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Ludwig input_features" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "input_features = []\n", + "# setup input features for number variables\n", + "for p in num_features:\n", + " a_feature = {\n", + " \"name\": p,\n", + " \"type\": \"number\",\n", + " \"preprocessing\": {\"missing_value_strategy\": \"fill_with_mean\", \"normalization\": \"zscore\"},\n", + " }\n", + " input_features.append(a_feature)\n", + "\n", + "# setkup input features for categorical variables\n", + "for p in cat_features:\n", + " a_feature = {\"name\": p, \"type\": \"category\"}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create Ludwig output features" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "output_features = [{\"name\": \"MPG\", \"type\": \"number\", \"num_fc_layers\": 2, \"fc_size\": 64}]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input_features': [{'name': 'Cylinders',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'Displacement',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'Horsepower',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'Weight',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'Acceleration',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}},\n", + " {'name': 'ModelYear',\n", + " 'type': 'number',\n", + " 'preprocessing': {'missing_value_strategy': 'fill_with_mean',\n", + " 'normalization': 'zscore'}}],\n", + " 'output_features': [{'name': 'MPG',\n", + " 'type': 'number',\n", + " 'num_fc_layers': 2,\n", + " 'fc_size': 64}],\n", + " 'training': {'epochs': 100, 'batch_size': 32}}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "config = {\n", + " \"input_features\": input_features,\n", + " \"output_features\": output_features,\n", + " \"trainer\": {\"epochs\": 100, \"batch_size\": 32},\n", + "}\n", + "config" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Perform K-fold Cross Validation analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "starting 5-fold cross validation\n", + "training on fold 1\n", + "CPU times: user 40.7 s, sys: 5.38 s, total: 46 s\n", + "Wall time: 40.6 s\n" + ] + } + ], + "source": [ + "%%time\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " data_csv_fp = os.path.join(tmpdir, \"train.csv\")\n", + " train_df.to_csv(data_csv_fp, index=False)\n", + "\n", + " (kfold_cv_stats, kfold_split_indices) = kfold_cross_validate(\n", + " num_folds=5, config=config, dataset=data_csv_fp, output_directory=tmpdir, logging_level=logging.ERROR\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'loss_mean': 8.111681,\n", + " 'loss_std': 2.4598064,\n", + " 'error_mean': 0.0380627,\n", + " 'error_std': 0.5965346,\n", + " 'mean_squared_error_mean': 8.111682,\n", + " 'mean_squared_error_std': 2.4598064,\n", + " 'mean_absolute_error_mean': 2.0598435,\n", + " 'mean_absolute_error_std': 0.2779836,\n", + " 'r2_mean': 0.8666786,\n", + " 'r2_std': 0.03552912}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kfold_cv_stats[\"overall\"][\"MPG\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Train model and assess model performance" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "model = LudwigModel(config=config, logging_level=logging.ERROR)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 8.34 s, sys: 1.78 s, total: 10.1 s\n", + "Wall time: 15 s\n" + ] + } + ], + "source": [ + "%%time\n", + "training_stats = model.train(\n", + " training_set=train_df,\n", + " output_directory=RESULTS_DIR,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/project/ludwig/data/preprocessing.py:1045: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " computed_fill_value,\n" + ] + } + ], + "source": [ + "test_stats, mpg_hat_df, _ = model.evaluate(dataset=test_df, collect_predictions=True, collect_overall_stats=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'MPG': {'loss': 8.303831,\n", + " 'error': -0.45136052,\n", + " 'mean_squared_error': 8.303831,\n", + " 'mean_absolute_error': 2.2274728,\n", + " 'r2': 0.8558148},\n", + " 'combined': {'loss': 8.303831}}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_stats" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.6/dist-packages/seaborn/_decorators.py:43: FutureWarning: Pass the following variables as keyword args: x, y. From version 0.12, the only valid positional argument will be `data`, and passing other arguments without an explicit keyword will result in an error or misinterpretation.\n", + " FutureWarning\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "a = plt.axes(aspect=\"equal\")\n", + "sns.scatterplot(test_df[\"MPG\"].values, mpg_hat_df[\"MPG_predictions\"].values, s=50)\n", + "plt.xlabel(\"True Values [MPG]\")\n", + "plt.ylabel(\"Predictions [MPG]\")\n", + "lims = [0, 50]\n", + "plt.xlim(lims)\n", + "plt.ylim(lims)\n", + "_ = plt.plot(lims, lims)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compare K-fold Cross Validation metrics against hold-out test metrics" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Hold-out Test Metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'loss': 8.303831,\n", + " 'error': -0.45136052,\n", + " 'mean_squared_error': 8.303831,\n", + " 'mean_absolute_error': 2.2274728,\n", + " 'r2': 0.8558148}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_stats[\"MPG\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### K-fold Cross Validation Metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'loss_mean': 8.111681,\n", + " 'loss_std': 2.4598064,\n", + " 'error_mean': 0.0380627,\n", + " 'error_std': 0.5965346,\n", + " 'mean_squared_error_mean': 8.111682,\n", + " 'mean_squared_error_std': 2.4598064,\n", + " 'mean_absolute_error_mean': 2.0598435,\n", + " 'mean_absolute_error_std': 0.2779836,\n", + " 'r2_mean': 0.8666786,\n", + " 'r2_std': 0.03552912}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "kfold_cv_stats[\"overall\"][\"MPG\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/lbfgs/config.yaml b/examples/lbfgs/config.yaml new file mode 100644 index 0000000..ec79984 --- /dev/null +++ b/examples/lbfgs/config.yaml @@ -0,0 +1,40 @@ +input_features: + - name: RESOURCE + type: category + - name: MGR_ID + type: category + - name: ROLE_ROLLUP_1 + type: category + - name: ROLE_ROLLUP_2 + type: category + - name: ROLE_DEPTNAME + type: category + - name: ROLE_TITLE + type: category + - name: ROLE_FAMILY_DESC + type: category + - name: ROLE_FAMILY + type: category + - name: ROLE_CODE + type: category +output_features: + - name: ACTION + type: binary +preprocessing: + split: + type: fixed +defaults: + category: + encoder: + type: sparse +trainer: + batch_size: 32769 # entire training set + train_steps: 1 + steps_per_checkpoint: 1 + learning_rate: 1 + regularization_lambda: 0.0000057 + optimizer: + type: lbfgs + max_iter: 100 + tolerance_grad: 0.0001 + history_size: 10 diff --git a/examples/lbfgs/model.py b/examples/lbfgs/model.py new file mode 100644 index 0000000..d68967e --- /dev/null +++ b/examples/lbfgs/model.py @@ -0,0 +1,32 @@ +import logging + +import pandas as pd + +from ludwig.api import LudwigModel +from ludwig.datasets import amazon_employee_access_challenge + +df = amazon_employee_access_challenge.load() + +model = LudwigModel(config="config.yaml", logging_level=logging.INFO) + +training_statistics, preprocessed_data, output_directory = model.train( + df, + skip_save_processed_input=True, + skip_save_log=True, + skip_save_progress=True, + skip_save_training_description=True, + skip_save_training_statistics=True, +) + +# Predict on unlabeled test +config = model.config +config["preprocessing"] = {} +model.config = config +unlabeled_test = df[df.split == 2].reset_index(drop=True) +preds, _ = model.predict(unlabeled_test) + +# Save predictions to csv +action = preds.ACTION_probabilities_True +submission = pd.merge(unlabeled_test.reset_index(drop=True).id.astype(int), action, left_index=True, right_index=True) +submission.rename(columns={"ACTION_probabilities_True": "Action", "id": "Id"}, inplace=True) +submission.to_csv("submission.csv", index=False) diff --git a/examples/llama2_7b_finetuning_4bit/README.md b/examples/llama2_7b_finetuning_4bit/README.md new file mode 100644 index 0000000..fd95211 --- /dev/null +++ b/examples/llama2_7b_finetuning_4bit/README.md @@ -0,0 +1,43 @@ +# Llama2-7b Fine-Tuning 4bit (QLoRA) + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1c3AO8l_H6V_x37RwQ8V7M6A-RmcBf2tG?usp=sharing) + +This example shows how to fine-tune [Llama2-7b](https://huggingface.co/meta-llama/Llama-2-7b-hf) to follow instructions. +Instruction tuning is the first step in adapting a general purpose Large Language Model into a chatbot. + +This example uses no distributed training or big data functionality. It is designed to run locally on any machine +with GPU availability. + +## Prerequisites + +- [HuggingFace API Token](https://huggingface.co/docs/hub/security-tokens) +- Access approval to [Llama2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf) +- GPU with at least 12 GiB of VRAM (in our tests, we used an Nvidia T4) + +## Running + +### Command Line + +Set your token environment variable from the terminal, then run the API script: + +```bash +export HUGGING_FACE_HUB_TOKEN="" +./run_train.sh +``` + +### Python API + +Set your token environment variable from the terminal, then run the API script: + +```bash +export HUGGING_FACE_HUB_TOKEN="" +python train_alpaca.py +``` + +## Upload to HuggingFace + +You can upload to the HuggingFace Hub from the command line: + +```bash +ludwig upload hf_hub -r / -m +``` diff --git a/examples/llama2_7b_finetuning_4bit/llama2_7b_4bit.yaml b/examples/llama2_7b_finetuning_4bit/llama2_7b_4bit.yaml new file mode 100644 index 0000000..8bd613c --- /dev/null +++ b/examples/llama2_7b_finetuning_4bit/llama2_7b_4bit.yaml @@ -0,0 +1,28 @@ +model_type: llm +base_model: meta-llama/Llama-2-7b-hf + +quantization: + bits: 4 + +adapter: + type: lora + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +trainer: + type: finetune + learning_rate: 0.0003 + batch_size: 2 + gradient_accumulation_steps: 8 + epochs: 3 + learning_rate_scheduler: + warmup_fraction: 0.01 + +backend: + type: local diff --git a/examples/llama2_7b_finetuning_4bit/run_train.sh b/examples/llama2_7b_finetuning_4bit/run_train.sh new file mode 100755 index 0000000..372cc22 --- /dev/null +++ b/examples/llama2_7b_finetuning_4bit/run_train.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Fail fast if an error occurs +set -e + +# Get the directory of this script, which contains the config file +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Train +ludwig train --config ${SCRIPT_DIR}/llama2_7b_4bit.yaml --dataset ludwig://alpaca diff --git a/examples/llama2_7b_finetuning_4bit/train_alpaca.py b/examples/llama2_7b_finetuning_4bit/train_alpaca.py new file mode 100644 index 0000000..f643d73 --- /dev/null +++ b/examples/llama2_7b_finetuning_4bit/train_alpaca.py @@ -0,0 +1,56 @@ +import logging +import os + +import yaml + +from ludwig.api import LudwigModel + +config = yaml.safe_load(""" +model_type: llm +base_model: meta-llama/Llama-2-7b-hf + +quantization: + bits: 4 + +adapter: + type: lora + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +trainer: + type: finetune + learning_rate: 0.0003 + batch_size: 2 + gradient_accumulation_steps: 8 + epochs: 3 + learning_rate_scheduler: + warmup_fraction: 0.01 + +backend: + type: local +""") + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset="ludwig://alpaca", + experiment_name="alpaca_instruct_4bit", + model_name="llama2_7b", +) + +# list contents of output directory +print("contents of output directory:", output_directory) +for item in os.listdir(output_directory): + print("\t", item) diff --git a/examples/llm_base_model_dequantization/README.md b/examples/llm_base_model_dequantization/README.md new file mode 100644 index 0000000..6e05336 --- /dev/null +++ b/examples/llm_base_model_dequantization/README.md @@ -0,0 +1,74 @@ +# Convert quantized base model to fp16 + +Ludwig has utility functions to convert nf4 quantized bitsandbytes base models back to fp16 +for more efficient inference. This is desireable since inference with bitsandbytes is slow because +every forward pass through the model requires dequantizing the model weights from nf4 to fp16 layer +by layer and then quantizing it back to nf4 to keep memory usage constant. + +By dequantizing the base model in fp16 upfront, you can get the same effect of the quantized weights +without sacrificing on inference performance. + +## Visual Illustration + +### Without dequantization upfront + +| **Request 1:** | **Request 2:** | **Request 3:** | +| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| - Quantized bitsandbytes model | - Quantized bitsandbytes model | - Quantized bitsandbytes model | +| - Dequantization of layer 1 (nf4 to fp16) | - Dequantization of layer 1 (nf4 to fp16) | - Dequantization of layer 1 (nf4 to fp16) | +| - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) | +| - Quantization of layer 1 (fp16 to nf4) | - Quantization of layer 1 (fp16 to nf4) | - Quantization of layer 1 (fp16 to nf4) | +| - Dequantization of layer 2 (nf4 to fp16) | - Dequantization of layer 2 (nf4 to fp16) | - Dequantization of layer 2 (nf4 to fp16) | +| - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) | +| - Quantization of layer 2 (fp16 to nf4) | - Quantization of layer 2 (fp16 to nf4) | - Quantization of layer 2 (fp16 to nf4) | +| - ... | - ... | - ... | +| - Final Output | - Final Output | - Final Output | + +### With dequantization upfront + +| **Request 1:** | **Request 2:** | **Request 3:** | +| -------------------------------- | -------------------------------- | -------------------------------- | +| - Dequantized base model in fp16 | - Dequantized base model in fp16 | - Dequantized base model in fp16 | +| - Forward pass through layer 1 | - Forward pass through layer 1 | - Forward pass through layer 1 | +| - Forward pass through layer 2 | - Forward pass through layer 2 | - Forward pass through layer 2 | +| - ... | - ... | - ... | +| - Final Output | - Final Output | - Final Output | + +## Running the example script + +The example `phi_2_dequantization.py` shows how you how you can quantize and then dequantized Phi-2. This process +can be repeated for any other base model supported by Ludwig that is quantized using 4 bits nf4 bitsandbytes quantization. You will need a GPU to run the script successfully. + +Beneath the surface, this script: + +1. Loads the base model in 4 bit nf4 quantization +1. Dequantizes the model layer by layer back into fp16 in-place. +1. Write the new dequantized weights to disk at `save_path` +1. Write the tokenizer to disk at `save_path` + +Make sure you update the paths at the top of the file for base model, save path, and huggingface repo ID! + +## Bonus + +If desired, you can also use Ludwig to push the new dequantized model weights straight to HuggingFace hub! + +```python +from ludwig.utils.hf_utils import upload_folder_to_hfhub + +upload_folder_to_hfhub(repo_id=hfhub_repo_id, folder_path=save_path) +``` + +### Dequantized base models already on huggingface hub + +- [CodeLlama 7b Instruct](https://huggingface.co/arnavgrg/codallama-7b-instruct-nf4-fp16-upscaled) +- [CodeLlama 13b Instruct](https://huggingface.co/arnavgrg/codellama-13b-instruct-nf4-fp16-upscaled) +- [CodeLlama 70b Instruct](https://huggingface.co/arnavgrg/codellama-70b-instruct-nf4-fp16-upscaled) +- [Llama 2 7b](https://huggingface.co/arnavgrg/llama-2-7b-nf4-fp16-upscaled) +- [Llama 2 7b Chat](https://huggingface.co/arnavgrg/llama-2-7b-chat-nf4-fp16-upscaled) +- [Llama 2 13b Chat](https://huggingface.co/arnavgrg/llama-2-13b-chat-nf4-fp16-upscaled) +- [Llama 2 70b Chat](https://huggingface.co/arnavgrg/llama-2-70b-chat-nf4-fp16-upscaled) +- [Mistral 7b](https://huggingface.co/arnavgrg/mistral-7b-nf4-fp16-upscaled) +- [Mistral 7b Instruct](https://huggingface.co/arnavgrg/mistral-7b-instruct-nf4-fp16-upscaled) +- [NousMistral Yarn 7b 128K](https://huggingface.co/arnavgrg/NousResearch-Yarn-Mistral-7b-128k-nf4-fp16-upscaled) +- [Microsoft Phi-2](https://huggingface.co/arnavgrg/phi-2-nf4-fp16-upscaled) +- [Zephyr 7b Beta](https://huggingface.co/arnavgrg/zephyr-7b-beta-nf4-fp16-upscaled) diff --git a/examples/llm_base_model_dequantization/phi_2_dequantization.py b/examples/llm_base_model_dequantization/phi_2_dequantization.py new file mode 100644 index 0000000..e8e9183 --- /dev/null +++ b/examples/llm_base_model_dequantization/phi_2_dequantization.py @@ -0,0 +1,44 @@ +import logging +import os + +import yaml +from huggingface_hub import whoami + +from ludwig.api import LudwigModel +from ludwig.utils.hf_utils import upload_folder_to_hfhub + +hf_username = whoami().get("name") +base_model_name = "microsoft/phi-2" +dequantized_path = "microsoft-phi-2-dequantized" +save_path = "/home/ray/" + dequantized_path +hfhub_repo_id = os.path.join(hf_username, dequantized_path) + + +config = yaml.safe_load(f""" + model_type: llm + base_model: {base_model_name} + + quantization: + bits: 4 + + input_features: + - name: instruction + type: text + + output_features: + - name: output + type: text + + trainer: + type: none + + backend: + type: local + """) + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) +model.save_dequantized_base_model(save_path=save_path) + +# Optional: Upload to Huggingface Hub +upload_folder_to_hfhub(repo_id=hfhub_repo_id, folder_path=save_path) diff --git a/examples/llm_config_generation/README.md b/examples/llm_config_generation/README.md new file mode 100644 index 0000000..ae9d5d9 --- /dev/null +++ b/examples/llm_config_generation/README.md @@ -0,0 +1,93 @@ +# LLM-Driven Config Generation + +> **Note:** This feature requires PR #4092 to be merged into Ludwig, or `pip install ludwig>=0.14`. + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/llm_config_generation/llm_config_generation.ipynb) + +## What is this? + +Ludwig's config generation feature lets you describe your machine learning task in plain English and receive a fully validated Ludwig configuration file in return. An LLM (Claude or GPT-4) interprets your description, maps column names to Ludwig feature types, selects an appropriate model architecture, and emits a config dict that passes Ludwig's Pydantic schema validation before it ever reaches your code. + +This is particularly useful for: + +- **New users** who are unfamiliar with Ludwig's YAML schema and want a working starting point. +- **Rapid prototyping** — describe the task, inspect the generated config, tweak if needed, and run. +- **Multi-task problems** — describing simultaneous outputs (e.g. classify + regress) is often easier in prose than in YAML. + +## Prerequisites + +You need an API key for at least one of the supported backends: + +| Backend | Environment variable | +| ------------------ | -------------------- | +| Anthropic (Claude) | `ANTHROPIC_API_KEY` | +| OpenAI (GPT) | `OPENAI_API_KEY` | + +The library reads the key automatically from the environment. You can also pass `api_key=` explicitly. + +Install the required packages: + +```bash +pip install "ludwig>=0.14" anthropic # for Claude +# or +pip install "ludwig>=0.14" openai # for GPT +``` + +## Quick start + +```python +import os +import yaml +from ludwig.config_generation import generate_config # requires PR #4092 / ludwig>=0.14 + +config = generate_config( + "I have customer data with age, income, and purchase history. " + "I want to predict churn (binary) and lifetime value (number).", + model="claude-sonnet-4-20250514", + # api_key is read from ANTHROPIC_API_KEY by default + validate=True, +) + +print(yaml.dump(config, default_flow_style=False)) +``` + +You can also use an OpenAI model by passing its name: + +```python +config = generate_config( + "Predict apartment rent price from sqft, bedrooms, and neighborhood.", + model="gpt-4o", + validate=True, +) +``` + +The backend is chosen automatically based on whether the model name starts with `"claude"` or `"gpt"`. + +## Files + +| File | Description | +| ----------------------------- | ------------------------------------------------------- | +| `README.md` | This file | +| `llm_config_generation.ipynb` | Interactive walkthrough notebook | +| `generate_and_train.py` | Standalone CLI script — describe a task, confirm, train | + +## Running the standalone script + +```bash +# Use the default task description +python generate_and_train.py + +# Or pass your own description +python generate_and_train.py "predict house price from bedrooms, sqft, and location" + +# Use a specific model +python generate_and_train.py --model gpt-4o "classify email sentiment as positive, neutral, or negative" +``` + +## Tips for writing good task descriptions + +- **Name your columns** — "age, income, and purchase_count" is more actionable than "some user features". +- **State the target and its type** — "predict churn (binary)" or "predict revenue (continuous number)". +- **Mention modalities** — "text product description and tabular price, category" helps Ludwig pick the right encoder. +- **Include rough dataset size** — "~50 k rows" lets the LLM suggest appropriate model complexity. +- **Describe multi-output tasks explicitly** — "simultaneously predict price (regression) and category (classification)". diff --git a/examples/llm_config_generation/example_description.txt b/examples/llm_config_generation/example_description.txt new file mode 100644 index 0000000..856ea8a --- /dev/null +++ b/examples/llm_config_generation/example_description.txt @@ -0,0 +1,21 @@ +I have a tabular dataset from UCI Adult Census Income with the following columns: + + - age (number) + - workclass (category) + - education (category, ordered from preschool through doctorate) + - education-num (number, 1-16) + - marital-status (category) + - occupation (category, 14 unique values) + - relationship (category) + - race (category) + - sex (binary: Male / Female) + - capital-gain (number, heavily skewed, mostly zero) + - capital-loss (number, similar to capital-gain) + - hours-per-week (number, 1-99) + - native-country (category, high cardinality ~40 classes) + +The target column is "income" (binary: >50K or <=50K). + +The dataset has about 48k rows. Training should be reasonably fast — prefer the +medium_quality preset. Use the concat combiner with two FC layers. Use AdamW with a +learning-rate scheduler. diff --git a/examples/llm_config_generation/generate_and_train.py b/examples/llm_config_generation/generate_and_train.py new file mode 100644 index 0000000..9d56380 --- /dev/null +++ b/examples/llm_config_generation/generate_and_train.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""LLM-Driven Config Generation — standalone script. + +Describe a machine learning task in plain English, receive a validated Ludwig +config, then optionally train a model on synthetic data. + +NOTE: Requires PR #4092 to be merged into Ludwig (or ludwig>=0.14). + https://github.com/ludwig-ai/ludwig/pull/4092 + +Usage +----- +# Use the built-in default task +python generate_and_train.py + +# Describe your own task +python generate_and_train.py "predict house price from bedrooms, sqft, location" + +# Use a specific LLM backend +python generate_and_train.py --model gpt-4o "predict house price from bedrooms, sqft" +python generate_and_train.py --model claude-sonnet-4-20250514 "predict house price" + +# Colab: !pip install ludwig anthropic --quiet +""" + +import argparse +import sys +import textwrap + +import numpy as np +import pandas as pd +import yaml + +# NOTE: `ludwig.config_generation` is provided by PR #4092 (ludwig>=0.14). +# If you see an ImportError here, that PR has not yet been merged. +try: + from ludwig.config_generation import generate_config +except ImportError as exc: + print( + "ImportError: ludwig.config_generation not found.\n" + "This feature requires PR #4092 to be merged, or ludwig>=0.14.\n" + f"Original error: {exc}" + ) + sys.exit(1) + +DEFAULT_DESCRIPTION = ( + "I have customer data with the columns age (integer), annual_income (float), " + "num_purchases (integer), and days_since_last_purchase (integer). " + "I want to predict churn (binary: 0 or 1)." +) + +DEFAULT_MODEL = "claude-sonnet-4-20250514" + + +# --------------------------------------------------------------------------- +# Synthetic data helpers +# --------------------------------------------------------------------------- + + +def _make_tabular_row(feature: dict) -> pd.Series: + """Return a single synthetic value for a Ludwig input feature dict.""" + ftype = feature.get("type", "number") + + rng = np.random.default_rng() + + if ftype in ("number", "numerical"): + return rng.uniform(0, 100) + if ftype in ("category", "categorical"): + return rng.choice(["A", "B", "C"]) + if ftype == "binary": + return rng.choice([True, False]) + if ftype == "text": + words = ["quick", "brown", "fox", "lazy", "dog", "jumps", "over"] + return " ".join(rng.choice(words, size=rng.integers(4, 12))) + # fallback + return rng.uniform(0, 100) + + +def _output_value(feature: dict): + """Return a synthetic target value for a Ludwig output feature dict.""" + ftype = feature.get("type", "binary") + rng = np.random.default_rng() + if ftype == "binary": + return rng.choice([0, 1]) + if ftype in ("number", "numerical"): + return float(rng.uniform(0, 1000)) + if ftype in ("category", "categorical"): + return rng.choice(["cat_A", "cat_B", "cat_C"]) + return float(rng.uniform(0, 1)) + + +def build_synthetic_dataframe(config: dict, n_rows: int = 200) -> pd.DataFrame: + """Build a small synthetic DataFrame that matches the config schema.""" + records = [] + input_features = config.get("input_features", []) + output_features = config.get("output_features", []) + + for _ in range(n_rows): + row = {} + for feat in input_features: + row[feat["name"]] = _make_tabular_row(feat) + for feat in output_features: + row[feat["name"]] = _output_value(feat) + records.append(row) + + return pd.DataFrame(records) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Generate a Ludwig config from a plain-English task description and optionally train.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent("""\ + Examples: + python generate_and_train.py + python generate_and_train.py "predict house price from bedrooms, sqft, location" + python generate_and_train.py --model gpt-4o "classify email sentiment" + """), + ) + parser.add_argument( + "description", + nargs="?", + default=None, + help="Plain-English task description. Defaults to the built-in churn example.", + ) + parser.add_argument( + "--model", + default=DEFAULT_MODEL, + help=f"LLM model name (default: {DEFAULT_MODEL}). Claude models start with 'claude', OpenAI with 'gpt'.", + ) + parser.add_argument( + "--no-train", + action="store_true", + help="Print the config but skip training.", + ) + parser.add_argument( + "--rows", + type=int, + default=200, + help="Number of synthetic rows to generate for training (default: 200).", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + description = args.description or DEFAULT_DESCRIPTION + + print("=" * 70) + print("Ludwig LLM-Driven Config Generation") + print("NOTE: Requires PR #4092 (ludwig>=0.14).") + print("=" * 70) + print() + print("Task description:") + print(textwrap.fill(description, width=70, initial_indent=" ", subsequent_indent=" ")) + print() + print(f"LLM model : {args.model}") + print() + + # ------------------------------------------------------------------ + # Generate config + # ------------------------------------------------------------------ + print("Generating config ... (this may take a few seconds)") + try: + config = generate_config( + description, + model=args.model, + validate=True, + ) + except Exception as exc: + print(f"\nError during config generation: {exc}") + sys.exit(1) + + print("\nGenerated Ludwig config (YAML):") + print("-" * 70) + print(yaml.dump(config, default_flow_style=False, sort_keys=False)) + print("-" * 70) + + if args.no_train: + print("--no-train flag set. Exiting without training.") + return + + # ------------------------------------------------------------------ + # Confirm before training + # ------------------------------------------------------------------ + try: + answer = input("\nTrain a quick model on synthetic data? [y/N] ").strip().lower() + except EOFError: + # non-interactive environment + answer = "n" + + if answer not in ("y", "yes"): + print("Skipping training.") + return + + # ------------------------------------------------------------------ + # Build synthetic dataset and train + # ------------------------------------------------------------------ + print(f"\nBuilding synthetic dataset ({args.rows} rows) ...") + df = build_synthetic_dataframe(config, n_rows=args.rows) + print(f" Columns : {list(df.columns)}") + print(f" Shape : {df.shape}") + + import os + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".csv", delete=False, mode="w") as f: + df.to_csv(f, index=False) + csv_path = f.name + + print(f" Saved to: {csv_path}") + print("\nStarting Ludwig training ...") + + try: + from ludwig.api import LudwigModel + + model = LudwigModel(config=config, logging_level="WARNING") + result = model.train( + dataset=csv_path, + output_directory=tempfile.mkdtemp(prefix="ludwig_output_"), + ) + print(f"\nTraining complete. Outputs saved to: {result.output_directory}") + print("\nFinal validation metrics:") + for feat_name, metrics in (result.train_stats.validation or {}).items(): + for metric_name, value in metrics.items(): + if isinstance(value, (int, float)): + print(f" {feat_name}/{metric_name}: {value:.4f}") + except Exception as exc: + print(f"\nTraining error: {exc}") + print("The config generation succeeded — try adjusting the config manually.") + finally: + os.unlink(csv_path) + + +if __name__ == "__main__": + main() diff --git a/examples/llm_config_generation/llm_config_generation.ipynb b/examples/llm_config_generation/llm_config_generation.ipynb new file mode 100644 index 0000000..32eb7c2 --- /dev/null +++ b/examples/llm_config_generation/llm_config_generation.ipynb @@ -0,0 +1,394 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0001", + "metadata": {}, + "source": [ + "# LLM-Driven Config Generation\n", + "\n", + "> **Note:** This notebook requires **PR #4092** to be merged into Ludwig, or `pip install ludwig>=0.14`.\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/llm_config_generation/llm_config_generation.ipynb)\n", + "\n", + "This notebook demonstrates Ludwig's config generation feature: describe your ML task in plain English and receive a fully validated Ludwig configuration dictionary. The LLM maps your column names to Ludwig feature types, selects an appropriate architecture, and returns a config that has already been validated against Ludwig's Pydantic schema.\n", + "\n", + "**What you need:**\n", + "- An Anthropic API key (stored as `ANTHROPIC_API_KEY`) **or** an OpenAI API key (`OPENAI_API_KEY`)\n", + "- Ludwig ≥ 0.14 (provides `ludwig.config_generation`)\n", + "\n", + "Run the install cell below to get started." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0002", + "metadata": {}, + "outputs": [], + "source": [ + "# Install dependencies\n", + "# For the Anthropic backend (Claude models):\n", + "!pip install \"ludwig>=0.14\" anthropic --quiet\n", + "# For the OpenAI backend (GPT models), also run:\n", + "# !pip install openai --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "0003", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Set your API key. In Google Colab you can store it under **Secrets** (the key icon in the left sidebar) and retrieve it with `userdata.get()`. Outside Colab, set the `ANTHROPIC_API_KEY` environment variable before launching the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0004", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# --- Colab: read from Colab Secrets ---\n", + "try:\n", + " from google.colab import userdata\n", + "\n", + " os.environ[\"ANTHROPIC_API_KEY\"] = userdata.get(\"ANTHROPIC_API_KEY\")\n", + " print(\"Loaded ANTHROPIC_API_KEY from Colab Secrets.\")\n", + "except Exception:\n", + " # Outside Colab: make sure the variable is already set in the environment,\n", + " # or uncomment and fill in the line below:\n", + " # os.environ[\"ANTHROPIC_API_KEY\"] = \"sk-ant-...\"\n", + " if os.environ.get(\"ANTHROPIC_API_KEY\"):\n", + " print(\"ANTHROPIC_API_KEY found in environment.\")\n", + " else:\n", + " print(\"WARNING: ANTHROPIC_API_KEY not set. Set it before running generation cells.\")\n", + "\n", + "# Choose your model:\n", + "CLAUDE_MODEL = \"claude-sonnet-4-20250514\" # Anthropic\n", + "# GPT_MODEL = \"gpt-4o\" # OpenAI (set OPENAI_API_KEY instead)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0005", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: ludwig.config_generation is provided by PR #4092 (ludwig>=0.14).\n", + "# If you see an ImportError, that PR has not yet been merged into your installed version.\n", + "import yaml\n", + "\n", + "from ludwig.config_generation import generate_config" + ] + }, + { + "cell_type": "markdown", + "id": "0006", + "metadata": {}, + "source": [ + "## Example 1: Tabular classification (churn prediction)\n", + "\n", + "We describe a binary classification task over a tabular customer dataset. The LLM will assign Ludwig feature types, choose an encoder, and produce a validated config." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0007", + "metadata": {}, + "outputs": [], + "source": [ + "churn_description = (\n", + " \"I have customer data with the following columns: \"\n", + " \"age (integer), annual_income (float), num_purchases (integer), \"\n", + " \"days_since_last_purchase (integer), and country (string category). \"\n", + " \"I want to predict churn (binary: 0 or 1). \"\n", + " \"The dataset has about 50 000 rows.\"\n", + ")\n", + "\n", + "churn_config = generate_config(\n", + " churn_description,\n", + " model=CLAUDE_MODEL,\n", + " validate=True,\n", + ")\n", + "\n", + "print(yaml.dump(churn_config, default_flow_style=False, sort_keys=False))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0008", + "metadata": {}, + "outputs": [], + "source": [ + "# Build a small synthetic dataset matching the generated schema and train\n", + "import os\n", + "import tempfile\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "np.random.seed(42)\n", + "n = 300\n", + "\n", + "synthetic_churn = pd.DataFrame(\n", + " {\n", + " \"age\": np.random.randint(18, 70, n),\n", + " \"annual_income\": np.random.uniform(20_000, 150_000, n),\n", + " \"num_purchases\": np.random.randint(0, 200, n),\n", + " \"days_since_last_purchase\": np.random.randint(0, 365, n),\n", + " \"country\": np.random.choice([\"US\", \"UK\", \"DE\", \"FR\", \"CA\"], n),\n", + " \"churn\": np.random.randint(0, 2, n),\n", + " }\n", + ")\n", + "\n", + "with tempfile.NamedTemporaryFile(suffix=\".csv\", delete=False, mode=\"w\") as f:\n", + " synthetic_churn.to_csv(f, index=False)\n", + " churn_csv = f.name\n", + "\n", + "print(synthetic_churn.head())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0009", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.api import LudwigModel\n", + "\n", + "churn_model = LudwigModel(config=churn_config, logging_level=\"WARNING\")\n", + "train_stats, _, output_dir = churn_model.train(\n", + " dataset=churn_csv,\n", + " output_directory=tempfile.mkdtemp(prefix=\"ludwig_churn_\"),\n", + ")\n", + "os.unlink(churn_csv)\n", + "print(f\"Training complete. Output: {output_dir}\")\n", + "\n", + "# Print validation accuracy\n", + "for feat, metrics in train_stats.get(\"validation\", {}).items():\n", + " for m, v in metrics.items():\n", + " if isinstance(v, float):\n", + " print(f\" {feat}/{m}: {v:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0010", + "metadata": {}, + "source": [ + "## Example 2: Multi-task (classification + regression)\n", + "\n", + "Ludwig natively supports training on multiple output targets in one pass. Here we describe a task where we want both a classification output and a regression output simultaneously." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0011", + "metadata": {}, + "outputs": [], + "source": [ + "multitask_description = (\n", + " \"I have an e-commerce dataset with columns: \"\n", + " \"customer_age (integer), account_tenure_days (integer), \"\n", + " \"total_spend (float), product_category (category string), \"\n", + " \"and region (category string). \"\n", + " \"I want to simultaneously predict: \"\n", + " \"1) will_churn (binary classification) and \"\n", + " \"2) predicted_lifetime_value (continuous regression). \"\n", + " \"Dataset size is roughly 100 000 rows.\"\n", + ")\n", + "\n", + "multitask_config = generate_config(\n", + " multitask_description,\n", + " model=CLAUDE_MODEL,\n", + " validate=True,\n", + ")\n", + "\n", + "print(\"Output features:\")\n", + "for out in multitask_config.get(\"output_features\", []):\n", + " print(f\" {out['name']} ({out['type']})\")\n", + "\n", + "print(\"\\nFull config (YAML):\")\n", + "print(yaml.dump(multitask_config, default_flow_style=False, sort_keys=False))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0012", + "metadata": {}, + "outputs": [], + "source": [ + "# Synthetic multi-task dataset\n", + "np.random.seed(0)\n", + "n = 300\n", + "\n", + "synthetic_multitask = pd.DataFrame(\n", + " {\n", + " \"customer_age\": np.random.randint(18, 75, n),\n", + " \"account_tenure_days\": np.random.randint(1, 3650, n),\n", + " \"total_spend\": np.random.uniform(10, 10_000, n),\n", + " \"product_category\": np.random.choice([\"Electronics\", \"Clothing\", \"Food\", \"Books\"], n),\n", + " \"region\": np.random.choice([\"North\", \"South\", \"East\", \"West\"], n),\n", + " \"will_churn\": np.random.randint(0, 2, n),\n", + " \"predicted_lifetime_value\": np.random.uniform(0, 5000, n),\n", + " }\n", + ")\n", + "\n", + "with tempfile.NamedTemporaryFile(suffix=\".csv\", delete=False, mode=\"w\") as f:\n", + " synthetic_multitask.to_csv(f, index=False)\n", + " multitask_csv = f.name\n", + "\n", + "multitask_model = LudwigModel(config=multitask_config, logging_level=\"WARNING\")\n", + "train_stats_mt, _, output_dir_mt = multitask_model.train(\n", + " dataset=multitask_csv,\n", + " output_directory=tempfile.mkdtemp(prefix=\"ludwig_multitask_\"),\n", + ")\n", + "os.unlink(multitask_csv)\n", + "print(f\"Multi-task training complete. Output: {output_dir_mt}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0013", + "metadata": {}, + "source": [ + "## Example 3: Text + tabular (multimodal)\n", + "\n", + "Ludwig can encode both free-text columns and structured tabular columns in the same model. Describe the mixed modality and the LLM will configure the appropriate text encoder alongside the tabular encoder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0014", + "metadata": {}, + "outputs": [], + "source": [ + "multimodal_description = (\n", + " \"I have a product review dataset. Each row has: \"\n", + " \"review_text (free-form English text, typically 1-5 sentences), \"\n", + " \"product_price (float), product_category (category string), \"\n", + " \"and verified_purchase (binary). \"\n", + " \"I want to predict star_rating, which is an integer from 1 to 5 (treat as category). \"\n", + " \"There are about 200 000 rows.\"\n", + ")\n", + "\n", + "multimodal_config = generate_config(\n", + " multimodal_description,\n", + " model=CLAUDE_MODEL,\n", + " validate=True,\n", + ")\n", + "\n", + "print(\"Input features and encoders:\")\n", + "for feat in multimodal_config.get(\"input_features\", []):\n", + " encoder = feat.get(\"encoder\", {}).get(\"type\", \"default\")\n", + " print(f\" {feat['name']} ({feat['type']}) — encoder: {encoder}\")\n", + "\n", + "print(\"\\nFull config:\")\n", + "print(yaml.dump(multimodal_config, default_flow_style=False, sort_keys=False))" + ] + }, + { + "cell_type": "markdown", + "id": "0015", + "metadata": {}, + "source": [ + "## Validation in action\n", + "\n", + "The `validate=True` flag (the default) runs the generated config through Ludwig's Pydantic schema before returning it. Here we intentionally pass a vague or contradictory description to observe how the validation step catches problems — and how clearer descriptions fix them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0016", + "metadata": {}, + "outputs": [], + "source": [ + "# Intentionally vague — the LLM may produce an ambiguous or invalid config.\n", + "bad_description = \"predict stuff from other stuff\"\n", + "\n", + "try:\n", + " bad_config = generate_config(\n", + " bad_description,\n", + " model=CLAUDE_MODEL,\n", + " validate=True,\n", + " )\n", + " print(\"Config generated (LLM filled in defaults):\")\n", + " print(yaml.dump(bad_config, default_flow_style=False, sort_keys=False))\n", + "except Exception as exc:\n", + " print(f\"Validation error (expected for a vague description):\\n {exc}\")\n", + " print(\"\\nTip: be explicit about column names, types, and target variables.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0017", + "metadata": {}, + "outputs": [], + "source": [ + "# Same task — but with a clear description. Validation passes.\n", + "clear_description = (\n", + " \"Predict house_price (continuous float) from bedrooms (integer), \"\n", + " \"sqft (float), and location (string category). Dataset has 20 000 rows.\"\n", + ")\n", + "\n", + "clear_config = generate_config(\n", + " clear_description,\n", + " model=CLAUDE_MODEL,\n", + " validate=True,\n", + ")\n", + "print(\"Config generated and validated successfully:\")\n", + "print(yaml.dump(clear_config, default_flow_style=False, sort_keys=False))" + ] + }, + { + "cell_type": "markdown", + "id": "0018", + "metadata": {}, + "source": [ + "## Tips for good task descriptions\n", + "\n", + "Getting a high-quality config back from `generate_config` is largely about giving the LLM enough context. Here are the key guidelines:\n", + "\n", + "- **Name your columns explicitly.** `age (integer), annual_income (float)` is far better than \"some user features\". The LLM maps column names directly to Ludwig feature definitions.\n", + "- **State the target variable and its type.** \"predict `churn` (binary: 0 or 1)\" or \"predict `revenue` (continuous float)\" removes all ambiguity about what Ludwig's output feature should be.\n", + "- **Describe multi-output tasks explicitly.** For multi-task learning say: \"simultaneously predict `price` (regression) and `category` (classification)\".\n", + "- **Mention text columns and their nature.** \"free-form English product review (typically 1-3 sentences)\" lets the LLM choose a suitable text encoder rather than falling back to a bag-of-words default.\n", + "- **Include rough dataset size.** `~50 k rows` helps the LLM suggest appropriate model complexity and training settings.\n", + "- **Specify cardinality of category columns when known.** \"country (category, ~200 unique values)\" vs. \"gender (binary category: M/F)\" leads to different encoder choices.\n", + "- **If you have domain knowledge, share it.** \"income values are right-skewed and range from 10 000 to 500 000\" may improve preprocessing suggestions.\n", + "\n", + "Even if the first generated config is not perfect, it provides an excellent starting point. Inspect the YAML, make targeted edits, and re-run training — far faster than writing the config from scratch." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/llm_few_shot_learning/simple_model_training.py b/examples/llm_few_shot_learning/simple_model_training.py new file mode 100644 index 0000000..a757e00 --- /dev/null +++ b/examples/llm_few_shot_learning/simple_model_training.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +# # Simple Model Training Example +# +# This is a simple example of how to use the LLM model type to train +# a zero shot classification model. It uses the facebook/opt-350m model +# as the base LLM model. + +# Import required libraries +import logging +import shutil + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +review_label_pairs = [ + {"review": "I loved this movie!", "label": "positive"}, + {"review": "The food was okay, but the service was terrible.", "label": "negative"}, + {"review": "I can't believe how rude the staff was.", "label": "negative"}, + {"review": "This book was a real page-turner.", "label": "positive"}, + {"review": "The hotel room was dirty and smelled bad.", "label": "negative"}, + {"review": "I had a great experience at this restaurant.", "label": "positive"}, + {"review": "The concert was amazing!", "label": "positive"}, + {"review": "The traffic was terrible on my way to work this morning.", "label": "negative"}, + {"review": "The customer service was excellent.", "label": "positive"}, + {"review": "I was disappointed with the quality of the product.", "label": "negative"}, + {"review": "The scenery on the hike was breathtaking.", "label": "positive"}, + {"review": "I had a terrible experience at this hotel.", "label": "negative"}, + {"review": "The coffee at this cafe was delicious.", "label": "positive"}, + {"review": "The weather was perfect for a day at the beach.", "label": "positive"}, + {"review": "I would definitely recommend this product.", "label": "positive"}, + {"review": "The wait time at the doctor's office was ridiculous.", "label": "negative"}, + {"review": "The museum was a bit underwhelming.", "label": "neutral"}, + {"review": "I had a fantastic time at the amusement park.", "label": "positive"}, + {"review": "The staff at this store was extremely helpful.", "label": "positive"}, + {"review": "The airline lost my luggage and was very unhelpful.", "label": "negative"}, + {"review": "This album is a must-listen for any music fan.", "label": "positive"}, + {"review": "The food at this restaurant was just okay.", "label": "neutral"}, + {"review": "I was pleasantly surprised by how great this movie was.", "label": "positive"}, + {"review": "The car rental process was quick and easy.", "label": "positive"}, + {"review": "The service at this hotel was top-notch.", "label": "positive"}, +] + +df = pd.DataFrame(review_label_pairs) +df["split"] = [0] * 15 + [2] * 10 + +config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +generation: + temperature: 0.1 + top_p: 0.75 + top_k: 40 + num_beams: 4 + max_new_tokens: 64 +prompt: + task: "Classify the sample input as either negative, neutral, or positive." + retrieval: + type: semantic + k: 3 + model_name: paraphrase-MiniLM-L3-v2 +input_features: +- + name: review + type: text +output_features: +- + name: label + type: category + preprocessing: + fallback_label: "neutral" + decoder: + type: category_extractor + match: + "negative": + type: contains + value: "positive" + "neural": + type: contains + value: "neutral" + "positive": + type: contains + value: "positive" +preprocessing: + split: + type: fixed + """) + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset=df, experiment_name="simple_experiment", model_name="simple_model", skip_save_processed_input=True +) + +training_set, val_set, test_set, _ = preprocessed_data + +# batch prediction +preds, _ = model.predict(test_set, skip_save_predictions=False) +print(preds) diff --git a/examples/llm_finetuning/README.md b/examples/llm_finetuning/README.md new file mode 100644 index 0000000..a3f78e7 --- /dev/null +++ b/examples/llm_finetuning/README.md @@ -0,0 +1,57 @@ +# LLM Fine-tuning + +These examples show you how to fine-tune Large Language Models by taking advantage of model parallelism +with [DeepSpeed](https://www.deepspeed.ai/), allowing Ludwig to scale to very large models with billions of +parameters. + +The task here will be to fine-tune a large billion+ LLM to classify the sentiment of [IMDB movie reviews](https://www.kaggle.com/datasets/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews). As such, we'll be taking a pretrained LLM, attaching a classification head, +and fine-tuning the weights to improve performance of the LLM on the task. Ludwig will do this for you without no machine learning +code, just configuration. + +## Prerequisites + +- Installed Ludwig with `ludwig[distributed]` dependencies +- Have a CUDA-enabled version of PyTorch installed +- Have access to a machine or cluster of machines with multiple GPUs +- The IMDB dataset used in these examples comes from Kaggle, so make sure you have your credentials set (e.g., `$HOME/.kaggle.kaggle.json`) + +## Running DeepSpeed on Ray + +This is the recommended way to use DeepSpeed, which supports auto-batch size tuning and distributed data processing. +There is some overhead from using Ray with small datasets (\<100MB), but in most cases performance should be comparable +to using native DeepSpeed. + +From the head node of your Ray cluster: + +```bash +./run_train_dsz3_ray.sh +``` + +### Python API + +If you want to run Ludwig programatically (from a notebook or as part of a larger workflow), you can run the following +Python script using the Ray cluster launcher from your local machine. + +```bash +ray submit cluster.yaml train_imdb_ray.py +``` + +If running directly on the Ray head node, you can omit the `ray submit` portion and run like an ordinary Python script: + +```bash +python train_imdb_ray.py +``` + +## Running DeepSpeed Native + +This mode is suitable for datasets small enough to fit in memory on a single machine, as it doesn't make use of +distributed data processing (requires use of the Ray backend). + +The following example assumes you have 4 GPUs available, but can easily be modified to support your preferred +setup. + +From a terminal on your machine: + +```bash +./run_train_dsz3.sh +``` diff --git a/examples/llm_finetuning/imdb_deepspeed_zero3.yaml b/examples/llm_finetuning/imdb_deepspeed_zero3.yaml new file mode 100644 index 0000000..8ce53f2 --- /dev/null +++ b/examples/llm_finetuning/imdb_deepspeed_zero3.yaml @@ -0,0 +1,25 @@ +input_features: + - name: review + type: text + encoder: + type: auto_transformer + pretrained_model_name_or_path: bigscience/bloom-3b + trainable: true + adapter: lora + +output_features: + - name: sentiment + type: category + +trainer: + batch_size: 4 + epochs: 3 + gradient_accumulation_steps: 8 + +backend: + type: deepspeed + zero_optimization: + stage: 3 + offload_optimizer: + device: cpu + pin_memory: true diff --git a/examples/llm_finetuning/imdb_deepspeed_zero3_ray.yaml b/examples/llm_finetuning/imdb_deepspeed_zero3_ray.yaml new file mode 100644 index 0000000..c40f274 --- /dev/null +++ b/examples/llm_finetuning/imdb_deepspeed_zero3_ray.yaml @@ -0,0 +1,29 @@ +input_features: + - name: review + type: text + encoder: + type: auto_transformer + pretrained_model_name_or_path: bigscience/bloom-3b + trainable: true + adapter: lora + +output_features: + - name: sentiment + type: category + +trainer: + batch_size: 4 + epochs: 3 + gradient_accumulation_steps: 8 + +backend: + type: ray + trainer: + use_gpu: true + strategy: + type: deepspeed + zero_optimization: + stage: 3 + offload_optimizer: + device: cpu + pin_memory: true diff --git a/examples/llm_finetuning/run_train_dsz3.sh b/examples/llm_finetuning/run_train_dsz3.sh new file mode 100755 index 0000000..9a834a9 --- /dev/null +++ b/examples/llm_finetuning/run_train_dsz3.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Fail fast if an error occurs +set -e + +# Get the directory of this script, which contains the config file +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Train +deepspeed --no_python --no_local_rank --num_gpus 4 ludwig train --config ${SCRIPT_DIR}/imdb_deepspeed_zero3.yaml --dataset ludwig://imdb diff --git a/examples/llm_finetuning/run_train_dsz3_ray.sh b/examples/llm_finetuning/run_train_dsz3_ray.sh new file mode 100755 index 0000000..176924f --- /dev/null +++ b/examples/llm_finetuning/run_train_dsz3_ray.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Fail fast if an error occurs +set -e + +# Get the directory of this script, which contains the config file +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Train +ludwig train --config ${SCRIPT_DIR}/imdb_deepspeed_zero3_ray.yaml --dataset ludwig://imdb diff --git a/examples/llm_finetuning/train_imdb_ray.py b/examples/llm_finetuning/train_imdb_ray.py new file mode 100644 index 0000000..7fe2448 --- /dev/null +++ b/examples/llm_finetuning/train_imdb_ray.py @@ -0,0 +1,58 @@ +import logging +import os + +import yaml + +from ludwig.api import LudwigModel + +config = yaml.safe_load(""" +input_features: + - name: review + type: text + + encoder: + type: auto_transformer + pretrained_model_name_or_path: bigscience/bloom-3b + trainable: true + adapter: + type: lora + +output_features: + - name: sentiment + type: category + +trainer: + batch_size: 4 + epochs: 3 + +backend: + type: ray + trainer: + use_gpu: true + strategy: + type: deepspeed + zero_optimization: + stage: 3 + offload_optimizer: + device: cpu + pin_memory: true +""") + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset="ludwig://imdb", + experiment_name="imdb_sentiment", + model_name="bloom3b", +) + +# list contents of output directory +print("contents of output directory:", output_directory) +for item in os.listdir(output_directory): + print("\t", item) diff --git a/examples/llm_instruction_tuning/train_alpaca_ray.py b/examples/llm_instruction_tuning/train_alpaca_ray.py new file mode 100644 index 0000000..2d5f486 --- /dev/null +++ b/examples/llm_instruction_tuning/train_alpaca_ray.py @@ -0,0 +1,58 @@ +import logging +import os + +import yaml + +from ludwig.api import LudwigModel + +config = yaml.safe_load(""" +model_type: llm +base_model: bigscience/bloomz-3b + +adapter: + type: lora + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +trainer: + type: finetune + batch_size: 4 + epochs: 3 + +backend: + type: ray + trainer: + use_gpu: true + strategy: + type: deepspeed + zero_optimization: + stage: 3 + offload_optimizer: + device: cpu + pin_memory: true +""") + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset="ludwig://alpaca", + experiment_name="alpaca_instruct", + model_name="bloom560m", +) + +# list contents of output directory +print("contents of output directory:", output_directory) +for item in os.listdir(output_directory): + print("\t", item) diff --git a/examples/llm_structured_output/README.md b/examples/llm_structured_output/README.md new file mode 100644 index 0000000..b607b36 --- /dev/null +++ b/examples/llm_structured_output/README.md @@ -0,0 +1,71 @@ +# Structured and Constrained LLM Output + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/llm_structured_output/structured_output.ipynb) + +## Why constrained decoding matters + +Large language models are trained to produce fluent text, but they have no built-in guarantee that their output follows a particular format. When you ask an LLM to return JSON, classify into one of three labels, or follow a grammar, it may occasionally hallucinate, add extra text, or produce a structurally invalid response. + +**Constrained decoding** solves this by modifying the token sampling process at inference time. A constraint (JSON schema, regular expression, or context-free grammar) is compiled into a set of masks that are applied to the model's logit distribution at each step. Tokens that would violate the constraint are assigned negative infinity logit, so the model can only ever produce valid output. + +Ludwig supports three forms of constrained output: + +| Constraint type | Use case | Config key | +| --------------- | ----------------------------------------------- | --------------------- | +| JSON schema | Structured data extraction, tool-call responses | `decoder.json_schema` | +| Regex | Classification, fixed-format fields | `decoder.regex` | +| Grammar (EBNF) | Complex structured formats | `decoder.grammar` | + +## Quick start + +```bash +pip install "ludwig[llm]" +``` + +### Entity extraction (JSON schema) + +```bash +python run_structured.py +``` + +Or use the Ludwig API directly with one of the provided configs: + +```python +import pandas as pd +from ludwig.api import LudwigModel + +model = LudwigModel(config="config_json_schema.yaml") +preds, _, _ = model.predict(dataset=pd.DataFrame({"text": ["Apple was founded by Steve Jobs in Cupertino."]})) +print(preds["output_predictions"].iloc[0]) +# -> {"entities": [{"text": "Apple", "type": "ORG"}, ...]} +``` + +### Sentiment classification (regex) + +```python +model = LudwigModel(config="config_constrained.yaml") +preds, _, _ = model.predict(dataset=pd.DataFrame({"text": ["I loved this product!"]})) +print(preds["sentiment_predictions"].iloc[0]) +# -> positive +``` + +## Files + +| File | Description | +| ------------------------- | ------------------------------------------------------------ | +| `structured_output.ipynb` | Interactive notebook (Colab-compatible) | +| `config_json_schema.yaml` | Ludwig config for JSON schema entity extraction | +| `config_constrained.yaml` | Ludwig config for regex-constrained sentiment classification | +| `run_structured.py` | Standalone script showing all three features | + +## Models used + +Both configs use freely available models that fit on a free Colab GPU (T4, 16 GB): + +- [`microsoft/phi-2`](https://huggingface.co/microsoft/phi-2) — 2.7 B parameters, JSON schema examples +- [`Qwen/Qwen2-0.5B-Instruct`](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) — 0.5 B parameters, classification examples + +## Further reading + +- [Structured and Constrained LLM Output — Ludwig User Guide](../../docs/user_guide/llms/structured_output.md) +- [Ludwig LLM configuration reference](../../docs/configuration/large_language_model.md) diff --git a/examples/llm_structured_output/config_constrained.yaml b/examples/llm_structured_output/config_constrained.yaml new file mode 100644 index 0000000..03dece2 --- /dev/null +++ b/examples/llm_structured_output/config_constrained.yaml @@ -0,0 +1,29 @@ +model_type: llm +base_model: Qwen/Qwen2-0.5B-Instruct + +prompt: + task: > + Classify the sentiment of the following text. + Respond with exactly one word: positive, negative, or neutral. + +input_features: + - name: text + type: text + +output_features: + - name: sentiment + type: text + decoder: + type: text_parser + # Regex constraint: only allow the three valid sentiment labels. + # The LLM's token sampling is restricted so it can only produce one + # of these strings, eliminating hallucinated or malformed outputs. + regex: "(positive|negative|neutral)" + +generation: + max_new_tokens: 10 + temperature: 0.0 + do_sample: false + +backend: + type: local diff --git a/examples/llm_structured_output/config_json_schema.yaml b/examples/llm_structured_output/config_json_schema.yaml new file mode 100644 index 0000000..e1412ee --- /dev/null +++ b/examples/llm_structured_output/config_json_schema.yaml @@ -0,0 +1,49 @@ +model_type: llm +base_model: microsoft/phi-2 + +prompt: + task: > + Extract the named entities from the input text and return them as a JSON object + with the following structure: {"entities": [{"text": "...", "type": "PERSON|ORG|LOC|DATE"}]}. + Return only valid JSON, nothing else. + +input_features: + - name: text + type: text + +output_features: + - name: output + type: text + decoder: + type: text_parser + json_schema: + type: object + properties: + entities: + type: array + items: + type: object + properties: + text: + type: string + type: + type: string + enum: + - PERSON + - ORG + - LOC + - DATE + required: + - text + - type + required: + - entities + additionalProperties: false + +generation: + max_new_tokens: 200 + temperature: 0.1 + do_sample: false + +backend: + type: local diff --git a/examples/llm_structured_output/run_structured.py b/examples/llm_structured_output/run_structured.py new file mode 100644 index 0000000..efce847 --- /dev/null +++ b/examples/llm_structured_output/run_structured.py @@ -0,0 +1,255 @@ +"""Structured and constrained LLM output with Ludwig. + +Demonstrates: +1. Entity extraction using JSON schema constraints +2. Sentiment classification with regex-constrained decoding +3. Side-by-side comparison of constrained vs unconstrained output + +Run: + python run_structured.py +""" + +import json +import textwrap + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel + +# --------------------------------------------------------------------------- +# 1. Entity extraction with JSON schema constraints +# --------------------------------------------------------------------------- + +ENTITY_EXTRACTION_CONFIG = yaml.safe_load(""" +model_type: llm +base_model: microsoft/phi-2 + +prompt: + task: > + Extract the named entities from the input text and return them as a JSON + object with this structure: + {"entities": [{"text": "...", "type": "PERSON|ORG|LOC|DATE"}]}. + Return only valid JSON, nothing else. + +input_features: + - name: text + type: text + +output_features: + - name: output + type: text + decoder: + type: text_parser + json_schema: + type: object + properties: + entities: + type: array + items: + type: object + properties: + text: + type: string + type: + type: string + enum: [PERSON, ORG, LOC, DATE] + required: [text, type] + required: [entities] + additionalProperties: false + +generation: + max_new_tokens: 200 + temperature: 0.1 + do_sample: false + +backend: + type: local +""") + +ENTITY_SAMPLES = [ + "Apple Inc. was founded by Steve Jobs in Cupertino, California on April 1, 1976.", + "Elon Musk announced that Tesla will open a new Gigafactory in Berlin next year.", + "The United Nations headquarters is located in New York City.", +] + + +def run_entity_extraction() -> None: + print("=" * 60) + print("Entity Extraction with JSON Schema Constraints") + print("=" * 60) + + model = LudwigModel(config=ENTITY_EXTRACTION_CONFIG) + df = pd.DataFrame({"text": ENTITY_SAMPLES}) + predictions, _, _ = model.predict(dataset=df) + + for i, (text, pred) in enumerate(zip(ENTITY_SAMPLES, predictions["output_predictions"])): + print(f"\n[{i + 1}] Input: {text}") + try: + parsed = json.loads(pred) + entities = parsed.get("entities", []) + print(f" Entities ({len(entities)}):") + for ent in entities: + print(f" - '{ent['text']}' ({ent['type']})") + except json.JSONDecodeError: + print(f" Raw output: {pred}") + + +# --------------------------------------------------------------------------- +# 2. Sentiment classification with regex-constrained decoding +# --------------------------------------------------------------------------- + +SENTIMENT_CONFIG_CONSTRAINED = yaml.safe_load(""" +model_type: llm +base_model: Qwen/Qwen2-0.5B-Instruct + +prompt: + task: > + Classify the sentiment of the following text. + Respond with exactly one word: positive, negative, or neutral. + +input_features: + - name: text + type: text + +output_features: + - name: sentiment + type: text + decoder: + type: text_parser + regex: "(positive|negative|neutral)" + +generation: + max_new_tokens: 10 + temperature: 0.0 + do_sample: false + +backend: + type: local +""") + +SENTIMENT_CONFIG_UNCONSTRAINED = yaml.safe_load(""" +model_type: llm +base_model: Qwen/Qwen2-0.5B-Instruct + +prompt: + task: > + Classify the sentiment of the following text. + Respond with exactly one word: positive, negative, or neutral. + +input_features: + - name: text + type: text + +output_features: + - name: sentiment + type: text + +generation: + max_new_tokens: 30 + temperature: 0.7 + +backend: + type: local +""") + +SENTIMENT_SAMPLES = [ + "I absolutely loved this product! It exceeded all my expectations.", + "The service was terrible and the food was cold.", + "The movie was okay, nothing special.", + "This is the best laptop I have ever owned. Highly recommend.", + "I waited two hours and they still got my order wrong.", + "The weather today is neither good nor bad.", +] + + +def run_sentiment_comparison() -> None: + print("\n" + "=" * 60) + print("Sentiment Classification: Constrained vs Unconstrained") + print("=" * 60) + + df = pd.DataFrame({"text": SENTIMENT_SAMPLES}) + + print("\nRunning UNCONSTRAINED model...") + model_unconstrained = LudwigModel(config=SENTIMENT_CONFIG_UNCONSTRAINED) + preds_unconstrained, _, _ = model_unconstrained.predict(dataset=df) + + print("Running CONSTRAINED model (regex: positive|negative|neutral)...") + model_constrained = LudwigModel(config=SENTIMENT_CONFIG_CONSTRAINED) + preds_constrained, _, _ = model_constrained.predict(dataset=df) + + print(f"\n{'Input':<52} {'Unconstrained':<30} {'Constrained':<15}") + print("-" * 97) + for text, unconstrained, constrained in zip( + SENTIMENT_SAMPLES, + preds_unconstrained["sentiment_predictions"], + preds_constrained["sentiment_predictions"], + ): + short_text = textwrap.shorten(text, width=50) + print(f"{short_text:<52} {unconstrained!s:<30} {constrained!s:<15}") + + # Count invalid outputs in unconstrained + valid_labels = {"positive", "negative", "neutral"} + invalid = [p for p in preds_unconstrained["sentiment_predictions"] if str(p).strip().lower() not in valid_labels] + print(f"\nUnconstrained invalid outputs: {len(invalid)}/{len(SENTIMENT_SAMPLES)}") + print("Constrained invalid outputs: 0 (guaranteed by regex constraint)") + + +# --------------------------------------------------------------------------- +# 3. Logits extraction +# --------------------------------------------------------------------------- + +LOGITS_CONFIG = yaml.safe_load(""" +model_type: llm +base_model: Qwen/Qwen2-0.5B-Instruct + +prompt: + task: "Answer with a single word." + +input_features: + - name: text + type: text + +output_features: + - name: response + type: text + +generation: + max_new_tokens: 5 + temperature: 0.0 + do_sample: false + +backend: + type: local +""") + + +def run_logits_extraction() -> None: + print("\n" + "=" * 60) + print("Logits Extraction") + print("=" * 60) + + model = LudwigModel(config=LOGITS_CONFIG) + df = pd.DataFrame({"text": ["Is Python a programming language?"]}) + + # collect_activations returns intermediate layer activations alongside predictions + predictions, output_df, _ = model.predict(dataset=df, collect_predictions=True) + + print("Prediction:", predictions["response_predictions"].iloc[0]) + + # When logits are available they appear as response_logits in the output + if "response_logits" in output_df.columns: + logits = output_df["response_logits"].iloc[0] + print(f"Logits shape: {logits.shape if hasattr(logits, 'shape') else 'N/A'}") + print(f"First 5 logit values: {logits[:5] if hasattr(logits, '__iter__') else logits}") + else: + print("Logits not present in output (enable output_logits in config to collect them).") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- +if __name__ == "__main__": + run_entity_extraction() + run_sentiment_comparison() + run_logits_extraction() diff --git a/examples/llm_structured_output/structured_output.ipynb b/examples/llm_structured_output/structured_output.ipynb new file mode 100644 index 0000000..ed3f391 --- /dev/null +++ b/examples/llm_structured_output/structured_output.ipynb @@ -0,0 +1,382 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Structured and Constrained LLM Output with Ludwig\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/llm_structured_output/structured_output.ipynb)\n", + "\n", + "Large language models are trained to produce fluent text, but they have no built-in guarantee that their output follows a particular format. **Constrained decoding** solves this by restricting token sampling at inference time so the model can only ever produce output that satisfies a given constraint.\n", + "\n", + "**This notebook covers:**\n", + "1. Entity extraction with a JSON schema constraint\n", + "2. Sentiment classification with a regex constraint (guaranteed to produce only `positive`, `negative`, or `neutral`)\n", + "3. Logits extraction from LLM output\n", + "4. Side-by-side comparison: constrained vs unconstrained decoding\n", + "\n", + "All examples use small, freely available models that run on a free Colab T4 GPU." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install \"ludwig[llm]\" --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import textwrap\n", + "\n", + "import pandas as pd\n", + "import torch\n", + "import yaml\n", + "\n", + "from ludwig.api import LudwigModel\n", + "\n", + "# Check GPU availability\n", + "if torch.cuda.is_available():\n", + " device_name = torch.cuda.get_device_name(0)\n", + " vram_gb = torch.cuda.get_device_properties(0).total_memory / 1e9\n", + " print(f\"GPU: {device_name} ({vram_gb:.1f} GB VRAM)\")\n", + "else:\n", + " print(\"No GPU detected. Running on CPU — inference will be slower.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Entity extraction with JSON schema\n", + "\n", + "We configure Ludwig to constrain the LLM's output to a specific JSON schema. The schema describes the shape of the expected output — Ludwig compiles it into logit masks so only valid JSON tokens can be sampled.\n", + "\n", + "The model is `microsoft/phi-2` (2.7 B parameters, fits comfortably on a T4 with 4-bit quantization)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "entity_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: microsoft/phi-2\n", + "\n", + "quantization:\n", + " bits: 4\n", + "\n", + "prompt:\n", + " task: >\n", + " Extract the named entities from the input text and return them as a JSON\n", + " object with this structure:\n", + " {\"entities\": [{\"text\": \"...\", \"type\": \"PERSON|ORG|LOC|DATE\"}]}.\n", + " Return only valid JSON, nothing else.\n", + "\n", + "input_features:\n", + " - name: text\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: output\n", + " type: text\n", + " decoder:\n", + " type: text_parser\n", + " json_schema:\n", + " type: object\n", + " properties:\n", + " entities:\n", + " type: array\n", + " items:\n", + " type: object\n", + " properties:\n", + " text:\n", + " type: string\n", + " type:\n", + " type: string\n", + " enum: [PERSON, ORG, LOC, DATE]\n", + " required: [text, type]\n", + " required: [entities]\n", + " additionalProperties: false\n", + "\n", + "generation:\n", + " max_new_tokens: 200\n", + " temperature: 0.1\n", + " do_sample: false\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "entity_samples = [\n", + " \"Apple Inc. was founded by Steve Jobs in Cupertino, California on April 1, 1976.\",\n", + " \"Elon Musk announced that Tesla will open a new Gigafactory in Berlin next year.\",\n", + " \"The United Nations headquarters is located in New York City.\",\n", + "]\n", + "\n", + "entity_df = pd.DataFrame({\"text\": entity_samples})\n", + "entity_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "entity_model = LudwigModel(config=entity_config)\n", + "entity_preds, _, _ = entity_model.predict(dataset=entity_df)\n", + "\n", + "for text, pred in zip(entity_samples, entity_preds[\"output_predictions\"]):\n", + " print(f\"Input: {text}\")\n", + " try:\n", + " parsed = json.loads(pred)\n", + " entities = parsed.get(\"entities\", [])\n", + " print(f\"Output: {len(entities)} entities\")\n", + " for ent in entities:\n", + " print(f\" - '{ent['text']}' ({ent['type']})\")\n", + " except json.JSONDecodeError:\n", + " print(f\"Output (raw): {pred}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The output is guaranteed to be valid JSON matching the schema. The LLM cannot produce malformed JSON, extra prose, or entity types outside the allowed enum.\n", + "\n", + "## Classification with constrained tokens\n", + "\n", + "For classification tasks we can constrain the output to a regex that only allows the valid class labels. Here we restrict the model to `positive`, `negative`, or `neutral`.\n", + "\n", + "We use `Qwen/Qwen2-0.5B-Instruct` (0.5 B parameters) — small enough to run quickly even on CPU." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sentiment_constrained_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: Qwen/Qwen2-0.5B-Instruct\n", + "\n", + "prompt:\n", + " task: >\n", + " Classify the sentiment of the following text.\n", + " Respond with exactly one word: positive, negative, or neutral.\n", + "\n", + "input_features:\n", + " - name: text\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: sentiment\n", + " type: text\n", + " decoder:\n", + " type: text_parser\n", + " # Regex constraint — only these three tokens can be emitted.\n", + " regex: \"(positive|negative|neutral)\"\n", + "\n", + "generation:\n", + " max_new_tokens: 10\n", + " temperature: 0.0\n", + " do_sample: false\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "sentiment_samples = [\n", + " \"I absolutely loved this product! It exceeded all my expectations.\",\n", + " \"The service was terrible and the food was cold.\",\n", + " \"The movie was okay, nothing special.\",\n", + " \"This is the best laptop I have ever owned. Highly recommend.\",\n", + " \"I waited two hours and they still got my order wrong.\",\n", + " \"The weather today is neither good nor bad.\",\n", + "]\n", + "\n", + "sentiment_df = pd.DataFrame({\"text\": sentiment_samples})\n", + "\n", + "sentiment_model = LudwigModel(config=sentiment_constrained_config)\n", + "sentiment_preds, _, _ = sentiment_model.predict(dataset=sentiment_df)\n", + "\n", + "for text, label in zip(sentiment_samples, sentiment_preds[\"sentiment_predictions\"]):\n", + " print(f\"{label!s:<10} {text}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Every prediction is one of the three valid labels. No post-processing or error handling is required.\n", + "\n", + "## Logits extraction\n", + "\n", + "Ludwig can return the raw logits (pre-softmax scores over the vocabulary) for each generated token. This is useful for:\n", + "\n", + "- Computing token-level confidence scores\n", + "- Calibration and uncertainty estimation\n", + "- Analysing what the model \"considered\" at each step\n", + "- Downstream ensemble or reranking tasks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "logits_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: Qwen/Qwen2-0.5B-Instruct\n", + "\n", + "prompt:\n", + " task: \"Answer with a single word.\"\n", + "\n", + "input_features:\n", + " - name: text\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: response\n", + " type: text\n", + " # Request logits to be returned alongside the prediction.\n", + " output_logits: true\n", + "\n", + "generation:\n", + " max_new_tokens: 5\n", + " temperature: 0.0\n", + " do_sample: false\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "logits_model = LudwigModel(config=logits_config)\n", + "logits_df = pd.DataFrame({\"text\": [\"Is Python a programming language?\"]})\n", + "\n", + "logits_preds, output_df, _ = logits_model.predict(dataset=logits_df, collect_predictions=True)\n", + "\n", + "print(\"Prediction:\", logits_preds[\"response_predictions\"].iloc[0])\n", + "\n", + "if \"response_logits\" in output_df.columns:\n", + " import numpy as np\n", + "\n", + " logits = output_df[\"response_logits\"].iloc[0]\n", + " logits_arr = np.array(logits)\n", + " print(f\"Logits shape: {logits_arr.shape}\")\n", + " # Convert to probabilities for the first generated token\n", + " probs = np.exp(logits_arr[0]) / np.exp(logits_arr[0]).sum()\n", + " top5_idx = np.argsort(probs)[::-1][:5]\n", + " print(\"Top-5 token probabilities (first generated token):\")\n", + " for idx in top5_idx:\n", + " print(f\" token {idx}: {probs[idx]:.4f}\")\n", + "else:\n", + " print(\"Logits column not present in output.\")\n", + " print(\"Available columns:\", list(output_df.columns))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparison: constrained vs unconstrained\n", + "\n", + "The following cell runs the same sentiment classification task with and without the regex constraint, then prints both outputs side by side to show how constrained decoding eliminates invalid responses." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Unconstrained config — same prompt, no decoder constraint\n", + "sentiment_unconstrained_config = yaml.safe_load(\"\"\"\n", + "model_type: llm\n", + "base_model: Qwen/Qwen2-0.5B-Instruct\n", + "\n", + "prompt:\n", + " task: >\n", + " Classify the sentiment of the following text.\n", + " Respond with exactly one word: positive, negative, or neutral.\n", + "\n", + "input_features:\n", + " - name: text\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: sentiment\n", + " type: text\n", + "\n", + "generation:\n", + " max_new_tokens: 30\n", + " temperature: 0.7\n", + "\n", + "backend:\n", + " type: local\n", + "\"\"\")\n", + "\n", + "unconstrained_model = LudwigModel(config=sentiment_unconstrained_config)\n", + "unconstrained_preds, _, _ = unconstrained_model.predict(dataset=sentiment_df)\n", + "\n", + "# The constrained model was already run above\n", + "constrained_labels = sentiment_preds[\"sentiment_predictions\"].tolist()\n", + "unconstrained_labels = unconstrained_preds[\"sentiment_predictions\"].tolist()\n", + "\n", + "valid_labels = {\"positive\", \"negative\", \"neutral\"}\n", + "\n", + "print(f\"{'Input':<48} {'Unconstrained':<32} {'Constrained'}\")\n", + "print(\"-\" * 100)\n", + "for text, unc, con in zip(sentiment_samples, unconstrained_labels, constrained_labels):\n", + " short = textwrap.shorten(text, width=46)\n", + " unc_str = str(unc).strip()\n", + " # Highlight invalid outputs\n", + " flag = \" *** INVALID\" if unc_str.lower() not in valid_labels else \"\"\n", + " print(f\"{short:<48} {unc_str:<32} {con!s}{flag}\")\n", + "\n", + "n_invalid = sum(1 for u in unconstrained_labels if str(u).strip().lower() not in valid_labels)\n", + "print(f\"\\nUnconstrained — invalid outputs: {n_invalid}/{len(sentiment_samples)}\")\n", + "print(f\"Constrained — invalid outputs: 0/{len(sentiment_samples)} (guaranteed)\")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/llm_text_generation/simple_model_training.py b/examples/llm_text_generation/simple_model_training.py new file mode 100644 index 0000000..75ff66b --- /dev/null +++ b/examples/llm_text_generation/simple_model_training.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +# # Simple Model Training Example +# +# This is a simple example of how to use the LLM model type to train +# a model on a simple question and answer dataset. It uses the +# facebook/opt-350m model as the base LLM model. + +# Import required libraries +import logging +import shutil + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +qa_pairs = [ + {"Question": "What is the capital of Uzbekistan?", "Answer": "Tashkent"}, + {"Question": "Who is the founder of Microsoft?", "Answer": "Bill Gates"}, + {"Question": "What is the tallest building in the world?", "Answer": "Burj Khalifa"}, + {"Question": "What is the currency of Brazil?", "Answer": "Real"}, + {"Question": "What is the boiling point of mercury in Celsius?", "Answer": "-38.83"}, + {"Question": "What is the most commonly spoken language in the world?", "Answer": "Mandarin"}, + {"Question": "What is the diameter of the Earth?", "Answer": "12,742 km"}, + {"Question": 'Who wrote the novel "1984"?', "Answer": "George Orwell"}, + {"Question": "What is the name of the largest moon of Neptune?", "Answer": "Triton"}, + {"Question": "What is the speed of light in meters per second?", "Answer": "299,792,458 m/s"}, + {"Question": "What is the smallest country in Africa by land area?", "Answer": "Seychelles"}, + {"Question": "What is the largest organ in the human body?", "Answer": "Skin"}, + {"Question": 'Who directed the film "The Godfather"?', "Answer": "Francis Ford Coppola"}, + {"Question": "What is the name of the smallest planet in our solar system?", "Answer": "Mercury"}, + {"Question": "What is the largest lake in Africa?", "Answer": "Lake Victoria"}, + {"Question": "What is the smallest country in Asia by land area?", "Answer": "Maldives"}, + {"Question": "Who is the current president of Russia?", "Answer": "Vladimir Putin"}, + {"Question": "What is the chemical symbol for gold?", "Answer": "Au"}, + {"Question": "What is the name of the famous Swiss mountain known for skiing?", "Answer": "The Matterhorn"}, + {"Question": "What is the largest flower in the world?", "Answer": "Rafflesia arnoldii"}, +] + +df = pd.DataFrame(qa_pairs) + +config = yaml.safe_load(""" + input_features: + - name: Question + type: text + output_features: + - name: Answer + type: text + model_type: llm + generation: + temperature: 0.1 + top_p: 0.75 + top_k: 40 + num_beams: 4 + max_new_tokens: 5 + base_model: facebook/opt-350m + """) + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset=df, experiment_name="simple_experiment", model_name="simple_model", skip_save_processed_input=True +) + +training_set, val_set, test_set, _ = preprocessed_data + +# batch prediction +preds, _ = model.predict(test_set, skip_save_predictions=False) +print(preds) diff --git a/examples/llm_zero_shot_learning/simple_model_training.py b/examples/llm_zero_shot_learning/simple_model_training.py new file mode 100644 index 0000000..3bd9da3 --- /dev/null +++ b/examples/llm_zero_shot_learning/simple_model_training.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +# # Simple Model Training Example +# +# This is a simple example of how to use the LLM model type to train +# a zero shot classification model. It uses the facebook/opt-350m model +# as the base LLM model. + +# Import required libraries +import logging +import shutil + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +review_label_pairs = [ + {"review": "I loved this movie!", "label": "positive"}, + {"review": "The food was okay, but the service was terrible.", "label": "negative"}, + {"review": "I can't believe how rude the staff was.", "label": "negative"}, + {"review": "This book was a real page-turner.", "label": "positive"}, + {"review": "The hotel room was dirty and smelled bad.", "label": "negative"}, + {"review": "I had a great experience at this restaurant.", "label": "positive"}, + {"review": "The concert was amazing!", "label": "positive"}, + {"review": "The traffic was terrible on my way to work this morning.", "label": "negative"}, + {"review": "The customer service was excellent.", "label": "positive"}, + {"review": "I was disappointed with the quality of the product.", "label": "negative"}, + {"review": "The scenery on the hike was breathtaking.", "label": "positive"}, + {"review": "I had a terrible experience at this hotel.", "label": "negative"}, + {"review": "The coffee at this cafe was delicious.", "label": "positive"}, + {"review": "The weather was perfect for a day at the beach.", "label": "positive"}, + {"review": "I would definitely recommend this product.", "label": "positive"}, + {"review": "The wait time at the doctor's office was ridiculous.", "label": "negative"}, + {"review": "The museum was a bit underwhelming.", "label": "neutral"}, + {"review": "I had a fantastic time at the amusement park.", "label": "positive"}, + {"review": "The staff at this store was extremely helpful.", "label": "positive"}, + {"review": "The airline lost my luggage and was very unhelpful.", "label": "negative"}, + {"review": "This album is a must-listen for any music fan.", "label": "positive"}, + {"review": "The food at this restaurant was just okay.", "label": "neutral"}, + {"review": "I was pleasantly surprised by how great this movie was.", "label": "positive"}, + {"review": "The car rental process was quick and easy.", "label": "positive"}, + {"review": "The service at this hotel was top-notch.", "label": "positive"}, +] + +df = pd.DataFrame(review_label_pairs) + +config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +generation: + temperature: 0.1 + top_p: 0.75 + top_k: 40 + num_beams: 4 + max_new_tokens: 64 +prompt: + task: "Classify the sample input as either negative, neutral, or positive." +input_features: +- + name: review + type: text +output_features: +- + name: label + type: category + preprocessing: + fallback_label: "neutral" + decoder: + type: category_extractor + match: + "negative": + type: contains + value: "positive" + "neutral": + type: contains + value: "neutral" + "positive": + type: contains + value: "positive" + """) + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset=df, experiment_name="simple_experiment", model_name="simple_model", skip_save_processed_input=True +) + +training_set, val_set, test_set, _ = preprocessed_data + +# batch prediction +preds, _ = model.predict(test_set, skip_save_predictions=False) +print(preds) diff --git a/examples/mamba_encoders/README.md b/examples/mamba_encoders/README.md new file mode 100644 index 0000000..b97e406 --- /dev/null +++ b/examples/mamba_encoders/README.md @@ -0,0 +1,73 @@ +# Mamba-2 and Jamba Sequence Encoders + +Ludwig ships two SSM-based sequence encoders for text, audio, and timeseries features: + +| Encoder | Architecture | Best for | +| -------- | -------------------------------------------- | ------------------------------------------ | +| `mamba2` | Pure SSM stack (Dao & Gu, 2024) | Long sequences, compute-budget constrained | +| `jamba` | Hybrid SSM + Attention (Lieber et al., 2024) | Balance between speed and global context | + +Both run in **linear time** in sequence length, making them competitive with Transformers on +documents longer than ~1 K tokens. + +## Files + +| file | description | +| --------------------------------- | --------------------------------------------- | +| `mamba2_text_classification.yaml` | News article classification with pure Mamba-2 | +| `jamba_sequence.yaml` | Sentiment analysis with hybrid Jamba encoder | + +## Quick start + +```bash +# Download a sample dataset (AG News) +python -c " +from datasets import load_dataset +ds = load_dataset('ag_news', split='train[:5000]') +ds.to_csv('ag_news_train.csv', index=False) +ds = load_dataset('ag_news', split='test[:1000]') +ds.to_csv('ag_news_test.csv', index=False) +" + +# Rename columns to match the config +python -c " +import pandas as pd +for split in ['train', 'test']: + df = pd.read_csv(f'ag_news_{split}.csv') + df = df.rename(columns={'text': 'article', 'label': 'category'}) + df.to_csv(f'ag_news_{split}.csv', index=False) +" + +# Train with Mamba-2 +ludwig train \ + --config mamba2_text_classification.yaml \ + --dataset ag_news_train.csv + +# Train with Jamba +ludwig train \ + --config jamba_sequence.yaml \ + --dataset ag_news_train.csv # rename 'article' column to 'review_text' first +``` + +## Key hyperparameters + +| parameter | default | description | +| ------------------- | ---------------------- | ---------------------------------------------------- | +| `d_model` | 256 | Hidden dimension (must be divisible by `num_heads`) | +| `n_layers` | 4 (mamba2) / 8 (jamba) | Number of blocks | +| `num_heads` | 8 | Heads for the multi-head SSD decay | +| `d_conv` | 4 | Depthwise convolution kernel size | +| `expand_factor` | 2 | Inner expansion: `d_inner = d_model × expand_factor` | +| `attention_every_k` | 4 | *(Jamba only)* Insert attention every k layers | +| `reduce_output` | `"mean"` | `"mean"` / `"sum"` / `"max"` / `"last"` / `None` | + +## When to prefer Mamba-2 over a Transformer + +- Documents > 1 K tokens (Transformer attention is quadratic, Mamba-2 is linear) +- Compute or memory budget is tight (Mamba-2 ≈ 2-3× less memory than same-depth Transformer) +- Audio / timeseries features with very long windows + +## When Jamba beats pure Mamba-2 + +- Tasks where cross-token position-insensitive dependencies matter (e.g., long-range co-reference) +- Medium-length sequences where the occasional attention layer helps without blowing the budget diff --git a/examples/mamba_encoders/jamba_sequence.yaml b/examples/mamba_encoders/jamba_sequence.yaml new file mode 100644 index 0000000..abdc7a7 --- /dev/null +++ b/examples/mamba_encoders/jamba_sequence.yaml @@ -0,0 +1,54 @@ +# Jamba-style hybrid encoder for sequence classification. +# +# JambaEncoder interleaves Mamba-2 SSM blocks with Transformer attention blocks +# following Lieber et al. (2024). With the default attention_every_k=4 and +# n_layers=8 the pattern is [S S S A S S S A] — a 1:3 attention:SSM ratio. +# +# The hybrid offers a middle ground between pure-attention Transformers +# (quadratic complexity, strong global context) and pure-SSM Mamba models +# (linear complexity, weaker position-insensitive global context). +# Attention layers handle cross-sentence dependencies; SSM layers handle the +# bulk of cheap sequential processing. + +model_type: ecd + +input_features: + - name: review_text + type: text + encoder: + type: jamba + d_model: 256 + # Total number of layers (SSM + attention combined). + n_layers: 8 + # Insert one attention block every k layers. + attention_every_k: 4 + # Attention heads — used by both the TransformerEncoderLayer and _Mamba2Block. + num_heads: 8 + # Feed-forward size inside the attention blocks. + ffn_size: 1024 + d_conv: 4 + expand_factor: 2 + dropout: 0.1 + reduce_output: mean + output_size: 256 + +output_features: + - name: sentiment + type: category + +combiner: + type: concat + num_fc_layers: 1 + output_size: 128 + +trainer: + epochs: 15 + batch_size: 32 + learning_rate: 2.0e-4 + optimizer: + type: adamw + weight_decay: 0.01 + learning_rate_scheduler: + decay: cosine + warmup_fraction: 0.05 + early_stop: 5 diff --git a/examples/mamba_encoders/mamba2_text_classification.yaml b/examples/mamba_encoders/mamba2_text_classification.yaml new file mode 100644 index 0000000..aca0977 --- /dev/null +++ b/examples/mamba_encoders/mamba2_text_classification.yaml @@ -0,0 +1,51 @@ +# Mamba-2 encoder for text classification. +# +# Mamba-2 is a multi-head selective SSM with per-head scalar decay (Dao & Gu, 2024). +# It processes sequences in linear time (O(L) vs O(L²) for attention), making it +# attractive for long documents where standard Transformers are prohibitively expensive. +# +# This config classifies news articles into categories using the Mamba-2 encoder. +# Swap the dataset path for any text classification task. + +model_type: ecd + +input_features: + - name: article + type: text + encoder: + type: mamba2 + # Model dimension — must be divisible by num_heads. + d_model: 256 + # Number of stacked Mamba-2 blocks. + n_layers: 4 + # Multi-head SSD: number of decay scalars. + num_heads: 8 + # Depthwise convolution kernel size (local context mixing). + d_conv: 4 + # Inner expansion factor (inner dim = d_model * expand_factor). + expand_factor: 2 + dropout: 0.1 + # Aggregate the final hidden state by mean-pooling over sequence. + reduce_output: mean + output_size: 256 + +output_features: + - name: category + type: category + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + +trainer: + epochs: 20 + batch_size: 64 + learning_rate: 3.0e-4 + optimizer: + type: adamw + weight_decay: 0.01 + learning_rate_scheduler: + decay: cosine + warmup_fraction: 0.05 + early_stop: 5 diff --git a/examples/mnist/README.md b/examples/mnist/README.md new file mode 100644 index 0000000..2815685 --- /dev/null +++ b/examples/mnist/README.md @@ -0,0 +1,12 @@ +# MNIST Hand-written Digit Classification + +This API example is based on [Ludwig's MNIST Hand-written Digit image classification example](https://ludwig-ai.github.io/ludwig-docs/examples/#image-classification-mnist). + +### Examples + +| File | Description | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| simple_model_training.py | Demonstrates using Ludwig api for training a model. | +| advance_model_training.py | Demonstrates a method to assess alternative model architectures. | +| assess_model_performance.py | Assess model performance on hold-out test data set. This shows how to load a previously trained model to make predictions. | +| visualize_model_test_results.ipynb | Example for extracting training statistics and generate custom visualizations. | diff --git a/examples/mnist/advanced_model_training.py b/examples/mnist/advanced_model_training.py new file mode 100644 index 0000000..e380aab --- /dev/null +++ b/examples/mnist/advanced_model_training.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# # Multiple Model Training Example +# +# This example trains multiple models and extracts training statistics + +import glob +import logging +import os +import shutil +from collections import namedtuple + +import yaml + +# ## Import required libraries +from ludwig.api import LudwigModel +from ludwig.constants import TRAINER +from ludwig.datasets import mnist +from ludwig.visualize import learning_curves + +# clean out old results +shutil.rmtree("./results", ignore_errors=True) +shutil.rmtree("./visualizations", ignore_errors=True) + +file_list = glob.glob("./data/*.json") +file_list += glob.glob("./data/*.hdf5") +for f in file_list: + try: + os.remove(f) + except FileNotFoundError: + pass + +# read in base config +with open("./config.yaml") as f: + base_model = yaml.safe_load(f.read()) + +# Specify named tuple to keep track of training results +TrainingResult = namedtuple("TrainingResult", ["name", "train_stats"]) + +# specify alternative architectures to test +FullyConnectedLayers = namedtuple("FullyConnectedLayers", ["name", "fc_layers"]) + +list_of_fc_layers = [ + FullyConnectedLayers(name="Option1", fc_layers=[{"output_size": 64}]), + FullyConnectedLayers(name="Option2", fc_layers=[{"output_size": 128}, {"output_size": 64}]), + FullyConnectedLayers(name="Option3", fc_layers=[{"output_size": 128}]), +] + +# +list_of_train_stats = [] + +# load and split MNIST dataset +training_set, test_set, _ = mnist.load(split=True) + +# ## Train models +for model_option in list_of_fc_layers: + print(">>>> training: ", model_option.name) + + # set up Python dictionary to hold model training parameters + config = base_model.copy() + config["input_features"][0]["fc_layers"] = model_option.fc_layers + config[TRAINER]["epochs"] = 5 + + # Define Ludwig model object that drive model training + model = LudwigModel(config, logging_level=logging.INFO) + + # initiate model training + train_stats, _, _ = model.train( + training_set=training_set, + test_set=test_set, + experiment_name="multiple_experiment", + model_name=model_option.name, + ) + + # save training stats for later use + list_of_train_stats.append(TrainingResult(name=model_option.name, train_stats=train_stats)) + + print(">>>>>>> completed: ", model_option.name, "\n") + + +# generating learning curves from training +option_names = [trs.name for trs in list_of_train_stats] +train_stats = [trs.train_stats for trs in list_of_train_stats] +learning_curves( + train_stats, "Survived", model_names=option_names, output_directory="./visualizations", file_format="png" +) diff --git a/examples/mnist/assess_model_performance.py b/examples/mnist/assess_model_performance.py new file mode 100644 index 0000000..b50f5c2 --- /dev/null +++ b/examples/mnist/assess_model_performance.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +# +# Load a previously saved model and make predictions on the test data set +# + +import os.path + +# ## Import required libraries +import pandas as pd +from sklearn.metrics import accuracy_score + +from ludwig.api import LudwigModel +from ludwig.datasets import mnist + +# create data set for predictions +test_data = {"image_path": [], "label": []} +dataset = mnist.Mnist() +test_dir = os.path.join(dataset.processed_dataset_path, "testing") +for label in os.listdir(test_dir): + files = os.listdir(os.path.join(test_dir, label)) + test_data["image_path"] += [os.path.join(test_dir, label, f) for f in files] + test_data["label"] += len(files) * [label] + +# collect data into a data frame +test_df = pd.DataFrame(test_data) +print(test_df.head()) + +# retrieve a trained model +model = LudwigModel.load("./results/multiple_experiment_Option3/model") + +# make predictions +pred_df, _ = model.predict(dataset=test_df) +print(pred_df.head()) + +# print accuracy on test data set +print("predicted accuracy", accuracy_score(test_df["label"], pred_df["label_predictions"])) diff --git a/examples/mnist/config.yaml b/examples/mnist/config.yaml new file mode 100644 index 0000000..65a8255 --- /dev/null +++ b/examples/mnist/config.yaml @@ -0,0 +1,26 @@ +input_features: + - name: image_path + type: image + preprocessing: + num_processes: 4 + encoder: stacked_cnn + conv_layers: + - num_filters: 32 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + - num_filters: 64 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + dropout: 0.4 + fc_layers: + - output_size: 128 + dropout: 0.4 + +output_features: + - name: label + type: category + +trainer: + epochs: 5 diff --git a/examples/mnist/simple_model_training.py b/examples/mnist/simple_model_training.py new file mode 100644 index 0000000..a999405 --- /dev/null +++ b/examples/mnist/simple_model_training.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# # Simple Model Training Example +# +# This example is the API example for this Ludwig command line example +# (https://ludwig-ai.github.io/ludwig-docs/latest/examples/mnist/). +import logging +import shutil + +import yaml + +from ludwig.api import LudwigModel +from ludwig.datasets import mnist + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +# set up Python dictionary to hold model training parameters +with open("./config.yaml") as f: + config = yaml.safe_load(f.read()) + +# Define Ludwig model object that drive model training +model = LudwigModel(config, logging_level=logging.INFO) + +# load and split MNIST dataset +training_set, test_set, _ = mnist.load(split=True) + +# initiate model training +train_stats, _, output_directory = model.train( # training statistics # location for training results saved to disk + training_set=training_set, + test_set=test_set, + experiment_name="simple_image_experiment", + model_name="single_model", + skip_save_processed_input=True, +) diff --git a/examples/mnist/visualize_model_test_results.ipynb b/examples/mnist/visualize_model_test_results.ipynb new file mode 100644 index 0000000..f417b49 --- /dev/null +++ b/examples/mnist/visualize_model_test_results.ipynb @@ -0,0 +1,391 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ludwig Visualization Demonstration" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "\n", + "warnings.simplefilter(\"ignore\")\n", + "import os\n", + "import os.path\n", + "import shutil\n", + "\n", + "import pandas as pd\n", + "\n", + "from ludwig.api import LudwigModel\n", + "from ludwig.datasets import mnist\n", + "from ludwig.utils.data_utils import load_json\n", + "from ludwig.visualize import compare_classifiers_performance_from_pred, compare_performance, confusion_matrix\n", + "\n", + "shutil.rmtree(\"./viz2\", ignore_errors=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prepare test data set for use" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " image_path label\n", + "0 /opt/project/examples/mnist/data/mnist_png/tes... 0\n", + "1 /opt/project/examples/mnist/data/mnist_png/tes... 0\n", + "2 /opt/project/examples/mnist/data/mnist_png/tes... 0\n", + "3 /opt/project/examples/mnist/data/mnist_png/tes... 0\n", + "4 /opt/project/examples/mnist/data/mnist_png/tes... 0\n" + ] + } + ], + "source": [ + "# create test dataframe\n", + "test_data = {\"image_path\": [], \"label\": []}\n", + "dataset = mnist.Mnist()\n", + "test_dir = os.path.join(dataset.processed_dataset_path, \"testing\")\n", + "for label in os.listdir(test_dir):\n", + " files = os.listdir(os.path.join(test_dir, label))\n", + " test_data[\"image_path\"] += [os.path.join(test_dir, label, f) for f in files]\n", + " test_data[\"label\"] += len(files) * [label]\n", + "\n", + "# collect data into a data frame\n", + "test_df = pd.DataFrame(test_data)\n", + "print(test_df.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate predictions the test data set for the different neural network options" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# get list of models to visualize results\n", + "models_list = [\"Option1\", \"Option2\", \"Option3\"]\n", + "test_stats_list = []\n", + "preds_list = []\n", + "\n", + "for m in models_list:\n", + " # retrieve a trained model\n", + " model = LudwigModel.load(\"./results/multiple_experiment_\" + m + \"/model\")\n", + "\n", + " # make predictions\n", + " test_stats, pred_df, _ = model.evaluate(dataset=test_df, collect_predictions=True, collect_overall_stats=True)\n", + "\n", + " # collect test statsitics\n", + " preds_list.append(pred_df[\"label_predictions\"].astype(\"int\"))\n", + " test_stats_list.append(test_stats)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Show model performance on test data set" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# overall model performance\n", + "compare_performance(test_stats_list, \"label\", model_names=models_list, output_directory=\"./viz2\", file_format=\"png\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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POjswtGCAtq3b8/o0aPx8fFh06ZNbNmyhdzcXKDieZS/vz9btmxh48aNpKenAxUJZ/LkydfVzWg04uEoz6BsgfSghBB1QlEUdhUV8XleLmfKynBUq3nQxYUZ9euzu6iI7woKMaPQUqvF086OGIOByyYTGpWKYAdHJrq709/FhQvl5YTnZHP019Ui3DUaujg68aSHB7uKCvmhqIgrZjNalYquTk487u6Bu0bDu1czuWI2X1evtjodb3p5U9/OTnpQdUwSlBCizimKUmWTwFuVA26r7F9xniSouiW3+IQQde52E8efTTB3ep6oWzKKTwghhE26Z3tQitVK4O+650II8WdZjUbUOtlwo67csz0o0w0eft6OrKysP33OpUuXai2WxLONePfytUm830hyqlv3bIISQghxd5MEJYQQwiZJghJCCGGTJEEJIYSwSZKghBBC2CRJUEIIIWySJCghhBA2SRKUEEIImyQJSgghhE2SBCWEEMIm3bPbbcTFxaGTZUqEEH8Ro9FIcHBwXVfjb+WeTVBCCCHubnKLTwghhE2SBCWEEMImSYISQghhkyRBCSGEsEmSoIQQQtgkSVBCCCFs0l2doPbt28fgwYMZOHAgH3300XXHy8vLmT17NgMHDmTcuHGkpaXVaLyff/6Z0aNHExgYSERExB3Fup14n376KcOGDSM0NJTHH3+c9PT0Go23fv16QkNDGTlyJI8++ihJSUk1Gu+a3bt306ZNG+Lj42ss1ubNm+nevTsjR45k5MiRbNiwodqxbicewM6dOxk2bBjDhw/nxRdfrNF477zzTuW1DR48mK5du9ZovMuXLzNp0iRGjRpFaGgoP/30U43FSk9P5/HHHyc0NJRJkyZx5cqVascCePXVVwkJCWHEiBE3PK4oCm+99RYDBw4kNDSU06dP31E8cQvKXcpsNiv9+/dXUlJSFKPRqISGhiqJiYlVyqxbt05ZsGCBoiiK8v333yvPP/98jcZLTU1VEhISlJdfflnZtWtXtWPdbrzDhw8rBoNBURRF+fLLL2v8+oqKiiq/37t3r/Lkk0/WaLxrMR977DFl3LhxysmTJ2ss1qZNm5RFixZV6/2rEy85OVkZOXKkkp+fryiKomRnZ9dovN9bu3atMm/evBqN9/rrrytffvmloiiKkpiYqDz44IM1FuvZZ59VNm/erCiKohw6dEh56aWXqhXrmqNHjyqnTp1Shg8ffsPj0dHRytSpUxWr1aocP35cGTt27B3FEzd31/agTp48SdOmTfHz80Or1TJ8+HAiIyOrlImKimL06NEADB48mMOHD6NUc17y7cTz9fWlbdu2qNV33qy3E6979+44OjoCEBwcfEd/Od5OPBcXl8rvS0tLUalUNRoP4P3332f69Ol3tCrI7cb6q9xOvG+//ZYJEybg5uYGQP369Ws03u/t2LHjpr2DvyqeSqWiuLgYgKKiIjw9PWss1vnz5+nevTtQ8X/iTn+23bp1q/y53EhkZCSjRo1CpVIRHBxMYWEhV69evaOY4sbu2gSVmZmJt7d35b+9vLzIzMy8royPjw8AdnZ26PV68vLyaizeX+nPxtu4cSO9e/eu8XhffvklAwYM4L333uP111+v0XinT5/mypUr9O3bt9pxbjcWwJ49ewgNDeW5554jIyOjRuNdvHiR5ORkxo8fz8MPP8y+fftqNN416enppKWlVX6g11S8WbNmsX37dnr37s3//M//VPt35XZitW3blj179gDwww8/UFJSUu3/59Wpk7e3d41+Fvyd3bUJSvxm27ZtnDp1imlslNdMAAADgUlEQVTTptV4rAkTJrB3715eeuklwsPDayyO1Wrl3Xff5ZVXXqmxGL/34IMPEhUVxfbt2+nRo0eNx7VYLFy6dIkvvviCf/3rXyxYsIDCwsIajQkVvafBgwej0WhqPM7o0aPZt28fH330EXPnzsVqtdZIrLlz5/Lzzz8zatQojh49ipeXV41fn6gdd22C8vLyqnJLKzMzEy8vr+vKXPtL2Gw2U1RUhLu7e43F+yvdbrxDhw7xv//7v4SHh6PVams83jXDhw9n7969NRavpKSEc+fOMXnyZPr160dcXBwzZ86s1kCJ27k2d3f3yvYbN27cHT34vt3fzX79+mFvb4+fnx/NmjXj4sWLNRbvmp07dzJ8+PBqxfkz8TZu3MjQoUMB6NSpE0ajsVq9mtttyw8++ICtW7cyZ84cAFxdXf90rOrW6cqVKzX6WfB3dtcmqKCgIC5evEhqairl5eXs2LGDfv36VSnTr18/tmzZAlSMBOvevXu1n5vcTry/0u3EO3PmDG+88Qbh4eF39AzjduP9/gM0Ojqapk2b1lg8vV7PkSNHiIqKIioqiuDgYMLDwwkKCqqRa/v9M4SoqChatmxZY9cGMGDAAI4ePQpAbm4uFy9exM/Pr8biQcWzmsLCQjp16lStOH8mno+PD4cPH66MazQa8fDwqJFYubm5lb2zjz76iDFjxlTzym5Pv3792Lp1K4qiEBcXh16vr/YzNvEH6nqUxp2Ijo5WBg0apPTv319ZtWqVoiiKsmLFCmXv3r2KoihKWVmZ8uyzzyoDBgxQxowZo6SkpNRovBMnTii9evVSOnbsqNx3333KsGHDajTe448/roSEhChhYWFKWFiY8tRTT9VovCVLlijDhg1TwsLClIkTJyrnzp2r0Xi/N3HixGqP4rudWP/85z+VYcOGKaGhocrEiROVpKSkase6nXhWq1V55513lKFDhyojRoxQvv/++xqNpyiKsnLlSuW99967ozi3Gy8xMVF55JFHlNDQUCUsLEzZv39/jcXatWuXMnDgQGXQoEHKa6+9phiNxju6tjlz5ig9e/ZUAgMDlV69einffvut8tVXXylfffWVoigVP7uFCxcq/fv3V0aMGHFHv5fi1mS7DSGEEDbprr3FJ4QQ4t4mCUoIIYRNkgQlhBDCJkmCEkIIYZMkQQkhhLBJkqCEEELYJElQQgghbNL/B54HaTLIbbgHAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Classifiction performance metrics by model\n", + "train_metadata_json = load_json(\"./results/multiple_experiment_Option1/model/training_set_metadata.json\")\n", + "compare_classifiers_performance_from_pred(\n", + " preds_list,\n", + " test_df[\"label\"].to_numpy().astype(\"int\"),\n", + " train_metadata_json,\n", + " \"label\",\n", + " 10,\n", + " model_names=models_list,\n", + " output_directory=\"./viz2\",\n", + " file_format=\"png\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deVyU5fr48c+jKKKAisqgZv4Cw0xN+2apiRsGorgnUsdMLLOjJalpWppLLnROWma2kWVanUxcwNTcMCX3XSvTErE0YThfRECRbeb+/cFhvnlUZhhmmGG43q/X83o12z3XEFzecz33c92aUkohhBDCpqo5OgAhhHBFklyFEMIOJLkKIYQdSHIVQgg7kOQqhBB2IMlVCCHsQJKruEmrVq0YOHAg/fr1Izo6mhs3blg91rRp09iyZQsA06dP59y5c3d87sGDBzl27FiZ3yM4OJgrV65YfP9fPfjgg2V6r/fee49PP/20TK8RVZckV3GTWrVqkZCQwMaNG6lRowarVq266fGioiKrxp0/fz4tWrS44+OHDh3i+PHjVo0thDNyc3QAwnl16NCBs2fPcvDgQd599128vb1JSUlh8+bNLFy4kEOHDlFQUMDw4cN54oknUEoxd+5c9u7dS+PGjalRo4ZprBEjRvDKK6/Qtm1bkpKSeOeddzAYDNSvX5/58+ezatUqqlWrxoYNG3j99dfx9/dn1qxZXL58GYDXXnuNhx56iMzMTF5++WX0ej3t27fHkmtgxo0bR1paGvn5+Tz99NNERkaaHluwYAF79+6lYcOGvPPOO/j4+PDHH38wZ84cMjMzqVWrFnPnziUgIMD2P2Dh2pQQf9G+fXullFKFhYXq73//u/rqq6/UgQMHVLt27dQff/yhlFJq1apV6v3331dKKZWfn68GDx6s/vjjD7V161YVFRWlioqKVFpamnrooYfUd999p5RS6qmnnlKnTp1SGRkZqlu3bqaxMjMzlVJKLVmyRC1btswUx6RJk9Thw4eVUkr9+eefKiwsTCml1Ny5c9V7772nlFLq+++/V4GBgSojI+OWz9GzZ0/T/SXvcePGDRUeHq6uXLmilFIqMDBQJSQkKKWUeu+999ScOXOUUko9/fTTKiUlRSml1IkTJ9SIESNuG6MQpZGZq7hJXl4eAwcOBIpnrkOHDuX48eO0bduWZs2aAbB3717Onj3L1q1bAcjJyeH333/n8OHDhIeHU716dXQ6HZ06dbpl/BMnTtChQwfTWPXq1bttHPv27bupRnvt2jWuX7/O4cOHWbp0KQA9evSgbt26Zj/TF198wfbt2wFITU3l999/p379+lSrVo2+ffsCMHDgQF588UWuX7/O8ePHeemll0yvLygoMPseQvw3Sa7iJiU11/9Wu3Zt038rpZgxYwZdu3a96Tm7d++2WRxGo5HVq1fj7u5ernEOHjzIvn37+Oabb/Dw8GDEiBHk5+ff9rmapqGUwtvb+7Y/AyHKQk5oiTILCgri66+/prCwEICUlBRyc3N5+OGH+e677zAYDKSnp3Pw4MFbXtu+fXuOHDnCxYsXAbh69SoAderU4fr16ze9xxdffGG6/csvvwDw8MMP8+233wLFyTwrK6vUWHNycqhbty4eHh4kJydz4sQJ02NGo9E0+/7222956KGH8PT05K677uK7774Div8hOXPmTNl+QEIgyVVYISIighYtWjBkyBD69evHzJkzMRgMhISE0Lx5c/r27cvUqVNp3779La/18fHhjTfeYPz48QwYMICJEycC0LNnT7Zv387AgQM5cuQI06dP56effqJ///707duXr7/+GoAXXniBI0eOEB4ezvbt22nSpEmpsXbr1o2ioiL69OnDokWLboqpdu3anDp1in79+nHgwAFeeOEFAN566y3WrFnDgAEDCA8PZ8eOHbb60YkqRFNKWg4KIYStycxVCCHsQJKrEELYgSRXIYSwA0muQghhB5JchRDCDiS5CiGEHUhyFUIIO5Dk6iRkubEQrkWSqwP9NaFqmsa1a9c4c+YMc+bMYefOnQ6MTAhRXpJcHUjTNADS0tI4efIkY8eOZfPmzWzfvh03N+mpI0RlJn/BDnD58mV0Oh3Vq1fniy++ICkpCV9fX4YNG8Zdd93FDz/8gL+/v6PDFEKUg/QWqEBKKVJTUxk5ciRfffUVtWvXZvPmzbRr1w5fX1/q16/PW2+9xT333MPQoUMdHa4Qohxk5lqBNE3D09OTRo0a4evrC8DQoUOpVq24OnP9+nXS0tLo06ePI8MUQtiA1FwryIULF4DiZtTVq1e/aaO/ki8Pb7/9NgBt2rSp8PiEELYlM1c7U0pRWFjI+PHjefTRRxk7diyZmZnk5OSYthopERQUxH333Wd6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40bq169P3759UUpVmt0IhBCVmCtfRNCnTx86dOjAxx9/bNoe5fPPP6+ouIQQVVxl30PrjjXXpUuX0qhRI55++mlmzJjB/v37TZfAVgZJSUn07t2bkJAQYmNjHR2OEKIMXLrm+thjj/HYY4+Rm5tLYmIiK1as4MqVK8yaNYuQkBCCgoIqMs4yMRgMvPHGGyxfvhydTsfQoUMJDg6mRYsWjg5NCGEJFygLmF0tULt2bfr3789HH33E7t27uf/++/nkk08qIjarnTp1iubNm9OsWTNq1qxJeHg4iYmJjg5LCFEWNrz81WAwMGjQIJ5//nkALl68SEREBCEhIUyYMIGCggIACgoKmDBhAiEhIURERHDp0iWrwy/TxlJ169YlMjLS6Xu56vV6/Pz8TLd1Op20SRSiktGUmaMMY61cuZKAgADT7YULFxIVFcX27dvx9vZmzZo1AMTFxeHt7c327duJiopi4cKFVscvu/YJIZyPucRahplrWloau3btYujQocVDK8WBAwdMm68OHjzY9M12586dDB48GCjenLU855pcMrnqdDrS0tJMt/V6vWxQKERlY6OywIIFC5gyZYppB+jMzEy8vb1xcys+5eTn52f6ZqvX62ncuDEAbm5ueHl5kZmZaVX4Lplc27Zty4ULF7h48SIFBQVs2rSJ4OBgR4clhLCUhS0Hk5OTGTJkiOn45ptvbhrm+++/x8fHhzZt2lRs/FixtXZl4ObmxsyZMxk9ejQGg4HHH3+ce++919FhCSEsZFFXLAUBAQGsW7fujs85duwYO3fuJCkpifz8fK5du8b8+fPJzs6mqKgINzc30tLSTN9sdTodqamp+Pn5UVRURE5ODvXr17fqM7hkcgXo3r073bt3d3QYQggr2WIngpdffpmXX34ZgIMHD/LZZ5+xaNEioqOj2bp1K+Hh4axfv970zTY4OJj169fz4IMPsnXrVjp16mRqt1pWLlkWEEJUcnbeiWDKlCksX76ckJAQrl69SkREBABDhw7l6tWrhISEsHz5ciZPnmz1e7jszFUIUXnZY/fXjh070rFjRwCaNWtmWn71V+7u7ixZsqRsA9+BJFchhPNRgLnLW538Ci1JrkIIp+TyOxEI4YpW3ne3zcfs87N16yHN+a51PbuM69RcoLeAJFchhNMpXopVevYsa821oklyFUI4JVuf0KpoklyFEM5H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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deVyU5fr48c+jKKKAisqgZv4Cw0xN+2apiRsGorgnUsdMLLOjJalpWppLLnROWma2kWVanUxcwNTcMCX3XSvTErE0YThfRECRbeb+/cFhvnlUZhhmmGG43q/X83o12z3XEFzecz33c92aUkohhBDCpqo5OgAhhHBFklyFEMIOJLkKIYQdSHIVQgg7kOQqhBB2IMlVCCHsQJKruEmrVq0YOHAg/fr1Izo6mhs3blg91rRp09iyZQsA06dP59y5c3d87sGDBzl27FiZ3yM4OJgrV65YfP9fPfjgg2V6r/fee49PP/20TK8RVZckV3GTWrVqkZCQwMaNG6lRowarVq266fGioiKrxp0/fz4tWrS44+OHDh3i+PHjVo0thDNyc3QAwnl16NCBs2fPcvDgQd599128vb1JSUlh8+bNLFy4kEOHDlFQUMDw4cN54oknUEoxd+5c9u7dS+PGjalRo4ZprBEjRvDKK6/Qtm1bkpKSeOeddzAYDNSvX5/58+ezatUqqlWrxoYNG3j99dfx9/dn1qxZXL58GYDXXnuNhx56iMzMTF5++WX0ej3t27fHkmtgxo0bR1paGvn5+Tz99NNERkaaHluwYAF79+6lYcOGvPPOO/j4+PDHH38wZ84cMjMzqVWrFnPnziUgIMD2P2Dh2pQQf9G+fXullFKFhYXq73//u/rqq6/UgQMHVLt27dQff/yhlFJq1apV6v3331dKKZWfn68GDx6s/vjjD7V161YVFRWlioqKVFpamnrooYfUd999p5RS6qmnnlKnTp1SGRkZqlu3bqaxMjMzlVJKLVmyRC1btswUx6RJk9Thw4eVUkr9+eefKiwsTCml1Ny5c9V7772nlFLq+++/V4GBgSojI+OWz9GzZ0/T/SXvcePGDRUeHq6uXLmilFIqMDBQJSQkKKWUeu+999ScOXOUUko9/fTTKiUlRSml1IkTJ9SIESNuG6MQpZGZq7hJXl4eAwcOBIpnrkOHDuX48eO0bduWZs2aAbB3717Onj3L1q1bAcjJyeH333/n8OHDhIeHU716dXQ6HZ06dbpl/BMnTtChQwfTWPXq1bttHPv27bupRnvt2jWuX7/O4cOHWbp0KQA9evSgbt26Zj/TF198wfbt2wFITU3l999/p379+lSrVo2+ffsCMHDgQF588UWuX7/O8ePHeemll0yvLygoMPseQvw3Sa7iJiU11/9Wu3Zt038rpZgxYwZdu3a96Tm7d++2WRxGo5HVq1fj7u5ernEOHjzIvn37+Oabb/Dw8GDEiBHk5+ff9rmapqGUwtvb+7Y/AyHKQk5oiTILCgri66+/prCwEICUlBRyc3N5+OGH+e677zAYDKSnp3Pw4MFbXtu+fXuOHDnCxYsXAbh69SoAderU4fr16ze9xxdffGG6/csvvwDw8MMP8+233wLFyTwrK6vUWHNycqhbty4eHh4kJydz4sQJ02NGo9E0+/7222956KGH8PT05K677uK7774Div8hOXPmTNl+QEIgyVVYISIighYtWjBkyBD69evHzJkzMRgMhISE0Lx5c/r27cvUqVNp3779La/18fHhjTfeYPz48QwYMICJEycC0LNnT7Zv387AgQM5cuQI06dP56effqJ///707duXr7/+GoAXXniBI0eOEB4ezvbt22nSpEmpsXbr1o2ioiL69OnDokWLboqpdu3anDp1in79+nHgwAFeeOEFAN566y3WrFnDgAEDCA8PZ8eOHbb60YkqRFNKWg4KIYStycxVCCHsQJKrEELYgSRXIYSwA0muQghhB5JchRDCDiS5CiGEHUhyFUIIO5Dk6iRkubEQrkWSqwP9NaFqmsa1a9c4c+YMc+bMYefOnQ6MTAhRXpJcHUjTNADS0tI4efIkY8eOZfPmzWzfvh03N+mpI0RlJn/BDnD58mV0Oh3Vq1fniy++ICkpCV9fX4YNG8Zdd93FDz/8gL+/v6PDFEKUg/QWqEBKKVJTUxk5ciRfffUVtWvXZvPmzbRr1w5fX1/q16/PW2+9xT333MPQoUMdHa4Qohxk5lqBNE3D09OTRo0a4evrC8DQoUOpVq24OnP9+nXS0tLo06ePI8MUQtiA1FwryIULF4DiZtTVq1e/aaO/ki8Pb7/9NgBt2rSp8PiEELYlM1c7U0pRWFjI+PHjefTRRxk7diyZmZnk5OSYthopERQUxH333Wd6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40bq169P3759UUpVmt0IhBCVmCtfRNCnTx86dOjAxx9/bNoe5fPPP6+ouIQQVVxl30PrjjXXpUuX0qhRI55++mlmzJjB/v37TZfAVgZJSUn07t2bkJAQYmNjHR2OEKIMXLrm+thjj/HYY4+Rm5tLYmIiK1as4MqVK8yaNYuQkBCCgoIqMs4yMRgMvPHGGyxfvhydTsfQoUMJDg6mRYsWjg5NCGEJFygLmF0tULt2bfr3789HH33E7t27uf/++/nkk08qIjarnTp1iubNm9OsWTNq1qxJeHg4iYmJjg5LCFEWNrz81WAwMGjQIJ5//nkALl68SEREBCEhIUyYMIGCggIACgoKmDBhAiEhIURERHDp0iWrwy/TxlJ169YlMjLS6Xu56vV6/Pz8TLd1Op20SRSiktGUmaMMY61cuZKAgADT7YULFxIVFcX27dvx9vZmzZo1AMTFxeHt7c327duJiopi4cKFVscvu/YJIZyPucRahplrWloau3btYujQocVDK8WBAwdMm68OHjzY9M12586dDB48GCjenLU855pcMrnqdDrS0tJMt/V6vWxQKERlY6OywIIFC5gyZYppB+jMzEy8vb1xcys+5eTn52f6ZqvX62ncuDEAbm5ueHl5kZmZaVX4Lplc27Zty4ULF7h48SIFBQVs2rSJ4OBgR4clhLCUhS0Hk5OTGTJkiOn45ptvbhrm+++/x8fHhzZt2lRs/FixtXZl4ObmxsyZMxk9ejQGg4HHH3+ce++919FhCSEsZFFXLAUBAQGsW7fujs85duwYO3fuJCkpifz8fK5du8b8+fPJzs6mqKgINzc30tLSTN9sdTodqamp+Pn5UVRURE5ODvXr17fqM7hkcgXo3r073bt3d3QYQggr2WIngpdffpmXX34ZgIMHD/LZZ5+xaNEioqOj2bp1K+Hh4axfv970zTY4OJj169fz4IMPsnXrVjp16mRqt1pWLlkWEEJUcnbeiWDKlCksX76ckJAQrl69SkREBABDhw7l6tWrhISEsHz5ciZPnmz1e7jszFUIUXnZY/fXjh070rFjRwCaNWtmWn71V+7u7ixZsqRsA9+BJFchhPNRgLnLW538Ci1JrkIIp+TyOxEI4YpW3ne3zcfs87N16yHN+a51PbuM69RcoLeAJFchhNMpXopVevYsa821oklyFUI4JVuf0KpoklyFEM5H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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deVyU5fr48c+jKKKAisqgZv4Cw0xN+2apiRsGorgnUsdMLLOjJalpWppLLnROWma2kWVanUxcwNTcMCX3XSvTErE0YThfRECRbeb+/cFhvnlUZhhmmGG43q/X83o12z3XEFzecz33c92aUkohhBDCpqo5OgAhhHBFklyFEMIOJLkKIYQdSHIVQgg7kOQqhBB2IMlVCCHsQJKruEmrVq0YOHAg/fr1Izo6mhs3blg91rRp09iyZQsA06dP59y5c3d87sGDBzl27FiZ3yM4OJgrV65YfP9fPfjgg2V6r/fee49PP/20TK8RVZckV3GTWrVqkZCQwMaNG6lRowarVq266fGioiKrxp0/fz4tWrS44+OHDh3i+PHjVo0thDNyc3QAwnl16NCBs2fPcvDgQd599128vb1JSUlh8+bNLFy4kEOHDlFQUMDw4cN54oknUEoxd+5c9u7dS+PGjalRo4ZprBEjRvDKK6/Qtm1bkpKSeOeddzAYDNSvX5/58+ezatUqqlWrxoYNG3j99dfx9/dn1qxZXL58GYDXXnuNhx56iMzMTF5++WX0ej3t27fHkmtgxo0bR1paGvn5+Tz99NNERkaaHluwYAF79+6lYcOGvPPOO/j4+PDHH38wZ84cMjMzqVWrFnPnziUgIMD2P2Dh2pQQf9G+fXullFKFhYXq73//u/rqq6/UgQMHVLt27dQff/yhlFJq1apV6v3331dKKZWfn68GDx6s/vjjD7V161YVFRWlioqKVFpamnrooYfUd999p5RS6qmnnlKnTp1SGRkZqlu3bqaxMjMzlVJKLVmyRC1btswUx6RJk9Thw4eVUkr9+eefKiwsTCml1Ny5c9V7772nlFLq+++/V4GBgSojI+OWz9GzZ0/T/SXvcePGDRUeHq6uXLmilFIqMDBQJSQkKKWUeu+999ScOXOUUko9/fTTKiUlRSml1IkTJ9SIESNuG6MQpZGZq7hJXl4eAwcOBIpnrkOHDuX48eO0bduWZs2aAbB3717Onj3L1q1bAcjJyeH333/n8OHDhIeHU716dXQ6HZ06dbpl/BMnTtChQwfTWPXq1bttHPv27bupRnvt2jWuX7/O4cOHWbp0KQA9evSgbt26Zj/TF198wfbt2wFITU3l999/p379+lSrVo2+ffsCMHDgQF588UWuX7/O8ePHeemll0yvLygoMPseQvw3Sa7iJiU11/9Wu3Zt038rpZgxYwZdu3a96Tm7d++2WRxGo5HVq1fj7u5ernEOHjzIvn37+Oabb/Dw8GDEiBHk5+ff9rmapqGUwtvb+7Y/AyHKQk5oiTILCgri66+/prCwEICUlBRyc3N5+OGH+e677zAYDKSnp3Pw4MFbXtu+fXuOHDnCxYsXAbh69SoAderU4fr16ze9xxdffGG6/csvvwDw8MMP8+233wLFyTwrK6vUWHNycqhbty4eHh4kJydz4sQJ02NGo9E0+/7222956KGH8PT05K677uK7774Div8hOXPmTNl+QEIgyVVYISIighYtWjBkyBD69evHzJkzMRgMhISE0Lx5c/r27cvUqVNp3779La/18fHhjTfeYPz48QwYMICJEycC0LNnT7Zv387AgQM5cuQI06dP56effqJ///707duXr7/+GoAXXniBI0eOEB4ezvbt22nSpEmpsXbr1o2ioiL69OnDokWLboqpdu3anDp1in79+nHgwAFeeOEFAN566y3WrFnDgAEDCA8PZ8eOHbb60YkqRFNKWg4KIYStycxVCCHsQJKrEELYgSRXIYSwA0muQghhB5JchRDCDiS5CiGEHUhyFUIIO5Dk6iRkubEQrkWSqwP9NaFqmsa1a9c4c+YMc+bMYefOnQ6MTAhRXpJcHUjTNADS0tI4efIkY8eOZfPmzWzfvh03N+mpI0RlJn/BDnD58mV0Oh3Vq1fniy++ICkpCV9fX4YNG8Zdd93FDz/8gL+/v6PDFEKUg/QWqEBKKVJTUxk5ciRfffUVtWvXZvPmzbRr1w5fX1/q16/PW2+9xT333MPQoUMdHa4Qohxk5lqBNE3D09OTRo0a4evrC8DQoUOpVq24OnP9+nXS0tLo06ePI8MUQtiA1FwryIULF4DiZtTVq1e/aaO/ki8Pb7/9NgBt2rSp8PiEELYlM1c7U0pRWFjI+PHjefTRRxk7diyZmZnk5OSYthopERQUxH333Wd6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40bq169P3759UUpVmt0IhBCVmCtfRNCnTx86dOjAxx9/bNoe5fPPP6+ouIQQVVxl30PrjjXXpUuX0qhRI55++mlmzJjB/v37TZfAVgZJSUn07t2bkJAQYmNjHR2OEKIMXLrm+thjj/HYY4+Rm5tLYmIiK1as4MqVK8yaNYuQkBCCgoIqMs4yMRgMvPHGGyxfvhydTsfQoUMJDg6mRYsWjg5NCGEJFygLmF0tULt2bfr3789HH33E7t27uf/++/nkk08qIjarnTp1iubNm9OsWTNq1qxJeHg4iYmJjg5LCFEWNrz81WAwMGjQIJ5//nkALl68SEREBCEhIUyYMIGCggIACgoKmDBhAiEhIURERHDp0iWrwy/TxlJ169YlMjLS6Xu56vV6/Pz8TLd1Op20SRSiktGUmaMMY61cuZKAgADT7YULFxIVFcX27dvx9vZmzZo1AMTFxeHt7c327duJiopi4cKFVscvu/YJIZyPucRahplrWloau3btYujQocVDK8WBAwdMm68OHjzY9M12586dDB48GCjenLU855pcMrnqdDrS0tJMt/V6vWxQKERlY6OywIIFC5gyZYppB+jMzEy8vb1xcys+5eTn52f6ZqvX62ncuDEAbm5ueHl5kZmZaVX4Lplc27Zty4ULF7h48SIFBQVs2rSJ4OBgR4clhLCUhS0Hk5OTGTJkiOn45ptvbhrm+++/x8fHhzZt2lRs/FixtXZl4ObmxsyZMxk9ejQGg4HHH3+ce++919FhCSEsZFFXLAUBAQGsW7fujs85duwYO3fuJCkpifz8fK5du8b8+fPJzs6mqKgINzc30tLSTN9sdTodqamp+Pn5UVRURE5ODvXr17fqM7hkcgXo3r073bt3d3QYQggr2WIngpdffpmXX34ZgIMHD/LZZ5+xaNEioqOj2bp1K+Hh4axfv970zTY4OJj169fz4IMPsnXrVjp16mRqt1pWLlkWEEJUcnbeiWDKlCksX76ckJAQrl69SkREBABDhw7l6tWrhISEsHz5ciZPnmz1e7jszFUIUXnZY/fXjh070rFjRwCaNWtmWn71V+7u7ixZsqRsA9+BJFchhPNRgLnLW538Ci1JrkIIp+TyOxEI4YpW3ne3zcfs87N16yHN+a51PbuM69RcoLeAJFchhNMpXopVevYsa821oklyFUI4JVuf0KpoklyFEM5H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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deVzU5dr48c8XUUQBFZVBzfwFhpWa9mS54YaBKO6J1DETy+yoSWqaluaSC50n9ZjZRnZMq5OJG6Xmhim571qZloilCUMPIqDINnP//uBhnjwqMwwzzDBc79fr+3qdmfnOPdd49Oqe63t/r1tTSimEEELYlJujAxBCCFckyVUIIexAkqsQQtiBJFchhLADSa5CCGEHklyFEMIOJLmKWzz44IMMGDCAvn37EhMTw82bN60ea9q0aWzduhWA6dOnc/78+buee+jQIY4fP17mzwgJCeHq1asWP/9XjzzySJk+69133+WTTz4p03tE1SXJVdyiZs2aJCQksGnTJqpXr87q1atveb2oqMiqcefPn0/z5s3v+vrhw4c5ceKEVWML4YzcHR2AcF7t2rXj3LlzHDp0iHfeeQcfHx9SUlLYsmULCxcu5PDhwxQUFDBs2DCeeuoplFLMnTuXffv20ahRI6pXr24aa/jw4bz66qu0bt2apKQk/vnPf2IwGKhXrx7z589n9erVuLm58fXXX/PGG28QEBDArFmzuHLlCgCvv/46jz76KJmZmbzyyivo9Xratm2LJffAjB07lrS0NPLz83n22WeJiooyvbZgwQL27dtHgwYN+Oc//4mvry+///47c+bMITMzk5o1azJ37lwCAwNt/wcsXJsS4i/atm2rlFKqsLBQ/f3vf1dffPGFOnjwoGrTpo36/ffflVJKrV69Wr333ntKKaXy8/PVoEGD1O+//662bdumoqOjVVFRkUpLS1OPPvqo+vbbb5VSSj3zzDPq9OnTKiMjQ3Xt2tU0VmZmplJKqaVLl6rly5eb4pg0aZI6cuSIUkqpP/74Q4WHhyullJo7d6569913lVJKfffddyooKEhlZGTc9j169Ohher7kM27evKkiIiLU1atXlVJKBQUFqYSEBKWUUu+++66aM2eOUkqpZ599VqWkpCillDp58qQaPnz4HWMUojQycxW3yMvLY8CAAUDxzHXIkCGc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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deVzU5dr48c8XUUQBFZVBzfwFhpWa9mS54YaBKO6J1DETy+yoSWqaluaSC50n9ZjZRnZMq5OJG6Xmhim571qZloilCUMPIqDINnP//uBhnjwqMwwzzDBc79fr+3qdmfnOPdd49Oqe63t/r1tTSimEEELYlJujAxBCCFckyVUIIexAkqsQQtiBJFchhLADSa5CCGEHklyFEMIOJLmKWzz44IMMGDCAvn37EhMTw82bN60ea9q0aWzduhWA6dOnc/78+buee+jQIY4fP17mzwgJCeHq1asWP/9XjzzySJk+69133+WTTz4p03tE1SXJVdyiZs2aJCQksGnTJqpXr87q1atveb2oqMiqcefPn0/z5s3v+vrhw4c5ceKEVWML4YzcHR2AcF7t2rXj3LlzHDp0iHfeeQcfHx9SUlLYsmULCxcu5PDhwxQUFDBs2DCeeuoplFLMnTuXffv20ahRI6pXr24aa/jw4bz66qu0bt2apKQk/vnPf2IwGKhXrx7z589n9erVuLm58fXXX/PGG28QEBDArFmzuHLlCgCvv/46jz76KJmZmbzyyivo9Xratm2LJffAjB07lrS0NPLz83n22WeJiooyvbZgwQL27dtHgwYN+Oc//4mvry+///47c+bMITMzk5o1azJ37lwCAwNt/wcsXJsS4i/atm2rlFKqsLBQ/f3vf1dffPGFOnjwoGrTpo36/ffflVJKrV69Wr333ntKKaXy8/PVoEGD1O+//662bdumoqOjVVFRkUpLS1OPPvqo+vbbb5VSSj3zzDPq9OnTKiMjQ3Xt2tU0VmZmplJKqaVLl6rly5eb4pg0aZI6cuSIUkqpP/74Q4WHhyullJo7d6569913lVJKfffddyooKEhlZGTc9j169Ohher7kM27evKkiIiLU1atXlVJKBQUFqYSEBKWUUu+++66aM2eOUkqpZ599VqWkpCillDp58qQaPnz4HWMUojQycxW3yMvLY8CAAUDxzHXIkCGc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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deVzU5dr48c8XUUQBFZVBzfwFhpWa9mS54YaBKO6J1DETy+yoSWqaluaSC50n9ZjZRnZMq5OJG6Xmhim571qZloilCUMPIqDINnP//uBhnjwqMwwzzDBc79fr+3qdmfnOPdd49Oqe63t/r1tTSimEEELYlJujAxBCCFckyVUIIexAkqsQQtiBJFchhLADSa5CCGEHklyFEMIOJLmKWzz44IMMGDCAvn37EhMTw82bN60ea9q0aWzduhWA6dOnc/78+buee+jQIY4fP17mzwgJCeHq1asWP/9XjzzySJk+69133+WTTz4p03tE1SXJVdyiZs2aJCQksGnTJqpXr87q1atveb2oqMiqcefPn0/z5s3v+vrhw4c5ceKEVWML4YzcHR2AcF7t2rXj3LlzHDp0iHfeeQcfHx9SUlLYsmULCxcu5PDhwxQUFDBs2DCeeuoplFLMnTuXffv20ahRI6pXr24aa/jw4bz66qu0bt2apKQk/vnPf2IwGKhXrx7z589n9erVuLm58fXXX/PGG28QEBDArFmzuHLlCgCvv/46jz76KJmZmbzyyivo9Xratm2LJffAjB07lrS0NPLz83n22WeJiooyvbZgwQL27dtHgwYN+Oc//4mvry+///47c+bMITMzk5o1azJ37lwCAwNt/wcsXJsS4i/atm2rlFKqsLBQ/f3vf1dffPGFOnjwoGrTpo36/ffflVJKrV69Wr333ntKKaXy8/PVoEGD1O+//662bdumoqOjVVFRkUpLS1OPPvqo+vbbb5VSSj3zzDPq9OnTKiMjQ3Xt2tU0VmZmplJKqaVLl6rly5eb4pg0aZI6cuSIUkqpP/74Q4WHhyullJo7d6569913lVJKfffddyooKEhlZGTc9j169Ohher7kM27evKkiIiLU1atXlVJKBQUFqYSEBKWUUu+++66aM2eOUkqpZ599VqWkpCillDp58qQaPnz4HWMUojQycxW3yMvLY8CAAUDxzHXIkCGc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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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unUr95h79+5l9uzZJCcns2PHDjp27FjuC3Pffvstr7/+OsePH+fgwYO0aNGCevXqlWvMXbt2MWfOHJKSkjh16hSXL1+mefPm5boDbseOHcycOZMffviBw4cPo9frCQwMpEaNGlaPeeTIERYsWEBUVBQtW7a0epw/S0hIICYmhvz8fI4ePUrr1q2pW7eu1eMZjUZyc3MZP348x48fx83NjTZt2qBpGkajUbq2maNEmRgMBqWUUu+//77atWvXLa+dP39ebdmyRa1atUpdunTJ7Fi7d+9WDz/8sJo8ebLpuaKionLFt3//fvXEE0+on3/+WRUUFKhRo0apI0eOlGvMQ4cOqdDQUHX69GmllFLjxo1T+/fvL9eYx48fV2FhYerHH39USik1e/ZsNX369HKNefXqVfX888+rX375RSmlVFxcnBoyZIhavny5ysnJsWrMgoIC9dJLL6ljx44ppZTatm2bevPNN9WSJUusHlMppf71r3+pFStWKKWUSktLU/v27VOnTp1S2dnZVo139epV9dxzz6nz588rpZSaPn262rp1q/rf//1flZeXZ3WcSikVGxurPv74YzV16lS1cuXKco1VlUhZoIzc3NxISUlh//79t3Sw+vXXX2nRogV9+/bl2WefpVmzZqWOk5uby2effcZrr71G9erVmTJlCgDVqlUr19dOg8HA3//+d+6//35u3rzJfffdxy+//AJYXy9r0KABc+fO5eGHH+aPP/7g9OnTfPbZZ8yaNYtt27ZZPe4LL7zAQw89BEB0dDRZWVnl+irr7u5Obm4uf/zxB1C8y0OTJk3IzMxkz549Vo97/fp1fv31VwBCQkLo2bMnhYWFfP3111Z/9j+3lXzppZdYv349n332GXPnziUrK6vM47m7u5OXl8fFixe5fv06R44cIT4+noULF/Lee++Vq7br7u5OamoqgwcP5syZM8TExLB48WKUUhiNRqvHdXVSFigDpRRFRUUsXbqU4OBgunTpQlJSEjNmzODixYvcf//9eHl5WfR1qXr16nTs2JHWrVvTsWNHEhISSEhIIDQ0tFylgWbNmtGoUSOMRiM1a9ZE0zRiYmIICgqiQYMGVo3p6+vLPffcA8Dq1atp06YNc+bMITMzk927d/P444/j6elZpjH9/Pxo1qwZNWrUwGAwkJOTwxdffEGfPn3w9PQkMzOzzGN6eHhQ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAVcAAAEYCAYAAADoP7WhAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3de1yUZfr48c+DKKKAisqgZv4C00pN+2Z5whMGonhOpNZMLHNXTVLTtDQPeaDd1DWzE9maVpuJJ0rNE6bk+ayVaYlYmDD0RQQUOc3cvz/4MpurMsMwwwzD9X69ntdrZ+bhnmtYvLrneu7nujWllEIIIYRNuTk6ACGEcEWSXIUQwg4kuQohhB1IchVCCDuQ5CqEEHYgyVUIIexAkqu4xYMPPsjAgQPp168f0dHR3Lx50+qxpk+fzrZt2wCYMWMGFy5cuOu5hw8f5sSJE2V+j+DgYK5evWrx83/2yCOPlOm93nnnHT7++OMy/YyouiS5ilvUrFmT+Ph4Nm/eTPXq1VmzZs0trxcVFVk17oIFC2jevPldXz9y5AgnT560amwhnJG7owMQzqt9+/acP3+ew4cP8/bbb+Pj40NycjJbt25l0aJFHDlyhIKCAoYPH85TTz2FUop58+axf/9+GjVqRPXq1U1jjRgxgldeeYU2bdqQmJjIP//5TwwGA/Xq1WPBggWsWbMGNzc3vvrqK15//XUCAgKYPXs2V65cAeC1117j0UcfJTMzk5dffhm9Xk+7du2w5B6YcePGkZaWRn5+Ps8++yyRkZGm1xYuXMj+/ftp0KAB//znP/H19eW3335j7ty5ZGZmUrNmTebNm0dgYKDtf8HCtSkh/qRdu3ZKKaUKCwvV3/72N/X555+rQ4cOqbZt26rffvtNKaXUmjVr1LvvvquUUio/P18NHjxY/fbbb2r79u0qKipKFRUVqbS0NPXoo4+qb775Riml1DPPPKPOnDmjMjIyVLdu3UxjZWZmKqWUWrZsmVqxYoUpjsmTJ6ujR48qpZT6/fffVVhYmFJKqXnz5ql33nlHKaXUt99+q1q0aKEyMjJu+xw9e/Y0PV/yHjdv3lTh4eHq6tWrSimlWrRooeLj45VSSr3zzjtq7ty5Simlnn32WZWcnKyUUurUqVNqxIgRd4xRiNLIzFXcIi8vj4EDBwLFM9ehQ4dy8uRJ2rRpQ9OmTQHYv38/58+fZ/v27QDk5OTw66+/cvToUcLDw6lWrRo6nY6OHTveNv6pU6do3769aay6deveMY4DBw7cUqO9fv06N27c4OjRoyxfvhyAHj16UKdOHbOf6dNPP2Xnzp0ApKam8uuvv1KvXj3c3Nzo27cvAAMHDuTFF1/kxo0bnDx5kpdeesn08wUFBWbfQ4j/JslV3KKk5vrfatWqZfrfSilmzpxJ165dbzln7969NovDaDSydu1aPDw8yjXO4cOHOXDgAF9++SWenp6MGDGC/Pz8O56raRpKKXx8fO74OxCiLOSCliizoKAgvvjiCwoLCwFITk4mNzeXxx57jG+++QaDwUB6ejqHDx++7WfbtWvHsWPHSElJAeDatWsA1K5dmxs3btzyHp9++qnp8U8//QTAY489xtdffw0UJ/OsrKxSY83JyaFOnTp4enqSlJTEqVOnTK8ZjUbT7Pvrr7/m0UcfxcvLi3vuuYdvvvkGKP4Pyblz58r2CxICSa7CChERETRv3pwhQ4bQr18/Zs2ahcFgICQkhGbNmtG3b1+mTZtGu3btbvtZX19f3njjDSZMmMCAAQOYNGkSAD179mTnzp0MHDiQY8eOMWPGDH744Qf69+9P3759+eKLLwAYP348x44dIzw8nJ07d9K4ceNSY+3WrRtFRUX06dOHxYsX3xJTrVq1OHPmDP369ePQoUOMHz8egLfeeot169YxYMAAwsPD2bVrl61+daIK0ZSSloNCCGFrMnMVQgg7kOQqhBB2IMlVCCHsQJKrEELYgSRXIYSwA0muQghhB5JchRDCDiS5OglZbiyEa5Hk6kB/TqiapnH9+nXOnTvH3Llz2b17twMjE0KUlyRXB9I0DYC0tDROnz7N2LFj2bp1Kzt37sTdXXrqCFGZyb9gB7hy5Qo6nY5q1arx6aefkpiYiJ+fH8OGDeOee+7hu+++IyAgwNFhCiHKQXoLVCClFKmpqYwcOZLPP/+cWrVqsXXrVtq2bYufnx/16tXjrbfe4r777mPo0KGODlcIUQ4yc61Amqbh5eVFw4YN8fPzA2Do0KG4uRVXZ27cuEFaWhp9+vRxZJhCCBuQmmsFuXTpElDcjLpatWq3bPRX8uVhyZIlALRu3brC4xNC2JbMXO1MKUVhYSETJkygc+fOjB07lszMTHJyckxbjZQICgrigQceMP1cyQUvIUTlIzVXO9Pr9eh0OlJTUxk7diyPPPIISUlJ9OjRA19fX2rUqIGvry/5+fn4+Pjw8MMPU61aNUeHLYQoJ0mudqKUIisri+HDhzNy5EiGDRuGXq9nypQpHD16lBdeeIHffvuN/Px8NE3j6tWrLFu2DJ1O5+jQhRA2IMnVzvbs2cPbb7/NiBEjGDJkCFevXmXMmDGEhYUxevRo03n5+fnl3oxPCOE8pOZqBz///DP169fHy8uLHj164Onpyfz58zEYDERERPDuu+/yt7/9jZSUFObOnQtA9erVHRy1EMKWZOZqY6mpqYSGhtKwYUNatGjB8OHDCQwMJCsri1deeYWxY8fSt29f0tLSmDx5MsuXL8fX19fRYQshbKzanDlz5jg6CFeRnZ1NgwYNqFWrlqlXgIeHB2+//Tbe3t7k5OSwbds2PDw86NChA4MGDaJ27dqODlsIYQeyztVG9Ho9kydP5ujRozz77LN06tSJ5s2b07JlSz755BN8fX1p1qwZaWlpLF68mKysrFuWYQkhXIuUBWzk2rVrbNmyhX379jFmzBjatGnDunXrOHXqFOHh4XTt2hWApKQkfHx8aNiwoYMjFkLYk5QFbKRmzZo0a9YMg8HA2rVruffeewkODubq1ascPnyYnJwcWrZsia+vr5QChKgC5HtpORw9epT4+HjT4zp16tC7d2+eeOIJPvzwQ3755RcGDBhA8+bN+f7777l+/boDoxVCVCRZilUORUVFxMTE4ObmRv/+/YHiBBsaGkpeXh47d+5kwoQJhIWFUbNmTby8vBwcsRCiosjM1QpKKZRSdOrUiWXLlrF06VK++uor02t169YlMDCQ5ORkjEYjfn5++Pj4VHicf/zxh2wf4+Ryc3MdHUKZyN+T5SS5WkHTNDRN49y5czz++OPMnz+ft99+m82bN5uareTm5pKfn2/xPx6DwWDTGL/77jtefPFFUlNTbTbmL7/8wpEjR8jMzLTZmL/++ivff/89BQUFNhvz0qVLfP/99xiNRpv9Xi9evMjJkycpLCy02Zi7du1i0aJFZGRk2GQ8gFOnTrFp0yZOnTpls9/psWPH2LRpE1D8ty8J1jJyQctK69at49133yU8PJyAgABatmzJu+++y6VLl9i7dy/x8fHMnDmTRo0alTpOcnKyqTuWwWCwyfKsffv2sWjRIjIzM7l27RrdunUr95h79+5l9uzZJCcns2PHDjp27FjuC3Pffvstr7/+OsePH+fgwYO0aNGCevXqlWvMXbt2MWfOHJKSkjh16hSXL1+mefPm5boDbseOHcycOZMffviBw4cPo9frCQwMpEaNGlaPeeTIERYsWEBUVBQtW7a0epw/S0hIICYmhvz8fI4ePUrr1q2pW7eu1eMZjUZyc3MZP348x48fx83NjTZt2qBpGkajUbq2maNEmRgMBqWUUu+//77atWvXLa+dP39ebdmyRa1atUpdunTJ7Fi7d+9WDz/8sJo8ebLpuaKionLFt3//fvXEE0+on3/+WRUUFKhRo0apI0eOlGvMQ4cOqdDQUHX69GmllFLjxo1T+/fvL9eYx48fV2FhYerHH39USik1e/ZsNX369HKNefXqVfX888+rX375RSmlVFxcnBoyZIhavny5ysnJsWrMgoIC9dJLL6ljx44ppZTatm2bevPNN9WSJUusHlMppf71r3+pFStWKKWUSktLU/v27VOnTp1S2dnZVo139epV9dxzz6nz588rpZSaPn262rp1q/rf//1flZeXZ3WcSikVGxurPv74YzV16lS1cuXKco1VlUhZoIzc3NxISUlh//79t3Sw+vXXX2nRogV9+/bl2WefpVmzZqWOk5uby2effcZrr71G9erVmTJlCgDVqlUr19dOg8HA3//+d+6//35u3rzJfffdxy+//AJYXy9r0KABc+fO5eGHH+aPP/7g9OnTfPbZZ8yaNYtt27ZZPe4LL7zAQw89BEB0dDRZWVnl+irr7u5Obm4uf/zxB1C8y0OTJk3IzMxkz549Vo97/fp1fv31VwBCQkLo2bMnhYWFfP3111Z/9j+3lXzppZdYv349n332GXPnziUrK6vM47m7u5OXl8fFixe5fv06R44cIT4+noULF/Lee++Vq7br7u5OamoqgwcP5syZM8TExLB48WKUUhiNRqvHdXVSFigDpRRFRUUsXbqU4OBgunTpQlJSEjNmzODixYvcf//9eHl5WfR1qXr16nTs2JHWrVvTsWNHEhISSEhIIDQ0tFylgWbNmtGoUSOMRiM1a9ZE0zRiYmIICgqiQYMGVo3p6+vLPffcA8Dq1atp06YNc+bMITMzk927d/P444/j6elZpjH9/Pxo1qwZNWrUwGAwkJOTwxdffEGfPn3w9PQkMzOzzGN6eHhQ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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unUr95h79+5l9uzZJCcns2PHDjp27FjuC3Pffvstr7/+OsePH+fgwYO0aNGCevXqlWvMXbt2MWfOHJKSkjh16hSXL1+mefPm5boDbseOHcycOZMffviBw4cPo9frCQwMpEaNGlaPeeTIERYsWEBUVBQtW7a0epw/S0hIICYmhvz8fI4ePUrr1q2pW7eu1eMZjUZyc3MZP348x48fx83NjTZt2qBpGkajUbq2maNEmRgMBqWUUu+//77atWvXLa+dP39ebdmyRa1atUpdunTJ7Fi7d+9WDz/8sJo8ebLpuaKionLFt3//fvXEE0+on3/+WRUUFKhRo0apI0eOlGvMQ4cOqdDQUHX69GmllFLjxo1T+/fvL9eYx48fV2FhYerHH39USik1e/ZsNX369HKNefXqVfX888+rX375RSmlVFxcnBoyZIhavny5ysnJsWrMgoIC9dJLL6ljx44ppZTatm2bevPNN9WSJUusHlMppf71r3+pFStWKKWUSktLU/v27VOnTp1S2dnZVo139epV9dxzz6nz588rpZSaPn262rp1q/rf//1flZeXZ3WcSikVGxurPv74YzV16lS1cuXKco1VlUhZoIzc3NxISUlh//79t3Sw+vXXX2nRogV9+/bl2WefpVmzZqWOk5uby2effcZrr71G9erVmTJlCgDVqlUr19dOg8HA3//+d+6//35u3rzJfffdxy+//AJYXy9r0KABc+fO5eGHH+aPP/7g9OnTfPbZZ8yaNYtt27ZZPe4LL7zAQw89BEB0dDRZWVnl+irr7u5Obm4uf/zxB1C8y0OTJk3IzMxkz549Vo97/fp1fv31VwBCQkLo2bMnhYWFfP3111Z/9j+3lXzppZdYv349n332GXPnziUrK6vM47m7u5OXl8fFixe5fv06R44cIT4+noULF/Lee++Vq7br7u5OamoqgwcP5syZM8TExLB48WKUUhiNRqvHdXVSFigDpRRFRUUsXbqU4OBgunTpQlJSEjNmzODixYvcf//9eHl5WfR1qXr16nTs2JHWrVvTsWNHEhISSEhIIDQ0tFylgWbNmtGoUSOMRiM1a9ZE0zRiYmIICgqiQYMGVo3p6+vLPffcA8Dq1atp06YNc+bMITMzk927d/P444/j6elZpjH9/Pxo1qwZNWrUwGAwkJOTwxdffEGfPn3w9PQkMzOzzGN6eHhQ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Confustion matrix by model\n", + "confusion_matrix(\n", + " test_stats_list,\n", + " train_metadata_json,\n", + " \"label\",\n", + " [10, 10, 10],\n", + " False,\n", + " model_names=models_list,\n", + " output_directory=\"./viz2\",\n", + " file_format=\"png\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/multi_task/README.md b/examples/multi_task/README.md new file mode 100644 index 0000000..6985216 --- /dev/null +++ b/examples/multi_task/README.md @@ -0,0 +1,96 @@ +# Multi-Task Learning with Nash-MTL Loss Balancing + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/multi_task/multi_task.ipynb) + +> **Note:** Nash-MTL requires PR #4092 (`future-capabilities` branch) and is not yet available in the main Ludwig release. The FAMO and uncertainty weighting methods shown here are available now. + +## Overview + +This example demonstrates multi-task learning with Ludwig: training a single model to predict multiple outputs simultaneously, and using **loss balancing** to prevent one task from dominating training. + +The dataset is the [UCI Wine Quality dataset](https://archive.ics.uci.edu/ml/datasets/wine+quality). We predict two outputs at once: + +- `quality_score` — the raw 0–10 quality score (regression) +- `quality_binary` — whether the wine is good (quality ≥ 7, binary classification) + +These two tasks have different loss magnitudes. Without balancing, the regression loss typically dominates and the classifier under-trains. + +## What You Will Learn + +1. How to define multiple output features in a Ludwig config +1. Why loss magnitudes differ between regression and classification tasks +1. How FAMO and uncertainty weighting improve multi-task training (available now) +1. What Nash-MTL does and how it compares to heuristic methods (requires PR #4092) +1. How to read a comparison table and choose the right balancing strategy + +## Loss Balancing Methods Compared + +| Method | Status | When to use | +| --------------- | ----------------- | ---------------------------------------------- | +| `none` | Available | Baseline; tasks have similar loss scales | +| `log_transform` | Available | Quick improvement with no hyperparameters | +| `uncertainty` | Available | Tasks have stable, learnable scale differences | +| `famo` | Available | General purpose; good default choice | +| `gradnorm` | Available | Gradient-level balancing; more expensive | +| `nash_mtl` | Requires PR #4092 | Most principled; best when tasks conflict | + +## Quick Start + +```bash +pip install ludwig + +# Baseline +ludwig train --config config_no_balancing.yaml --dataset wine_quality_dual.csv + +# FAMO (available now) +ludwig train --config config_famo.yaml --dataset wine_quality_dual.csv + +# Uncertainty weighting (available now) +ludwig train --config config_uncertainty.yaml --dataset wine_quality_dual.csv + +# Nash-MTL (requires PR #4092) +ludwig train --config config_nash_mtl.yaml --dataset wine_quality_dual.csv +``` + +Or run the full comparison script: + +```bash +python train_multi_task.py +``` + +## Files + +| File | Description | +| -------------------------- | ------------------------------------------ | +| `multi_task.ipynb` | Interactive notebook with full walkthrough | +| `train_multi_task.py` | Standalone Python script | +| `config_no_balancing.yaml` | Baseline config — no loss balancing | +| `config_famo.yaml` | FAMO balancing (available now) | +| `config_uncertainty.yaml` | Uncertainty weighting (available now) | +| `config_nash_mtl.yaml` | Nash-MTL balancing (requires PR #4092) | + +## Prerequisites + +- Python 3.9+ +- Ludwig installed (`pip install ludwig`) +- Internet access to download the UCI Wine Quality dataset (~80 KB) + +Optional: GPU for faster training (not required). + +## Background + +### Multi-Task Learning + +Multi-task learning trains a shared model to predict several outputs simultaneously. The shared representation encourages the model to learn features useful across tasks, often improving generalisation compared to separate single-task models — especially when training data is limited. + +### The Loss Balancing Problem + +When tasks have different loss scales (e.g., MSE for regression vs. cross-entropy for binary classification), their gradients have different magnitudes. During backpropagation, the task with larger gradients dominates parameter updates and the other task effectively under-trains. + +Loss balancing methods assign adaptive weights to each task's loss so that all tasks contribute proportionately to the total gradient. + +### Nash-MTL + +Nash-MTL (Navon et al., ICML 2022) frames loss balancing as a Nash bargaining game. Rather than using heuristic rules or hand-tuned weights, it finds the unique solution where no task can improve its loss without worsening another task's loss. This makes it the most principled approach, particularly valuable when tasks genuinely conflict. + +See [Navon et al., 2022](https://arxiv.org/abs/2202.01017) for the theoretical grounding. diff --git a/examples/multi_task/compare_balancers.py b/examples/multi_task/compare_balancers.py new file mode 100644 index 0000000..ef08295 --- /dev/null +++ b/examples/multi_task/compare_balancers.py @@ -0,0 +1,113 @@ +"""Compare multi-task loss balancing strategies on a joint classification + regression task. + +The dataset is UCI Wine Quality (red) with two output features: + +- ``quality`` — the usual 0–10 score, trained as number regression. +- ``recommended`` — a synthetic binary target set to ``quality >= 6``, trained as binary + classification. The two outputs share everything except the final decoder head, so they + compete for the combiner's representational capacity. + +For each balancer in :data:`STRATEGIES` the script trains the same model end-to-end and +records validation metrics. The summary table prints the per-task scores plus a +balance-aware geometric mean so you can see which strategy gets both tasks right. + +Requires Ludwig 0.15 / PR #4092 for ``nash_mtl``. + +Run: ``python compare_balancers.py`` +""" + +from __future__ import annotations + +import logging +import math +from pathlib import Path + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel +from ludwig.datasets import wine_quality + +HERE = Path(__file__).parent + +# Strategies to compare. nash_mtl is included only on the future-capabilities branch. +STRATEGIES = [ + "none", + "log_transform", + "uncertainty", + "famo", + "gradnorm", + "nash_mtl", +] + + +def add_binary_target(df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["recommended"] = (df["quality"] >= 6).astype(int) + return df + + +def build_config(balancer: str) -> dict: + with (HERE / "config_nash_mtl.yaml").open() as f: + config = yaml.safe_load(f) + config["trainer"]["loss_balancing"] = balancer + return config + + +def run(balancer: str, dataset: pd.DataFrame) -> dict[str, float]: + config = build_config(balancer) + model = LudwigModel(config=config, logging_level=logging.WARNING) + result = model.train( + dataset=dataset, + output_directory=str(HERE / f"results_{balancer}"), + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_predictions=True, + skip_save_model=True, + ) + val = result.train_stats.validation or {} + + quality_rmse = min(val["quality"].get("root_mean_squared_error", [float("nan")])) + recommended_acc = max(val["recommended"].get("accuracy", [float("nan")])) + quality_loss = min(val["quality"].get("loss", [float("nan")])) + recommended_loss = min(val["recommended"].get("loss", [float("nan")])) + + # Geometric mean of losses is a balance-aware aggregate: a strategy that wrecks one task + # to win the other pays more than a strategy that keeps both reasonable. + geomean = math.sqrt(quality_loss * recommended_loss) if quality_loss and recommended_loss else float("nan") + return { + "quality_rmse": quality_rmse, + "recommended_acc": recommended_acc, + "geomean_loss": geomean, + } + + +def main() -> None: + dataset = add_binary_target(wine_quality.load()) + + rows = [] + for balancer in STRATEGIES: + print(f"\n=== Training with loss_balancing: {balancer} ===") + try: + scores = run(balancer, dataset) + except Exception as exc: + print(f"[skip] {balancer}: {exc}") + continue + rows.append({"balancer": balancer, **scores}) + + if not rows: + raise SystemExit("No balancer runs completed successfully.") + + summary = pd.DataFrame(rows).set_index("balancer") + summary = summary.sort_values("geomean_loss") + print("\nResults (best-of-training per task, sorted by geomean_loss):") + print(summary.to_string(float_format=lambda v: f"{v:.4f}")) + + csv_path = HERE / "balancer_comparison.csv" + summary.to_csv(csv_path) + print(f"\nWrote {csv_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/multi_task/config_famo.yaml b/examples/multi_task/config_famo.yaml new file mode 100644 index 0000000..c68152d --- /dev/null +++ b/examples/multi_task/config_famo.yaml @@ -0,0 +1,65 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality_score + type: number + - name: quality_binary + type: binary + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + dropout: 0.1 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 + loss_balancing: famo diff --git a/examples/multi_task/config_nash_mtl.yaml b/examples/multi_task/config_nash_mtl.yaml new file mode 100644 index 0000000..a742567 --- /dev/null +++ b/examples/multi_task/config_nash_mtl.yaml @@ -0,0 +1,65 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality_score + type: number + - name: quality_binary + type: binary + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + dropout: 0.1 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 + loss_balancing: nash_mtl # Requires PR #4092 / Ludwig >= 0.14 diff --git a/examples/multi_task/config_no_balancing.yaml b/examples/multi_task/config_no_balancing.yaml new file mode 100644 index 0000000..61d615a --- /dev/null +++ b/examples/multi_task/config_no_balancing.yaml @@ -0,0 +1,65 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality_score + type: number + - name: quality_binary + type: binary + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + dropout: 0.1 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 + loss_balancing: none diff --git a/examples/multi_task/config_uncertainty.yaml b/examples/multi_task/config_uncertainty.yaml new file mode 100644 index 0000000..a29acf1 --- /dev/null +++ b/examples/multi_task/config_uncertainty.yaml @@ -0,0 +1,65 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality_score + type: number + - name: quality_binary + type: binary + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + dropout: 0.1 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 + loss_balancing: uncertainty diff --git a/examples/multi_task/multi_task.ipynb b/examples/multi_task/multi_task.ipynb new file mode 100644 index 0000000..afdcdda --- /dev/null +++ b/examples/multi_task/multi_task.ipynb @@ -0,0 +1,459 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "# Multi-Task Learning with Nash-MTL Loss Balancing\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/multi_task/multi_task.ipynb)\n", + "\n", + "This notebook demonstrates **multi-task learning** with Ludwig: training a single model to predict multiple outputs simultaneously, and using **loss balancing** to prevent one task from dominating training.\n", + "\n", + "**Use case:** The [UCI Wine Quality dataset](https://archive.ics.uci.edu/ml/datasets/wine+quality) has a 0–10 quality score. We simultaneously predict:\n", + "- `quality_score` — the raw numerical score (regression)\n", + "- `quality_binary` — whether the wine is good (quality ≥ 7, binary classification)\n", + "\n", + "These two tasks have different loss magnitudes. Without balancing, regression loss dominates and the classifier under-trains.\n", + "\n", + "**Methods compared:**\n", + "\n", + "| Method | Status | Description |\n", + "|---|---|---|\n", + "| `none` | Available | Static weighted sum — baseline |\n", + "| `famo` | Available | Fast Adaptive Multitask Optimization (Liu et al., NeurIPS 2023) |\n", + "| `uncertainty` | Available | Homoscedastic uncertainty weighting (Kendall et al., CVPR 2018) |\n", + "| `nash_mtl` | Requires PR #4092 | Nash bargaining solution (Navon et al., ICML 2022) |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acae54e37e7d407bbb7b55eff062a284", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a63283cbaf04dbcab1f6479b197f3a8", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import shutil\n", + "import warnings\n", + "\n", + "import pandas as pd\n", + "\n", + "logging.basicConfig(level=logging.WARNING)" + ] + }, + { + "cell_type": "markdown", + "id": "8dd0d8092fe74a7c96281538738b07e2", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "We use the [UCI Wine Quality dataset](https://archive.ics.uci.edu/ml/datasets/wine+quality) (red wine, ~1 600 rows, 11 physicochemical input features).\n", + "\n", + "Two output columns are derived from the original `quality` score:\n", + "- `quality_score`: the raw numerical score (0–10) — a regression target\n", + "- `quality_binary`: 1 if quality ≥ 7 (\"good\"), else 0 — a classification target\n", + "\n", + "These tasks share the same inputs and are naturally correlated, making them a good multi-task setup." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72eea5119410473aa328ad9291626812", + "metadata": {}, + "outputs": [], + "source": [ + "WINE_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\"\n", + "\n", + "WINE_FEATURES = [\n", + " \"fixed_acidity\",\n", + " \"volatile_acidity\",\n", + " \"citric_acid\",\n", + " \"residual_sugar\",\n", + " \"chlorides\",\n", + " \"free_sulfur_dioxide\",\n", + " \"total_sulfur_dioxide\",\n", + " \"density\",\n", + " \"pH\",\n", + " \"sulphates\",\n", + " \"alcohol\",\n", + "]\n", + "\n", + "print(\"Downloading wine quality dataset...\")\n", + "df = pd.read_csv(WINE_URL, sep=\";\")\n", + "df.columns = [c.replace(\" \", \"_\") for c in df.columns]\n", + "\n", + "df[\"quality_score\"] = df[\"quality\"].astype(float)\n", + "df[\"quality_binary\"] = (df[\"quality\"] >= 7).astype(int)\n", + "df = df.drop(columns=[\"quality\"])\n", + "\n", + "print(f\"Shape: {df.shape}\")\n", + "print(f\"Good wines (quality >= 7): {df['quality_binary'].mean():.1%}\")\n", + "print(f\"Quality score range: {df['quality_score'].min():.0f} – {df['quality_score'].max():.0f}\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "8edb47106e1a46a883d545849b8ab81b", + "metadata": {}, + "source": [ + "### Why does loss balancing matter here?\n", + "\n", + "MSE for regression and binary cross-entropy for classification operate on different scales. Typically:\n", + "- Regression MSE: values in the range 0.5–2.0 (for a 0–10 quality score)\n", + "- Binary cross-entropy: values in the range 0.3–0.7\n", + "\n", + "Without balancing, the regression task provides larger raw gradients and the model updates are biased toward minimising the regression loss at the expense of classification performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10185d26023b46108eb7d9f57d49d2b3", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.api import LudwigModel\n", + "\n", + "\n", + "def input_features():\n", + " return [{\"name\": feat, \"type\": \"number\", \"preprocessing\": {\"normalization\": \"zscore\"}} for feat in WINE_FEATURES]\n", + "\n", + "\n", + "def make_config(loss_balancing: str) -> dict:\n", + " return {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": input_features(),\n", + " \"output_features\": [\n", + " {\"name\": \"quality_score\", \"type\": \"number\"},\n", + " {\"name\": \"quality_binary\", \"type\": \"binary\"},\n", + " ],\n", + " \"combiner\": {\n", + " \"type\": \"concat\",\n", + " \"num_fc_layers\": 2,\n", + " \"output_size\": 128,\n", + " \"dropout\": 0.1,\n", + " },\n", + " \"trainer\": {\n", + " \"epochs\": 30,\n", + " \"learning_rate\": 0.001,\n", + " \"batch_size\": 128,\n", + " \"loss_balancing\": loss_balancing,\n", + " },\n", + " }\n", + "\n", + "\n", + "def train_and_evaluate(name: str, loss_balancing: str) -> dict | None:\n", + " result_dir = f\"./results/{name}\"\n", + " shutil.rmtree(result_dir, ignore_errors=True)\n", + " print(f\"\\n{'=' * 50}\")\n", + " print(f\"Training: {name} (loss_balancing={loss_balancing})\")\n", + " print(f\"{'=' * 50}\")\n", + " config = make_config(loss_balancing)\n", + " model = LudwigModel(config=config, logging_level=logging.WARNING)\n", + " train_stats, _, _ = model.train(\n", + " dataset=df,\n", + " experiment_name=\"multi_task\",\n", + " model_name=name,\n", + " output_directory=result_dir,\n", + " )\n", + " # Extract final validation metrics\n", + " vset = train_stats.get(\"validation\", {})\n", + " score_mae = _last(vset.get(\"quality_score\", {}).get(\"mean_absolute_error\", []))\n", + " binary_auc = _last(vset.get(\"quality_binary\", {}).get(\"roc_auc\", []))\n", + " print(f\" quality_score MAE : {score_mae:.4f}\")\n", + " print(f\" quality_binary ROC-AUC : {binary_auc:.4f}\")\n", + " return {\"method\": name, \"score_mae\": score_mae, \"binary_roc_auc\": binary_auc}\n", + "\n", + "\n", + "def _last(series):\n", + " if not series:\n", + " return float(\"nan\")\n", + " v = series[-1]\n", + " if isinstance(v, (list, tuple)):\n", + " v = v[-1]\n", + " return float(v)\n", + "\n", + "\n", + "results = []" + ] + }, + { + "cell_type": "markdown", + "id": "8763a12b2bbd4a93a75aff182afb95dc", + "metadata": {}, + "source": [ + "## Baseline: No Loss Balancing\n", + "\n", + "The default `loss_balancing: none` computes the total loss as a static weighted sum:\n", + "\n", + "```\n", + "L_total = w_score * L_score + w_binary * L_binary\n", + "```\n", + "\n", + "where weights are set from the config (defaulting to 1.0 each). The regression task typically has a larger loss value, so it tends to dominate gradient updates.\n", + "\n", + "Config snippet:\n", + "```yaml\n", + "trainer:\n", + " loss_balancing: none\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7623eae2785240b9bd12b16a66d81610", + "metadata": {}, + "outputs": [], + "source": [ + "result = train_and_evaluate(\"none\", \"none\")\n", + "results.append(result)" + ] + }, + { + "cell_type": "markdown", + "id": "7cdc8c89c7104fffa095e18ddfef8986", + "metadata": {}, + "source": [ + "## FAMO Balancing\n", + "\n", + "> **Available now** in the current Ludwig release.\n", + "\n", + "**FAMO** (Fast Adaptive Multitask Optimization, Liu et al., NeurIPS 2023) maintains an exponential moving average of each task's loss and updates task weights at every step to equalise the rate of loss decrease across tasks.\n", + "\n", + "Key properties:\n", + "- No gradient computation overhead (unlike gradient-based methods)\n", + "- Converges quickly; good default choice for most multi-task problems\n", + "- One hyperparameter: `loss_balancing_lr` (EMA learning rate, default 0.025)\n", + "\n", + "Config snippet:\n", + "```yaml\n", + "trainer:\n", + " loss_balancing: famo\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b118ea5561624da68c537baed56e602f", + "metadata": {}, + "outputs": [], + "source": [ + "result = train_and_evaluate(\"famo\", \"famo\")\n", + "results.append(result)" + ] + }, + { + "cell_type": "markdown", + "id": "938c804e27f84196a10c8828c723f798", + "metadata": {}, + "source": [ + "## Uncertainty Weighting\n", + "\n", + "> **Available now** in the current Ludwig release.\n", + "\n", + "**Uncertainty weighting** (Kendall et al., CVPR 2018) treats each task's weight as a learned parameter representing homoscedastic (task-level) uncertainty. Tasks with higher uncertainty receive lower weight automatically.\n", + "\n", + "Key properties:\n", + "- Learns a single scalar per task — minimal parameter overhead\n", + "- Principled Bayesian interpretation\n", + "- Works best when tasks have stable, intrinsically different noise levels\n", + "\n", + "Config snippet:\n", + "```yaml\n", + "trainer:\n", + " loss_balancing: uncertainty\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "504fb2a444614c0babb325280ed9130a", + "metadata": {}, + "outputs": [], + "source": [ + "result = train_and_evaluate(\"uncertainty\", \"uncertainty\")\n", + "results.append(result)" + ] + }, + { + "cell_type": "markdown", + "id": "59bbdb311c014d738909a11f9e486628", + "metadata": {}, + "source": [ + "## Nash-MTL Balancing\n", + "\n", + "> **Requires PR #4092** (`future-capabilities` branch). Not yet available in the main Ludwig release.\n", + "> \n", + "> To try it now:\n", + "> ```bash\n", + "> pip install git+https://github.com/ludwig-ai/ludwig@future-capabilities\n", + "> ```\n", + "\n", + "**Nash-MTL** (Navon et al., ICML 2022) finds the [Nash bargaining solution](https://en.wikipedia.org/wiki/Nash_bargaining_solution) for task weight allocation. In game-theoretic terms, it selects a Pareto-optimal point where no task can improve its loss without worsening another task's loss.\n", + "\n", + "Concretely, Nash-MTL solves a quadratic programme at each step to find weights `alpha` such that:\n", + "\n", + "```\n", + "alpha = argmax sum_i log(alpha_i * loss_i) subject to sum alpha_i = 1, alpha_i >= 0\n", + "```\n", + "\n", + "This is equivalent to weights being inversely proportional to task losses — tasks with larger losses receive proportionally higher weight, equalising marginal contributions.\n", + "\n", + "Key properties:\n", + "- Theoretically principled (game theory, not heuristic)\n", + "- Most effective when tasks genuinely conflict (gradients point in different directions)\n", + "- Slightly higher per-step overhead than FAMO (solves a small QP per step)\n", + "\n", + "Config snippet:\n", + "```yaml\n", + "trainer:\n", + " loss_balancing: nash_mtl # Requires PR #4092 / Ludwig >= 0.14\n", + "```\n", + "\n", + "Reference: Navon et al., [Multi-Task Learning as a Bargaining Game](https://arxiv.org/abs/2202.01017), ICML 2022." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b43b363d81ae4b689946ece5c682cd59", + "metadata": {}, + "outputs": [], + "source": [ + "# Nash-MTL is attempted; a warning is printed if PR #4092 is not yet available.\n", + "try:\n", + " result = train_and_evaluate(\"nash_mtl\", \"nash_mtl\")\n", + " results.append(result)\n", + "except Exception as exc:\n", + " warnings.warn(\n", + " f\"nash_mtl is not available in this Ludwig version ({exc}). \"\n", + " \"Install from 'future-capabilities' branch to enable it.\"\n", + " )\n", + " results.append({\"method\": \"nash_mtl\", \"score_mae\": float(\"nan\"), \"binary_roc_auc\": float(\"nan\")})" + ] + }, + { + "cell_type": "markdown", + "id": "8a65eabff63a45729fe45fb5ade58bdc", + "metadata": {}, + "source": [ + "## Comparison Table\n", + "\n", + "Side-by-side comparison of all four methods on the validation split." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3933fab20d04ec698c2621248eb3be0", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "comparison = pd.DataFrame(results).set_index(\"method\")\n", + "comparison.columns = [\"Score MAE (lower better)\", \"Binary ROC-AUC (higher better)\"]\n", + "print(comparison.to_string())\n", + "comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dd4641cc4064e0191573fe9c69df29b", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", + "\n", + "methods = comparison.index.tolist()\n", + "mae_vals = comparison[\"Score MAE (lower better)\"].tolist()\n", + "auc_vals = comparison[\"Binary ROC-AUC (higher better)\"].tolist()\n", + "\n", + "colors = [\"tab:gray\", \"tab:blue\", \"tab:orange\", \"tab:green\"]\n", + "\n", + "axes[0].bar(methods, mae_vals, color=colors[: len(methods)])\n", + "axes[0].set_title(\"Quality Score — MAE (lower is better)\")\n", + "axes[0].set_ylabel(\"MAE\")\n", + "axes[0].set_ylim(0, max(mae_vals) * 1.2)\n", + "\n", + "axes[1].bar(methods, auc_vals, color=colors[: len(methods)])\n", + "axes[1].set_title(\"Quality Binary — ROC-AUC (higher is better)\")\n", + "axes[1].set_ylabel(\"ROC-AUC\")\n", + "axes[1].set_ylim(0.5, 1.0)\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(\"results/comparison.png\", dpi=150)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8309879909854d7188b41380fd92a7c3", + "metadata": {}, + "source": [ + "## When to Use Nash-MTL\n", + "\n", + "Use **Nash-MTL** when:\n", + "\n", + "- You have two or more output features with significantly different loss scales\n", + "- Your tasks conflict (improving one task hurts another), as measured by negative gradient cosine similarity\n", + "- You want the most principled, theoretically grounded balancing approach\n", + "- You can tolerate a small additional per-step compute cost (solving a small QP)\n", + "\n", + "Use **FAMO** when:\n", + "- You want a strong default with no tuning required\n", + "- Training speed is a priority (FAMO has lower overhead than Nash-MTL)\n", + "\n", + "Use **uncertainty weighting** when:\n", + "- Tasks have stable, intrinsically different noise levels\n", + "- You want a simple, interpretable approach with a Bayesian justification\n", + "\n", + "Use **none** (baseline) when:\n", + "- You have a single output feature\n", + "- Your tasks have similar loss scales and do not conflict\n", + "- You have manually tuned `loss_weight` values on each output feature\n", + "\n", + "### Further Reading\n", + "\n", + "- Navon et al., [Multi-Task Learning as a Bargaining Game](https://arxiv.org/abs/2202.01017), ICML 2022\n", + "- Liu et al., [FAMO: Fast Adaptive Multitask Optimization](https://arxiv.org/abs/2306.03792), NeurIPS 2023\n", + "- Kendall et al., [Multi-Task Learning Using Uncertainty to Weigh Losses](https://arxiv.org/abs/1705.07115), CVPR 2018\n", + "- Chen et al., [GradNorm: Gradient Normalization for Adaptive Loss Balancing](https://arxiv.org/abs/1711.02257), ICML 2018" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_task/train_multi_task.py b/examples/multi_task/train_multi_task.py new file mode 100644 index 0000000..0d72c03 --- /dev/null +++ b/examples/multi_task/train_multi_task.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python +"""Multi-Task Learning with Loss Balancing in Ludwig. + +Trains four models on the UCI Wine Quality dataset with two output features: + - quality_score : raw 0-10 quality score (regression) + - quality_binary : quality >= 7 is "good" (binary classification) + +Compares loss balancing strategies: + 1. none — static weighted sum (baseline) + 2. famo — Fast Adaptive Multitask Optimization (available now) + 3. uncertainty — Homoscedastic uncertainty weighting (available now) + 4. nash_mtl — Nash bargaining solution (requires PR #4092) + +# Colab: !pip install ludwig + +Usage: + python train_multi_task.py +""" + +import logging +import os +import shutil +import warnings + +import pandas as pd + +logging.basicConfig(level=logging.WARNING) + + +# --------------------------------------------------------------------------- +# Dataset +# --------------------------------------------------------------------------- + +WINE_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv" + +WINE_FEATURES = [ + "fixed_acidity", + "volatile_acidity", + "citric_acid", + "residual_sugar", + "chlorides", + "free_sulfur_dioxide", + "total_sulfur_dioxide", + "density", + "pH", + "sulphates", + "alcohol", +] + + +def load_dataset() -> pd.DataFrame: + """Download and prepare the dual-output wine quality dataset.""" + print("Downloading wine quality dataset...") + df = pd.read_csv(WINE_URL, sep=";") + df.columns = [c.replace(" ", "_") for c in df.columns] + # quality_score: keep the raw 0-10 numerical score + df["quality_score"] = df["quality"].astype(float) + # quality_binary: 1 if quality >= 7 (good wine), else 0 + df["quality_binary"] = (df["quality"] >= 7).astype(int) + df = df.drop(columns=["quality"]) + print(f" {len(df)} rows | good wines (quality >= 7): {df['quality_binary'].mean():.1%}") + print(f" quality_score range: {df['quality_score'].min():.0f} – {df['quality_score'].max():.0f}") + return df + + +# --------------------------------------------------------------------------- +# Ludwig config helpers +# --------------------------------------------------------------------------- + + +def _input_features() -> list: + return [{"name": feat, "type": "number", "preprocessing": {"normalization": "zscore"}} for feat in WINE_FEATURES] + + +def _base_config(loss_balancing: str) -> dict: + return { + "model_type": "ecd", + "input_features": _input_features(), + "output_features": [ + {"name": "quality_score", "type": "number"}, + {"name": "quality_binary", "type": "binary"}, + ], + "combiner": { + "type": "concat", + "num_fc_layers": 2, + "output_size": 128, + "dropout": 0.1, + }, + "trainer": { + "epochs": 30, + "learning_rate": 0.001, + "batch_size": 128, + "loss_balancing": loss_balancing, + }, + } + + +# --------------------------------------------------------------------------- +# Training helper +# --------------------------------------------------------------------------- + + +def train_and_evaluate( + name: str, + config: dict, + dataset: pd.DataFrame, + output_dir: str, +) -> dict | None: + """Train a Ludwig model and return evaluation metrics. + + Returns a dict with metric values, or None if training failed. + """ + from ludwig.api import LudwigModel + + result_dir = os.path.join(output_dir, name) + shutil.rmtree(result_dir, ignore_errors=True) + + print(f"\n--- Training: {name} ---") + try: + model = LudwigModel(config=config, logging_level=logging.WARNING) + result = model.train( + dataset=dataset, + experiment_name="multi_task", + model_name=name, + output_directory=result_dir, + ) + + # Extract final validation metrics + metrics = {} + vset = result.train_stats.validation or {} + # quality_score: mean absolute error (lower is better) + score_metrics = vset.get("quality_score", {}) + metrics["score_mae"] = _last_value(score_metrics.get("mean_absolute_error", [])) + # quality_binary: ROC AUC (higher is better) + binary_metrics = vset.get("quality_binary", {}) + metrics["binary_roc_auc"] = _last_value(binary_metrics.get("roc_auc", [])) + return metrics + + except Exception as exc: + warnings.warn(f"Training '{name}' failed: {exc}", stacklevel=2) + return None + + +def _last_value(series) -> float | None: + """Return the last numeric value in a list, or None.""" + if not series: + return None + val = series[-1] + if isinstance(val, (list, tuple)): + val = val[-1] + try: + return float(val) + except (TypeError, ValueError): + return None + + +# --------------------------------------------------------------------------- +# Comparison table +# --------------------------------------------------------------------------- + + +def print_comparison_table(results: dict) -> None: + """Print a formatted side-by-side comparison of all methods.""" + col_w = 14 + header = f"{'Method':<{col_w}} | {'Score MAE':>{col_w}} | {'Binary ROC-AUC':>{col_w}}" + separator = "-" * len(header) + print() + print("=" * len(header)) + print(" Multi-Task Loss Balancing — Comparison") + print("=" * len(header)) + print(header) + print(separator) + for method, metrics in results.items(): + if metrics is None: + mae_str = "FAILED" + auc_str = "FAILED" + else: + mae = metrics.get("score_mae") + auc = metrics.get("binary_roc_auc") + mae_str = f"{mae:.4f}" if mae is not None else "n/a" + auc_str = f"{auc:.4f}" if auc is not None else "n/a" + print(f"{method:<{col_w}} | {mae_str:>{col_w}} | {auc_str:>{col_w}}") + print(separator) + print(" Score MAE: lower is better | Binary ROC-AUC: higher is better") + print() + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + output_dir = "./results" + os.makedirs(output_dir, exist_ok=True) + + df = load_dataset() + + # Methods to compare. nash_mtl is attempted but skipped gracefully if + # PR #4092 is not yet merged. + methods = [ + ("none", False), + ("famo", False), + ("uncertainty", False), + ("nash_mtl", True), # requires PR #4092 + ] + + results = {} + for method, requires_pr in methods: + if requires_pr: + print(f"\n--- Skipping {method} (requires PR #4092 / Ludwig >= 0.14) ---") + print(" To enable, install Ludwig from the 'future-capabilities' branch:") + print(" pip install git+https://github.com/ludwig-ai/ludwig@future-capabilities") + results[method] = None + continue + + config = _base_config(method) + results[method] = train_and_evaluate(method, config, df, output_dir) + + # Attempt nash_mtl — will succeed if PR #4092 is available + try: + from ludwig.api import LudwigModel + + config = _base_config("nash_mtl") + # Try instantiating to check if nash_mtl is a valid option + model = LudwigModel(config=config, logging_level=logging.WARNING) + del model + print("\n nash_mtl is available — training now...") + results["nash_mtl"] = train_and_evaluate("nash_mtl", config, df, output_dir) + except Exception: + pass # already marked as None above + + print_comparison_table(results) + + +if __name__ == "__main__": + main() diff --git a/examples/open_set_recognition/README.md b/examples/open_set_recognition/README.md new file mode 100644 index 0000000..d1e605e --- /dev/null +++ b/examples/open_set_recognition/README.md @@ -0,0 +1,117 @@ +# Open-Set Recognition with Agnostophobia Losses + +## MNIST Tutorial + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/open_set_recognition/open_set_mnist.ipynb) + +The notebook `open_set_mnist.ipynb` walks through the full open-set recognition workflow on a +real image dataset: + +- **Dataset**: MNIST digits — classes 0–7 are *known*, classes 8–9 act as *unknown/background* +- **Models**: three Ludwig image classifiers using `stacked_cnn` encoder and `category` output + - CE Baseline (`softmax_cross_entropy`) — trained on known classes only + - Entropic Open-Set (`entropic_open_set`) — entropy maximisation on background samples + - Objectosphere (`objectosphere`) — norm push on known + norm suppression on background +- **Evaluation**: confidence histograms and ROC curves for unknown detection + +The notebook is Colab-compatible — it installs Ludwig and torchvision, downloads MNIST, saves +images to disk, builds `train.csv`/`test.csv`, trains all three models, and plots results. + +YAML configs for standalone use: + +- `config_baseline_mnist.yaml` +- `config_entropic_mnist.yaml` +- `config_objectosphere_mnist.yaml` + +______________________________________________________________________ + +## Quick Validation Script + +This example reproduces the key findings from: + +> Dhamija, A. R., Günther, M., & Boult, T. (2018). +> **Reducing Network Agnostophobia.** +> *NeurIPS 2018.* https://arxiv.org/abs/1811.04110 + +Standard classifiers are trained to output high-confidence predictions for every input — even inputs +from classes never seen during training. This is called *network agnostophobia*: the network is +incapable of expressing "I don't know." + +The paper proposes two loss functions that address this: + +| Loss | Description | +| --------------------- | -------------------------------------------------------------------------- | +| **Entropic Open-Set** | CE on known samples + entropy maximisation on background samples | +| **Objectosphere** | CE + logit-norm push for known + entropy + norm suppression for background | + +Both are available in Ludwig's category and binary output features. + +### Quick start + +```bash +pip install ludwig +python train_open_set.py +``` + +The script generates a synthetic two-class-family dataset (four known Gaussian clusters + two unknown +clusters), trains three classifiers, and prints a comparison table showing mean max probability on +unknowns — lower is better for open-set recognition. + +Expected output (approximate): + +``` +Model | Max-prob (known) | Max-prob (unknown) | Norm known | Norm unknown +-----------------------|-----------------|-------------------|------------|------------- +CE Baseline | 0.998 | 0.741 | 8.828 | 5.375 +Entropic Open-Set | 0.974 | 0.273 | 6.254 | 0.637 +Objectosphere | 0.874 | 0.363 | 13.843 | 2.361 +``` + +### Ludwig configuration + +#### Entropic Open-Set Loss + +```yaml +output_features: + - name: label + type: category + loss: + type: entropic_open_set + background_class: 4 # integer index of the background/unknown class +``` + +#### Objectosphere Loss + +```yaml +output_features: + - name: label + type: category + loss: + type: objectosphere + background_class: 4 + xi: 10.0 # minimum logit norm for known-class samples + zeta: 0.1 # weight for unknown-class magnitude suppression +``` + +`background_class` is the **integer index** of the background/unknown class in Ludwig's +vocabulary for that feature. You can discover it by inspecting the saved model's +`training_set_metadata.json` file after a training run — look for the `str2idx` field of the +relevant output feature. + +### Inference-time unknown detection + +For **Objectosphere** models, unknown inputs can be detected using a simple threshold on the logit +L2 norm: + +```python +predictions = model.predict(dataset=df) + +# Retrieve raw logits via the API (requires model.collect_activations) +import torch + +norms = logit_tensor.norm(dim=-1) +is_unknown = norms < threshold # choose threshold from validation set +``` + +For both loss types, you can also use the **maximum softmax probability** as a simpler threshold: +samples with max-prob below some value (e.g. 0.5) are flagged as unknown. diff --git a/examples/open_set_recognition/config_baseline.yaml b/examples/open_set_recognition/config_baseline.yaml new file mode 100644 index 0000000..ea1f943 --- /dev/null +++ b/examples/open_set_recognition/config_baseline.yaml @@ -0,0 +1,20 @@ +# Baseline: standard softmax cross-entropy classifier. +# Trained only on known classes; will assign high confidence to unknown inputs. + +model_type: ecd + +input_features: + - name: feature_1 + type: number + - name: feature_2 + type: number + +output_features: + - name: label + type: category + loss: + type: softmax_cross_entropy + +trainer: + epochs: 50 + learning_rate: 0.001 diff --git a/examples/open_set_recognition/config_baseline_mnist.yaml b/examples/open_set_recognition/config_baseline_mnist.yaml new file mode 100644 index 0000000..bd8c1f8 --- /dev/null +++ b/examples/open_set_recognition/config_baseline_mnist.yaml @@ -0,0 +1,39 @@ +# Baseline: standard softmax cross-entropy classifier on MNIST digits 0-7. +# Trained only on known classes; will assign high confidence to unknown inputs +# (digits 8 and 9) at test time. + +model_type: ecd + +input_features: + - name: image_path + type: image + encoder: + type: stacked_cnn + conv_layers: + - num_filters: 32 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + - num_filters: 64 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + fc_layers: + - output_size: 128 + dropout: 0.3 + +output_features: + - name: label + type: category + loss: + type: softmax_cross_entropy + +trainer: + epochs: 10 + learning_rate: 0.001 + batch_size: 128 + +preprocessing: + split: + type: random + probabilities: [0.8, 0.1, 0.1] diff --git a/examples/open_set_recognition/config_entropic.yaml b/examples/open_set_recognition/config_entropic.yaml new file mode 100644 index 0000000..6fb9334 --- /dev/null +++ b/examples/open_set_recognition/config_entropic.yaml @@ -0,0 +1,28 @@ +# Entropic Open-Set classifier (Dhamija et al., NeurIPS 2018). +# +# Training set must include background/unknown samples labelled with the +# background class. For known samples the loss is standard cross-entropy; +# for background samples the loss maximises output entropy (reduces max-prob). +# +# background_class: integer index of the background class in Ludwig's +# vocabulary for this feature. Inspect training_set_metadata.json after +# a first training run to find the correct index for your dataset. + +model_type: ecd + +input_features: + - name: feature_1 + type: number + - name: feature_2 + type: number + +output_features: + - name: label + type: category + loss: + type: entropic_open_set + background_class: 4 # adjust to match your vocabulary index + +trainer: + epochs: 50 + learning_rate: 0.001 diff --git a/examples/open_set_recognition/config_entropic_mnist.yaml b/examples/open_set_recognition/config_entropic_mnist.yaml new file mode 100644 index 0000000..bdab8e3 --- /dev/null +++ b/examples/open_set_recognition/config_entropic_mnist.yaml @@ -0,0 +1,49 @@ +# Entropic Open-Set classifier (Dhamija et al., NeurIPS 2018) on MNIST. +# +# Training set includes digits 0-7 (known) and digits 8-9 labelled as +# "background" (unknown). For known samples the loss is standard cross-entropy; +# for background samples the loss maximises output entropy, driving the softmax +# probabilities toward uniform. +# +# background_class: integer index of the "background" label in Ludwig's +# vocabulary for this feature. Run the baseline config first, then inspect +# results//model/training_set_metadata.json and look for +# output_features -> label -> str2idx -> "background". +# See the notebook (open_set_mnist.ipynb) for a step-by-step walkthrough. + +model_type: ecd + +input_features: + - name: image_path + type: image + encoder: + type: stacked_cnn + conv_layers: + - num_filters: 32 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + - num_filters: 64 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + fc_layers: + - output_size: 128 + dropout: 0.3 + +output_features: + - name: label + type: category + loss: + type: entropic_open_set + background_class: 1 # see notebook for how to determine this value + +trainer: + epochs: 10 + learning_rate: 0.001 + batch_size: 128 + +preprocessing: + split: + type: random + probabilities: [0.8, 0.1, 0.1] diff --git a/examples/open_set_recognition/config_objectosphere.yaml b/examples/open_set_recognition/config_objectosphere.yaml new file mode 100644 index 0000000..d3be537 --- /dev/null +++ b/examples/open_set_recognition/config_objectosphere.yaml @@ -0,0 +1,31 @@ +# Objectosphere classifier (Dhamija et al., NeurIPS 2018). +# +# Extends the Entropic Open-Set loss with a logit-norm objective: +# - Known samples: CE + hinge pushing ||logits|| >= xi +# - Background samples: entropy maximisation + magnitude suppression (zeta * ||logits||^2) +# +# At inference time, flag unknown inputs with a norm threshold: +# is_unknown = logit_norm < threshold +# Choose the threshold from the validation set (e.g. 5th percentile of +# known-class norms). + +model_type: ecd + +input_features: + - name: feature_1 + type: number + - name: feature_2 + type: number + +output_features: + - name: label + type: category + loss: + type: objectosphere + background_class: 4 # adjust to match your vocabulary index + xi: 10.0 # minimum logit norm for known-class samples + zeta: 0.1 # weight for unknown-class magnitude suppression + +trainer: + epochs: 50 + learning_rate: 0.001 diff --git a/examples/open_set_recognition/config_objectosphere_mnist.yaml b/examples/open_set_recognition/config_objectosphere_mnist.yaml new file mode 100644 index 0000000..566f023 --- /dev/null +++ b/examples/open_set_recognition/config_objectosphere_mnist.yaml @@ -0,0 +1,53 @@ +# Objectosphere classifier (Dhamija et al., NeurIPS 2018) on MNIST. +# +# Extends the Entropic Open-Set loss with a logit-norm objective: +# - Known samples: CE + hinge pushing ||logits|| >= xi +# - Background samples: entropy maximisation + magnitude suppression (zeta * ||logits||^2) +# +# At inference time, flag unknown inputs with a norm threshold: +# is_unknown = logit_norm < threshold +# Choose the threshold from the validation set (e.g. 5th percentile of +# known-class norms). +# +# background_class: integer index of the "background" label in Ludwig's +# vocabulary for this feature. See open_set_mnist.ipynb for how to +# discover this value from training_set_metadata.json. + +model_type: ecd + +input_features: + - name: image_path + type: image + encoder: + type: stacked_cnn + conv_layers: + - num_filters: 32 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + - num_filters: 64 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + fc_layers: + - output_size: 128 + dropout: 0.3 + +output_features: + - name: label + type: category + loss: + type: objectosphere + background_class: 1 # see notebook for how to determine this value + xi: 10.0 # minimum logit norm for known-class samples + zeta: 0.1 # weight for unknown-class magnitude suppression + +trainer: + epochs: 10 + learning_rate: 0.001 + batch_size: 128 + +preprocessing: + split: + type: random + probabilities: [0.8, 0.1, 0.1] diff --git a/examples/open_set_recognition/open_set_mnist.ipynb b/examples/open_set_recognition/open_set_mnist.ipynb new file mode 100644 index 0000000..1d6d996 --- /dev/null +++ b/examples/open_set_recognition/open_set_mnist.ipynb @@ -0,0 +1,677 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "title-cell", + "metadata": {}, + "source": [ + "# Open-Set Recognition with MNIST: Reducing Network Agnostophobia\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/open_set_recognition/open_set_mnist.ipynb)\n", + "\n", + "Standard image classifiers assign high-confidence predictions to *every* input — including images\n", + "from classes never seen during training. This is called **network agnostophobia**: the model is\n", + "incapable of saying \"I don't know.\"\n", + "\n", + "This notebook demonstrates three Ludwig models trained on MNIST digits **0–7** (known classes)\n", + "with digits **8–9** acting as the unknown/background:\n", + "\n", + "| Model | Loss | Expected behaviour |\n", + "|-------|------|--------------------|\n", + "| CE Baseline | `softmax_cross_entropy` | High confidence even on unknown digits |\n", + "| Entropic Open-Set | `entropic_open_set` | Pushes unknown confidence toward uniform |\n", + "| Objectosphere | `objectosphere` | Creates a clear logit-norm gap between known and unknown |\n", + "\n", + "**Paper:** Dhamija, Günther, Boult — *Reducing Network Agnostophobia*, NeurIPS 2018.\n", + "https://arxiv.org/abs/1811.04110" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "install-cell", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig torchvision --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "setup-header", + "metadata": {}, + "source": [ + "## Setup and data preparation\n", + "\n", + "We download MNIST via `torchvision`, save each digit image as a PNG file, and build two CSVs:\n", + "\n", + "- **`train.csv`** — digits 0–7 with their true labels, plus digits 8–9 labelled as `\"background\"`\n", + "- **`test.csv`** — digits 0–7 (known) and digits 8–9 (unknown) with their true labels\n", + "\n", + "The training set teaches the open-set models what \"background\" looks like. The test set lets us\n", + "measure how confidently each model handles the unseen classes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "data-prep", + "metadata": {}, + "outputs": [], + "source": [ + "import csv\n", + "from pathlib import Path\n", + "\n", + "import torch\n", + "from PIL import Image\n", + "from torchvision import datasets, transforms\n", + "\n", + "# ── configuration ──────────────────────────────────────────────────────────────\n", + "DATA_DIR = Path(\"mnist_data\") # raw MNIST download\n", + "IMG_DIR = Path(\"mnist_images\") # saved PNG files\n", + "KNOWN_CLASSES = list(range(8)) # digits 0-7\n", + "UNKNOWN_CLASSES = [8, 9] # background / unknown\n", + "\n", + "# Limit samples per class so training is fast on CPU\n", + "MAX_TRAIN_KNOWN = 500 # per known class\n", + "MAX_TRAIN_UNKNOWN = 500 # per unknown class (background)\n", + "MAX_TEST_KNOWN = 200 # per known class\n", + "MAX_TEST_UNKNOWN = 200 # per unknown class\n", + "\n", + "IMG_DIR.mkdir(parents=True, exist_ok=True)\n", + "\n", + "# ── download ───────────────────────────────────────────────────────────────────\n", + "mnist_train = datasets.MNIST(str(DATA_DIR), train=True, download=True, transform=transforms.ToTensor())\n", + "mnist_test = datasets.MNIST(str(DATA_DIR), train=False, download=True, transform=transforms.ToTensor())\n", + "\n", + "print(f\"Downloaded: {len(mnist_train)} train / {len(mnist_test)} test samples\")\n", + "\n", + "\n", + "# ── helper: save image and return path ────────────────────────────────────────\n", + "def save_image(tensor: torch.Tensor, split: str, digit: int, idx: int) -> str:\n", + " \"\"\"Save a (1, H, W) float tensor as a grayscale PNG; return the file path.\"\"\"\n", + " folder = IMG_DIR / split / str(digit)\n", + " folder.mkdir(parents=True, exist_ok=True)\n", + " fpath = folder / f\"{idx:05d}.png\"\n", + " img = Image.fromarray((tensor.squeeze(0).numpy() * 255).astype(\"uint8\"), mode=\"L\")\n", + " img.save(fpath)\n", + " return str(fpath)\n", + "\n", + "\n", + "# ── build train.csv ───────────────────────────────────────────────────────────\n", + "# class counter for capping per-class samples\n", + "from collections import defaultdict\n", + "\n", + "\n", + "def build_csv(\n", + " dataset,\n", + " csv_path: str,\n", + " split: str,\n", + " known_classes,\n", + " unknown_classes,\n", + " max_known: int,\n", + " max_unknown: int,\n", + " label_unknown_as_background: bool,\n", + "):\n", + " \"\"\"\n", + " Walk *dataset*, save PNGs, write *csv_path*.\n", + "\n", + " label_unknown_as_background=True → training split (unknown → \"background\")\n", + " label_unknown_as_background=False → test split (unknown keeps true digit string)\n", + " \"\"\"\n", + " counts_known = defaultdict(int)\n", + " counts_unknown = defaultdict(int)\n", + " rows = []\n", + "\n", + " for global_idx, (img_tensor, digit) in enumerate(dataset):\n", + " digit = int(digit)\n", + " if digit in known_classes:\n", + " if counts_known[digit] >= max_known:\n", + " continue\n", + " path = save_image(img_tensor, split, digit, global_idx)\n", + " label = str(digit)\n", + " counts_known[digit] += 1\n", + " elif digit in unknown_classes:\n", + " if counts_unknown[digit] >= max_unknown:\n", + " continue\n", + " path = save_image(img_tensor, split, digit, global_idx)\n", + " label = \"background\" if label_unknown_as_background else str(digit)\n", + " counts_unknown[digit] += 1\n", + " else:\n", + " continue\n", + " rows.append({\"image_path\": path, \"label\": label})\n", + "\n", + " # stop early once all caps are met\n", + " if all(counts_known[c] >= max_known for c in known_classes) and all(\n", + " counts_unknown[c] >= max_unknown for c in unknown_classes\n", + " ):\n", + " break\n", + "\n", + " with open(csv_path, \"w\", newline=\"\") as f:\n", + " writer = csv.DictWriter(f, fieldnames=[\"image_path\", \"label\"])\n", + " writer.writeheader()\n", + " writer.writerows(rows)\n", + "\n", + " known_total = sum(counts_known.values())\n", + " unknown_total = sum(counts_unknown.values())\n", + " print(f\" {csv_path}: {known_total} known, {unknown_total} unknown/background\")\n", + " return rows\n", + "\n", + "\n", + "print(\"Building train.csv ...\")\n", + "train_rows = build_csv(\n", + " mnist_train,\n", + " \"train.csv\",\n", + " \"train\",\n", + " KNOWN_CLASSES,\n", + " UNKNOWN_CLASSES,\n", + " MAX_TRAIN_KNOWN,\n", + " MAX_TRAIN_UNKNOWN,\n", + " label_unknown_as_background=True,\n", + ")\n", + "\n", + "print(\"Building test.csv ...\")\n", + "test_rows = build_csv(\n", + " mnist_test,\n", + " \"test.csv\",\n", + " \"test\",\n", + " KNOWN_CLASSES,\n", + " UNKNOWN_CLASSES,\n", + " MAX_TEST_KNOWN,\n", + " MAX_TEST_UNKNOWN,\n", + " label_unknown_as_background=False,\n", + ")\n", + "\n", + "print(\"Done.\")" + ] + }, + { + "cell_type": "markdown", + "id": "inspect-csv-header", + "metadata": {}, + "source": [ + "Let's quickly inspect what our CSVs look like." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "inspect-csv", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "train_df = pd.read_csv(\"train.csv\")\n", + "test_df = pd.read_csv(\"test.csv\")\n", + "\n", + "print(\"train.csv label distribution:\")\n", + "print(train_df[\"label\"].value_counts().sort_index())\n", + "print()\n", + "print(\"test.csv label distribution:\")\n", + "print(test_df[\"label\"].value_counts().sort_index())\n", + "print()\n", + "print(train_df.head())" + ] + }, + { + "cell_type": "markdown", + "id": "background-discovery-header", + "metadata": {}, + "source": [ + "## Discover background class index\n", + "\n", + "Ludwig assigns integer indices to category labels based on frequency in the training data\n", + "(most frequent first, with index 0 reserved for ``). Before using `entropic_open_set`\n", + "or `objectosphere` loss we need to know the integer index that Ludwig assigns to the\n", + "`\"background\"` label.\n", + "\n", + "The safest approach:\n", + "1. Run a short training job with the baseline config (standard cross-entropy).\n", + "2. Open `training_set_metadata.json` in the saved model directory.\n", + "3. Look up `output_features -> label -> str2idx -> \"background\"`.\n", + "\n", + "We do exactly this below — first training the baseline, then extracting the index programmatically." + ] + }, + { + "cell_type": "markdown", + "id": "baseline-header", + "metadata": {}, + "source": [ + "## Baseline: softmax cross-entropy\n", + "\n", + "The baseline model is a standard image classifier trained with softmax cross-entropy. It is\n", + "trained **only on known classes** (digits 0–7); digits 8–9 are excluded from its training data.\n", + "\n", + "At test time the baseline will happily assign high confidence to unknown digits because it has\n", + "no mechanism to express uncertainty." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baseline-config", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "ENCODER = {\n", + " \"type\": \"stacked_cnn\",\n", + " \"conv_layers\": [\n", + " {\"num_filters\": 32, \"filter_size\": 3, \"pool_size\": 2, \"pool_stride\": 2},\n", + " {\"num_filters\": 64, \"filter_size\": 3, \"pool_size\": 2, \"pool_stride\": 2},\n", + " ],\n", + " \"fc_layers\": [{\"output_size\": 128, \"dropout\": 0.3}],\n", + "}\n", + "\n", + "config_baseline = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [{\"name\": \"image_path\", \"type\": \"image\", \"encoder\": ENCODER}],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\", \"loss\": {\"type\": \"softmax_cross_entropy\"}}],\n", + " \"trainer\": {\"epochs\": 10, \"learning_rate\": 0.001, \"batch_size\": 128},\n", + "}\n", + "\n", + "print(yaml.dump(config_baseline, default_flow_style=False))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baseline-train", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.api import LudwigModel\n", + "\n", + "# The baseline is trained only on known classes — filter out the background rows\n", + "train_known_df = train_df[train_df[\"label\"] != \"background\"].copy()\n", + "print(f\"Training baseline on {len(train_known_df)} samples (known classes only)\")\n", + "\n", + "model_baseline = LudwigModel(config=config_baseline, logging_level=\"WARNING\")\n", + "_, _, output_dir_baseline = model_baseline.train(\n", + " dataset=train_known_df,\n", + " experiment_name=\"open_set_mnist\",\n", + " model_name=\"baseline\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "print(f\"Baseline model saved to: {output_dir_baseline}\")" + ] + }, + { + "cell_type": "markdown", + "id": "background-index-discovery", + "metadata": {}, + "source": [ + "### Find the background class index from `training_set_metadata.json`\n", + "\n", + "Now that we have a trained model we can inspect its vocabulary. The agnostophobia models will\n", + "be trained on the *full* training set (known + background), so the background label will appear\n", + "in the vocabulary. We run a quick vocabulary fit to discover its index before training those\n", + "models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "find-bg-index", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "# The agnostophobia models are trained on the full dataset (including background).\n", + "# We need to know what index Ludwig will assign to \"background\" in *that* vocabulary.\n", + "# The simplest way is to train the entropic model first (or do a preprocessing run),\n", + "# but we can also use Ludwig's preprocessing API directly.\n", + "from ludwig.api import LudwigModel\n", + "\n", + "# Build a minimal config for preprocessing only\n", + "config_for_vocab = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [{\"name\": \"image_path\", \"type\": \"image\", \"encoder\": ENCODER}],\n", + " \"output_features\": [{\"name\": \"label\", \"type\": \"category\", \"loss\": {\"type\": \"softmax_cross_entropy\"}}],\n", + " \"trainer\": {\"epochs\": 1, \"batch_size\": 128},\n", + "}\n", + "\n", + "vocab_model = LudwigModel(config=config_for_vocab, logging_level=\"WARNING\")\n", + "_, _, output_dir_vocab = vocab_model.train(\n", + " dataset=train_df, # full training set including background\n", + " experiment_name=\"open_set_mnist\",\n", + " model_name=\"vocab_probe\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "\n", + "metadata_path = Path(output_dir_vocab) / \"model\" / \"training_set_metadata.json\"\n", + "with open(metadata_path) as f:\n", + " metadata = json.load(f)\n", + "\n", + "str2idx = metadata[\"label\"][\"str2idx\"]\n", + "BACKGROUND_CLASS = str2idx[\"background\"]\n", + "\n", + "print(\"label str2idx:\")\n", + "for k, v in sorted(str2idx.items(), key=lambda x: x[1]):\n", + " marker = \" <-- background\" if k == \"background\" else \"\"\n", + " print(f\" {v:3d} : {k!r}{marker}\")\n", + "\n", + "print(f\"\\nBACKGROUND_CLASS = {BACKGROUND_CLASS}\")" + ] + }, + { + "cell_type": "markdown", + "id": "entropic-header", + "metadata": {}, + "source": [ + "## Entropic Open-Set loss\n", + "\n", + "The Entropic Open-Set model is trained on the full dataset — known digits plus digits 8–9\n", + "labelled as `\"background\"`. For known samples the loss is standard cross-entropy. For\n", + "background samples the loss *maximises* Shannon entropy, pushing the softmax output toward\n", + "the uniform distribution.\n", + "\n", + "$$\n", + "\\mathcal{L} =\n", + " \\underbrace{-\\log p_y}_{\\text{CE on known}}\n", + " \\;+\\;\n", + " \\underbrace{\\sum_k p_k \\log p_k}_{\\text{neg-entropy on background}}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "entropic-train", + "metadata": {}, + "outputs": [], + "source": [ + "config_entropic = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [{\"name\": \"image_path\", \"type\": \"image\", \"encoder\": ENCODER}],\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"label\",\n", + " \"type\": \"category\",\n", + " \"loss\": {\n", + " \"type\": \"entropic_open_set\",\n", + " \"background_class\": BACKGROUND_CLASS,\n", + " },\n", + " }\n", + " ],\n", + " \"trainer\": {\"epochs\": 10, \"learning_rate\": 0.001, \"batch_size\": 128},\n", + "}\n", + "\n", + "print(f\"background_class = {BACKGROUND_CLASS}\")\n", + "print(yaml.dump(config_entropic, default_flow_style=False))\n", + "\n", + "model_entropic = LudwigModel(config=config_entropic, logging_level=\"WARNING\")\n", + "_, _, output_dir_entropic = model_entropic.train(\n", + " dataset=train_df,\n", + " experiment_name=\"open_set_mnist\",\n", + " model_name=\"entropic\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "print(f\"Entropic model saved to: {output_dir_entropic}\")" + ] + }, + { + "cell_type": "markdown", + "id": "objectosphere-header", + "metadata": {}, + "source": [ + "## Objectosphere loss\n", + "\n", + "The Objectosphere loss extends the Entropic Open-Set loss with a logit-norm objective:\n", + "\n", + "- **Known samples**: CE + hinge `max(0, ξ – ||z||)²` pushes logit L2 norms above ξ\n", + "- **Background samples**: entropy maximisation + `ζ||z||²` suppresses logit norms toward zero\n", + "\n", + "This creates a clear norm gap between known and unknown samples, enabling a simple threshold\n", + "detector at inference time.\n", + "\n", + "$$\n", + "\\mathcal{L} =\n", + " \\underbrace{\\text{CE}(z_{\\text{known}}) + \\max(0,\\,\\xi - \\|z\\|)^2}_{\\text{known}}\n", + " \\;+\\;\n", + " \\underbrace{\\sum_k p_k \\log p_k + \\zeta\\,\\|z\\|^2}_{\\text{background}}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "objectosphere-train", + "metadata": {}, + "outputs": [], + "source": [ + "config_objectosphere = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": [{\"name\": \"image_path\", \"type\": \"image\", \"encoder\": ENCODER}],\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"label\",\n", + " \"type\": \"category\",\n", + " \"loss\": {\n", + " \"type\": \"objectosphere\",\n", + " \"background_class\": BACKGROUND_CLASS,\n", + " \"xi\": 10.0,\n", + " \"zeta\": 0.1,\n", + " },\n", + " }\n", + " ],\n", + " \"trainer\": {\"epochs\": 10, \"learning_rate\": 0.001, \"batch_size\": 128},\n", + "}\n", + "\n", + "model_objectosphere = LudwigModel(config=config_objectosphere, logging_level=\"WARNING\")\n", + "_, _, output_dir_objectosphere = model_objectosphere.train(\n", + " dataset=train_df,\n", + " experiment_name=\"open_set_mnist\",\n", + " model_name=\"objectosphere\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "print(f\"Objectosphere model saved to: {output_dir_objectosphere}\")" + ] + }, + { + "cell_type": "markdown", + "id": "compare-header", + "metadata": {}, + "source": [ + "## Compare: confidence on known vs unknown\n", + "\n", + "We now predict on the test set with each model and collect the **maximum softmax probability**\n", + "for each sample. A well-calibrated open-set model should:\n", + "\n", + "- Produce **high** max-prob on known digits (it is confident)\n", + "- Produce **low** max-prob on unknown digits (8 and 9) — ideally near `1/num_classes`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "run-predictions", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# Separate test rows into known and unknown\n", + "test_known_df = test_df[~test_df[\"label\"].isin([\"8\", \"9\"])].copy()\n", + "test_unknown_df = test_df[test_df[\"label\"].isin([\"8\", \"9\"])].copy()\n", + "\n", + "print(f\"Test known: {len(test_known_df)} samples\")\n", + "print(f\"Test unknown: {len(test_unknown_df)} samples\")\n", + "\n", + "\n", + "def get_max_probs(model, df):\n", + " \"\"\"Return an array of max softmax probabilities for each row in df.\"\"\"\n", + " preds, _ = model.predict(dataset=df, skip_save_predictions=True)\n", + " return preds[\"label_probability\"].values\n", + "\n", + "\n", + "print(\"Predicting with baseline ...\")\n", + "probs_baseline_known = get_max_probs(model_baseline, test_known_df)\n", + "probs_baseline_unknown = get_max_probs(model_baseline, test_unknown_df)\n", + "\n", + "print(\"Predicting with entropic ...\")\n", + "probs_entropic_known = get_max_probs(model_entropic, test_known_df)\n", + "probs_entropic_unknown = get_max_probs(model_entropic, test_unknown_df)\n", + "\n", + "print(\"Predicting with objectosphere ...\")\n", + "probs_obj_known = get_max_probs(model_objectosphere, test_known_df)\n", + "probs_obj_unknown = get_max_probs(model_objectosphere, test_unknown_df)\n", + "\n", + "print(\"Done.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "plot-histograms", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)\n", + "bins = np.linspace(0, 1, 30)\n", + "\n", + "model_data = [\n", + " (\"CE Baseline\", probs_baseline_known, probs_baseline_unknown),\n", + " (\"Entropic Open-Set\", probs_entropic_known, probs_entropic_unknown),\n", + " (\"Objectosphere\", probs_obj_known, probs_obj_unknown),\n", + "]\n", + "\n", + "for ax, (title, known, unknown) in zip(axes, model_data):\n", + " ax.hist(known, bins=bins, alpha=0.6, color=\"steelblue\", label=f\"Known (0-7)\\nmean={known.mean():.3f}\")\n", + " ax.hist(unknown, bins=bins, alpha=0.6, color=\"orangered\", label=f\"Unknown (8-9)\\nmean={unknown.mean():.3f}\")\n", + " ax.set_title(title, fontsize=13)\n", + " ax.set_xlabel(\"Max softmax probability\")\n", + " ax.legend(fontsize=9)\n", + "\n", + "axes[0].set_ylabel(\"Count\")\n", + "fig.suptitle(\"Confidence on known vs unknown MNIST digits\", fontsize=14, y=1.02)\n", + "plt.tight_layout()\n", + "plt.savefig(\"confidence_histograms.png\", dpi=120, bbox_inches=\"tight\")\n", + "plt.show()\n", + "print(\"Saved confidence_histograms.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "threshold-header", + "metadata": {}, + "source": [ + "## Threshold-based detection\n", + "\n", + "We can turn max softmax probability into a binary known/unknown detector by choosing a threshold\n", + "on a held-out validation set. A sample with max-prob below the threshold is flagged as unknown.\n", + "\n", + "Below we sweep thresholds and plot the True Positive Rate (TPR — fraction of unknowns correctly\n", + "flagged) against the False Positive Rate (FPR — fraction of known samples incorrectly flagged)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "threshold-detection", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import roc_auc_score, roc_curve\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)\n", + "\n", + "model_data_thresh = [\n", + " (\"CE Baseline\", probs_baseline_known, probs_baseline_unknown),\n", + " (\"Entropic Open-Set\", probs_entropic_known, probs_entropic_unknown),\n", + " (\"Objectosphere\", probs_obj_known, probs_obj_unknown),\n", + "]\n", + "\n", + "for ax, (title, known, unknown) in zip(axes, model_data_thresh):\n", + " # Label: 0 = known, 1 = unknown; detector score = 1 - max_prob\n", + " y_true = np.concatenate([np.zeros(len(known)), np.ones(len(unknown))])\n", + " y_score = np.concatenate([1 - known, 1 - unknown])\n", + "\n", + " fpr, tpr, thresholds = roc_curve(y_true, y_score)\n", + " auc = roc_auc_score(y_true, y_score)\n", + "\n", + " ax.plot(fpr, tpr, lw=2, label=f\"AUC = {auc:.3f}\")\n", + " ax.plot([0, 1], [0, 1], \"--\", color=\"gray\", lw=1)\n", + " ax.set_title(title, fontsize=13)\n", + " ax.set_xlabel(\"False Positive Rate\")\n", + " ax.legend(fontsize=10)\n", + "\n", + "axes[0].set_ylabel(\"True Positive Rate\")\n", + "fig.suptitle(\"ROC curve for unknown detection (1 - max_prob threshold)\", fontsize=14, y=1.02)\n", + "plt.tight_layout()\n", + "plt.savefig(\"roc_curves.png\", dpi=120, bbox_inches=\"tight\")\n", + "plt.show()\n", + "print(\"Saved roc_curves.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "summary-header", + "metadata": {}, + "source": [ + "## Summary table\n", + "\n", + "The table below summarises the mean maximum softmax probability on known vs unknown test samples.\n", + "A lower mean max-prob on unknowns indicates better open-set recognition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "summary-table", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.metrics import roc_auc_score\n", + "\n", + "rows = [\n", + " (\"CE Baseline\", probs_baseline_known, probs_baseline_unknown),\n", + " (\"Entropic Open-Set\", probs_entropic_known, probs_entropic_unknown),\n", + " (\"Objectosphere\", probs_obj_known, probs_obj_unknown),\n", + "]\n", + "\n", + "header = f\"{'Model':<22} | {'Mean max-prob (known)':>21} | {'Mean max-prob (unknown)':>23} | {'AUC (unknown det.)':>18}\"\n", + "sep = \"-\" * len(header)\n", + "print(sep)\n", + "print(header)\n", + "print(sep)\n", + "\n", + "for name, known, unknown in rows:\n", + " y_true = np.concatenate([np.zeros(len(known)), np.ones(len(unknown))])\n", + " y_score = np.concatenate([1 - known, 1 - unknown])\n", + " auc = roc_auc_score(y_true, y_score)\n", + " print(f\"{name:<22} | {known.mean():>21.4f} | {unknown.mean():>23.4f} | {auc:>18.4f}\")\n", + "\n", + "print(sep)\n", + "print()\n", + "print(\"Interpretation:\")\n", + "print(\" - CE Baseline: high confidence on unknowns — the model cannot say 'I don't know'\")\n", + "print(\" - Entropic Open-Set: lower mean max-prob on unknowns — closer to uniform distribution\")\n", + "print(\" - Objectosphere: similar to entropic; also creates a logit-norm gap (not shown here)\")" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/open_set_recognition/train_open_set.py b/examples/open_set_recognition/train_open_set.py new file mode 100644 index 0000000..84f8536 --- /dev/null +++ b/examples/open_set_recognition/train_open_set.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +"""Reproduce Dhamija et al. NeurIPS 2018 "Reducing Network Agnostophobia". + +Trains three classifiers on a synthetic Gaussian dataset: + - CE Baseline (SoftmaxCrossEntropy) + - Entropic Open-Set Loss + - Objectosphere Loss + +then reports the mean max-probability on held-out background/unknown +samples. Lower max-prob = better open-set recognition. + +Usage: + python train_open_set.py +""" + +import torch +import torch.nn as nn +import torch.nn.functional as F + +EPSILON = 1e-10 + + +# --------------------------------------------------------------------------- +# Synthetic data +# --------------------------------------------------------------------------- + + +def make_dataset(seed: int = 42): + """Four known Gaussian clusters + two unknown clusters in 2D.""" + rng = torch.Generator() + rng.manual_seed(seed) + + known_centers = torch.tensor([[3.0, 3.0], [-3.0, 3.0], [-3.0, -3.0], [3.0, -3.0]]) + unknown_centers = torch.tensor([[0.0, 0.0], [6.0, 0.0]]) + std = 0.8 + + n_known = 200 # per class + n_unknown = 100 # per class (background) + + known_x, known_y = [], [] + for cls_idx, center in enumerate(known_centers): + pts = center + std * torch.randn(n_known, 2, generator=rng) + known_x.append(pts) + known_y.append(torch.full((n_known,), cls_idx, dtype=torch.long)) + + unknown_x = [] + for center in unknown_centers: + pts = center + std * torch.randn(n_unknown, 2, generator=rng) + unknown_x.append(pts) + + # Background class index = 4 + BG = 4 + unknown_y_bg = torch.full((n_unknown * 2,), BG, dtype=torch.long) + + X_known = torch.cat(known_x) + y_known = torch.cat(known_y) + X_unknown = torch.cat(unknown_x) + + # Training set: all known + all unknown (for agnostophobia models) + X_train = torch.cat([X_known, X_unknown]) + y_train = torch.cat([y_known, unknown_y_bg]) + + # Test set: fresh samples from known and unknown distributions + test_known_x, test_known_y = [], [] + for cls_idx, center in enumerate(known_centers): + pts = center + std * torch.randn(50, 2, generator=rng) + test_known_x.append(pts) + test_known_y.append(torch.full((50,), cls_idx, dtype=torch.long)) + + test_unknown_x = [] + for center in unknown_centers: + pts = center + std * torch.randn(50, 2, generator=rng) + test_unknown_x.append(pts) + + X_test_known = torch.cat(test_known_x) + y_test_known = torch.cat(test_known_y) + X_test_unknown = torch.cat(test_unknown_x) + + return X_train, y_train, X_test_known, y_test_known, X_test_unknown, BG + + +# --------------------------------------------------------------------------- +# Model +# --------------------------------------------------------------------------- + + +class MLP(nn.Module): + def __init__(self, in_dim: int = 2, hidden: int = 64, n_classes: int = 5): + super().__init__() + self.net = nn.Sequential( + nn.Linear(in_dim, hidden), + nn.ReLU(), + nn.Linear(hidden, hidden), + nn.ReLU(), + nn.Linear(hidden, n_classes), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.net(x) + + +# --------------------------------------------------------------------------- +# Loss functions (mirrors the Ludwig module implementations) +# --------------------------------------------------------------------------- + + +class EntropicOpenSetLoss(nn.Module): + def __init__(self, background_class: int): + super().__init__() + self.bg = background_class + + def forward(self, logits: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + known_mask = target != self.bg + unknown_mask = ~known_mask + loss = logits.new_tensor(0.0) + if known_mask.any(): + loss = loss + F.cross_entropy(logits[known_mask], target[known_mask]) + if unknown_mask.any(): + probs = torch.softmax(logits[unknown_mask], dim=-1) + loss = loss + (probs * torch.log(probs + EPSILON)).sum(dim=-1).mean() + return loss + + +class ObjectosphereLoss(nn.Module): + def __init__(self, background_class: int, xi: float = 10.0, zeta: float = 0.1): + super().__init__() + self.bg = background_class + self.xi = xi + self.zeta = zeta + + def forward(self, logits: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + known_mask = target != self.bg + unknown_mask = ~known_mask + loss = logits.new_tensor(0.0) + if known_mask.any(): + kl = logits[known_mask] + ce = F.cross_entropy(kl, target[known_mask]) + hinge = torch.clamp(self.xi - kl.norm(dim=-1), min=0.0).pow(2).mean() + loss = loss + ce + hinge + if unknown_mask.any(): + ul = logits[unknown_mask] + probs = torch.softmax(ul, dim=-1) + neg_entropy = (probs * torch.log(probs + EPSILON)).sum(dim=-1).mean() + loss = loss + neg_entropy + self.zeta * ul.norm(dim=-1).pow(2).mean() + return loss + + +# --------------------------------------------------------------------------- +# Training loop +# --------------------------------------------------------------------------- + + +def train( + model: nn.Module, loss_fn: nn.Module, X: torch.Tensor, y: torch.Tensor, epochs: int = 200, lr: float = 1e-3 +) -> None: + optimizer = torch.optim.Adam(model.parameters(), lr=lr) + for epoch in range(epochs): + model.train() + optimizer.zero_grad() + logits = model(X) + loss = loss_fn(logits, y) + loss.backward() + optimizer.step() + + +# --------------------------------------------------------------------------- +# Evaluation helpers +# --------------------------------------------------------------------------- + + +@torch.no_grad() +def mean_max_prob(model: nn.Module, X: torch.Tensor) -> float: + model.eval() + logits = model(X) + return torch.softmax(logits, dim=-1).max(dim=-1).values.mean().item() + + +@torch.no_grad() +def mean_norm(model: nn.Module, X: torch.Tensor) -> float: + model.eval() + return model(X).norm(dim=-1).mean().item() + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + torch.manual_seed(0) + X_train, y_train, X_test_known, y_test_known, X_test_unknown, BG = make_dataset() + + # Baseline: train only on known samples with standard CE + X_known_train = X_train[y_train != BG] + y_known_train = y_train[y_train != BG] + + configs = [ + ( + "CE Baseline", + MLP(n_classes=4), # no background class in output + nn.CrossEntropyLoss(), + X_known_train, + y_known_train, + ), + ( + "Entropic Open-Set", + MLP(n_classes=5), # includes background class output node + EntropicOpenSetLoss(background_class=BG), + X_train, + y_train, + ), + ( + "Objectosphere", + MLP(n_classes=5), + ObjectosphereLoss(background_class=BG, xi=10.0, zeta=0.1), + X_train, + y_train, + ), + ] + + results = [] + for name, model, loss_fn, X, y in configs: + torch.manual_seed(0) + train(model, loss_fn, X, y, epochs=300) + mmp_known = mean_max_prob(model, X_test_known) + mmp_unknown = mean_max_prob(model, X_test_unknown) + norm_known = mean_norm(model, X_test_known) + norm_unknown = mean_norm(model, X_test_unknown) + results.append((name, mmp_known, mmp_unknown, norm_known, norm_unknown)) + + cols = ("Model", "Max-prob (known)", "Max-prob (unknown)", "Norm known", "Norm unknown") + header = f"{cols[0]:<22} | {cols[1]:>16} | {cols[2]:>18} | {cols[3]:>10} | {cols[4]:>12}" + sep = "-" * len(header) + print(sep) + print(header) + print(sep) + for name, mpk, mpu, nk, nu in results: + print(f"{name:<22} | {mpk:>16.3f} | {mpu:>18.3f} | {nk:>10.3f} | {nu:>12.3f}") + print(sep) + print() + print("Expected behaviour from the paper:") + print(" - CE Baseline: high max-prob on unknowns (≈0.7-0.9)") + print(" - Entropic Open-Set: lower max-prob on unknowns (≈0.2-0.3, near uniform)") + print(" - Objectosphere: similar to entropic, plus norm(known) >> norm(unknown)") + + # --- Assertions so this script can double as a smoke test --- + ce_unknown = results[0][2] + eos_unknown = results[1][2] + obj_unknown = results[2][2] + + assert eos_unknown < ce_unknown, ( + f"Entropic loss should reduce unknown confidence: {eos_unknown:.3f} < {ce_unknown:.3f}" + ) + assert obj_unknown < ce_unknown, ( + f"Objectosphere loss should reduce unknown confidence: {obj_unknown:.3f} < {ce_unknown:.3f}" + ) + + obj_norm_known = results[2][3] + obj_norm_unknown = results[2][4] + assert obj_norm_known > obj_norm_unknown * 1.5, ( + f"Objectosphere should create norm gap: known={obj_norm_known:.3f} unknown={obj_norm_unknown:.3f}" + ) + + print("\nAll assertions passed.") + + +if __name__ == "__main__": + main() diff --git a/examples/optimizers/README.md b/examples/optimizers/README.md new file mode 100644 index 0000000..ccebc3c --- /dev/null +++ b/examples/optimizers/README.md @@ -0,0 +1,75 @@ +# Optimizer Comparison: Schedule-Free, Muon, Adafactor, and More + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/optimizers/optimizer_comparison.ipynb) + +## Why optimizer choice matters + +The optimizer is more than a training detail — it controls how fast gradients +are translated into weight updates, whether training is stable in early epochs, +how much memory the optimizer state consumes, and whether you need to tune a +separate learning-rate schedule at all. + +Ludwig 0.11 added five production-ready optimizers beyond the classic Adam/SGD +family: **RAdam**, **Adafactor**, **Schedule-Free AdamW**, **Muon**, and **SOAP**. +This example shows how to configure each one and compares them on a real dataset. + +## What this example shows + +- How to set `trainer.optimizer.type` in a Ludwig YAML config +- The one rule for Schedule-Free AdamW: no `learning_rate_scheduler` +- Side-by-side training curves (validation loss + accuracy) for all optimizers +- A summary table of final metrics and wall-clock training time + +## Prerequisites + +```bash +pip install ludwig +``` + +No GPU required. The notebook runs on CPU in a few minutes. + +## Quick start + +### Run the notebook (recommended) + +Open [`optimizer_comparison.ipynb`](optimizer_comparison.ipynb) in Jupyter or +click the Colab badge above. + +### Run the script + +```bash +python optimizer_comparison.py +``` + +This downloads the UCI Wine Quality dataset, trains all five configs, and +prints a comparison table. + +### Use a standalone YAML config + +Each optimizer has its own config file you can use directly with the Ludwig CLI: + +```bash +ludwig train --config config_schedule_free_adamw.yaml --dataset winequality-red.csv +``` + +| File | Optimizer | +| --------------------------------- | ------------------- | +| `config_adamw.yaml` | AdamW (baseline) | +| `config_radam.yaml` | RAdam | +| `config_adafactor.yaml` | Adafactor | +| `config_schedule_free_adamw.yaml` | Schedule-Free AdamW | +| `config_muon.yaml` | Muon | + +## Key insight: Schedule-Free AdamW needs no LR scheduler + +```yaml +trainer: + optimizer: + type: schedule_free_adamw + lr: 0.001 + # Do NOT add learning_rate_scheduler here +``` + +Adding a `learning_rate_scheduler` on top of `schedule_free_adamw` fights the +built-in schedule and hurts convergence. See the notebook for a detailed +explanation. diff --git a/examples/optimizers/config_adafactor.yaml b/examples/optimizers/config_adafactor.yaml new file mode 100644 index 0000000..a3ed0e5 --- /dev/null +++ b/examples/optimizers/config_adafactor.yaml @@ -0,0 +1,33 @@ +model_type: ecd +input_features: + - name: fixed_acidity + type: number + - name: volatile_acidity + type: number + - name: citric_acid + type: number + - name: residual_sugar + type: number + - name: chlorides + type: number + - name: free_sulfur_dioxide + type: number + - name: total_sulfur_dioxide + type: number + - name: density + type: number + - name: pH + type: number + - name: sulphates + type: number + - name: alcohol + type: number +output_features: + - name: quality + type: binary +trainer: + epochs: 30 + optimizer: + type: adafactor + lr: 0.001 + # Adafactor uses internal second-moment approximation — no external LR scheduler needed diff --git a/examples/optimizers/config_adamw.yaml b/examples/optimizers/config_adamw.yaml new file mode 100644 index 0000000..c458c56 --- /dev/null +++ b/examples/optimizers/config_adamw.yaml @@ -0,0 +1,34 @@ +model_type: ecd +input_features: + - name: fixed_acidity + type: number + - name: volatile_acidity + type: number + - name: citric_acid + type: number + - name: residual_sugar + type: number + - name: chlorides + type: number + - name: free_sulfur_dioxide + type: number + - name: total_sulfur_dioxide + type: number + - name: density + type: number + - name: pH + type: number + - name: sulphates + type: number + - name: alcohol + type: number +output_features: + - name: quality + type: binary +trainer: + epochs: 30 + optimizer: + type: adamw + lr: 0.001 + learning_rate_scheduler: + type: cosine diff --git a/examples/optimizers/config_muon.yaml b/examples/optimizers/config_muon.yaml new file mode 100644 index 0000000..24af775 --- /dev/null +++ b/examples/optimizers/config_muon.yaml @@ -0,0 +1,34 @@ +model_type: ecd +input_features: + - name: fixed_acidity + type: number + - name: volatile_acidity + type: number + - name: citric_acid + type: number + - name: residual_sugar + type: number + - name: chlorides + type: number + - name: free_sulfur_dioxide + type: number + - name: total_sulfur_dioxide + type: number + - name: density + type: number + - name: pH + type: number + - name: sulphates + type: number + - name: alcohol + type: number +output_features: + - name: quality + type: binary +trainer: + epochs: 30 + optimizer: + type: muon + lr: 0.001 + learning_rate_scheduler: + type: cosine diff --git a/examples/optimizers/config_radam.yaml b/examples/optimizers/config_radam.yaml new file mode 100644 index 0000000..d7afb29 --- /dev/null +++ b/examples/optimizers/config_radam.yaml @@ -0,0 +1,34 @@ +model_type: ecd +input_features: + - name: fixed_acidity + type: number + - name: volatile_acidity + type: number + - name: citric_acid + type: number + - name: residual_sugar + type: number + - name: chlorides + type: number + - name: free_sulfur_dioxide + type: number + - name: total_sulfur_dioxide + type: number + - name: density + type: number + - name: pH + type: number + - name: sulphates + type: number + - name: alcohol + type: number +output_features: + - name: quality + type: binary +trainer: + epochs: 30 + optimizer: + type: radam + lr: 0.001 + learning_rate_scheduler: + type: cosine diff --git a/examples/optimizers/config_schedule_free_adamw.yaml b/examples/optimizers/config_schedule_free_adamw.yaml new file mode 100644 index 0000000..500e8a2 --- /dev/null +++ b/examples/optimizers/config_schedule_free_adamw.yaml @@ -0,0 +1,34 @@ +model_type: ecd +input_features: + - name: fixed_acidity + type: number + - name: volatile_acidity + type: number + - name: citric_acid + type: number + - name: residual_sugar + type: number + - name: chlorides + type: number + - name: free_sulfur_dioxide + type: number + - name: total_sulfur_dioxide + type: number + - name: density + type: number + - name: pH + type: number + - name: sulphates + type: number + - name: alcohol + type: number +output_features: + - name: quality + type: binary +trainer: + epochs: 30 + optimizer: + type: schedule_free_adamw + lr: 0.001 + # Do NOT add a learning_rate_scheduler here — Schedule-Free AdamW + # eliminates the need for a separate LR schedule by design. diff --git a/examples/optimizers/optimizer_comparison.ipynb b/examples/optimizers/optimizer_comparison.ipynb new file mode 100644 index 0000000..f4d0d70 --- /dev/null +++ b/examples/optimizers/optimizer_comparison.ipynb @@ -0,0 +1,583 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "title-cell", + "metadata": {}, + "source": [ + "# Optimizer Comparison: Schedule-Free, Muon, Adafactor, and More\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/optimizers/optimizer_comparison.ipynb)\n", + "\n", + "Optimizer choice is one of the most impactful but often overlooked hyperparameters in deep learning.\n", + "This notebook benchmarks five optimizers available in Ludwig on the UCI Wine Quality dataset:\n", + "\n", + "| Optimizer | Key idea |\n", + "|---|---|\n", + "| **AdamW** (baseline) | Adam with decoupled weight decay — the standard choice |\n", + "| **RAdam** | Rectified Adam — warm-up-free, stable at the start of training |\n", + "| **Adafactor** | No second-moment buffer — large models, small memory footprint |\n", + "| **Schedule-Free AdamW** | No LR scheduler needed — the optimizer is the schedule |\n", + "| **Muon** | Newton-Schulz orthogonalisation of weight-matrix gradients |\n", + "\n", + "The task is binary classification: predict whether a wine has quality >= 7.\n", + "Everything runs on CPU and completes in a few minutes." + ] + }, + { + "cell_type": "markdown", + "id": "install-header", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "install-cell", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig scikit-learn --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "imports-cell", + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile\n", + "import time\n", + "import warnings\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.ticker as ticker\n", + "import pandas as pd\n", + "\n", + "from ludwig.api import LudwigModel" + ] + }, + { + "cell_type": "markdown", + "id": "dataset-header", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "We use the **UCI Wine Quality** dataset (red wines). \n", + "The target is binarised: `quality >= 7` → True, otherwise False." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dataset-cell", + "metadata": {}, + "outputs": [], + "source": [ + "DATA_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\"\n", + "\n", + "df = pd.read_csv(DATA_URL, sep=\";\")\n", + "df.columns = [c.strip().replace(\" \", \"_\") for c in df.columns]\n", + "df[\"quality\"] = (df[\"quality\"] >= 7).astype(str) # binary: \"True\" / \"False\"\n", + "\n", + "print(f\"Shape: {df.shape}\")\n", + "print(f\"Class distribution:\\n{df['quality'].value_counts()}\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "shared-config-header", + "metadata": {}, + "source": [ + "## Shared configuration\n", + "\n", + "All models share the same architecture and 30 training epochs.\n", + "Only the `optimizer` block (and `learning_rate_scheduler`) differs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "shared-config-cell", + "metadata": {}, + "outputs": [], + "source": [ + "FEATURE_NAMES = [\n", + " \"fixed_acidity\",\n", + " \"volatile_acidity\",\n", + " \"citric_acid\",\n", + " \"residual_sugar\",\n", + " \"chlorides\",\n", + " \"free_sulfur_dioxide\",\n", + " \"total_sulfur_dioxide\",\n", + " \"density\",\n", + " \"pH\",\n", + " \"sulphates\",\n", + " \"alcohol\",\n", + "]\n", + "\n", + "INPUT_FEATURES = [{\"name\": n, \"type\": \"number\"} for n in FEATURE_NAMES]\n", + "OUTPUT_FEATURES = [{\"name\": \"quality\", \"type\": \"binary\"}]\n", + "\n", + "BASE_TRAINER = {\n", + " \"epochs\": 30,\n", + "}\n", + "\n", + "# Storage for training curves and summary metrics\n", + "all_results = {}\n", + "summary = []" + ] + }, + { + "cell_type": "markdown", + "id": "adamw-header", + "metadata": {}, + "source": [ + "## Optimizer 1 — AdamW (baseline)\n", + "\n", + "AdamW adds **decoupled weight decay** to Adam. \n", + "It is the default optimizer in Ludwig and a solid baseline for most tasks.\n", + "\n", + "```yaml\n", + "trainer:\n", + " epochs: 30\n", + " optimizer:\n", + " type: adamw\n", + " lr: 0.001\n", + " learning_rate_scheduler:\n", + " type: cosine\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "adamw-cell", + "metadata": {}, + "outputs": [], + "source": [ + "config_adamw = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": INPUT_FEATURES,\n", + " \"output_features\": OUTPUT_FEATURES,\n", + " \"trainer\": {\n", + " **BASE_TRAINER,\n", + " \"optimizer\": {\"type\": \"adamw\", \"lr\": 0.001},\n", + " \"learning_rate_scheduler\": {\"type\": \"cosine\"},\n", + " },\n", + "}\n", + "\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " model = LudwigModel(config_adamw, logging_level=30)\n", + " t0 = time.time()\n", + " train_stats, _, _ = model.train(\n", + " dataset=df,\n", + " output_directory=tmpdir,\n", + " skip_save_model=True,\n", + " skip_save_progress=True,\n", + " skip_save_log=True,\n", + " )\n", + " elapsed_adamw = time.time() - t0\n", + "\n", + "all_results[\"adamw\"] = train_stats.validation\n", + "summary.append(\n", + " {\n", + " \"optimizer\": \"adamw\",\n", + " \"final_val_loss\": train_stats.validation[\"quality\"][\"loss\"][-1],\n", + " \"final_val_accuracy\": train_stats.validation[\"quality\"][\"accuracy\"][-1],\n", + " \"training_time_s\": round(elapsed_adamw, 1),\n", + " }\n", + ")\n", + "print(f\"AdamW done in {elapsed_adamw:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "radam-header", + "metadata": {}, + "source": [ + "## Optimizer 2 — RAdam\n", + "\n", + "**Rectified Adam** (Liu et al., 2019) automatically warms up the adaptive learning rate\n", + "by rectifying the variance of the second moment. \n", + "Unlike AdamW, it does not need a manual warm-up schedule.\n", + "\n", + "```yaml\n", + "trainer:\n", + " epochs: 30\n", + " optimizer:\n", + " type: radam\n", + " lr: 0.001\n", + " learning_rate_scheduler:\n", + " type: cosine\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "radam-cell", + "metadata": {}, + "outputs": [], + "source": [ + "config_radam = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": INPUT_FEATURES,\n", + " \"output_features\": OUTPUT_FEATURES,\n", + " \"trainer\": {\n", + " **BASE_TRAINER,\n", + " \"optimizer\": {\"type\": \"radam\", \"lr\": 0.001},\n", + " \"learning_rate_scheduler\": {\"type\": \"cosine\"},\n", + " },\n", + "}\n", + "\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " model = LudwigModel(config_radam, logging_level=30)\n", + " t0 = time.time()\n", + " train_stats, _, _ = model.train(\n", + " dataset=df,\n", + " output_directory=tmpdir,\n", + " skip_save_model=True,\n", + " skip_save_progress=True,\n", + " skip_save_log=True,\n", + " )\n", + " elapsed_radam = time.time() - t0\n", + "\n", + "all_results[\"radam\"] = train_stats.validation\n", + "summary.append(\n", + " {\n", + " \"optimizer\": \"radam\",\n", + " \"final_val_loss\": train_stats.validation[\"quality\"][\"loss\"][-1],\n", + " \"final_val_accuracy\": train_stats.validation[\"quality\"][\"accuracy\"][-1],\n", + " \"training_time_s\": round(elapsed_radam, 1),\n", + " }\n", + ")\n", + "print(f\"RAdam done in {elapsed_radam:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "adafactor-header", + "metadata": {}, + "source": [ + "## Optimizer 3 — Adafactor\n", + "\n", + "**Adafactor** (Shazeer & Stern, 2018) avoids storing a full second-moment matrix\n", + "by using a factored approximation. \n", + "This makes it memory-efficient for large models (e.g., T5 was trained with Adafactor).\n", + "It includes internal step-size scaling, so an external LR scheduler is optional.\n", + "\n", + "```yaml\n", + "trainer:\n", + " epochs: 30\n", + " optimizer:\n", + " type: adafactor\n", + " lr: 0.001\n", + " # no learning_rate_scheduler needed\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "adafactor-cell", + "metadata": {}, + "outputs": [], + "source": [ + "config_adafactor = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": INPUT_FEATURES,\n", + " \"output_features\": OUTPUT_FEATURES,\n", + " \"trainer\": {\n", + " **BASE_TRAINER,\n", + " \"optimizer\": {\"type\": \"adafactor\", \"lr\": 0.001},\n", + " # No learning_rate_scheduler — Adafactor handles step-size internally\n", + " },\n", + "}\n", + "\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " model = LudwigModel(config_adafactor, logging_level=30)\n", + " t0 = time.time()\n", + " train_stats, _, _ = model.train(\n", + " dataset=df,\n", + " output_directory=tmpdir,\n", + " skip_save_model=True,\n", + " skip_save_progress=True,\n", + " skip_save_log=True,\n", + " )\n", + " elapsed_adafactor = time.time() - t0\n", + "\n", + "all_results[\"adafactor\"] = train_stats.validation\n", + "summary.append(\n", + " {\n", + " \"optimizer\": \"adafactor\",\n", + " \"final_val_loss\": train_stats.validation[\"quality\"][\"loss\"][-1],\n", + " \"final_val_accuracy\": train_stats.validation[\"quality\"][\"accuracy\"][-1],\n", + " \"training_time_s\": round(elapsed_adafactor, 1),\n", + " }\n", + ")\n", + "print(f\"Adafactor done in {elapsed_adafactor:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "sfa-header", + "metadata": {}, + "source": [ + "## Optimizer 4 — Schedule-Free AdamW\n", + "\n", + "**Schedule-Free AdamW** (Defazio et al., 2024) eliminates the need for a separate\n", + "learning-rate schedule by building the schedule into the optimizer itself via\n", + "a momentum-based interpolation between two internal sequences.\n", + "\n", + "> **Important:** do _not_ add a `learning_rate_scheduler` block when using this optimizer.\n", + "> Adding one would counteract the built-in schedule and harm convergence.\n", + "\n", + "```yaml\n", + "trainer:\n", + " epochs: 30\n", + " optimizer:\n", + " type: schedule_free_adamw\n", + " lr: 0.001\n", + " # No learning_rate_scheduler — this is the whole point!\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "sfa-cell", + "metadata": {}, + "outputs": [], + "source": [ + "config_sfa = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": INPUT_FEATURES,\n", + " \"output_features\": OUTPUT_FEATURES,\n", + " \"trainer\": {\n", + " **BASE_TRAINER,\n", + " \"optimizer\": {\"type\": \"schedule_free_adamw\", \"lr\": 0.001},\n", + " # No learning_rate_scheduler — Schedule-Free AdamW handles it internally\n", + " },\n", + "}\n", + "\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " model = LudwigModel(config_sfa, logging_level=30)\n", + " t0 = time.time()\n", + " train_stats, _, _ = model.train(\n", + " dataset=df,\n", + " output_directory=tmpdir,\n", + " skip_save_model=True,\n", + " skip_save_progress=True,\n", + " skip_save_log=True,\n", + " )\n", + " elapsed_sfa = time.time() - t0\n", + "\n", + "all_results[\"schedule_free_adamw\"] = train_stats.validation\n", + "summary.append(\n", + " {\n", + " \"optimizer\": \"schedule_free_adamw\",\n", + " \"final_val_loss\": train_stats.validation[\"quality\"][\"loss\"][-1],\n", + " \"final_val_accuracy\": train_stats.validation[\"quality\"][\"accuracy\"][-1],\n", + " \"training_time_s\": round(elapsed_sfa, 1),\n", + " }\n", + ")\n", + "print(f\"Schedule-Free AdamW done in {elapsed_sfa:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "muon-header", + "metadata": {}, + "source": [ + "## Optimizer 5 — Muon\n", + "\n", + "**Muon** (Kosson et al.) applies Nesterov momentum updates orthogonalised via\n", + "Newton-Schulz iterations to weight matrices, while using AdamW for embeddings\n", + "and other non-matrix parameters. \n", + "The orthogonalisation acts as a preconditioner with minimal overhead.\n", + "\n", + "```yaml\n", + "trainer:\n", + " epochs: 30\n", + " optimizer:\n", + " type: muon\n", + " lr: 0.001\n", + " learning_rate_scheduler:\n", + " type: cosine\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "muon-cell", + "metadata": {}, + "outputs": [], + "source": [ + "config_muon = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": INPUT_FEATURES,\n", + " \"output_features\": OUTPUT_FEATURES,\n", + " \"trainer\": {\n", + " **BASE_TRAINER,\n", + " \"optimizer\": {\"type\": \"muon\", \"lr\": 0.001},\n", + " \"learning_rate_scheduler\": {\"type\": \"cosine\"},\n", + " },\n", + "}\n", + "\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " model = LudwigModel(config_muon, logging_level=30)\n", + " t0 = time.time()\n", + " train_stats, _, _ = model.train(\n", + " dataset=df,\n", + " output_directory=tmpdir,\n", + " skip_save_model=True,\n", + " skip_save_progress=True,\n", + " skip_save_log=True,\n", + " )\n", + " elapsed_muon = time.time() - t0\n", + "\n", + "all_results[\"muon\"] = train_stats.validation\n", + "summary.append(\n", + " {\n", + " \"optimizer\": \"muon\",\n", + " \"final_val_loss\": train_stats.validation[\"quality\"][\"loss\"][-1],\n", + " \"final_val_accuracy\": train_stats.validation[\"quality\"][\"accuracy\"][-1],\n", + " \"training_time_s\": round(elapsed_muon, 1),\n", + " }\n", + ")\n", + "print(f\"Muon done in {elapsed_muon:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "id": "results-header", + "metadata": {}, + "source": [ + "## Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "results-table-cell", + "metadata": {}, + "outputs": [], + "source": [ + "results_df = pd.DataFrame(summary)\n", + "results_df = results_df.set_index(\"optimizer\")\n", + "results_df[\"final_val_loss\"] = results_df[\"final_val_loss\"].round(4)\n", + "results_df[\"final_val_accuracy\"] = results_df[\"final_val_accuracy\"].round(4)\n", + "print(results_df.to_string())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "results-plot-cell", + "metadata": {}, + "outputs": [], + "source": [ + "COLORS = {\n", + " \"adamw\": \"#1f77b4\",\n", + " \"radam\": \"#ff7f0e\",\n", + " \"adafactor\": \"#2ca02c\",\n", + " \"schedule_free_adamw\": \"#d62728\",\n", + " \"muon\": \"#9467bd\",\n", + "}\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(13, 4))\n", + "fig.suptitle(\"Optimizer Comparison — Wine Quality (30 epochs, CPU)\", fontsize=13)\n", + "\n", + "for opt_name, val_stats in all_results.items():\n", + " losses = val_stats[\"quality\"][\"loss\"]\n", + " accs = val_stats[\"quality\"][\"accuracy\"]\n", + " epochs = range(1, len(losses) + 1)\n", + " color = COLORS.get(opt_name)\n", + " axes[0].plot(epochs, losses, label=opt_name, color=color)\n", + " axes[1].plot(epochs, accs, label=opt_name, color=color)\n", + "\n", + "axes[0].set_title(\"Validation Loss\")\n", + "axes[0].set_xlabel(\"Epoch\")\n", + "axes[0].set_ylabel(\"Loss\")\n", + "axes[0].legend(fontsize=8)\n", + "axes[0].xaxis.set_major_locator(ticker.MaxNLocator(integer=True))\n", + "\n", + "axes[1].set_title(\"Validation Accuracy\")\n", + "axes[1].set_xlabel(\"Epoch\")\n", + "axes[1].set_ylabel(\"Accuracy\")\n", + "axes[1].legend(fontsize=8)\n", + "axes[1].xaxis.set_major_locator(ticker.MaxNLocator(integer=True))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(\"optimizer_comparison.png\", dpi=120, bbox_inches=\"tight\")\n", + "plt.show()\n", + "print(\"Figure saved to optimizer_comparison.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "when-to-use-header", + "metadata": {}, + "source": [ + "## When to use each optimizer\n", + "\n", + "| Optimizer | Best for | Watch out for |\n", + "|---|---|---|\n", + "| **AdamW** | General-purpose default; well-understood | Needs a good LR schedule and warm-up |\n", + "| **RAdam** | Short runs or when warm-up tuning is painful | Slightly more compute per step |\n", + "| **Adafactor** | Large models (LLMs, T5-scale); memory-constrained training | Can be slower to converge on small models |\n", + "| **Schedule-Free AdamW** | When you want to skip LR schedule tuning entirely | Must not add a `learning_rate_scheduler`; needs correct `warmup_steps` |\n", + "| **Muon** | Training deep networks with many weight matrices | Falls back to AdamW for embeddings/biases; newer, less battle-tested |\n", + "| **SOAP** | When you want Shampoo-quality second-order convergence with Adam memory | Higher per-step cost than Adam |" + ] + }, + { + "cell_type": "markdown", + "id": "sfa-tip-header", + "metadata": {}, + "source": [ + "## Schedule-Free AdamW — important tip\n", + "\n", + "> **Do not add `learning_rate_scheduler` when using `schedule_free_adamw`.**\n", + ">\n", + "> Schedule-Free AdamW internalises the learning-rate schedule via a\n", + "> momentum-based averaging trick. Adding an external cosine or step-decay\n", + "> scheduler on top fights the internal schedule and typically hurts final\n", + "> accuracy.\n", + ">\n", + "> Correct config:\n", + "> ```yaml\n", + "> trainer:\n", + "> optimizer:\n", + "> type: schedule_free_adamw\n", + "> lr: 0.001\n", + "> ```\n", + ">\n", + "> You may still set `warmup_steps` inside the optimizer block to give the\n", + "> internal schedule a short ramp-up." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/optimizers/optimizer_comparison.py b/examples/optimizers/optimizer_comparison.py new file mode 100644 index 0000000..557bc41 --- /dev/null +++ b/examples/optimizers/optimizer_comparison.py @@ -0,0 +1,153 @@ +# Colab: !pip install ludwig scikit-learn --quiet +""" +Optimizer Comparison on Wine Quality +===================================== +Compares AdamW (baseline), RAdam, Adafactor, Schedule-Free AdamW, and Muon +on a binary classification task (wine quality >= 7). + +Usage: + python optimizer_comparison.py +""" + +import tempfile +import time + +import pandas as pd + +# --------------------------------------------------------------------------- +# 1. Load and prepare data +# --------------------------------------------------------------------------- + +DATA_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv" + +print("Downloading wine quality data...") +df = pd.read_csv(DATA_URL, sep=";") +df.columns = [c.strip().replace(" ", "_") for c in df.columns] +df["quality"] = (df["quality"] >= 7).astype(str) # True/False binary target +print(f"Dataset shape: {df.shape}") + +# --------------------------------------------------------------------------- +# 2. Optimizer configs +# --------------------------------------------------------------------------- + +FEATURE_NAMES = [ + "fixed_acidity", + "volatile_acidity", + "citric_acid", + "residual_sugar", + "chlorides", + "free_sulfur_dioxide", + "total_sulfur_dioxide", + "density", + "pH", + "sulphates", + "alcohol", +] + +INPUT_FEATURES = [{"name": name, "type": "number"} for name in FEATURE_NAMES] +OUTPUT_FEATURES = [{"name": "quality", "type": "binary"}] + +OPTIMIZERS = { + "adamw": { + "trainer": { + "epochs": 30, + "optimizer": {"type": "adamw", "lr": 0.001}, + "learning_rate_scheduler": {"type": "cosine"}, + } + }, + "radam": { + "trainer": { + "epochs": 30, + "optimizer": {"type": "radam", "lr": 0.001}, + "learning_rate_scheduler": {"type": "cosine"}, + } + }, + "adafactor": { + "trainer": { + "epochs": 30, + "optimizer": {"type": "adafactor", "lr": 0.001}, + } + }, + "schedule_free_adamw": { + "trainer": { + "epochs": 30, + "optimizer": {"type": "schedule_free_adamw", "lr": 0.001}, + # No learning_rate_scheduler — that is the whole point of + # Schedule-Free AdamW. + } + }, + "muon": { + "trainer": { + "epochs": 30, + "optimizer": {"type": "muon", "lr": 0.001}, + "learning_rate_scheduler": {"type": "cosine"}, + } + }, +} + +# --------------------------------------------------------------------------- +# 3. Train and collect results +# --------------------------------------------------------------------------- + +from ludwig.api import LudwigModel + +results = [] + +for opt_name, trainer_cfg in OPTIMIZERS.items(): + print(f"\n{'=' * 60}") + print(f"Training with optimizer: {opt_name}") + print("=" * 60) + + config = { + "model_type": "ecd", + "input_features": INPUT_FEATURES, + "output_features": OUTPUT_FEATURES, + **trainer_cfg, + } + + with tempfile.TemporaryDirectory() as tmpdir: + model = LudwigModel(config, logging_level=30) # WARNING level + + t0 = time.time() + train_stats, _, _ = model.train( + dataset=df, + output_directory=tmpdir, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + ) + elapsed = time.time() - t0 + + # Extract final epoch validation metrics + val_stats = train_stats.validation + epochs = val_stats["quality"]["loss"] + final_loss = epochs[-1] + final_acc = val_stats["quality"]["accuracy"][-1] + + results.append( + { + "optimizer": opt_name, + "final_val_loss": round(final_loss, 4), + "final_val_accuracy": round(final_acc, 4), + "training_time_s": round(elapsed, 1), + } + ) + print(f" val_loss={final_loss:.4f} val_acc={final_acc:.4f} time={elapsed:.1f}s") + +# --------------------------------------------------------------------------- +# 4. Print comparison table +# --------------------------------------------------------------------------- + +print("\n\nResults Summary") +print("=" * 60) +header = f"{'Optimizer':<24} {'Val Loss':>10} {'Val Acc':>10} {'Time (s)':>10}" +print(header) +print("-" * 60) +for r in results: + print( + f"{r['optimizer']:<24} " + f"{r['final_val_loss']:>10.4f} " + f"{r['final_val_accuracy']:>10.4f} " + f"{r['training_time_s']:>10.1f}" + ) +print("=" * 60) diff --git a/examples/peft_advanced/README.md b/examples/peft_advanced/README.md new file mode 100644 index 0000000..d2c84bc --- /dev/null +++ b/examples/peft_advanced/README.md @@ -0,0 +1,55 @@ +# Advanced PEFT Adapters in Ludwig + +This directory contains examples demonstrating Ludwig's extended PEFT (Parameter-Efficient Fine-Tuning) +adapter support, including: + +- **PiSSA / EVA / CorDA / LoftQ** — advanced LoRA initializers +- **rsLoRA** — rank-stabilized LoRA scaling +- **TinyLoRA** — extreme low-rank fine-tuning (LoRA-XS variant) +- **C3A** — contextual/conditional/compositional adapters +- **OFT / HRA** — orthogonal fine-tuning methods +- **WaveFT** — wavelet-domain fine-tuning +- **LN-Tuning** — layer normalization only +- **VBLoRA** — vector bank LoRA + +## Files + +| File | Description | +| --------------------- | ------------------------------------------------------------ | +| `pissa_lora.yaml` | PiSSA initialization (faster convergence than standard LoRA) | +| `eva_lora.yaml` | EVA initialization (data-driven, SOTA performance) | +| `corda_lora.yaml` | CorDA initialization (combines PiSSA + context signals) | +| `loftq_lora.yaml` | LoftQ (quantization-aware LoRA init) | +| `rslora_dora.yaml` | rsLoRA + DoRA combination | +| `tinylora_llm.yaml` | TinyLoRA for LLM fine-tuning on minimal hardware | +| `c3a_llm.yaml` | C3A adapter for multi-task scenarios | +| `oft_llm.yaml` | OFT adapter (orthogonal, preserves pretrained knowledge) | +| `hra_llm.yaml` | HRA adapter (Householder reflections) | +| `waveft_llm.yaml` | WaveFT adapter (frequency-domain updates) | +| `ln_tuning_llm.yaml` | LN-Tuning (ultra-lightweight: only LayerNorm weights) | +| `vblora_llm.yaml` | VBLoRA (shared vector bank for extreme compression) | +| `compare_adapters.py` | Script comparing adapters by parameter count | +| `train_example.py` | Full training example with adapter selection | + +## Quick Start + +```bash +# Train with PiSSA (recommended for most tasks — faster convergence) +ludwig train --config pissa_lora.yaml --dataset ludwig://imdb + +# Ultra-low memory: TinyLoRA +ludwig train --config tinylora_llm.yaml --dataset ludwig://imdb + +# Orthogonal fine-tuning (preserves pretrained knowledge) +ludwig train --config oft_llm.yaml --dataset ludwig://imdb +``` + +## Adapter Selection Guide + +| Hardware constraint | Recommended adapter | Params (7B model) | +| ------------------- | ------------------------ | ----------------- | +| 80 GB GPU | `lora` r=16 + PiSSA init | ~100M | +| 24 GB GPU | `lora` r=8 + rsLoRA | ~50M | +| 16 GB GPU | `tinylora` r=2 | ~1M | +| 8 GB GPU | `ln_tuning` | ~0.1M | +| Edge / CPU | `tinylora` r=2, u=13 | \<100K | diff --git a/examples/peft_advanced/compare_adapters.py b/examples/peft_advanced/compare_adapters.py new file mode 100644 index 0000000..7ea9673 --- /dev/null +++ b/examples/peft_advanced/compare_adapters.py @@ -0,0 +1,73 @@ +"""Compare trainable parameter counts across PEFT adapters on a tiny GPT-2 model.""" + +from __future__ import annotations + +from transformers import AutoModelForCausalLM + +ADAPTERS = [ + ("lora (r=8)", {"type": "lora", "r": 8, "alpha": 16}), + ("lora+pissa (r=8)", {"type": "lora", "r": 8, "alpha": 16, "init_lora_weights": "pissa"}), + ("lora+corda (r=8)", {"type": "lora", "r": 8, "alpha": 16, "init_lora_weights": "corda"}), + ("lora+rslora (r=8)", {"type": "lora", "r": 8, "alpha": 16, "use_rslora": True}), + ("lora+dora (r=8)", {"type": "lora", "r": 8, "alpha": 16, "use_dora": True}), + ("tinylora (r=2, u=64)", {"type": "tinylora", "r": 2, "u": 64}), + ("tinylora (r=2, u=13)", {"type": "tinylora", "r": 2, "u": 13}), + # OFT/HRA/VBLoRA require nn.Linear layers; not compatible with GPT2's Conv1D. + # They work correctly on Llama, Mistral, Falcon, etc. + # ("oft (block=32)", {"type": "oft", "oft_block_size": 32}), + # ("hra (r=8)", {"type": "hra", "r": 8}), + # ("vblora (r=4)", {"type": "vblora", "r": 4, "num_vectors": 256, "vector_length": 768, "topk": 2}), + ("ln_tuning", {"type": "ln_tuning"}), + ("ia3", {"type": "ia3"}), + ("vera (r=256)", {"type": "vera", "r": 256}), + ("adalora (r=8)", {"type": "adalora", "r": 8, "target_r": 4, "init_r": 12, "total_step": 100}), +] + +BASE_MODEL = "sshleifer/tiny-gpt2" + + +def count_trainable(model): + return sum(p.numel() for p in model.parameters() if p.requires_grad) + + +def count_total(model): + return sum(p.numel() for p in model.parameters()) + + +def main(): + from peft import get_peft_model + + from ludwig.schema.llms.peft import adapter_registry + + print(f"Base model: {BASE_MODEL}") + base = AutoModelForCausalLM.from_pretrained(BASE_MODEL) + total = count_total(base) + print(f"Total parameters: {total:,}\n") + print(f"{'Adapter':<30} {'Trainable':>12} {'% of total':>12}") + print("-" * 58) + + for name, config_dict in ADAPTERS: + try: + adapter_type = config_dict["type"] + if adapter_type not in adapter_registry: + print(f"{name:<30} {'N/A (not registered)':>25}") + continue + + cls = adapter_registry[adapter_type] + inst = cls.model_validate(config_dict) + peft_cfg = inst.to_config(task_type="CAUSAL_LM") + + model = AutoModelForCausalLM.from_pretrained(BASE_MODEL) + peft_model = get_peft_model(model, peft_cfg) + trainable = count_trainable(peft_model) + pct = 100.0 * trainable / total + print(f"{name:<30} {trainable:>12,} {pct:>11.4f}%") + except Exception as e: + print(f"{name:<30} {'ERROR: ' + str(e)[:40]:>50}") + + print() + print("Full fine-tuning would train all", f"{total:,}", "parameters (100%)") + + +if __name__ == "__main__": + main() diff --git a/examples/peft_advanced/corda_lora.yaml b/examples/peft_advanced/corda_lora.yaml new file mode 100644 index 0000000..165c306 --- /dev/null +++ b/examples/peft_advanced/corda_lora.yaml @@ -0,0 +1,26 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +adapter: + type: lora + r: 16 + alpha: 32 + # CorDA: Context-Oriented Decomposition Adaptation. + # Combines PiSSA-style SVD init with context signals from the fine-tuning data. + # Converges faster than PiSSA in instruction-previewed mode while preserving world knowledge. + # Paper: https://arxiv.org/abs/2406.05223 + init_lora_weights: corda + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 3 + batch_size: 4 diff --git a/examples/peft_advanced/eva_lora.yaml b/examples/peft_advanced/eva_lora.yaml new file mode 100644 index 0000000..8b45cd5 --- /dev/null +++ b/examples/peft_advanced/eva_lora.yaml @@ -0,0 +1,34 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +adapter: + type: lora + r: 16 + alpha: 32 + # EVA: Explained Variance Adaptation — initializes from SVD of layer input activations. + # Achieves SOTA performance by aligning adapter directions with the actual data distribution. + # Requires a calibration pass over the dataset before training. + # Paper: https://arxiv.org/abs/2410.07170 + init_lora_weights: eva + eva_config: + rho: 2.0 + tau: 0.99 + use_label_mask: true + adjust_scaling_factors: true + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 3 + batch_size: 4 + +preprocessing: + global_max_sequence_length: 512 diff --git a/examples/peft_advanced/ln_tuning_llm.yaml b/examples/peft_advanced/ln_tuning_llm.yaml new file mode 100644 index 0000000..11044dc --- /dev/null +++ b/examples/peft_advanced/ln_tuning_llm.yaml @@ -0,0 +1,24 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +# LN-Tuning: fine-tunes only the LayerNorm/RMSNorm weight and bias parameters. +# Typically <0.1% of total parameters. Surprisingly effective for domain shift tasks. +# Use this when: extreme memory budget, quick domain adaptation, or as a baseline. +adapter: + type: ln_tuning + # Optionally target specific norm layers: + # target_modules: ["input_layernorm", "post_attention_layernorm"] + +trainer: + type: finetune + learning_rate: 0.001 + epochs: 5 + batch_size: 16 diff --git a/examples/peft_advanced/loftq_lora.yaml b/examples/peft_advanced/loftq_lora.yaml new file mode 100644 index 0000000..ead0fea --- /dev/null +++ b/examples/peft_advanced/loftq_lora.yaml @@ -0,0 +1,28 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +adapter: + type: lora + r: 16 + alpha: 32 + # LoftQ: simultaneously quantizes base weights (4-bit) and initializes LoRA to minimize + # the quantization approximation error. Better starting point than QLoRA's zero-init. + # Paper: https://arxiv.org/abs/2310.08659 + init_lora_weights: loftq + loftq_config: + loftq_bits: 4 + loftq_iter: 1 + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 3 + batch_size: 4 diff --git a/examples/peft_advanced/oft_llm.yaml b/examples/peft_advanced/oft_llm.yaml new file mode 100644 index 0000000..bd65801 --- /dev/null +++ b/examples/peft_advanced/oft_llm.yaml @@ -0,0 +1,27 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +# OFT: Orthogonal Fine-Tuning. Applies orthogonal transforms to weight matrices. +# Preserves the hyperspherical energy of the pre-trained model, preventing forgetting. +# Particularly effective for subject-driven generation and tasks requiring output diversity. +# Paper: https://arxiv.org/abs/2306.07280 +adapter: + type: oft + oft_block_size: 32 + module_dropout: 0.0 + # Enable COFT for a stronger orthogonality constraint: + # coft: true + # eps: 0.00006 + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 3 diff --git a/examples/peft_advanced/pissa_lora.yaml b/examples/peft_advanced/pissa_lora.yaml new file mode 100644 index 0000000..ffec486 --- /dev/null +++ b/examples/peft_advanced/pissa_lora.yaml @@ -0,0 +1,30 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +adapter: + type: lora + r: 16 + alpha: 32 + dropout: 0.05 + # PiSSA: initialize via principal singular values/vectors of the weight matrix. + # Converges faster and often outperforms standard LoRA at the same rank. + # Paper: https://arxiv.org/abs/2404.02948 + init_lora_weights: pissa + use_rslora: true + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 3 + batch_size: 4 + +preprocessing: + global_max_sequence_length: 512 diff --git a/examples/peft_advanced/tinylora_llm.yaml b/examples/peft_advanced/tinylora_llm.yaml new file mode 100644 index 0000000..817e7ed --- /dev/null +++ b/examples/peft_advanced/tinylora_llm.yaml @@ -0,0 +1,26 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +# TinyLoRA: LoRA-XS variant. Uses SVD + fixed random projection matrices. +# Achieves fine-tuning with as few as 13 trainable parameters per layer. +# Ideal for: edge devices, CPU-only inference, extremely memory-constrained GPUs. +# Paper: https://arxiv.org/abs/2602.04118 +adapter: + type: tinylora + r: 2 # SVD rank — paper recommends r=2 + u: 64 # trainable vector dimension — set to 13 for extreme efficiency + weight_tying: 0.0 # set > 0 to share trainable vectors across layers + +trainer: + type: finetune + learning_rate: 0.001 + epochs: 3 + batch_size: 8 diff --git a/examples/peft_advanced/train_example.py b/examples/peft_advanced/train_example.py new file mode 100644 index 0000000..3846365 --- /dev/null +++ b/examples/peft_advanced/train_example.py @@ -0,0 +1,75 @@ +"""Minimal training demo for advanced PEFT adapters in Ludwig. + +Run with: python train_example.py --adapter pissa +""" + +from __future__ import annotations + +import argparse +import logging + +import yaml + +from ludwig.api import LudwigModel + +ADAPTER_CONFIGS = { + "lora": {"type": "lora", "r": 8, "alpha": 16}, + "pissa": {"type": "lora", "r": 8, "alpha": 16, "init_lora_weights": "pissa"}, + "corda": {"type": "lora", "r": 8, "alpha": 16, "init_lora_weights": "corda"}, + "rslora": {"type": "lora", "r": 8, "alpha": 16, "use_rslora": True}, + "dora": {"type": "lora", "r": 8, "alpha": 16, "use_dora": True}, + "tinylora": {"type": "tinylora", "r": 2, "u": 64}, + "oft": {"type": "oft", "oft_block_size": 32}, + "hra": {"type": "hra", "r": 8}, + "ln_tuning": {"type": "ln_tuning"}, + "vblora": {"type": "vblora", "r": 4, "num_vectors": 256, "vector_length": 768, "topk": 2}, + "adalora": {"type": "adalora", "r": 8, "target_r": 4, "init_r": 12, "total_step": 1000}, + "ia3": {"type": "ia3"}, + "vera": {"type": "vera", "r": 256}, +} + +BASE_CONFIG = """ +model_type: llm +base_model: sshleifer/tiny-gpt2 + +input_features: + - name: text + type: text + +output_features: + - name: label + type: text + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 1 + batch_size: 4 +""" + + +def main(): + parser = argparse.ArgumentParser(description="Train with a specific PEFT adapter") + parser.add_argument("--adapter", default="pissa", choices=list(ADAPTER_CONFIGS.keys())) + parser.add_argument("--dataset", default=None, help="Path to dataset CSV (optional)") + args = parser.parse_args() + + config = yaml.safe_load(BASE_CONFIG) + config["adapter"] = ADAPTER_CONFIGS[args.adapter] + + print(f"Training with adapter: {args.adapter}") + print(f"Adapter config: {config['adapter']}") + print() + + model = LudwigModel(config=config, logging_level=logging.INFO) + + if args.dataset: + stats, _, output_dir = model.train(dataset=args.dataset) + print(f"Training complete. Results in: {output_dir}") + else: + print("No dataset provided — config validated successfully.") + print("Pass --dataset to run training.") + + +if __name__ == "__main__": + main() diff --git a/examples/peft_advanced/vblora_llm.yaml b/examples/peft_advanced/vblora_llm.yaml new file mode 100644 index 0000000..8184ee4 --- /dev/null +++ b/examples/peft_advanced/vblora_llm.yaml @@ -0,0 +1,26 @@ +model_type: llm +base_model: meta-llama/Llama-3.2-1B + +input_features: + - name: instruction + type: text + +output_features: + - name: output + type: text + +# VBLoRA: Vector Bank LoRA. Expresses LoRA matrices as a sparse combination of +# shared vectors from a global bank, reused across all layers. +# Achieves 10-100x compression vs standard LoRA by sharing vectors across layers. +# Paper: https://arxiv.org/abs/2405.15179 +adapter: + type: vblora + r: 4 + num_vectors: 256 # global bank size + vector_length: 256 # set to model hidden size or head dim + topk: 2 # vectors selected per column — higher = more expressive + +trainer: + type: finetune + learning_rate: 0.0001 + epochs: 3 diff --git a/examples/ray/job_submission/README.md b/examples/ray/job_submission/README.md new file mode 100644 index 0000000..45f0a09 --- /dev/null +++ b/examples/ray/job_submission/README.md @@ -0,0 +1,107 @@ +# Ray Job Submission for Ludwig + +Run Ludwig training on a remote Ray cluster using Ray Job Submission instead of Ray Client. + +## Why Ray Job Submission? + +Ray Client mode (`ray.init("ray://head:10001")`) has known issues with `ray.data` operations ([ray-project/ray#47759](https://github.com/ray-project/ray/issues/47759)), causing `OwnerDiedError` and similar failures during distributed training. Ray Job Submission avoids this entirely by running the training script directly on the cluster head node. + +## How it works + +``` +Your machine Ray Cluster ++------------------+ +------------------+ +| submit_job.py | --- uploads ---> | train_on_cluster.py +| config.yaml | config + | (runs on head node) +| | script | ray.init() is local +| | <-- streams --- | ludwig.train() +| | logs back | saves to S3/NFS ++------------------+ +------------------+ +``` + +1. `submit_job.py` runs on your machine and uploads the config + training script +1. `train_on_cluster.py` runs on the cluster head node +1. `ray.init()` connects locally (no Client mode) +1. Ludwig distributes training across workers normally +1. Model is saved to shared storage (S3/GCS/NFS) + +## Prerequisites + +**Your machine:** + +```bash +pip install "ray[default]" +``` + +**Ray cluster:** + +```bash +pip install "ludwig[distributed]" +``` + +Or install at job start with `--pip ludwig[distributed]` (adds cold start time). + +## Usage + +```bash +# Basic usage +python submit_job.py \ + --ray-address http://ray-head:8265 \ + --config config.yaml \ + --dataset s3://my-bucket/data/train.csv \ + --output-dir s3://my-bucket/results/ + +# KubeRay cluster +python submit_job.py \ + --ray-address http://ray-head.ray.svc:8265 \ + --config config.yaml \ + --dataset s3://my-bucket/data/train.csv \ + --output-dir s3://my-bucket/results/ + +# Install Ludwig on the fly (no pre-install on cluster) +python submit_job.py \ + --ray-address http://ray-head:8265 \ + --config config.yaml \ + --dataset s3://my-bucket/data/train.csv \ + --output-dir /shared/nfs/results/ \ + --pip "ludwig[distributed]" + +# Submit without waiting for results +python submit_job.py \ + --ray-address http://ray-head:8265 \ + --config config.yaml \ + --dataset s3://my-bucket/data/train.csv \ + --output-dir s3://my-bucket/results/ \ + --no-follow +``` + +## Data access + +The dataset must be accessible **from the cluster**, not from your machine: + +| Storage | Example path | Notes | +| ------- | -------------------------- | ----------------------------- | +| S3 | `s3://bucket/data.csv` | Cluster needs AWS credentials | +| GCS | `gs://bucket/data.csv` | Cluster needs GCP credentials | +| NFS | `/shared/data/train.csv` | Must be mounted on all nodes | +| HDFS | `hdfs://namenode/data.csv` | Hadoop cluster | + +If your data is local, upload it first: + +```bash +aws s3 cp my_data.csv s3://my-bucket/data/my_data.csv +``` + +## Files + +- `submit_job.py` -- runs on your machine, submits the job +- `train_on_cluster.py` -- runs on the cluster, does the actual training +- `config.yaml` -- sample Ludwig config (customize for your task) + +## Customization + +**Using your own config**: Replace `config.yaml` with your Ludwig config. Any valid Ludwig config works. + +**Requesting GPUs for the driver**: Use `--num-gpus 1` if your training script needs GPU access on the head node. + +**Custom runtime environment**: Edit `submit_job.py` to add `runtime_env` options like `conda`, `container`, or `env_vars`. diff --git a/examples/ray/job_submission/config.yaml b/examples/ray/job_submission/config.yaml new file mode 100644 index 0000000..6652fe3 --- /dev/null +++ b/examples/ray/job_submission/config.yaml @@ -0,0 +1,38 @@ +# Sample Ludwig config for Ray Job Submission +# Customize this for your dataset and task. +# +# The Ray backend is set automatically when running on a Ray cluster. +# You do NOT need to specify backend: {type: ray} here -- Ludwig detects +# the Ray cluster and uses it for distributed training. + +input_features: + - name: age + type: number + - name: workclass + type: category + - name: education + type: category + - name: occupation + type: category + - name: hours-per-week + type: number + - name: capital-gain + type: number + +output_features: + - name: income + type: binary + +combiner: + type: ft_transformer + hidden_size: 128 + num_heads: 8 + num_layers: 2 + +trainer: + epochs: 50 + batch_size: 256 + early_stop: 10 + learning_rate: 0.0001 + optimizer: + type: adamw diff --git a/examples/ray/job_submission/submit_job.py b/examples/ray/job_submission/submit_job.py new file mode 100644 index 0000000..8a5526c --- /dev/null +++ b/examples/ray/job_submission/submit_job.py @@ -0,0 +1,231 @@ +"""Submit a Ludwig training job to a remote Ray cluster. + +This script runs on YOUR machine (laptop, CI server, etc.) and submits the +training to run on the Ray cluster. It avoids Ray Client mode entirely, +which has known issues with ray.data (ray-project/ray#47759). + +Usage: + python submit_job.py \ + --ray-address http://ray-head:8265 \ + --config config.yaml \ + --dataset s3://my-bucket/data/train.csv \ + --output-dir s3://my-bucket/results/ + +Requirements: + pip install ray[default] # for JobSubmissionClient + The Ray cluster must have ludwig[distributed] installed, OR you can + specify it in --pip-packages so it's installed at job start (slower). + +How it works: + 1. Uploads config.yaml + train_on_cluster.py to the cluster via runtime_env + 2. Submits a Ray Job that runs train_on_cluster.py on the head node + 3. Streams logs back to your terminal + 4. The trained model is saved to --output-dir (use S3/GCS for easy retrieval) + +Data handling: + The dataset must be accessible FROM the cluster, not from your machine. + Options: + - S3/GCS: pass an s3:// or gs:// URI. Ludwig uses fsspec to read it. + - NFS: if your cluster has a shared filesystem, pass the NFS path. + - HDFS: pass an hdfs:// URI. + Local files on your machine won't work unless you upload them first. + + If you need to upload a local CSV to S3 first: + aws s3 cp my_data.csv s3://my-bucket/data/my_data.csv + Then pass --dataset s3://my-bucket/data/my_data.csv +""" + +import argparse +import logging +import os +import time + +logger = logging.getLogger(__name__) + + +def submit( + ray_address: str, + config_path: str, + dataset_path: str, + output_dir: str, + pip_packages: list[str] | None = None, + num_gpus: float | None = None, + follow_logs: bool = True, +): + """Submit a Ludwig training job to a Ray cluster. + + Args: + ray_address: Ray Dashboard address, e.g. "http://ray-head:8265" + config_path: Path to Ludwig YAML config (local file, will be uploaded) + dataset_path: Dataset path accessible from the cluster (S3/GCS/NFS) + output_dir: Where to save the trained model (S3/GCS/NFS recommended) + pip_packages: Additional pip packages to install on the cluster + num_gpus: Number of GPUs to request for the head node entrypoint + follow_logs: Whether to stream logs until job completes + """ + from ray.job_submission import JobStatus, JobSubmissionClient + + client = JobSubmissionClient(ray_address) + + # Build runtime environment + # The working_dir uploads local files (config + training script) to the cluster + script_dir = os.path.dirname(os.path.abspath(__file__)) + runtime_env = { + "working_dir": script_dir, + # NOTE: If Ludwig is already installed on the cluster (recommended for + # production), you can remove the pip section. Installing at job start + # adds ~2-5 min of cold start time. + "env_vars": { + "CONFIG_PATH": os.path.basename(config_path), + "DATASET_PATH": dataset_path, + "OUTPUT_DIR": output_dir, + }, + } + + if pip_packages: + runtime_env["pip"] = pip_packages + + # Copy config to the script directory so it's included in working_dir upload + import shutil + + config_dest = os.path.join(script_dir, os.path.basename(config_path)) + if os.path.abspath(config_path) != os.path.abspath(config_dest): + shutil.copy2(config_path, config_dest) + + # Submit the job + # NOTE: entrypoint_num_gpus reserves GPUs for the driver script. The actual + # training workers request their own GPUs via Ludwig's Ray backend config. + entrypoint = "python train_on_cluster.py" + submit_kwargs = {"entrypoint": entrypoint, "runtime_env": runtime_env} + if num_gpus is not None: + submit_kwargs["entrypoint_num_gpus"] = num_gpus + + logger.info(f"Submitting job to {ray_address}") + logger.info(f" Config: {config_path}") + logger.info(f" Dataset: {dataset_path}") + logger.info(f" Output: {output_dir}") + + job_id = client.submit_job(**submit_kwargs) + logger.info(f"Job submitted: {job_id}") + + if not follow_logs: + print(f"Job ID: {job_id}") + print(f"Check status: ray job status {job_id} --address {ray_address}") + print(f"Stream logs: ray job logs {job_id} --address {ray_address} --follow") + return job_id + + # Stream logs and wait for completion + # NOTE: This blocks until the job finishes. For long training runs, you may + # prefer to use --no-follow and check status manually. + print(f"Job {job_id} submitted. Streaming logs...\n") + + prev_logs = "" + while True: + status = client.get_job_status(job_id) + logs = client.get_job_logs(job_id) + + # Print new log lines + if logs != prev_logs: + new_lines = logs[len(prev_logs) :] + print(new_lines, end="", flush=True) + prev_logs = logs + + if status in {JobStatus.SUCCEEDED, JobStatus.FAILED, JobStatus.STOPPED}: + break + + time.sleep(2) + + print(f"\nJob {job_id} finished with status: {status}") + + if status == JobStatus.FAILED: + # Print any error info + details = client.get_job_info(job_id) + if hasattr(details, "error_type"): + print(f"Error: {details.error_type}: {details.message}") + return None + + return job_id + + +def main(): + parser = argparse.ArgumentParser( + description="Submit a Ludwig training job to a Ray cluster", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Submit to a local Ray cluster + python submit_job.py --ray-address http://localhost:8265 \\ + --config config.yaml \\ + --dataset s3://my-bucket/train.csv \\ + --output-dir s3://my-bucket/results/ + + # Submit to a KubeRay cluster + python submit_job.py --ray-address http://ray-head.ray.svc:8265 \\ + --config config.yaml \\ + --dataset s3://my-bucket/train.csv \\ + --output-dir s3://my-bucket/results/ + + # Install Ludwig on the cluster at job start (slower but no pre-install needed) + python submit_job.py --ray-address http://ray-head:8265 \\ + --config config.yaml \\ + --dataset s3://my-bucket/train.csv \\ + --output-dir /shared/results/ \\ + --pip ludwig[distributed] + """, + ) + parser.add_argument( + "--ray-address", + required=True, + help="Ray Dashboard address (e.g. http://ray-head:8265)", + ) + parser.add_argument( + "--config", + required=True, + help="Path to Ludwig YAML config (local file, will be uploaded to cluster)", + ) + parser.add_argument( + "--dataset", + required=True, + help="Dataset path accessible from the cluster (s3://, gs://, /nfs/...)", + ) + parser.add_argument( + "--output-dir", + default="/tmp/ludwig_results", + help="Where to save results on the cluster (default: /tmp/ludwig_results)", + ) + parser.add_argument( + "--pip", + nargs="*", + default=None, + dest="pip_packages", + help="Additional pip packages to install on the cluster at job start", + ) + parser.add_argument( + "--num-gpus", + type=float, + default=None, + help="GPUs to reserve for the driver script", + ) + parser.add_argument( + "--no-follow", + action="store_true", + help="Don't stream logs. Print job ID and exit.", + ) + + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + submit( + ray_address=args.ray_address, + config_path=args.config, + dataset_path=args.dataset, + output_dir=args.output_dir, + pip_packages=args.pip_packages, + num_gpus=args.num_gpus, + follow_logs=not args.no_follow, + ) + + +if __name__ == "__main__": + main() diff --git a/examples/ray/job_submission/train_on_cluster.py b/examples/ray/job_submission/train_on_cluster.py new file mode 100644 index 0000000..657f225 --- /dev/null +++ b/examples/ray/job_submission/train_on_cluster.py @@ -0,0 +1,76 @@ +"""Training script that runs INSIDE the Ray cluster. + +This is the entrypoint for the Ray Job. It runs on the cluster head node, +so ray.init() connects locally (no Ray Client issues with ray.data). + +The script expects: + - Ludwig config at CONFIG_PATH (env var or default "config.yaml") + - Dataset accessible from the cluster at DATASET_PATH (env var) + This can be: S3/GCS path, NFS mount, or any path visible to the cluster. + - Output saved to OUTPUT_DIR (env var or default "/tmp/ludwig_results") + +After training, the model is saved to OUTPUT_DIR. If OUTPUT_DIR is on shared +storage (S3, GCS, NFS), it's automatically available to the submitter. +""" + +import logging +import os +import sys + +import yaml + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main(): + # These are passed via runtime_env or set in the submission script + config_path = os.environ.get("CONFIG_PATH", "config.yaml") + dataset_path = os.environ.get("DATASET_PATH", None) + output_dir = os.environ.get("OUTPUT_DIR", "/tmp/ludwig_results") + + if dataset_path is None: + logger.error("DATASET_PATH environment variable is required.") + logger.error("Set it to an S3/GCS/NFS path accessible from the cluster.") + sys.exit(1) + + logger.info(f"Config: {config_path}") + logger.info(f"Dataset: {dataset_path}") + logger.info(f"Output: {output_dir}") + + # Load config + with open(config_path) as f: + config = yaml.safe_load(f) + + # Import Ludwig after ray.init() happens implicitly on the cluster + from ludwig.api import LudwigModel + + # If the dataset path is a remote URI (S3, GCS), Ludwig handles it via fsspec. + # If it's a local path, it must be accessible from the cluster node. + model = LudwigModel(config=config, logging_level=logging.INFO) + + # Train + # NOTE: Ludwig's Ray backend will distribute training across workers. + # The key difference from Ray Client: this script runs ON the cluster, + # so ray.data works correctly. + train_stats, preprocessed_data, output_directory = model.train( + dataset=dataset_path, + output_directory=output_dir, + ) + + logger.info(f"Training complete. Model saved to: {output_directory}") + + # Print key metrics for the job log + if hasattr(train_stats, "validation"): + for feat_name, feat_metrics in train_stats.validation.items(): + if isinstance(feat_metrics, dict): + for metric_name, values in feat_metrics.items(): + if values and isinstance(values, list): + best = min(values) if "loss" in metric_name else max(values) + logger.info(f" {feat_name}/{metric_name}: {best:.6f}") + + return output_directory + + +if __name__ == "__main__": + main() diff --git a/examples/ray/kubernetes/README.md b/examples/ray/kubernetes/README.md new file mode 100644 index 0000000..4d045e0 --- /dev/null +++ b/examples/ray/kubernetes/README.md @@ -0,0 +1,70 @@ +## Running on Kubernetes + +### Connect to k8s cluster with a Ray operator + +You should now be pointing to your cluster with `kubectl`. Check the nodes to make sure you're connected correctly: + +``` +kubectl get nodes +``` + +We recommend using the [Kuberay](https://github.com/ray-project/kuberay) implementation of the Ray Operator to launch Ray clusters. + +### Configure the Ray cluster + +First choose your preferred cluster template from `clusters`, for example: + +``` +export CLUSTER_NAME=ludwig-ray-cpu-cluster +``` + +### Start the cluster + +``` +./utils/ray_up.sh $CLUSTER_NAME +``` + +### Submit a script for execution + +``` +./utils/submit.sh $CLUSTER_NAME scripts/train.py +``` + +### SSH into the head node + +``` +./utils/attach.sh $CLUSTER_NAME +``` + +### Run the Ray Dashboard + +``` +./utils/dashboard.sh $CLUSTER_NAME +``` + +Navigate to http://localhost:8267 + +### (For Ludwig Developers) Sync local Ludwig repo + +``` +./utils/rsync_up.sh $CLUSTER_NAME ~/repos/ludwig +``` + +### Shutdown the cluster + +``` +./utils/ray_down.sh $CLUSTER_NAME +``` + +### Connecting to remote filesystems (S3, GCS, etc.) + +Build a custom Docker image deriving from `ludwig-ray` or `ludwig-ray-gpu` containing the library needed for your +data: + +- `s3fs` +- `adlfs` +- `gcsfs` + +Set environment variables into the cluster YAML definition with your credentials. For example, you can connect to S3 using the environment variables described in the [boto3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#using-environment-variables). + +You could also include the credentials directly into the Docker image if they don't need to be configured at runtime. diff --git a/examples/ray/kubernetes/clusters/ludwig-ray-cpu-cluster.yaml b/examples/ray/kubernetes/clusters/ludwig-ray-cpu-cluster.yaml new file mode 100644 index 0000000..92557cd --- /dev/null +++ b/examples/ray/kubernetes/clusters/ludwig-ray-cpu-cluster.yaml @@ -0,0 +1,136 @@ +apiVersion: ray.io/v1alpha1 +kind: RayCluster +metadata: + labels: + controller-tools.k8s.io: "1.0" + name: ludwig-ray-cpu-cluster +spec: + rayVersion: "2.3.1" + headGroupSpec: + serviceType: ClusterIP + replicas: 1 + rayStartParams: + port: "6379" + metrics-export-port: "8080" + node-manager-port: "22346" + object-manager-port: "22345" + object-store-memory: "200000000" + redis-password: "LetMeInRay" + dashboard-host: "0.0.0.0" + node-ip-address: $MY_POD_IP + block: "true" + template: + metadata: + labels: + rayCluster: ludwig-ray-cpu-cluster + rayNodeType: head + groupName: headgroup + annotations: + key: value + spec: + volumes: + - emptyDir: + medium: Memory + name: dshm + containers: + - name: ray-head + image: ludwigai/ludwig-ray:master + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - ray stop + env: + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + ports: + - containerPort: 6379 + name: redis + protocol: TCP + - containerPort: 10001 + name: client + protocol: TCP + - containerPort: 8265 + name: dashboard + protocol: TCP + - containerPort: 8000 + name: ray-serve + protocol: TCP + - containerPort: 8080 + name: metrics + protocol: TCP + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "4" + memory: 8Gi + securityContext: + capabilities: + add: + - SYS_PTRACE + workerGroupSpecs: + - replicas: 1 + minReplicas: 1 + maxReplicas: 1 + groupName: worker-cpu + rayStartParams: + redis-password: "LetMeInRay" + node-ip-address: $MY_POD_IP + block: "true" + template: + metadata: + labels: + rayCluster: ludwig-ray-cpu-cluster + rayNodeType: worker + groupName: worker-cpu + annotations: + key: value + spec: + volumes: + - emptyDir: + medium: Memory + name: dshm + initContainers: + - name: init-myservice + image: busybox:1.28 + command: + [ + "sh", + "-c", + "until nslookup $RAY_IP.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done", + ] + containers: + - name: machine-learning + image: ludwigai/ludwig-ray:master + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - ray stop + env: + - name: MY_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + ports: + - containerPort: 80 + protocol: TCP + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "4" + memory: 8Gi diff --git a/examples/ray/kubernetes/clusters/ludwig-ray-gpu-cluster.yaml b/examples/ray/kubernetes/clusters/ludwig-ray-gpu-cluster.yaml new file mode 100644 index 0000000..ee52967 --- /dev/null +++ b/examples/ray/kubernetes/clusters/ludwig-ray-gpu-cluster.yaml @@ -0,0 +1,138 @@ +apiVersion: ray.io/v1alpha1 +kind: RayCluster +metadata: + labels: + controller-tools.k8s.io: "1.0" + name: ludwig-ray-gpu-cluster +spec: + rayVersion: "2.3.1" + headGroupSpec: + serviceType: ClusterIP + replicas: 1 + rayStartParams: + port: "6379" + metrics-export-port: "8080" + node-manager-port: "22346" + object-manager-port: "22345" + object-store-memory: "200000000" + redis-password: "LetMeInRay" + dashboard-host: "0.0.0.0" + node-ip-address: $MY_POD_IP + block: "true" + template: + metadata: + labels: + rayCluster: ludwig-ray-gpu-cluster + rayNodeType: head + groupName: headgroup + annotations: + key: value + spec: + volumes: + - emptyDir: + medium: Memory + name: dshm + containers: + - name: ray-head + image: ludwigai/ludwig-ray-gpu:master + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - ray stop + env: + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + ports: + - containerPort: 6379 + name: redis + protocol: TCP + - containerPort: 10001 + name: client + protocol: TCP + - containerPort: 8265 + name: dashboard + protocol: TCP + - containerPort: 8000 + name: ray-serve + protocol: TCP + - containerPort: 8080 + name: metrics + protocol: TCP + resources: + limits: + cpu: "8" + memory: 16Gi + nvidia.com/gpu: "1" + requests: + cpu: "4" + memory: 8Gi + securityContext: + capabilities: + add: + - SYS_PTRACE + workerGroupSpecs: + - replicas: 1 + minReplicas: 1 + maxReplicas: 1 + groupName: worker-gpu + rayStartParams: + redis-password: "LetMeInRay" + node-ip-address: $MY_POD_IP + block: "true" + template: + metadata: + labels: + rayCluster: ludwig-ray-gpu-cluster + rayNodeType: worker + groupName: worker-gpu + annotations: + key: value + spec: + volumes: + - emptyDir: + medium: Memory + name: dshm + initContainers: + - name: init-myservice + image: busybox:1.28 + command: + [ + "sh", + "-c", + "until nslookup $RAY_IP.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done", + ] + containers: + - name: machine-learning + image: ludwigai/ludwig-ray-gpu:master + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - ray stop + env: + - name: MY_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + ports: + - containerPort: 80 + protocol: TCP + resources: + limits: + cpu: "8" + memory: 16Gi + nvidia.com/gpu: "1" + requests: + cpu: "4" + memory: 8Gi diff --git a/examples/ray/kubernetes/utils/attach.sh b/examples/ray/kubernetes/utils/attach.sh new file mode 100755 index 0000000..2428bff --- /dev/null +++ b/examples/ray/kubernetes/utils/attach.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cluster_name="${1:-$CLUSTER_NAME}" +head_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1) +kubectl exec -it $head_pod -- /bin/bash diff --git a/examples/ray/kubernetes/utils/dashboard.sh b/examples/ray/kubernetes/utils/dashboard.sh new file mode 100755 index 0000000..bed1d2b --- /dev/null +++ b/examples/ray/kubernetes/utils/dashboard.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cluster_name="${1:-$CLUSTER_NAME}" +head_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1) +kubectl port-forward ${head_pod} 8267:8265 diff --git a/examples/ray/kubernetes/utils/krsync.sh b/examples/ray/kubernetes/utils/krsync.sh new file mode 100755 index 0000000..6285b1d --- /dev/null +++ b/examples/ray/kubernetes/utils/krsync.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# https://serverfault.com/a/887402 + +if [ -z "$KRSYNC_STARTED" ]; then + export KRSYNC_STARTED=true + exec rsync --blocking-io --rsh "$0" $@ +fi + +# Running as --rsh +namespace='' +pod=$1 +shift + +# If use uses pod@namespace rsync passes as: {us} -l pod namespace ... +if [ "X$pod" = "X-l" ]; then + pod=$1 + shift + namespace="-n $1" + shift +fi + +exec kubectl $namespace exec -i $pod -- "$@" diff --git a/examples/ray/kubernetes/utils/ray_down.sh b/examples/ray/kubernetes/utils/ray_down.sh new file mode 100755 index 0000000..fd375ee --- /dev/null +++ b/examples/ray/kubernetes/utils/ray_down.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cluster_name="${1:-$CLUSTER_NAME}" +kubectl delete -f clusters/$cluster_name.yaml diff --git a/examples/ray/kubernetes/utils/ray_up.sh b/examples/ray/kubernetes/utils/ray_up.sh new file mode 100755 index 0000000..7ad07b9 --- /dev/null +++ b/examples/ray/kubernetes/utils/ray_up.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cluster_name="${1:-$CLUSTER_NAME}" +kubectl apply -f clusters/$cluster_name.yaml diff --git a/examples/ray/kubernetes/utils/rsync_up.sh b/examples/ray/kubernetes/utils/rsync_up.sh new file mode 100755 index 0000000..043059f --- /dev/null +++ b/examples/ray/kubernetes/utils/rsync_up.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Example: ./rsync_up.sh cluster-name ~/repos/ludwig + +cluster_name="${1:-$CLUSTER_NAME}" +ludwig_local_dir=$2 + +pods=$(kubectl get pods --no-headers -o custom-columns=":metadata.name" | grep ${cluster_name}-) +script_full_path=$(dirname "$0") + +echo "Rsync to head and workers..." +for pod in $pods +do + echo "Rsync to pod: $pod" + ${script_full_path}/krsync.sh -a --progress --stats ${ludwig_local_dir}/ludwig/ ${pod}:/home/ray/anaconda3/lib/python3.7/site-packages/ludwig +done diff --git a/examples/ray/kubernetes/utils/submit.sh b/examples/ray/kubernetes/utils/submit.sh new file mode 100755 index 0000000..9843112 --- /dev/null +++ b/examples/ray/kubernetes/utils/submit.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +cluster_name="${1:-$CLUSTER_NAME}" +py_script=$2 + +head_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1) +fname=$(basename $py_script) + +kubectl cp $py_script $head_pod:/home/ray/. && kubectl exec -it $head_pod -- python /home/ray/$fname diff --git a/examples/ray/kubernetes/utils/upload.sh b/examples/ray/kubernetes/utils/upload.sh new file mode 100755 index 0000000..257aeec --- /dev/null +++ b/examples/ray/kubernetes/utils/upload.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cluster_name="${1:-$CLUSTER_NAME}" +py_script=$2 + +head_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1) +fname=$(basename $py_script) + +kubectl cp $py_script $head_pod:/home/ray/. +echo /home/ray/$fname diff --git a/examples/regex_freezing/ecd_freezing_with_regex_training.py b/examples/regex_freezing/ecd_freezing_with_regex_training.py new file mode 100644 index 0000000..a66a6fb --- /dev/null +++ b/examples/regex_freezing/ecd_freezing_with_regex_training.py @@ -0,0 +1,69 @@ +import logging +import os +import shutil + +import pandas as pd +import yaml +from datasets import load_dataset + +from ludwig.api import LudwigModel + +""" +To inspect model layers in the terminal, type: "ludwig collect_summary -pm resnet18" + +For some models, a HuggingFace Token will be necessary. +Once you obtain one, use "export HUGGING_FACE_HUB_TOKEN=""" in the terminal. +""" + +dataset = load_dataset("beans") +train_df = pd.DataFrame( + {"image_path": [f"train_{i}.jpg" for i in range(len(dataset["train"]))], "label": dataset["train"]["labels"]} +) +test_df = pd.DataFrame( + {"image_path": [f"test_{i}.jpg" for i in range(len(dataset["test"]))], "label": dataset["test"]["labels"]} +) + +os.makedirs("train_images", exist_ok=True) +os.makedirs("test_images", exist_ok=True) + +for i, img in enumerate(dataset["train"]["image"]): + img.save(f"train_images/train_{i}.jpg") +for i, img in enumerate(dataset["test"]["image"]): + img.save(f"test_images/test_{i}.jpg") + +train_df["image_path"] = train_df["image_path"].apply(lambda x: os.path.join("train_images", x)) +test_df["image_path"] = test_df["image_path"].apply(lambda x: os.path.join("test_images", x)) + +train_df.to_csv("beans_train.csv", index=False) +test_df.to_csv("beans_test.csv", index=False) + + +config = yaml.safe_load(r""" +input_features: + - name: image_path + type: image + encoder: + type: resnet + use_pretrained: true + trainable: true +output_features: + - name: label + type: category +trainer: + epochs: 1 + batch_size: 5 + layers_to_freeze_regex: '(layer1\.0\.*|layer2\.0\.*)' + + """) + +model = LudwigModel(config, logging_level=logging.INFO) +train_stats = model.train(dataset="beans_train.csv", skip_save_model=True) +eval_stats, predictions, output_directory = model.evaluate(dataset="beans_test.csv") + +print("Training Statistics: ", train_stats) +print("Evaluation Statistics: ", eval_stats) + +shutil.rmtree("train_images") +shutil.rmtree("test_images") +os.remove("beans_train.csv") +os.remove("beans_test.csv") diff --git a/examples/regex_freezing/llm_freezing_with_regex_training.py b/examples/regex_freezing/llm_freezing_with_regex_training.py new file mode 100644 index 0000000..44c4fd8 --- /dev/null +++ b/examples/regex_freezing/llm_freezing_with_regex_training.py @@ -0,0 +1,62 @@ +import logging + +import yaml + +from ludwig.api import LudwigModel + +""" +To inspect model layers in the terminal, type: "ludwig collect_summary -pm resnet18" + +For some models, a HuggingFace Token will be necessary. +Once you obtain one, use "export HUGGING_FACE_HUB_TOKEN=""" in the terminal. +""" + +config_str = yaml.safe_load(r""" +model_type: llm +base_model: facebook/opt-350m + +adapter: + type: lora + +prompt: + template: | + ### Instruction: + Generate a concise summary of the following text, capturing the main points and conclusions. + + ### Input: + {input} + + ### Response: + +input_features: + - name: prompt + type: text + preprocessing: + max_sequence_length: 256 + + +output_features: + - name: output + type: text + preprocessing: + max_sequence_length: 256 + +trainer: + type: finetune + layers_to_freeze_regex: (decoder\.layers\.22\.final_layer_norm\.*) + learning_rate: 0.0001 + batch_size: 5 + gradient_accumulation_steps: 16 + epochs: 1 + learning_rate_scheduler: + warmup_fraction: 0.01 + +preprocessing: + sample_ratio: 0.1 + +generation: + pad_token_id : 0 +""") + +model = LudwigModel(config=config_str, logging_level=logging.INFO) +results = model.train(dataset="ludwig://alpaca") diff --git a/examples/semantic_segmentation/README.md b/examples/semantic_segmentation/README.md new file mode 100644 index 0000000..939922d --- /dev/null +++ b/examples/semantic_segmentation/README.md @@ -0,0 +1,112 @@ +# Semantic Segmentation: UNet, SegFormer, and FPN + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/semantic_segmentation/semantic_segmentation.ipynb) + +Semantic segmentation assigns a class label to every pixel in an image. +This example trains three different decoder architectures on the **CamSeq01** +urban driving dataset (101 images, 32 semantic classes) and compares their +accuracy/speed trade-offs. + +## Decoder comparison + +| Decoder | Architecture | Recommended encoder | Approx. extra params | Best for | +| ----------- | -------------------------------------------------------------------------- | ---------------------------------- | ------------------------ | ------------------------------------------------------------------------ | +| `unet` | Symmetric encoder-decoder with skip connections; configurable `num_stages` | Built-in `unet` encoder | ~31M (depth 4) | General purpose baseline, no pretrained backbone needed | +| `segformer` | Lightweight all-MLP head fusing multi-scale ViT features | `dinov2` (DINOv2-base, pretrained) | ~2M head + ~86M backbone | Highest accuracy; transformer features transfer well to dense prediction | +| `fpn` | Feature Pyramid Network top-down pathway with lateral connections | `efficientnet` (pretrained) | ~2M head + ~5M backbone | Fast inference; handles objects at multiple scales efficiently | + +## Dataset + +[CamSeq01](https://mi.eng.cam.ac.uk/research/projects/VideoRec/CamSeq01/) is a +set of 101 road-scene images captured in Cambridge, UK at 960×720 resolution +with 32 semantic class annotations. + +Ludwig ships a built-in downloader — see [`camseq.py`](camseq.py) for the +standalone script or use `from ludwig.datasets import camseq` in Python. + +## Config files + +| File | Decoder | Notes | +| ------------------------ | ----------- | ---------------------------------------- | +| `config_camseq.yaml` | `unet` | Original baseline config | +| `config_unet_depth.yaml` | `unet` | Shows the `num_stages` parameter | +| `config_segformer.yaml` | `segformer` | DINOv2 backbone, fine-tuned end-to-end | +| `config_fpn.yaml` | `fpn` | EfficientNet backbone, larger batch size | + +## Running the examples + +**Prerequisites**: a CUDA-capable GPU. An A100 or equivalent is recommended +for the SegFormer run; the UNet and FPN configs run well on a single V100/3090. + +```bash +pip install 'ludwig[vision]' +``` + +### UNet (configurable depth) + +```bash +python camseq.py # uses config_camseq.yaml (depth 4 by default) +``` + +Or with the explicit depth config: + +```bash +ludwig train --config config_unet_depth.yaml +``` + +### SegFormer + +```bash +ludwig train --config config_segformer.yaml +``` + +### FPN + +```bash +ludwig train --config config_fpn.yaml +``` + +### UNet depth ablation + +```bash +python unet_depth_sweep.py +``` + +This script trains models with `num_stages` ∈ {2, 3, 4, 5} and prints a +summary table of parameter count vs. best validation loss vs. training time. + +### Interactive notebook + +Open `semantic_segmentation.ipynb` locally or click the Colab badge above. +The notebook walks through all three decoders and produces side-by-side +visualisations of their predictions. + +## Key config parameters + +### UNet decoder + +```yaml +decoder: + type: unet + num_stages: 4 # 2–5; input size must be divisible by 2^num_stages + num_fc_layers: 0 + conv_norm: batch +``` + +### SegFormer decoder + +```yaml +decoder: + type: segformer + hidden_size: 256 # MLP projection width + dropout: 0.1 +``` + +### FPN decoder + +```yaml +decoder: + type: fpn + num_channels: 256 # lateral projection width at each pyramid level + num_levels: 4 # number of pyramid levels (typical range 2–5) +``` diff --git a/examples/semantic_segmentation/camseq.py b/examples/semantic_segmentation/camseq.py new file mode 100644 index 0000000..61dc704 --- /dev/null +++ b/examples/semantic_segmentation/camseq.py @@ -0,0 +1,57 @@ +import logging +import os +import shutil + +import pandas as pd +import torch +import yaml +from torchvision.utils import save_image + +from ludwig.api import LudwigModel +from ludwig.datasets import camseq + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +# set up Python dictionary to hold model training parameters +with open("./config_camseq.yaml") as f: + config = yaml.safe_load(f.read()) + +# Define Ludwig model object that drive model training +model = LudwigModel(config, logging_level=logging.INFO) + +# load Camseq dataset +df = camseq.load(split=False) + +pred_set = df[0:1] # prediction hold-out 1 image +data_set = df[1:] # train,test,validate on remaining images + +# initiate model training +train_stats, _, output_directory = model.train( # training statistics # location for training results saved to disk + dataset=data_set, + experiment_name="simple_image_experiment", + model_name="single_model", + skip_save_processed_input=True, +) + +# print("{}".format(model.model)) + +# predict +pred_set.reset_index(inplace=True) +pred_out_df, results = model.predict(pred_set) + +if not isinstance(pred_out_df, pd.DataFrame): + pred_out_df = pred_out_df.compute() +pred_out_df["image_path"] = pred_set["image_path"] +pred_out_df["mask_path"] = pred_set["mask_path"] + +for index, row in pred_out_df.iterrows(): + pred_mask = torch.from_numpy(row["mask_path_predictions"]) + pred_mask_path = os.path.dirname(os.path.realpath(__file__)) + "/predicted_" + os.path.basename(row["mask_path"]) + print(f"\nSaving predicted mask to {pred_mask_path}") + if torch.any(pred_mask.gt(1)): + pred_mask = pred_mask.float() / 255 + save_image(pred_mask, pred_mask_path) + print("Input image_path: {}".format(row["image_path"])) + print("Label mask_path: {}".format(row["mask_path"])) + print(f"Predicted mask_path: {pred_mask_path}") diff --git a/examples/semantic_segmentation/config_camseq.yaml b/examples/semantic_segmentation/config_camseq.yaml new file mode 100644 index 0000000..8e018a5 --- /dev/null +++ b/examples/semantic_segmentation/config_camseq.yaml @@ -0,0 +1,33 @@ +input_features: + - name: image_path + type: image + preprocessing: + num_processes: 6 + infer_image_max_height: 1024 + infer_image_max_width: 1024 + encoder: unet + +output_features: + - name: mask_path + type: image + preprocessing: + num_processes: 6 + infer_image_max_height: 1024 + infer_image_max_width: 1024 + infer_image_num_classes: true + num_classes: 32 + decoder: + type: unet + num_fc_layers: 0 + loss: + type: softmax_cross_entropy + +combiner: + type: concat + num_fc_layers: 0 + +trainer: + epochs: 100 + early_stop: -1 + batch_size: 1 + max_batch_size: 1 diff --git a/examples/semantic_segmentation/config_fpn.yaml b/examples/semantic_segmentation/config_fpn.yaml new file mode 100644 index 0000000..e0d4733 --- /dev/null +++ b/examples/semantic_segmentation/config_fpn.yaml @@ -0,0 +1,37 @@ +input_features: + - name: image_path + type: image + preprocessing: + num_processes: 4 + height: 512 + width: 512 + encoder: + type: efficientnet + use_pretrained: true + trainable: true + +output_features: + - name: mask_path + type: image + preprocessing: + num_processes: 4 + height: 512 + width: 512 + num_classes: 32 + decoder: + type: fpn + num_channels: 256 + num_levels: 4 + loss: + type: softmax_cross_entropy + +combiner: + type: concat + num_fc_layers: 0 + +trainer: + epochs: 100 + early_stop: 10 + batch_size: 8 + max_batch_size: 8 + learning_rate: 0.0001 diff --git a/examples/semantic_segmentation/config_segformer.yaml b/examples/semantic_segmentation/config_segformer.yaml new file mode 100644 index 0000000..60d4840 --- /dev/null +++ b/examples/semantic_segmentation/config_segformer.yaml @@ -0,0 +1,38 @@ +input_features: + - name: image_path + type: image + preprocessing: + num_processes: 4 + height: 512 + width: 512 + encoder: + type: dinov2 + pretrained_model_name_or_path: facebook/dinov2-base + use_pretrained: true + trainable: true + +output_features: + - name: mask_path + type: image + preprocessing: + num_processes: 4 + height: 512 + width: 512 + num_classes: 32 + decoder: + type: segformer + hidden_size: 256 + dropout: 0.1 + loss: + type: softmax_cross_entropy + +combiner: + type: concat + num_fc_layers: 0 + +trainer: + epochs: 50 + early_stop: 10 + batch_size: 4 + max_batch_size: 4 + learning_rate: 0.0001 diff --git a/examples/semantic_segmentation/config_unet_depth.yaml b/examples/semantic_segmentation/config_unet_depth.yaml new file mode 100644 index 0000000..57ce610 --- /dev/null +++ b/examples/semantic_segmentation/config_unet_depth.yaml @@ -0,0 +1,36 @@ +input_features: + - name: image_path + type: image + preprocessing: + num_processes: 4 + height: 512 + width: 512 + encoder: + type: unet + +output_features: + - name: mask_path + type: image + preprocessing: + num_processes: 4 + height: 512 + width: 512 + num_classes: 32 + decoder: + type: unet + num_stages: 4 # was hardcoded before, now configurable (try 2, 3, 4, 5) + num_fc_layers: 0 + conv_norm: batch + loss: + type: softmax_cross_entropy + +combiner: + type: concat + num_fc_layers: 0 + +trainer: + epochs: 100 + early_stop: 10 + batch_size: 4 + max_batch_size: 4 + learning_rate: 0.0001 diff --git a/examples/semantic_segmentation/semantic_segmentation.ipynb b/examples/semantic_segmentation/semantic_segmentation.ipynb new file mode 100644 index 0000000..5537d47 --- /dev/null +++ b/examples/semantic_segmentation/semantic_segmentation.ipynb @@ -0,0 +1,578 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Semantic Segmentation with Ludwig\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/semantic_segmentation/semantic_segmentation.ipynb)\n", + "\n", + "This notebook demonstrates semantic segmentation on the **CamSeq01** driving dataset using three different image decoders available in Ludwig:\n", + "\n", + "| Decoder | Architecture | Encoder pairing | Best for |\n", + "|---------|-------------|-----------------|----------|\n", + "| `unet` | Symmetric encoder-decoder with skip connections | Built-in UNet encoder | General purpose, well-studied baseline |\n", + "| `segformer` | Lightweight MLP head on top of multi-scale features | `dinov2` / any ViT backbone | High accuracy, transformer features |\n", + "| `fpn` | Feature Pyramid Network top-down pathway | Any CNN (e.g. `efficientnet`) | Fast inference, good at multi-scale objects |\n", + "\n", + "**GPU required** — an A100 or similar is recommended for the SegFormer run.\n", + "\n", + "CamSeq01 contains 101 urban driving images at 960×720 with 32 semantic classes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install 'ludwig[vision]' --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import torch\n", + "\n", + "from ludwig.api import LudwigModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "Ludwig ships a built-in downloader for CamSeq01. \n", + "The call below downloads and caches the dataset, then returns a `pd.DataFrame`\n", + "with two columns: `image_path` and `mask_path`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.datasets import camseq\n", + "\n", + "df = camseq.load(split=False)\n", + "print(f\"Total samples: {len(df)}\")\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reserve first image for visual comparison at the end\n", + "pred_set = df[0:1].reset_index(drop=True)\n", + "train_set = df[1:]\n", + "\n", + "print(f\"Training samples : {len(train_set)}\")\n", + "print(f\"Prediction sample: {len(pred_set)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Quick look at the raw image and ground-truth mask\n", + "from PIL import Image as PILImage\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "axes[0].imshow(PILImage.open(pred_set[\"image_path\"][0]))\n", + "axes[0].set_title(\"Input image\")\n", + "axes[0].axis(\"off\")\n", + "axes[1].imshow(PILImage.open(pred_set[\"mask_path\"][0]))\n", + "axes[1].set_title(\"Ground-truth mask (32 classes)\")\n", + "axes[1].axis(\"off\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 1: UNet (default)\n", + "\n", + "The classic UNet uses a symmetric encoder-decoder with skip connections. \n", + "Ludwig's implementation supports a configurable number of stages (`num_stages`)\n", + "so you can trade off capacity against speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "unet_config = {\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\"num_processes\": 4, \"height\": 512, \"width\": 512},\n", + " \"encoder\": {\"type\": \"unet\"},\n", + " }\n", + " ],\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"mask_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\n", + " \"num_processes\": 4,\n", + " \"height\": 512,\n", + " \"width\": 512,\n", + " \"num_classes\": 32,\n", + " },\n", + " \"decoder\": {\n", + " \"type\": \"unet\",\n", + " \"num_stages\": 4, # configurable depth\n", + " \"num_fc_layers\": 0,\n", + " \"conv_norm\": \"batch\",\n", + " },\n", + " \"loss\": {\"type\": \"softmax_cross_entropy\"},\n", + " }\n", + " ],\n", + " \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 0},\n", + " \"trainer\": {\n", + " \"epochs\": 50,\n", + " \"early_stop\": 10,\n", + " \"batch_size\": 4,\n", + " \"learning_rate\": 0.0001,\n", + " },\n", + "}\n", + "\n", + "t0 = time.time()\n", + "unet_model = LudwigModel(unet_config, logging_level=logging.WARNING)\n", + "unet_stats, _, unet_output_dir = unet_model.train(\n", + " dataset=train_set,\n", + " experiment_name=\"seg_comparison\",\n", + " model_name=\"unet\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "unet_time = time.time() - t0\n", + "print(f\"UNet training time: {unet_time:.1f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "unet_preds, _ = unet_model.predict(pred_set)\n", + "if not isinstance(unet_preds, pd.DataFrame):\n", + " unet_preds = unet_preds.compute()\n", + "unet_pred_mask = torch.from_numpy(unet_preds[\"mask_path_predictions\"].iloc[0])\n", + "print(\"UNet prediction mask shape:\", unet_pred_mask.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 2: SegFormer (transformer backbone)\n", + "\n", + "SegFormer pairs a DINOv2 vision-transformer backbone with a lightweight MLP\n", + "decoder head. The hierarchical features from DINOv2 feed the SegFormer head\n", + "directly — no upsampling convolutions needed until the final prediction.\n", + "\n", + "- Encoder: `dinov2` (`facebook/dinov2-base`, ~86M params pretrained)\n", + "- Decoder: `segformer` (hidden MLP projection → bilinear upsample)\n", + "- Best suited to: high-accuracy segmentation where GPU memory is not the bottleneck" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "segformer_config = {\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\"num_processes\": 4, \"height\": 512, \"width\": 512},\n", + " \"encoder\": {\n", + " \"type\": \"dinov2\",\n", + " \"pretrained_model_name_or_path\": \"facebook/dinov2-base\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": True,\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"mask_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\n", + " \"num_processes\": 4,\n", + " \"height\": 512,\n", + " \"width\": 512,\n", + " \"num_classes\": 32,\n", + " },\n", + " \"decoder\": {\n", + " \"type\": \"segformer\",\n", + " \"hidden_size\": 256,\n", + " \"dropout\": 0.1,\n", + " },\n", + " \"loss\": {\"type\": \"softmax_cross_entropy\"},\n", + " }\n", + " ],\n", + " \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 0},\n", + " \"trainer\": {\n", + " \"epochs\": 50,\n", + " \"early_stop\": 10,\n", + " \"batch_size\": 4,\n", + " \"learning_rate\": 0.0001,\n", + " },\n", + "}\n", + "\n", + "t0 = time.time()\n", + "segformer_model = LudwigModel(segformer_config, logging_level=logging.WARNING)\n", + "segformer_stats, _, segformer_output_dir = segformer_model.train(\n", + " dataset=train_set,\n", + " experiment_name=\"seg_comparison\",\n", + " model_name=\"segformer\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "segformer_time = time.time() - t0\n", + "print(f\"SegFormer training time: {segformer_time:.1f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "segformer_preds, _ = segformer_model.predict(pred_set)\n", + "if not isinstance(segformer_preds, pd.DataFrame):\n", + " segformer_preds = segformer_preds.compute()\n", + "segformer_pred_mask = torch.from_numpy(segformer_preds[\"mask_path_predictions\"].iloc[0])\n", + "print(\"SegFormer prediction mask shape:\", segformer_pred_mask.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 3: FPN (lightweight)\n", + "\n", + "The Feature Pyramid Network decoder builds a top-down pathway over multi-scale\n", + "feature maps produced by any CNN backbone. Combined with EfficientNet it gives\n", + "a good accuracy/speed balance and fits comfortably in smaller GPU budgets.\n", + "\n", + "- Encoder: `efficientnet` (pretrained on ImageNet, ~5M params)\n", + "- Decoder: `fpn` (lateral projections + top-down merging over 4 pyramid levels)\n", + "- Best suited to: production deployments where inference latency matters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpn_config = {\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\"num_processes\": 4, \"height\": 512, \"width\": 512},\n", + " \"encoder\": {\n", + " \"type\": \"efficientnet\",\n", + " \"use_pretrained\": True,\n", + " \"trainable\": True,\n", + " },\n", + " }\n", + " ],\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"mask_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\n", + " \"num_processes\": 4,\n", + " \"height\": 512,\n", + " \"width\": 512,\n", + " \"num_classes\": 32,\n", + " },\n", + " \"decoder\": {\n", + " \"type\": \"fpn\",\n", + " \"num_channels\": 256,\n", + " \"num_levels\": 4,\n", + " },\n", + " \"loss\": {\"type\": \"softmax_cross_entropy\"},\n", + " }\n", + " ],\n", + " \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 0},\n", + " \"trainer\": {\n", + " \"epochs\": 100,\n", + " \"early_stop\": 10,\n", + " \"batch_size\": 8,\n", + " \"learning_rate\": 0.0001,\n", + " },\n", + "}\n", + "\n", + "t0 = time.time()\n", + "fpn_model = LudwigModel(fpn_config, logging_level=logging.WARNING)\n", + "fpn_stats, _, fpn_output_dir = fpn_model.train(\n", + " dataset=train_set,\n", + " experiment_name=\"seg_comparison\",\n", + " model_name=\"fpn\",\n", + " skip_save_processed_input=True,\n", + ")\n", + "fpn_time = time.time() - t0\n", + "print(f\"FPN training time: {fpn_time:.1f}s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fpn_preds, _ = fpn_model.predict(pred_set)\n", + "if not isinstance(fpn_preds, pd.DataFrame):\n", + " fpn_preds = fpn_preds.compute()\n", + "fpn_pred_mask = torch.from_numpy(fpn_preds[\"mask_path_predictions\"].iloc[0])\n", + "print(\"FPN prediction mask shape:\", fpn_pred_mask.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UNet depth ablation\n", + "\n", + "Ludwig's UNet decoder exposes a `num_stages` parameter that controls how many\n", + "encoder/decoder stage pairs are stacked. More stages → richer multi-scale\n", + "representations but also more parameters and longer training times. The input\n", + "spatial dimensions must be divisible by `2^num_stages`.\n", + "\n", + "Run the standalone sweep script to get the full table:\n", + "\n", + "```bash\n", + "python unet_depth_sweep.py\n", + "```\n", + "\n", + "Below we replicate a mini version for illustration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "SWEEP_BASE = {\n", + " \"input_features\": [\n", + " {\n", + " \"name\": \"image_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\"num_processes\": 4, \"height\": 512, \"width\": 512},\n", + " \"encoder\": {\"type\": \"unet\"},\n", + " }\n", + " ],\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"mask_path\",\n", + " \"type\": \"image\",\n", + " \"preprocessing\": {\"num_processes\": 4, \"height\": 512, \"width\": 512, \"num_classes\": 32},\n", + " \"decoder\": {\"type\": \"unet\", \"num_fc_layers\": 0, \"conv_norm\": \"batch\"},\n", + " \"loss\": {\"type\": \"softmax_cross_entropy\"},\n", + " }\n", + " ],\n", + " \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 0},\n", + " \"trainer\": {\"epochs\": 20, \"early_stop\": 5, \"batch_size\": 4, \"learning_rate\": 0.0001},\n", + "}\n", + "\n", + "sweep_results = []\n", + "\n", + "for depth in [2, 3, 4, 5]:\n", + " cfg = yaml.safe_load(yaml.dump(SWEEP_BASE))\n", + " cfg[\"output_features\"][0][\"decoder\"][\"num_stages\"] = depth\n", + "\n", + " m = LudwigModel(cfg, logging_level=logging.WARNING)\n", + " t0 = time.time()\n", + " stats, _, _ = m.train(\n", + " dataset=train_set,\n", + " experiment_name=\"depth_sweep\",\n", + " model_name=f\"unet_depth_{depth}\",\n", + " skip_save_processed_input=True,\n", + " )\n", + " elapsed = time.time() - t0\n", + "\n", + " n_params = sum(p.numel() for p in m.model.parameters() if p.requires_grad)\n", + "\n", + " val_loss = None\n", + " try:\n", + " val_loss = min(stats[\"validation\"][\"combined\"][\"loss\"])\n", + " except (KeyError, TypeError):\n", + " pass\n", + "\n", + " sweep_results.append(\n", + " {\n", + " \"num_stages\": depth,\n", + " \"trainable_params\": f\"{n_params:,}\",\n", + " \"best_val_loss\": round(val_loss, 4) if val_loss is not None else \"n/a\",\n", + " \"training_time_s\": round(elapsed, 1),\n", + " }\n", + " )\n", + " print(f\"depth={depth} params={n_params:,} val_loss={val_loss} time={elapsed:.1f}s\")\n", + "\n", + "sweep_df = pd.DataFrame(sweep_results)\n", + "sweep_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax1 = plt.subplots(figsize=(8, 4))\n", + "depths = [r[\"num_stages\"] for r in sweep_results]\n", + "times = [r[\"training_time_s\"] for r in sweep_results]\n", + "\n", + "ax1.bar(depths, times, color=\"steelblue\", alpha=0.7, label=\"Training time (s)\")\n", + "ax1.set_xlabel(\"UNet num_stages\")\n", + "ax1.set_ylabel(\"Training time (s)\", color=\"steelblue\")\n", + "ax1.tick_params(axis=\"y\", labelcolor=\"steelblue\")\n", + "ax1.set_xticks(depths)\n", + "\n", + "val_losses = [r[\"best_val_loss\"] for r in sweep_results if isinstance(r[\"best_val_loss\"], float)]\n", + "if len(val_losses) == len(depths):\n", + " ax2 = ax1.twinx()\n", + " ax2.plot(depths, val_losses, \"o-\", color=\"tomato\", label=\"Best val loss\")\n", + " ax2.set_ylabel(\"Best val loss\", color=\"tomato\")\n", + " ax2.tick_params(axis=\"y\", labelcolor=\"tomato\")\n", + "\n", + "plt.title(\"UNet depth: training time vs validation loss\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualisation\n", + "\n", + "Compare the predicted segmentation maps from the three models side by side." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def to_rgb(mask_tensor: torch.Tensor) -> np.ndarray:\n", + " \"\"\"Convert a CHW or HW class-index tensor to a displayable RGB image.\"\"\"\n", + " t = mask_tensor\n", + " if t.dim() == 3:\n", + " # (C, H, W) logits or class probabilities -> argmax\n", + " t = t.argmax(dim=0)\n", + " arr = t.cpu().numpy().astype(np.float32)\n", + " arr = (arr - arr.min()) / max(arr.max() - arr.min(), 1e-5)\n", + " return arr\n", + "\n", + "\n", + "input_img = PILImage.open(pred_set[\"image_path\"][0])\n", + "gt_mask = PILImage.open(pred_set[\"mask_path\"][0])\n", + "\n", + "fig, axes = plt.subplots(1, 5, figsize=(22, 5))\n", + "\n", + "axes[0].imshow(input_img)\n", + "axes[0].set_title(\"Input image\")\n", + "\n", + "axes[1].imshow(gt_mask)\n", + "axes[1].set_title(\"Ground truth\")\n", + "\n", + "axes[2].imshow(to_rgb(unet_pred_mask), cmap=\"tab20\")\n", + "axes[2].set_title(\"UNet (depth=4)\")\n", + "\n", + "axes[3].imshow(to_rgb(segformer_pred_mask), cmap=\"tab20\")\n", + "axes[3].set_title(\"SegFormer + DINOv2\")\n", + "\n", + "axes[4].imshow(to_rgb(fpn_pred_mask), cmap=\"tab20\")\n", + "axes[4].set_title(\"FPN + EfficientNet\")\n", + "\n", + "for ax in axes:\n", + " ax.axis(\"off\")\n", + "\n", + "plt.suptitle(\"Segmentation map comparison\", fontsize=14)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Print a summary comparison table\n", + "comparison = pd.DataFrame(\n", + " [\n", + " {\"model\": \"UNet (num_stages=4)\", \"encoder\": \"unet\", \"decoder\": \"unet\", \"training_time_s\": round(unet_time, 1)},\n", + " {\n", + " \"model\": \"SegFormer + DINOv2\",\n", + " \"encoder\": \"dinov2\",\n", + " \"decoder\": \"segformer\",\n", + " \"training_time_s\": round(segformer_time, 1),\n", + " },\n", + " {\n", + " \"model\": \"FPN + EfficientNet\",\n", + " \"encoder\": \"efficientnet\",\n", + " \"decoder\": \"fpn\",\n", + " \"training_time_s\": round(fpn_time, 1),\n", + " },\n", + " ]\n", + ")\n", + "comparison" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/semantic_segmentation/unet_depth_sweep.py b/examples/semantic_segmentation/unet_depth_sweep.py new file mode 100644 index 0000000..d67172f --- /dev/null +++ b/examples/semantic_segmentation/unet_depth_sweep.py @@ -0,0 +1,124 @@ +"""UNet depth sweep for semantic segmentation on CamSeq01. + +Trains UNet models with num_stages in [2, 3, 4, 5] and reports mIoU, +approximate parameter count, and training time so you can pick the +right capacity/speed trade-off for your use case. + +Usage: + python unet_depth_sweep.py + +Requirements: ludwig[vision], a CUDA-capable GPU is strongly recommended. +""" + +import logging +import time + +import pandas as pd +import torch +import yaml + +from ludwig.api import LudwigModel +from ludwig.datasets import camseq + +logging.basicConfig(level=logging.WARNING) + +# ── base config ─────────────────────────────────────────────────────────────── +BASE_CONFIG = { + "input_features": [ + { + "name": "image_path", + "type": "image", + "preprocessing": {"num_processes": 4, "height": 512, "width": 512}, + "encoder": {"type": "unet"}, + } + ], + "output_features": [ + { + "name": "mask_path", + "type": "image", + "preprocessing": { + "num_processes": 4, + "height": 512, + "width": 512, + "num_classes": 32, + }, + "decoder": {"type": "unet", "num_fc_layers": 0, "conv_norm": "batch"}, + "loss": {"type": "softmax_cross_entropy"}, + } + ], + "combiner": {"type": "concat", "num_fc_layers": 0}, + "trainer": { + "epochs": 30, + "early_stop": 5, + "batch_size": 4, + "max_batch_size": 4, + "learning_rate": 0.0001, + }, +} + +DEPTHS = [2, 3, 4, 5] + + +def count_parameters(model: torch.nn.Module) -> int: + return sum(p.numel() for p in model.parameters() if p.requires_grad) + + +def run_sweep(): + df = camseq.load(split=False) + train_set = df[1:] + + results = [] + + for depth in DEPTHS: + print(f"\n{'=' * 60}") + print(f" Training UNet with num_stages={depth}") + print(f"{'=' * 60}") + + config = yaml.safe_load(yaml.dump(BASE_CONFIG)) # deep copy via yaml round-trip + config["output_features"][0]["decoder"]["num_stages"] = depth + + model = LudwigModel(config, logging_level=logging.WARNING) + + t0 = time.time() + train_stats, _, output_dir = model.train( + dataset=train_set, + experiment_name="unet_depth_sweep", + model_name=f"unet_depth_{depth}", + skip_save_processed_input=True, + ) + elapsed = time.time() - t0 + + # Parameter count + n_params = count_parameters(model.model) + + # Best validation loss as proxy metric (mIoU not exposed by default) + val_loss = None + try: + val_history = train_stats["validation"]["combined"]["loss"] + val_loss = min(val_history) + except (KeyError, TypeError): + pass + + results.append( + { + "num_stages": depth, + "trainable_params": n_params, + "best_val_loss": round(val_loss, 4) if val_loss is not None else "n/a", + "training_time_s": round(elapsed, 1), + } + ) + + print(f" num_stages={depth} params={n_params:,} best_val_loss={val_loss} time={elapsed:.1f}s") + + # ── summary table ───────────────────────────────────────────────────────── + print("\n\nDepth sweep summary") + print("=" * 70) + summary = pd.DataFrame(results) + print(summary.to_string(index=False)) + print() + + return summary + + +if __name__ == "__main__": + run_sweep() diff --git a/examples/serve/README.md b/examples/serve/README.md new file mode 100644 index 0000000..70ceabd --- /dev/null +++ b/examples/serve/README.md @@ -0,0 +1,87 @@ +# Ludwig Model Serve Example + +This example shows Ludwig's http model serving capability, which is able to load a pre-trained Ludwig model and respond to REST APIs for predictions. +A simple client program illustrates how to invoke the REST API to retrieve predictions for provided input features. The two REST APIs covered by this example: + +| REST API | Description | +| ---------------- | ------------------------------- | +| `/predict` | Single record prediction | +| `/batch_predict` | Prediction for batch of records | + +### Preparatory Steps + +- Run the `simple_model_training.py` example in `examples/titanic`. This should result the following file structures: + +``` +examples/ + titantic/ + results/ + simple_experiment_simple_model/ + model/ + description.json + training_statistics.json +``` + +### Run Model Server Example + +- Open two terminal windows +- In first terminal window: + - Ensure current working directory is `examples/serve` + - Start ludwig model server with the `titanic` trained model. The following command uses the default host address (`0.0.0.0`) and port number (`8000`). + +``` +ludwig serve --model_path ../titanic/results/simple_experiment_simple_model/model +``` + +Sample start up messages for ludwig model server + +``` +███████████████████████ +█ █ █ █ ▜█ █ █ █ █ █ +█ █ █ █ █ █ █ █ █ █ ███ +█ █ █ █ █ █ █ █ █ ▌ █ +█ █████ █ █ █ █ █ █ █ █ +█ █ ▟█ █ █ █ +███████████████████████ +ludwig v0.3 - Serve + +INFO: Started server process [4429] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) + +``` + +- In the second terminal window: + - Ensure current working director is `examples/serve` + - Run the sample client program + +``` +python client_program.py +``` + +Output should look like this + +```` +retrieved 1309 records for predictions +single record for prediction: + {'PassengerId': 1, 'Survived': 0.0, 'Pclass': 3, 'Name': 'Braund, Mr. Owen Harris', 'Sex': 'male', 'Age': 22.0, 'SibSp': 1, 'Parch': 0, 'Ticket': 'A/5 21171', 'Fare': 7.25, 'Cabin': nan, 'Embarked': 'S', 'split': 0} + +invoking REST API /predict for single record... + +Received 1 predictions +Sample predictions: + Survived_predictions Survived_probabilities_False Survived_probabilities_True Survived_probability +0 False 0.906132 0.093868 0.906132 + +invoking REST API /batch_predict for entire dataframe... + +Received 1309 predictions +Sample predictions: + Survived_predictions Survived_probabilities_False Survived_probabilities_True Survived_probability +0 False 0.906132 0.093868 0.906132 +1 True 0.165714 0.834286 0.834286 +2 True 0.441169 0.558831 0.558831 +3 True 0.228311 0.771689 0.771689 +4 False 0.878072 0.121928 0.878072``` +```` diff --git a/examples/serve/client_program.py b/examples/serve/client_program.py new file mode 100644 index 0000000..7e0ad86 --- /dev/null +++ b/examples/serve/client_program.py @@ -0,0 +1,93 @@ +import sys + +import pandas as pd +import requests + +from ludwig.datasets import titanic + +# Ludwig model server default values +LUDWIG_HOST = "0.0.0.0" +LUDWIG_PORT = "8000" + + +# +# retrieve data to make predictions +# +test_df = titanic.load() +print(f"retrieved {test_df.shape[0]:d} records for predictions") + + +# +# execute REST API /predict for a single record +# + +# get a single record from dataframe and convert to list of dictionaries +prediction_request_dict_list = test_df.head(1).to_dict(orient="records") + +# extract dictionary for the single record only +prediction_request_dict = prediction_request_dict_list[0] + +print("single record for prediction:\n", prediction_request_dict) + +# construct URL +predict_url = "".join(["http://", LUDWIG_HOST, ":", LUDWIG_PORT, "/predict"]) + +print("\ninvoking REST API /predict for single record...") +# connect using the default host address and port number +try: + response = requests.post(predict_url, data=prediction_request_dict) +except requests.exceptions.ConnectionError as e: + print(e) + print("REST API /predict failed") + sys.exit(1) + + +# check if REST API worked +if response.status_code == 200: + # REST API successful + # convert JSON response to panda dataframe + pred_df = pd.read_json("[" + response.text + "]", orient="records") + + print(f"\nReceived {pred_df.shape[0]:d} predictions") + print("Sample predictions:") + print(pred_df.head()) + +else: + # Error encountered during REST API processing + print("\nError during predictions, error code: ", response.status_code, "reason code: ", response.text) + +# +# execute REST API /batch_predict on a pandas dataframe +# + +# create json representation of dataset for REST API +prediction_request_json = test_df.to_json(orient="split") + +print("\ninvoking REST API /batch_predict for entire dataframe...") + +# construct URL +batch_predict_url = "".join(["http://", LUDWIG_HOST, ":", LUDWIG_PORT, "/batch_predict"]) + +# connect using the default host address and port number +response = requests.post(batch_predict_url, data={"dataset": prediction_request_json}) +try: + response = requests.post(batch_predict_url, data={"dataset": prediction_request_json}) +except requests.exceptions.ConnectionError as e: + print(e) + print("REST API /batch_predict failed") + sys.exit(1) + + +# check if REST API worked +if response.status_code == 200: + # REST API successful + # convert JSON response to panda dataframe + pred_df = pd.read_json(response.text, orient="split") + + print(f"\nReceived {pred_df.shape[0]:d} predictions") + print("Sample predictions:") + print(pred_df.head()) + +else: + # Error encountered during REST API processing + print("\nError during predictions, error code: ", response.status_code, "reason code: ", response.text) diff --git a/examples/serve/prometheus_monitoring/README.md b/examples/serve/prometheus_monitoring/README.md new file mode 100644 index 0000000..7004f4c --- /dev/null +++ b/examples/serve/prometheus_monitoring/README.md @@ -0,0 +1,82 @@ +# Ludwig Serving + Prometheus + Grafana Monitoring + +This directory contains a Docker Compose stack for running a Ludwig vLLM server alongside Prometheus and Grafana so you can monitor inference metrics in real time. + +## Prerequisites + +- Docker 24+ and Docker Compose v2 +- NVIDIA Container Toolkit (for GPU support) +- A trained Ludwig LLM model directory on the host + +## Quick start + +1. **Set the model path** (the directory Ludwig will serve): + + ```bash + export MODEL_PATH=/absolute/path/to/your/ludwig_model + ``` + +1. **Start the stack**: + + ```bash + docker compose up -d + ``` + +1. **Check services are healthy**: + + ```bash + docker compose ps + ``` + +1. **Send a test prediction**: + + ```bash + curl http://localhost:8000/predict -X POST -F 'text=Hello world' + ``` + +## Services + +| Service | URL | Description | +| -------------- | --------------------- | -------------------------------- | +| `ludwig-serve` | http://localhost:8000 | Ludwig vLLM inference server | +| `prometheus` | http://localhost:9090 | Prometheus metrics scraper | +| `grafana` | http://localhost:3000 | Grafana dashboards (admin/admin) | + +## Prometheus metrics + +Ludwig exposes a `/metrics` endpoint in [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/). +Prometheus is configured to scrape it every 15 seconds (see `prometheus.yml`). + +Key metrics to watch: + +- `ludwig_request_latency_seconds` — per-request inference latency histogram +- `ludwig_requests_total` — total requests served (labelled by endpoint) +- `ludwig_batch_size` — distribution of batch sizes received by `/batch_predict` + +## Grafana + +Open http://localhost:3000 and log in with `admin` / `admin`. + +The Prometheus datasource is pre-provisioned. To import a ready-made dashboard: + +1. Click **Dashboards → Import** +1. Paste the dashboard ID or upload a JSON file +1. Select the **Prometheus** datasource + +## Stopping the stack + +```bash +docker compose down +``` + +To also remove persistent volumes (Prometheus TSDB and Grafana data): + +```bash +docker compose down -v +``` + +## Configuration + +- **`prometheus.yml`** — scrape interval and target configuration +- **`docker-compose.yml`** — service definitions, port mappings, GPU allocation +- Set `GF_SECURITY_ADMIN_PASSWORD` in `docker-compose.yml` before deploying to production diff --git a/examples/serve/prometheus_monitoring/docker-compose.yml b/examples/serve/prometheus_monitoring/docker-compose.yml new file mode 100644 index 0000000..4a3b98e --- /dev/null +++ b/examples/serve/prometheus_monitoring/docker-compose.yml @@ -0,0 +1,69 @@ +version: "3.9" + +services: + ludwig-serve: + image: ludwigai/ludwig:latest + command: > + ludwig serve + --model_path /models/my_llm_model + --backend vllm + --num_gpus 1 + ports: + - "8000:8000" + volumes: + - ${MODEL_PATH:-./model}:/models/my_llm_model:ro + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 5 + networks: + - monitoring + + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/etc/prometheus/console_libraries" + - "--web.console.templates=/etc/prometheus/consoles" + depends_on: + - ludwig-serve + networks: + - monitoring + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning:ro + depends_on: + - prometheus + networks: + - monitoring + +volumes: + prometheus_data: + grafana_data: + +networks: + monitoring: + driver: bridge diff --git a/examples/serve/prometheus_monitoring/prometheus.yml b/examples/serve/prometheus_monitoring/prometheus.yml new file mode 100644 index 0000000..2eba341 --- /dev/null +++ b/examples/serve/prometheus_monitoring/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'ludwig' + static_configs: + - targets: ['ludwig-serve:8000'] + metrics_path: '/metrics' diff --git a/examples/serve/vllm_client.py b/examples/serve/vllm_client.py new file mode 100644 index 0000000..c5ebac7 --- /dev/null +++ b/examples/serve/vllm_client.py @@ -0,0 +1,172 @@ +"""Ludwig vLLM server client. + +Demonstrates how to call a running Ludwig vLLM server via its REST API, +covering the /predict and /batch_predict endpoints and reporting throughput. + +Usage: + # Start a Ludwig vLLM server first: + # ludwig serve --model_path=/path/to/llm_model --backend vllm --num_gpus 1 + python vllm_client.py +""" + +import json +import sys +import time + +import requests + +SERVER_URL = "http://localhost:8000" + +PREDICT_URL = f"{SERVER_URL}/predict" +BATCH_PREDICT_URL = f"{SERVER_URL}/batch_predict" +OPENAI_URL = f"{SERVER_URL}/v1/completions" + +# --------------------------------------------------------------------------- +# Sample data — adjust field names to match your trained model's input features +# --------------------------------------------------------------------------- +SAMPLE_TEXT = "Explain the concept of retrieval-augmented generation in one sentence." + +BATCH_SAMPLES = [ + "What is Ludwig?", + "Summarise the key benefits of vLLM.", + "Describe how PagedAttention works.", + "What is tensor parallelism?", + "How does continuous batching improve GPU utilisation?", +] + + +def _check_server() -> bool: + """Return True when the server is reachable.""" + try: + resp = requests.get(f"{SERVER_URL}/", timeout=5) + return resp.status_code < 500 + except requests.exceptions.ConnectionError: + return False + + +# --------------------------------------------------------------------------- +# /predict (single example) +# --------------------------------------------------------------------------- +def predict_single(text: str) -> dict: + """Send a single text to the /predict endpoint and return the response.""" + t0 = time.perf_counter() + response = requests.post(PREDICT_URL, data={"text": text}, timeout=120) + elapsed = time.perf_counter() - t0 + + response.raise_for_status() + result = response.json() + print(f"[predict] latency: {elapsed * 1000:.1f} ms") + print(f"[predict] response: {json.dumps(result, indent=2)}") + return result + + +# --------------------------------------------------------------------------- +# /batch_predict (multiple examples) +# --------------------------------------------------------------------------- +def predict_batch(texts: list[str]) -> dict: + """Send a batch of texts to the /batch_predict endpoint.""" + dataset = json.dumps({"columns": ["text"], "data": [[t] for t in texts]}) + + t0 = time.perf_counter() + response = requests.post(BATCH_PREDICT_URL, data={"dataset": dataset}, timeout=300) + elapsed = time.perf_counter() - t0 + + response.raise_for_status() + result = response.json() + + n = len(texts) + throughput = n / elapsed + print(f"[batch_predict] {n} examples in {elapsed:.2f} s — {throughput:.1f} examples/s") + print(f"[batch_predict] columns: {result.get('columns')}") + for i, row in enumerate(result.get("data", [])[:3]): + print(f" [{i}] {row}") + if n > 3: + print(f" ... ({n - 3} more)") + return result + + +# --------------------------------------------------------------------------- +# /v1/completions (OpenAI-compatible, exposed by vLLM) +# --------------------------------------------------------------------------- +def openai_compat_request(prompt: str) -> dict: + """Call the OpenAI-compatible completions endpoint exposed by vLLM.""" + payload = { + "model": "ludwig-model", # vLLM uses this as a placeholder name + "prompt": prompt, + "max_tokens": 128, + "temperature": 0.7, + } + t0 = time.perf_counter() + response = requests.post(OPENAI_URL, json=payload, timeout=120) + elapsed = time.perf_counter() - t0 + + if response.status_code == 404: + print("[openai] /v1/completions not available on this server (requires --enable-openai-api flag)") + return {} + + response.raise_for_status() + result = response.json() + print(f"[openai] latency: {elapsed * 1000:.1f} ms") + choices = result.get("choices", []) + if choices: + print(f"[openai] completion: {choices[0].get('text', '').strip()}") + return result + + +# --------------------------------------------------------------------------- +# Throughput benchmark: compare repeated single calls vs one batch call +# --------------------------------------------------------------------------- +def throughput_benchmark(texts: list[str], warmup: int = 1) -> None: + """Compare per-request latency (sequential) vs batch throughput.""" + print("\n--- Throughput benchmark ---") + + # Warmup + for _ in range(warmup): + requests.post(PREDICT_URL, data={"text": texts[0]}, timeout=120) + + # Sequential single requests + t0 = time.perf_counter() + for text in texts: + requests.post(PREDICT_URL, data={"text": text}, timeout=120) + seq_elapsed = time.perf_counter() - t0 + seq_tps = len(texts) / seq_elapsed + print(f"Sequential /predict: {seq_elapsed:.2f} s total — {seq_tps:.1f} examples/s") + + # Single batch request + dataset = json.dumps({"columns": ["text"], "data": [[t] for t in texts]}) + t0 = time.perf_counter() + requests.post(BATCH_PREDICT_URL, data={"dataset": dataset}, timeout=300) + batch_elapsed = time.perf_counter() - t0 + batch_tps = len(texts) / batch_elapsed + print(f"Batch /batch_predict: {batch_elapsed:.2f} s total — {batch_tps:.1f} examples/s") + print(f"Batch speedup: {seq_tps / max(batch_tps, 1e-9):.2f}x") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- +def main() -> None: + if not _check_server(): + print( + f"Cannot reach Ludwig server at {SERVER_URL}.\n" + "Start it with:\n" + " ludwig serve --model_path=/path/to/llm_model --backend vllm --num_gpus 1", + file=sys.stderr, + ) + sys.exit(1) + + print("=== Single prediction ===") + predict_single(SAMPLE_TEXT) + + print("\n=== Batch prediction ===") + predict_batch(BATCH_SAMPLES) + + print("\n=== OpenAI-compatible endpoint ===") + openai_compat_request(SAMPLE_TEXT) + + print("\n=== Throughput benchmark ===") + throughput_benchmark(BATCH_SAMPLES) + + +if __name__ == "__main__": + main() diff --git a/examples/serve/vllm_serving.ipynb b/examples/serve/vllm_serving.ipynb new file mode 100644 index 0000000..a04db65 --- /dev/null +++ b/examples/serve/vllm_serving.ipynb @@ -0,0 +1,391 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Serving Ludwig LLMs with vLLM\n", + "\n", + "This notebook shows how to deploy a Ludwig LLM using the [vLLM](https://github.com/vllm-project/vllm) backend for high-throughput, production-grade inference.\n", + "\n", + "**What you will learn**\n", + "- Launch a Ludwig vLLM server from the command line\n", + "- Query the `/predict` and `/batch_predict` REST endpoints\n", + "- Use the OpenAI-compatible `/v1/completions` endpoint\n", + "- Benchmark vLLM latency vs the default FastAPI backend\n", + "- Production tips: tensor parallelism and continuous batching\n", + "\n", + "> **Prerequisites:** a trained Ludwig LLM model saved on disk. The notebook uses placeholder paths; replace them with your own." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install \"ludwig[llm,vllm]\" --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Launch vLLM server\n", + "\n", + "Start the Ludwig serve command with `--backend vllm`. This replaces the default FastAPI/HuggingFace inference loop with vLLM's continuous-batching engine.\n", + "\n", + "```bash\n", + "ludwig serve \\\n", + " --model_path=/path/to/model \\\n", + " --backend vllm \\\n", + " --num_gpus 1\n", + "```\n", + "\n", + "The server starts on `http://0.0.0.0:8000` by default. You can change the host and port with `--host` and `--port`.\n", + "\n", + "**Key flags:**\n", + "\n", + "| Flag | Default | Description |\n", + "|------|---------|-------------|\n", + "| `--backend vllm` | `local` | Use vLLM inference engine |\n", + "| `--num_gpus N` | `1` | Number of GPUs (enables tensor parallelism when N > 1) |\n", + "| `--host` | `0.0.0.0` | Bind address |\n", + "| `--port` | `8000` | Port |\n", + "\n", + "Once the server prints `Application startup complete`, it is ready to accept requests." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# In Colab, launch the server in the background via a shell cell.\n", + "# Replace /path/to/model with your actual model directory.\n", + "#\n", + "# !ludwig serve --model_path=/path/to/model --backend vllm --num_gpus 1 &\n", + "#\n", + "# Give the server a few seconds to start before running subsequent cells.\n", + "import time\n", + "\n", + "SERVER_URL = \"http://localhost:8000\"\n", + "print(f\"Server URL: {SERVER_URL}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Single prediction\n", + "\n", + "The `/predict` endpoint accepts a single example as form data. Field names must match the input feature names used when the model was trained.\n", + "\n", + "### curl\n", + "\n", + "```bash\n", + "curl http://localhost:8000/predict \\\n", + " -X POST \\\n", + " -F 'text=Explain what a transformer model is in one sentence.'\n", + "```\n", + "\n", + "### Python requests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import requests\n", + "\n", + "PREDICT_URL = f\"{SERVER_URL}/predict\"\n", + "\n", + "prompt = \"Explain what a transformer model is in one sentence.\"\n", + "\n", + "t0 = time.perf_counter()\n", + "response = requests.post(PREDICT_URL, data={\"text\": prompt})\n", + "latency_ms = (time.perf_counter() - t0) * 1000\n", + "\n", + "response.raise_for_status()\n", + "result = response.json()\n", + "\n", + "print(f\"Latency: {latency_ms:.1f} ms\")\n", + "print(json.dumps(result, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Batch prediction\n", + "\n", + "The `/batch_predict` endpoint accepts multiple examples encoded as a JSON string in the Pandas `split` format.\n", + "\n", + "### curl\n", + "\n", + "```bash\n", + "curl http://localhost:8000/batch_predict \\\n", + " -X POST \\\n", + " -F 'dataset={\"columns\":[\"text\"],\"data\":[[\"What is Ludwig?\"],[\"What is vLLM?\"],[\"What is PagedAttention?\"]]}'\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "BATCH_PREDICT_URL = f\"{SERVER_URL}/batch_predict\"\n", + "\n", + "prompts = [\n", + " \"What is Ludwig?\",\n", + " \"What is vLLM?\",\n", + " \"What is PagedAttention?\",\n", + " \"What is tensor parallelism?\",\n", + " \"Summarise continuous batching in one sentence.\",\n", + "]\n", + "\n", + "dataset_json = json.dumps({\"columns\": [\"text\"], \"data\": [[p] for p in prompts]})\n", + "\n", + "t0 = time.perf_counter()\n", + "response = requests.post(BATCH_PREDICT_URL, data={\"dataset\": dataset_json})\n", + "elapsed = time.perf_counter() - t0\n", + "\n", + "response.raise_for_status()\n", + "result = response.json()\n", + "\n", + "print(f\"{len(prompts)} examples in {elapsed:.2f} s ({len(prompts) / elapsed:.1f} examples/s)\")\n", + "print(\"Columns:\", result[\"columns\"])\n", + "for i, row in enumerate(result[\"data\"]):\n", + " print(f\" [{i}] {row[0][:120]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OpenAI-compatible endpoint\n", + "\n", + "When Ludwig serves via vLLM it optionally exposes an OpenAI-compatible `/v1/completions` endpoint. This lets you use the OpenAI Python client or any tool that speaks the OpenAI API without any code changes.\n", + "\n", + "Enable it with `--enable-openai-api` when starting the server:\n", + "\n", + "```bash\n", + "ludwig serve --model_path=/path/to/model --backend vllm --num_gpus 1 --enable-openai-api\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "OPENAI_URL = f\"{SERVER_URL}/v1/completions\"\n", + "\n", + "payload = {\n", + " \"model\": \"ludwig-model\", # vLLM accepts any string here\n", + " \"prompt\": \"Explain retrieval-augmented generation in two sentences.\",\n", + " \"max_tokens\": 128,\n", + " \"temperature\": 0.7,\n", + "}\n", + "\n", + "response = requests.post(OPENAI_URL, json=payload)\n", + "\n", + "if response.status_code == 404:\n", + " print(\"OpenAI-compatible endpoint not enabled. Start the server with --enable-openai-api.\")\n", + "else:\n", + " response.raise_for_status()\n", + " result = response.json()\n", + " print(result[\"choices\"][0][\"text\"].strip())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# You can also use the openai Python package directly.\n", + "# !pip install openai --quiet\n", + "\n", + "# from openai import OpenAI\n", + "\n", + "# client = OpenAI(\n", + "# base_url=f\"{SERVER_URL}/v1\",\n", + "# api_key=\"not-required\", # vLLM does not enforce API keys by default\n", + "# )\n", + "\n", + "# completion = client.completions.create(\n", + "# model=\"ludwig-model\",\n", + "# prompt=\"Describe the attention mechanism in transformers.\",\n", + "# max_tokens=150,\n", + "# )\n", + "# print(completion.choices[0].text.strip())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Latency comparison\n", + "\n", + "The following cells time the same requests against:\n", + "1. The **default FastAPI backend** (standard HuggingFace `model.generate`)\n", + "2. The **vLLM backend** (continuous batching + PagedAttention)\n", + "\n", + "The comparison requires two server instances to be running simultaneously — one on port 8000 (vLLM) and one on port 8001 (default). The cells below print results for whichever servers are reachable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import statistics\n", + "\n", + "DEFAULT_URL = \"http://localhost:8001\" # default FastAPI backend\n", + "VLLM_URL = \"http://localhost:8000\" # vLLM backend\n", + "\n", + "BENCH_PROMPTS = [\n", + " \"What is machine learning?\",\n", + " \"Explain gradient descent.\",\n", + " \"What is overfitting?\",\n", + " \"Describe a convolutional neural network.\",\n", + " \"What is the difference between supervised and unsupervised learning?\",\n", + "]\n", + "\n", + "\n", + "def measure_latencies(base_url: str, prompts: list, n_warmup: int = 2) -> list:\n", + " \"\"\"Return a list of per-request latencies (seconds) for the given server.\"\"\"\n", + " url = f\"{base_url}/predict\"\n", + " latencies = []\n", + " for i, prompt in enumerate(prompts * n_warmup + prompts):\n", + " t0 = time.perf_counter()\n", + " try:\n", + " r = requests.post(url, data={\"text\": prompt}, timeout=120)\n", + " r.raise_for_status()\n", + " except Exception as exc:\n", + " print(f\" Request failed: {exc}\")\n", + " continue\n", + " elapsed = time.perf_counter() - t0\n", + " if i >= len(prompts) * n_warmup: # skip warmup\n", + " latencies.append(elapsed)\n", + " return latencies\n", + "\n", + "\n", + "def print_stats(label: str, latencies: list) -> None:\n", + " if not latencies:\n", + " print(f\"{label}: no data (server not running?)\")\n", + " return\n", + " p50 = statistics.median(latencies)\n", + " p95 = sorted(latencies)[int(0.95 * len(latencies))]\n", + " mean = statistics.mean(latencies)\n", + " print(\n", + " f\"{label}: mean={mean * 1000:.0f} ms p50={p50 * 1000:.0f} ms p95={p95 * 1000:.0f} ms ({len(latencies)} requests)\"\n", + " )\n", + "\n", + "\n", + "print(\"Benchmarking default backend (port 8001)...\")\n", + "default_latencies = measure_latencies(DEFAULT_URL, BENCH_PROMPTS)\n", + "print_stats(\"Default backend\", default_latencies)\n", + "\n", + "print(\"\\nBenchmarking vLLM backend (port 8000)...\")\n", + "vllm_latencies = measure_latencies(VLLM_URL, BENCH_PROMPTS)\n", + "print_stats(\"vLLM backend \", vllm_latencies)\n", + "\n", + "if default_latencies and vllm_latencies:\n", + " speedup = statistics.mean(default_latencies) / statistics.mean(vllm_latencies)\n", + " print(f\"\\nvLLM speedup: {speedup:.1f}x on sequential single-request latency\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Production tips\n", + "\n", + "### Tensor parallelism\n", + "\n", + "For models that don't fit in a single GPU's VRAM, vLLM can split the model across multiple GPUs using tensor parallelism. Pass `--num_gpus N` to Ludwig serve:\n", + "\n", + "```bash\n", + "# Split a large model across 4 GPUs\n", + "ludwig serve \\\n", + " --model_path=/path/to/large_model \\\n", + " --backend vllm \\\n", + " --num_gpus 4\n", + "```\n", + "\n", + "vLLM will automatically set the tensor parallel degree to match `--num_gpus`. Each GPU handles a shard of the weight matrices and they communicate via NVLink or PCIe during inference.\n", + "\n", + "### Continuous batching\n", + "\n", + "Unlike traditional static batching (where all requests in a batch must finish before a new one starts), vLLM's continuous batching inserts new requests into an ongoing batch as soon as a slot becomes free. This means:\n", + "\n", + "- **GPU utilisation stays high** even when requests have very different output lengths\n", + "- **Tail latency is lower** because short requests are not held up by long ones\n", + "- **Throughput increases** roughly 2–10x vs static batching for typical LLM workloads\n", + "\n", + "No configuration is needed — continuous batching is enabled automatically when you use `--backend vllm`.\n", + "\n", + "### PagedAttention\n", + "\n", + "vLLM's PagedAttention manages the KV cache in fixed-size pages (similar to virtual memory in an OS). This eliminates almost all memory fragmentation from the KV cache, allowing you to serve more concurrent requests from the same GPU.\n", + "\n", + "### Memory and concurrency tuning\n", + "\n", + "Pass vLLM-specific arguments via `--vllm-extra-args` (a JSON string):\n", + "\n", + "```bash\n", + "ludwig serve \\\n", + " --model_path=/path/to/model \\\n", + " --backend vllm \\\n", + " --num_gpus 1 \\\n", + " --vllm-extra-args '{\"gpu_memory_utilization\": 0.9, \"max_num_seqs\": 256}'\n", + "```\n", + "\n", + "| Parameter | Default | Description |\n", + "|-----------|---------|-------------|\n", + "| `gpu_memory_utilization` | `0.9` | Fraction of GPU memory reserved for the KV cache |\n", + "| `max_num_seqs` | `256` | Maximum concurrent sequences in a single iteration |\n", + "| `max_model_len` | model default | Maximum total sequence length (prompt + output) |\n", + "\n", + "### Quantization\n", + "\n", + "vLLM supports AWQ, GPTQ, and SqueezeLLM quantization for reduced memory and faster inference:\n", + "\n", + "```bash\n", + "ludwig serve \\\n", + " --model_path=/path/to/awq_model \\\n", + " --backend vllm \\\n", + " --vllm-extra-args '{\"quantization\": \"awq\"}'\n", + "```\n", + "\n", + "### Monitoring\n", + "\n", + "Ludwig serve exposes a `/metrics` Prometheus endpoint. See the `prometheus_monitoring/` directory in this folder for a ready-to-use Docker Compose stack with Prometheus and Grafana." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/serving/kserve/README.md b/examples/serving/kserve/README.md new file mode 100644 index 0000000..87397f8 --- /dev/null +++ b/examples/serving/kserve/README.md @@ -0,0 +1,92 @@ +# Deploying Ludwig Models with KServe + +[KServe](https://kserve.github.io/website/) is the standard Kubernetes serving runtime +for ML models. Ludwig ships `ludwig.serve_kserve` which wraps any trained `LudwigModel` +behind the KServe **Open Inference Protocol v2** (`/v2/models/{name}/infer`) so Ludwig +models slot into existing MLOps pipelines that expect v2-compliant endpoints. + +## Local testing (no Kubernetes required) + +```bash +pip install "ludwig[serve]" kserve + +# Start the server +python -m ludwig.serve_kserve \ + --model_name titanic \ + --model_path ./results/experiment_run/model \ + --http_port 8080 + +# Predict with v2 protocol +curl -s -X POST http://localhost:8080/v2/models/titanic/infer \ + -H "Content-Type: application/json" \ + -d '{ + "inputs": [ + {"name": "Pclass", "shape": [2], "datatype": "INT64", "data": [1, 3]}, + {"name": "Sex", "shape": [2], "datatype": "BYTES", "data": ["female", "male"]}, + {"name": "Age", "shape": [2], "datatype": "FP32", "data": [28.0, 22.0]} + ] + }' +``` + +## Kubernetes deployment + +1. **Build and push the Ludwig image** (or use the public one): + +```bash +docker build -t your-registry/ludwig:latest . +docker push your-registry/ludwig:latest +``` + +2. **Copy your trained model** to a `PersistentVolume` or an object-store URI. + +1. **Apply the manifest**: + +```bash +# Edit serving_config.yaml to point to your model and image, then: +kubectl apply -f serving_config.yaml +kubectl get inferenceservice ludwig-titanic +``` + +4. **Send predictions**: + +```bash +INGRESS=$(kubectl get svc istio-ingressgateway -n istio-system \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + +curl -s -H "Host: ludwig-titanic.default.example.com" \ + http://${INGRESS}/v2/models/titanic/infer \ + -H "Content-Type: application/json" \ + -d '{ + "inputs": [ + {"name": "Pclass", "shape": [1], "datatype": "INT64", "data": [1]}, + {"name": "Sex", "shape": [1], "datatype": "BYTES", "data": ["female"]}, + {"name": "Age", "shape": [1], "datatype": "FP32", "data": [28.0]} + ] + }' +``` + +## Programmatic usage + +```python +from ludwig.serve_kserve import serve_ludwig_model + +# Blocking — runs until Ctrl-C +serve_ludwig_model( + model_name="titanic", + model_path="./results/experiment_run/model", + http_port=8080, +) +``` + +## v2 protocol reference + +| field | description | +| ------------------- | -------------------------------------------------------------------------- | +| `inputs[].name` | Ludwig input feature name | +| `inputs[].shape` | `[batch_size]` (1-D flat batch) | +| `inputs[].datatype` | `BYTES` for text/category, `FP32`/`FP64` for numbers, `INT64` for integers | +| `inputs[].data` | Flat list of values, length == batch_size | + +Response `outputs` follow the same shape. All output values are currently serialised +as `BYTES` (string representation); numeric output feature types will be exposed as +`FP32`/`INT64` in a future release. diff --git a/examples/serving/kserve/serving_config.yaml b/examples/serving/kserve/serving_config.yaml new file mode 100644 index 0000000..faf7d5f --- /dev/null +++ b/examples/serving/kserve/serving_config.yaml @@ -0,0 +1,44 @@ +# KServe InferenceService manifest for a Ludwig model. +# +# Deploys a Ludwig model on Kubernetes using KServe's custom predictor shim +# (ludwig.serve_kserve). The model binary is loaded from a persistent volume +# or from a cloud storage URI (S3 / GCS / AzureBlob) via KServe's model agent. +# +# Apply with: +# kubectl apply -f serving_config.yaml + +apiVersion: serving.kserve.io/v1beta1 +kind: InferenceService +metadata: + name: ludwig-titanic + namespace: default +spec: + predictor: + # Use Ludwig's KServe v2 shim as the custom runtime. + containers: + - name: ludwig-predictor + # Replace with your Ludwig image (must have ludwig + kserve installed). + image: ghcr.io/ludwig-ai/ludwig:latest + command: ["python", "-m", "ludwig.serve_kserve"] + args: + - "--model_name=titanic" + # Model path inside the container — mount via PVC or initContainer. + - "--model_path=/mnt/models/titanic" + - "--http_port=8080" + ports: + - containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "2" + memory: "4Gi" + volumeMounts: + - mountPath: /mnt/models + name: model-volume + volumes: + - name: model-volume + persistentVolumeClaim: + claimName: ludwig-models-pvc diff --git a/examples/serving/ray_serve/README.md b/examples/serving/ray_serve/README.md new file mode 100644 index 0000000..922092a --- /dev/null +++ b/examples/serving/ray_serve/README.md @@ -0,0 +1,85 @@ +# Deploying Ludwig Models with Ray Serve + +[Ray Serve](https://docs.ray.io/en/latest/serve/index.html) is a production-grade model +serving library built on Ray that supports autoscaling, traffic splitting, and rolling +updates. Ludwig ships `ludwig.serve_ray_serve` to wrap any trained `LudwigModel` as a +Ray Serve deployment with a single function call. + +## Prerequisites + +```bash +pip install "ludwig[distributed]" # pulls in ray[serve] +``` + +## Quick start + +1. **Train a model** (or use an existing one): + +```bash +ludwig train \ + --config examples/titanic/simple_model_training.yaml \ + --dataset examples/titanic/titanic.csv \ + --output_directory ./results +``` + +2. **Deploy**: + +```bash +python deploy.py --model_path ./results/experiment_run/model --block +``` + +3. **Predict**: + +```bash +# Single record +curl -s -X POST http://localhost:8000/ludwig \ + -H "Content-Type: application/json" \ + -d '{"Pclass": 1, "Sex": "female", "Age": 28}' + +# Batch +curl -s -X POST http://localhost:8000/ludwig \ + -H "Content-Type: application/json" \ + -d '[{"Pclass": 3, "Sex": "male", "Age": 22}, {"Pclass": 1, "Sex": "female", "Age": 35}]' +``` + +## GPU deployment + +```bash +python deploy.py \ + --model_path ./results/experiment_run/model \ + --num_replicas 2 \ + --gpu \ + --block +``` + +## Programmatic usage + +```python +import ray +from ludwig.serve_ray_serve import deploy_ludwig_model + +ray.init() + +handle = deploy_ludwig_model( + model_path="./results/experiment_run/model", + name="titanic", + num_replicas=2, + ray_actor_options={"num_gpus": 1}, +) + +# Programmatic call (no HTTP) +import asyncio, pandas as pd + +result = asyncio.get_event_loop().run_until_complete(handle.predict.remote({"Pclass": 1, "Sex": "female", "Age": 28})) +print(result) +``` + +## API contract + +| endpoint | method | body | response | +| --------- | ------ | ------------------------- | ------------------------------------------- | +| `/{name}` | POST | single JSON record (dict) | dict with one prediction per output feature | +| `/{name}` | POST | list of JSON records | `{"predictions": [...]}` | + +The payload shape mirrors Ludwig's existing `ludwig.serve_v2` FastAPI server so clients +can switch backends without code changes. diff --git a/examples/serving/ray_serve/deploy.py b/examples/serving/ray_serve/deploy.py new file mode 100644 index 0000000..78b2db0 --- /dev/null +++ b/examples/serving/ray_serve/deploy.py @@ -0,0 +1,87 @@ +"""Deploy a trained Ludwig model with Ray Serve. + +Usage: + # Single replica, CPU + python deploy.py --model_path ./results/my_model + + # Two GPU replicas + python deploy.py \ + --model_path ./results/my_model \ + --num_replicas 2 \ + --gpu + + # Custom name / port + python deploy.py \ + --model_path ./results/my_model \ + --name sentiment \ + --port 8080 + +After deploying, send predictions: + + # Single record + curl -s -X POST http://localhost:8000/ludwig \\ + -H "Content-Type: application/json" \\ + -d '{"text": "I love this product!", "stars": 5}' + + # Batch (list of records) + curl -s -X POST http://localhost:8000/ludwig \\ + -H "Content-Type: application/json" \\ + -d '[{"text": "great"}, {"text": "terrible"}]' +""" + +import argparse +import sys +import time + + +def parse_args(argv=None): + p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--model_path", required=True, help="Path to trained Ludwig model directory") + p.add_argument("--name", default="ludwig", help="Ray Serve application name (also URL prefix)") + p.add_argument("--num_replicas", type=int, default=1, help="Number of Ray Serve replicas") + p.add_argument("--gpu", action="store_true", help="Request 1 GPU per replica") + p.add_argument("--port", type=int, default=8000, help="Ray dashboard / Serve port") + p.add_argument("--block", action="store_true", help="Keep running until interrupted") + return p.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + + import ray + from ray import serve + + from ludwig.serve_ray_serve import deploy_ludwig_model + + ray_actor_options = {"num_gpus": 1} if args.gpu else {} + + print("Initialising Ray …") + ray.init(ignore_reinit_error=True) + + print(f"Deploying Ludwig model from {args.model_path!r} …") + deploy_ludwig_model( + model_path=args.model_path, + name=args.name, + num_replicas=args.num_replicas, + ray_actor_options=ray_actor_options, + ) + + url = f"http://localhost:{args.port}/{args.name}" + print(f"\nDeployment live at: {url}") + print(f" POST {url} with a JSON record or list of records to get predictions.") + + if args.block: + print("Press Ctrl-C to stop …") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nShutting down Ray Serve …") + serve.shutdown() + ray.shutdown() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/synthetic/train.py b/examples/synthetic/train.py new file mode 100644 index 0000000..e1dfcb6 --- /dev/null +++ b/examples/synthetic/train.py @@ -0,0 +1,26 @@ +"""Train a model from entirely synthetic data.""" + +import logging +import tempfile + +import yaml + +from ludwig.api import LudwigModel +from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df + +config = yaml.safe_load(""" +input_features: + - name: Pclass (new) + type: category + +output_features: + - name: Survived + type: binary + +""") + +df = build_synthetic_dataset_df(120, config) +model = LudwigModel(config, logging_level=logging.INFO) + +with tempfile.TemporaryDirectory() as tmpdir: + model.train(dataset=df, output_directory=tmpdir) diff --git a/examples/tabnet/higgs/medium_config.yaml b/examples/tabnet/higgs/medium_config.yaml new file mode 100644 index 0000000..b03b15c --- /dev/null +++ b/examples/tabnet/higgs/medium_config.yaml @@ -0,0 +1,85 @@ +input_features: + - name: lepton_pT + type: number + - name: lepton_eta + type: number + - name: lepton_phi + type: number + - name: missing_energy_magnitude + type: number + - name: missing_energy_phi + type: number + - name: jet_1_pt + type: number + - name: jet_1_eta + type: number + - name: jet_1_phi + type: number + - name: jet_1_b-tag + type: number + - name: jet_2_pt + type: number + - name: jet_2_eta + type: number + - name: jet_2_phi + type: number + - name: jet_2_b-tag + type: number + - name: jet_3_pt + type: number + - name: jet_3_eta + type: number + - name: jet_3_phi + type: number + - name: jet_3_b-tag + type: number + - name: jet_4_pt + type: number + - name: jet_4_eta + type: number + - name: jet_4_phi + type: number + - name: jet_4_b-tag + type: number + - name: m_jj + type: number + - name: m_jjj + type: number + - name: m_lv + type: number + - name: m_jlv + type: number + - name: m_bb + type: number + - name: m_wbb + type: number + - name: m_wwbb + type: number +output_features: + - name: label + type: binary + weight_regularization: null +combiner: + type: tabnet + size: 32 # N_a + output_size: 96 # N_d + sparsity: 0.000001 # lambda_sparse + bn_virtual_divider: 32 # factor to divide batch_size B to get B_v from the paper + bn_momentum: 0.1 # m_B + num_steps: 8 # N_steps + relaxation_factor: 2 # gamma + bn_virtual_bs: 256 # B_v +trainer: + batch_size: 8192 # B + eval_batch_size: 500000 # 65536 131072 262144 524288 + epochs: 1000 + early_stop: 20 + learning_rate: 0.025 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 10000 + decay_rate: 0.9 + staircase: true + validation_field: label diff --git a/examples/tabnet/higgs/small_config.yaml b/examples/tabnet/higgs/small_config.yaml new file mode 100644 index 0000000..e7412dc --- /dev/null +++ b/examples/tabnet/higgs/small_config.yaml @@ -0,0 +1,85 @@ +input_features: + - name: lepton_pT + type: number + - name: lepton_eta + type: number + - name: lepton_phi + type: number + - name: missing_energy_magnitude + type: number + - name: missing_energy_phi + type: number + - name: jet_1_pt + type: number + - name: jet_1_eta + type: number + - name: jet_1_phi + type: number + - name: jet_1_b-tag + type: number + - name: jet_2_pt + type: number + - name: jet_2_eta + type: number + - name: jet_2_phi + type: number + - name: jet_2_b-tag + type: number + - name: jet_3_pt + type: number + - name: jet_3_eta + type: number + - name: jet_3_phi + type: number + - name: jet_3_b-tag + type: number + - name: jet_4_pt + type: number + - name: jet_4_eta + type: number + - name: jet_4_phi + type: number + - name: jet_4_b-tag + type: number + - name: m_jj + type: number + - name: m_jjj + type: number + - name: m_lv + type: number + - name: m_jlv + type: number + - name: m_bb + type: number + - name: m_wbb + type: number + - name: m_wwbb + type: number +output_features: + - name: label + type: binary + weight_regularization: null +combiner: + type: tabnet + size: 24 # N_a + output_size: 26 # N_d + sparsity: 0.000001 # lambda_sparse + bn_virtual_divider: 32 # factor to divide batch_size B to get B_v from the paper + bn_momentum: 0.4 # m_B + num_steps: 5 # N_steps + relaxation_factor: 1.5 # gamma + bn_virtual_bs: 512 # B_v +trainer: + batch_size: 16384 # B + eval_batch_size: 500000 # 65536 131072 262144 524288 + epochs: 1000 + early_stop: 20 + learning_rate: 0.02 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 20000 + decay_rate: 0.9 + staircase: true + validation_field: label diff --git a/examples/tabnet/higgs/train_higgs_medium.py b/examples/tabnet/higgs/train_higgs_medium.py new file mode 100644 index 0000000..60b1f6a --- /dev/null +++ b/examples/tabnet/higgs/train_higgs_medium.py @@ -0,0 +1,12 @@ +import logging + +from ludwig.api import LudwigModel +from ludwig.datasets import higgs + +model = LudwigModel( + config="medium_config.yaml", + logging_level=logging.INFO, +) + +higgs_df = higgs.load() +model.train(dataset=higgs_df, experiment_name="higgs_medium", model_name="higgs_tabnet_medium") diff --git a/examples/tabnet/higgs/train_higgs_small.py b/examples/tabnet/higgs/train_higgs_small.py new file mode 100644 index 0000000..827f240 --- /dev/null +++ b/examples/tabnet/higgs/train_higgs_small.py @@ -0,0 +1,12 @@ +import logging + +from ludwig.api import LudwigModel +from ludwig.datasets import higgs + +model = LudwigModel( + config="small_config.yaml", + logging_level=logging.INFO, +) + +higgs_df = higgs.load() +model.train(dataset=higgs_df, experiment_name="higgs_small", model_name="higgs_tabnet_small") diff --git a/examples/timeseries_forecasting/nbeats_example.yaml b/examples/timeseries_forecasting/nbeats_example.yaml new file mode 100644 index 0000000..324596f --- /dev/null +++ b/examples/timeseries_forecasting/nbeats_example.yaml @@ -0,0 +1,20 @@ +model_type: ecd +input_features: + - name: history + type: timeseries + window_size: 128 + encoder: + type: nbeats + num_stacks: 2 + num_blocks: 3 + layer_size: 256 + output_size: 256 +output_features: + - name: forecast + type: timeseries + window_size: 24 +trainer: + epochs: 50 + optimizer: + type: adamw + lr: 1e-4 diff --git a/examples/timeseries_forecasting/patchtst_example.yaml b/examples/timeseries_forecasting/patchtst_example.yaml new file mode 100644 index 0000000..228b7a4 --- /dev/null +++ b/examples/timeseries_forecasting/patchtst_example.yaml @@ -0,0 +1,22 @@ +model_type: ecd +input_features: + - name: history + type: timeseries + window_size: 128 + encoder: + type: patchtst + patch_size: 16 + patch_stride: 8 + d_model: 128 + num_heads: 8 + num_layers: 3 + output_size: 256 +output_features: + - name: forecast + type: timeseries + window_size: 24 +trainer: + epochs: 50 + optimizer: + type: adamw + lr: 1e-4 diff --git a/examples/titanic/README.md b/examples/titanic/README.md new file mode 100644 index 0000000..14983a5 --- /dev/null +++ b/examples/titanic/README.md @@ -0,0 +1,42 @@ +# Kaggle Titanic Survivor Prediction + +This API example is based on [Ludwig's Kaggle Titanic example](https://ludwig-ai.github.io/ludwig-docs/examples/#kaggles-titanic-predicting-survivors) for predicting probability of surviving. + +### Preparatory Steps + +Create and download your [Kaggle API Credentials](https://github.com/Kaggle/kaggle-api#api-credentials). + +The Titanic dataset is hosted by Kaggle, and as such Ludwig will need to authenticate you through the Kaggle API to download the dataset. You will also need to join [the competition](https://www.kaggle.com/c/titanic) to enable downloading of the data. + +### Examples + +| File | Description | +| ---------------------------- | ------------------------------------------------------------------------------ | +| simple_model_training.py | Demonstrates using Ludwig api for training a model. | +| multiple_model_training.py | Trains two models and generates a visualization for results of training. | +| model_training_results.ipynb | Example for extracting training statistics and generate custom visualizations. | + +Enter `python simple_model_training.py` will train a single model. Results of model training will be stored in this location. + +``` +./results/ + simple_experiment_simple_model/ +``` + +Enter `python multiple_model_training.py` will train two models and generate standard Ludwig visualizations comparing the +two models. Results will in the following directories: + +``` +./results/ + multiple_model_experiment_model1/ + multiple_model_experiment_model2/ +./visualizations/ + learning_curves_Survived_accuracy.png + learning_curves_Survived_loss.png +``` + +This is the standard Ludwig learning curve plot from training the two models +![](../images/learning_curves_Survived_accuracy.png) + +This is the custom visualization created by the Jupyter notebook `model_training_results.ipynb`. +![](../images/custom_learning_curve.png) diff --git a/examples/titanic/model1_config.yaml b/examples/titanic/model1_config.yaml new file mode 100644 index 0000000..00ac815 --- /dev/null +++ b/examples/titanic/model1_config.yaml @@ -0,0 +1,23 @@ +input_features: + - name: Pclass + type: category + - name: Sex + type: category + - name: Age + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: SibSp + type: number + - name: Parch + type: number + - name: Fare + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: Embarked + type: category + +output_features: + - name: Survived + type: binary diff --git a/examples/titanic/model2_config.yaml b/examples/titanic/model2_config.yaml new file mode 100644 index 0000000..7a948c3 --- /dev/null +++ b/examples/titanic/model2_config.yaml @@ -0,0 +1,32 @@ +input_features: + - name: Pclass + type: category + - name: Sex + type: category + - name: Age + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: SibSp + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: Parch + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: Fare + type: number + preprocessing: + missing_value_strategy: fill_with_mean + normalization: zscore + - name: Embarked + type: category + +output_features: + - name: Survived + type: binary + fc_layers: [{ output_size: 50 }] diff --git a/examples/titanic/model_training_results.ipynb b/examples/titanic/model_training_results.ipynb new file mode 100644 index 0000000..da197b3 --- /dev/null +++ b/examples/titanic/model_training_results.ipynb @@ -0,0 +1,373 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Custom Analysis of Training Results\n", + "\n", + "Notebook demonstrates two methods for plotting training results. First method uses Ludwig's visualization api. Second method illustrates converting Ludwig training statistics into a pandas dataframe and plotting data with seaborn package.\n", + "\n", + "This notebook is dependent on running the multiple model training example beforehand. To run the mulitple model training example, enter this command:\n", + "``` \n", + "python multiple_model_training.py\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import required libraries" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "import os.path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "\n", + "from ludwig.utils.data_utils import load_json\n", + "from ludwig.visualize import learning_curves" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate Annotated Learning Curves Using Ludwig Visualization API" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAagAAAEYCAYAAAAJeGK1AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOzdeXxU1d348c+ZLTsJhJ0AIezIqlDRuiBiiywi4to+tq64UXBpK60/bB8fxaVWC1hpre1T20ctakFFsFXcLQoSiAoGBMKWsCUhezL7+f1x7ywJk5CETJbJ9/1yXjNz77n3njnB+c459yxKa40QQgjR3ljaOgNCCCFEJBKghBBCtEsSoIQQQrRLEqCEEEK0SxKghBBCtEsSoIQQQrRLEqBEzFJKna+U2tXW+eislFJvK6V+HIXz/lUp9XBLn1e0PxKgRFQopfYrpaa1ZR601p9orYdH6/xKqe8rpT5WSlUopQqVUh8ppS6L1vVaglJqjlIqRylVrpQqUkq9r5QaFI1raa0v1Vq/EI1zi85BApTosJRS1ja89pXAq8DfgAygF/AgMLsZ51JKqaj/v6iUGoKR3/uAVGAQ8HvA14xz2Vo2d0KcTAKUaFVKKYtSarFSaq9Sqlgp9YpSqlvY/leVUkeVUmVm7eSMsH1/VUqtVEqtV0pVAReZNbWfKqW+Mo9ZpZSKN9NPUUrlhx1fb1pz/8+VUkeUUoeVUrcopbT5pV73MyjgKeB/tNbPa63LtNZ+rfVHWutbzTS/Vkr9X9gxmeb5bOb7D5VSjyil/gNUAz9TSm2pc517lFJvmq/jlFJPKqUOKqWOKaX+oJRKMPd1V0q9pZQqVUqdUEp9Uk/AGw/s01q/pw0VWut/aq0PhpXvw2HXj1R+9yulvgKqzNev1cnzMqXU8rDPeIuZ91Kl1OiwdD2UUjVKqZ7m+1lmza5UKbVRKTU2LO0EpdRWs6a6CohHdAoSoERr+wlwOXAh0BcowfgVH/A2MBToCWwFXqxz/A+AR4AU4FNz29XAdIwawVjghgauHzGtUmo6cC8wDRgCTGngHMOB/sBrDaRpjOuB+Rif5Q/AcKXU0LD9PwBeMl8/BgzDCDJDgH4YNTYwakT5QA+MmtwvgUhzmG0FRiilnlZKXaSUSm5Gnq8DZgJpwD+AGUqpFAjWaK8OyzMAWmsXsNo8NuBq4COt9XGl1ATgL8BtQDrwR+BNM7A5gNeBvwPdMGqt85qRb9EBSYASre124AGtdb75xfVr4MpAzUJr/Rfzl31g3zilVGrY8W9orf9j1lic5rblWuvDWusTwFqML/H61Jf2auB/tdY7tNbV5rXrk24+H2nsh67HX83rebXWZcAbmF/iZqAagfFFrTAC2T1a6xNa6wpgKXCteR4P0AcYqLX2mPfeTgpQWus8jMDbD3gFKDJrTU0JVMu11oe01jVa6wMYQW+uuW8qUK21/jzCcS+F5RdqB9/5wB+11pu01j7zvpULmGw+7MDvzM/2GvBFE/IrOjAJUKK1DQTWmE05pUAuxj2QXkopq1LqMbP5rxzYbx7TPez4QxHOeTTsdTXQ0BdufWn71jl3pOsEFJvPfRpI0xh1r/ESoVrGD4DXzWDZA0gEssPK7V/mdoDfAHuAd5RSeUqpxfVdUGv9udb6aq11D+B84ALggRbM80tE9gGQqJQ6WymVifHDYI25byBwX+CzmZ+vP8bfpC9QUCfgHmhCfkUHJgFKtLZDwKVa67SwR7zWugDjC24ORjNbKpBpHqPCjo/W9PtHMDo7BPRvIO0ujM/RUFNTFUZQCegdIU3dz/Iu0EMpNR7jSz/wZV8E1ABnhJVZqtY6GcCscd6ntc4CLgPuVUpd3EDeMI/7AqPpLXBvqDl5fhWYopTKwKhJRQxQWmsfRq3tOvPxllkTBKMsH6nzbyJRa/0yxt+ln1mLDBhwqs8mYoMEKBFNdqVUfNjDhnGv5RGl1EAI3iyfY6ZPwWjaKcb4olzainl9BbhRKTVSKZUILKkvoflr/l5giVLqRqVUF2V0/jhPKfWcmSwHuEApNcBsovzFqTKgtfZgfOH/BuN+y7vmdj/wJ+DpsE4F/ZRS3zdfz1JKDTG/xMswaqT+uuc383dr2DlGYAS0QJNcDsY9pW5Kqd7A3Y3IcyHwIfC/GB0wchtI/hJwDfBDageyPwG3m7UrpZRKUkrNNO9tfQZ4gYVKKbtS6grgO6fKl4gNEqBENK3H+OUfePwaWAa8idEcVYHx5Xi2mf5vGM03BcA3hL44o05r/TawHKMpak/YtV31pH8N48v2JuAwcAx4GOM+Elrrd4FVwFdANvBWI7PyEkYN8lWttTds+/2BfJnNnxswOmuA0alkA1CJ8YX+rNb6gwjnLsUISF8rpSoxmgnXAE+Y+/8OfInRtPqOmf+m5Lm+5j0AtNabMGppfTE6wwS2bwFuBZ7B6DSzB7PzitbaDVxhvj+BUearG5kv0cEpWbBQiJMppUYC24G4OoFCCNFKpAYlhEkpNdfs2twVeBxYK8FJiLYjAUqIkNuA48BejPs4d7RtdoTo3KSJTwghRLskNSghhBDtUrua8LF79+46MzOzScfU1NSQkJAQnQx1cFI2kUm51E/KJjIpl8haqlyys7OLzMHjtbSrAJWZmcmWLVtOnTBMTk4O48c3NLNN5yVlE5mUS/2kbCKTcomspcpFKRVxdhBp4hNCCNEuSYASQgjRLkmAEkII0S61q3tQQojOw+PxkJ+fj9PpPHXiNqaUIje3oWkGO6emlkt8fDwZGRnY7fZGpZcAJYRoE/n5+aSkpJCZmUntycrbn+rqahITE0+dsJNpSrlorSkuLiY/P59BgwY16hhp4hNCtAmn00l6enq7D06iZSilSE9Pb1KNObYClM8D+qRVBoQQ7ZQEp86lqX/v2GniqzoO2c8DGibdCQld2zpHQgghTkPs1KAKc8FdAe5KOJLd1rkRQnQyU6dO5cSJE41O84tf/IJzzjmHWbNm1Zt+w4YN7Nmzp8l5ee+993juuecaTHPs2DEWLlzY5HO3ptgJUFZH6LWrov50QgjRDlxxxRU8//zzDaZpKEB5vfWvBHPxxRczf/78Bs/dq1cvli9ffuqMtqHYaeJzpIReS4ASQpxCfn4+t9xyC+PHj2fbtm2MHj2aefPmsXz5ck6cOMGTTz7J2LFjKS0t5f777+fw4cMkJCTw0EMPMWLECEpKSrjvvvs4duwY48ePJ3xliDfeeIO///3veDwexo0bx69+9SusVmut60+aNIn8/Px687d161bef/99Nm/ezMqVK1mxYgUPPPAAI0aMIDs7m1mzZpGZmcnKlSvxeDykpaXx5JNP0r17d1avXs327dt58MEHWbx4McnJyWzfvp3CwkJ+9rOfMX36dPLz87n99tt56623WL16Ne+//z41NTUcOnSIadOm8fOf/xyAV199leeff56UlBRGjBiBw+HgwQcfjM4fpY7YCVBxyaHX7sq2y4cQosn+9HEev9vwLVVuX4udM8lh5e5pw7j1gqx60xw8eJBly5axdOlSrrzyStauXcvLL7/Me++9xx/+8AeeffZZVqxYwYgRI/jjH//IZ599xv33388bb7zB73//e84880wWLFjAhx9+yGuvvQbA3r17efvtt3n55Zex2+38+te/Zu3atVx++eVNyv+ZZ57J1KlTmTJlCtOnTw9u93g8rF5trHpfVlbGK6+8glIqGEgWL1580rmOHz/OSy+9RF5eHnfccUet8wXk5uby+uuv43A4mD59Otdffz0Wi4WVK1eyevVqkpKS+PGPf8yIESOa9DlOR9QClFJqOLAqbFMW8KDW+nfRuF6RM47u5mtPTTmNGwYmhGgP/vRJXosGJ4Aqt48/fZLXYIDKyMhg+PDhAAwZMoRzzjkHpRTDhw+noKAAgOzsbJ544gkAzjnnHEpLS6msrOSLL77gmWeeAWDKlCmkpqYC8Nlnn7F9+3auvPJKINSdvqXMmDEj+Pro0aPcc889FBYW4na7ycjIiHjMtGnTsFgsDBkyhKKioohpzjnnHFJSjJaowYMHU1BQQGlpKZMmTSItLQ2A6dOns3///hb7LKcStQCltd4FjAdQSlmBAmBNtK7nUknB1xZPJaWVbtKSHQ0cIYRoL249PysqNahbz68/OAE4HKHvCIvFEnyvlMLna15etNbMnTuX++67r1nHn0r48hYPP/wwN9xwAxdffDGbNm0KBsy6wj9nfcLTWK3WZn/+ltRaTXwXA3u11hGnVG8JvXuk4FV2bNqDFR9bdx3n/HF9sdtipx+IELHq1guyGqzptKWJEyeyfv16hg0bxqZNm+jatSvJyclMmjSJtWvXcuedd/LRRx9RVlYGGDWRO++8kxtuuIH09HRKS0upqqqiX79+Tb52UlISVVVV9e6vqKigV69eALz++uvN+4ANGDNmDEuXLqWsrIykpCTeeecdhg0b1uLXqU9rBahrgZcj7VBKzQfmA/Tp04ecnJwmnbi4uDh4zAgVh017APC5Kvhg60F62EvprGMBw8tGhEi51K81y0YpRXV1datcKxKn04nf7w/mwev14nK5qK6urrXv5ptv5le/+hWzZs0iPj6eX//611RXV3PTTTfxi1/8grVr1zJu3Dh69+5NTU0Nffv25Y477uCGG25Aa43NZmPx4sV07doVv99PTU0N1dXVLF68mOzsbEpLSzn//PO5/fbbmTt3bq08XnzxxTz00EO88MIL/OY3v8Hn8+F0OoN5vvXWW1m4cCFdunRh0qRJ+Hw+qqurcbvdeL1eqqura30uMGp4dT9jeHoAn8+Hy+UiJSWFG2+8kXnz5pGamkpmZibx8fG1yqypf0O3293of2MqvOdJNCilHMBh4Ayt9bGG0k6cOFGf1oKFX/wByoxK2ifx8yiy9WdcZgpZvTvnHFqyyFpkUi71a82yyc3NZeTIka1yrdPVmefiq6qqIikpCa/Xy4IFC5g3bx6XXHIJ0LxyifR3V0pla60n1k3bGjWoS4GtpwpOLaHMH0+q+TpOG1F9+8EKeqXFkRRvrf9AIYQQET3zzDNs3LgRl8vFeeedx7Rp01rt2q0RoK6jnua9lrRxTxG7vynnx/2N92l2JwWAzw85+8o5d0SazPslhBBNdP/997fZtaPag0AplQRcAqyO5nUAissqGODZF3yfYgu1ix4vc1NQ7Ip2FoQQQrSgqAYorXWV1jpda10WzesAfN+yiYt8nwXfb91ziIz0uOD7rw5U4PbKTOdCCNFRxEwfbEfVEfCEak1J1PDnz/cQbzc+osvjZ8dBmWFCCCE6ipgJUCT3BndN8G2PODf/2nGMI2FjCPYfr6G4wt0WuRNCCNFEsROgUnqBO1SD6uEwAtHSf31DckLoY27LK8fvj27XeiFE5xON5TaaasWKFfz5z38GYNmyZWzcuPGkNJs2beK2225r8Dy5ubl89NFHwfeNWb4jGmInQCX3qtXEl+7wYkHj9Wv+d1MeVovRg6+ixsfuI203OFAIIaBxy22cjkWLFnHuuec269i6Aaoxy3dEQ+zMZp7c21ju3eMEezxWpekZ7+Oo08bGvCJmnNGXJHPNqJ35lfRLjyM5PnY+vhCiadr7chsVFRVcdtllvPfee1gsFqqrq7n00kvZsGEDa9asYdWqVXg8HgYOHMgTTzxRa44+gMWLFwdnQv/4449ZunQpCQkJnHXWWcE0X331FY888ggul4v4+HiWLl1KRkYGy5cvx+l0kp2dzW233YbT6Qwu35Gfn88vf/lLSkpKSEtL4/HHH6dv3771LutxOmLnGzqhK1hsRjOfPR6An5zXmwc2GDP3PvrvHTw19yyqXX78Gr7cVyFjo4RoJ3YfrmJnfhXeFmx+t1kUIzKSGNo3qd407Xm5jcD6S5s3b2by5Ml8+OGHnHfeedjtdi655BKuvvpqAJ5++mlee+01rr/++ojncblcLFmyhBdeeIGBAwdy9913B/dlZWXx4osvYrPZ2LhxI08//TQrVqxg4cKFwYAEBJf3AGOC2rlz5zJ37lxeeuklHn74YZ599lmgcct6NEXsNPFZLCc18109No3hvYzp46vcPt76JvRr5XiZm/xiZ6tnUwhxsj1Hqls0OAF4/Zo9p2jODyy3EViKor7lNmbOnAmcvNzGnDlzgPqX25gzZw6fffYZhw4datZnmDFjBuvXrwdg3bp1waU2du/ezQ9+8ANmz57N2rVr2b17d73nyMvLIyMjg8zMTJRSXHbZZcF9FRUVLFq0iFmzZvHoo482eJ6Abdu2Be+bzZw5k+zs7OC+xizr0RSxE6DACFBhHSXs3ioemzcmOFns2q8P47eEppD/ar+MjRKiPRjSJxGbpWVbM2wWxZA+Dc8TF83lNt544w3eeOMN/v3vf/OTn/ykWeeaOnUqn376KaWlpezYsYPJkycDRvPdgw8+yNq1a1mwYAFud/N6Jy9btoyzzz6bt956i5UrVzb7PAGNWdajKWKniQ/MGlTYmGB3BRMyu3LjuYP4y3+MWSYef/cbfvX9sbi9GrdXs+NgJROyurRRhoUQAEP7NtwU15baermN0aNH88gjjzBlypTgfayqqip69OiBx+Nh7dq1wSU3IsnKyqKgoICDBw8yYMAA1q1bF9wXvlzHmjWh5foaWuZjwoQJrFu3jssvv5y3336biRNPmuO1xcRWDapOV3PcFQD89PvDyOhq3EA8XuFiS0FxMMn+4zUUlcvYKCFEZAsWLCA3N5fZs2fz29/+lsceewyAu+66iy1btjBz5kzeffdd+vbtCxgr8959993cdNNNzJ49m5tuuonCwsKTznvvvfdy7bXXsm/fPi644AJeffXViNefMWMGb775Zq2VdBctWsRVV13FddddR1ZWw+toxcXF8dBDDzF//nzmzp1Lt27dgvtuueUWnnrqKS6//HK8Xm9w+9lnn82ePXuYM2dOsIkxYMmSJaxevZrZs2ezbt06HnjggVOUYPNFfbmNpjjt5TY+WAq5r8Owi433vcfB6GsB+PjbQn70l83B41ZcOZHA3yMlwcrUMelYWriJoa3JshKRSbnUT5bbiKwzL7fRkGgvtxFbNag696BwhaY2umBYD644M1TFXvHxLqzmpzfGRtW/aqUQQojWF1sBKqV3xCa+gCUzR5GeZNzE+/Z4BftLQwFsZ34VlTVehBBCtA+xFaDqdDOvG6C6Jjl4cPao4PvffbATu81o1vNryNlfQXtq8hRCiM4sBgOU05hRAsBTA/7ataLLxvXlouE9ACMovZidF9xXKGOjhBCi3YixANUT0EZgCnDXXmJDKcXDc8eQ5DC6a362r5gyd2gxQxkbJYQQ7UNsBShbnDHlUa2OEhUnJeuXlsDPp48Ivn9ywzfYrEZTn9ur2X7g5GOEEEK0rtgKUGCuCxV+HyryIoX/NXkgZw5IA6DS7eOdXQXBfQcKnTI2SgjRJE1ZbuPIkSNcf/31zJgxg5kzZ/LCCy9ETL9hwwb27NnT5Lw0ZnmMY8eOsXDhwiafuzXFYIDqWaejROQAZbUoHp83FrtZc1q7/TBuHbpftS2vHJ+sGyWEiAKr1crixYtZv349q1at4qWXXooYiBoKUOEDa+tqzPIYvXr1Yvny5U3LeCuLramOwOhqXnYg9D5CE1/A0F4p3HXREH63wZgg8en3d/KLS0bj90Ol08fuw1WMyEiOdo6FEG2gLZfb6NmzJz179gQgOTmZrKwsjh07xpAhQ4Jptm7dyvvvv8/mzZtZuXIlK1as4IEHHmDEiBFkZ2cza9YsMjMzWblyJR6Ph7S0NJ588km6d+/O6tWrg7OR17cMRn5+PrfffjtvvfUWq1ev5v3336empoZDhw4xbdo0fv7znwPw6quv8vzzzwdnV3c4HMFZzqMtqgFKKZUGPA+MBjRwk9b6s2hek+ReUJQbeu9u+H7SHVMGs+6rI+w+XsnRCic5h08wtrcxFciugir6pceTkhB7cVyIdmXjCvjwsXpbPJrFkQxTFsO59U/U2h6W28jPzyc3N5dx48bV2n7mmWcyderU4JpOAR6PJ7j8RVlZGa+88gpKqWAgWbx48UnXaMwyGLm5ubz++us4HA6mT5/O9ddfj8ViYeXKlaxevZqkpCR+/OMfM2LEiJOOjZZoN/EtA/6ltR4BjANyT5H+9NWdTeIU/+DjbFYemzc2OOP5Xz/PA2X8EgqsGyVjo4SIso3PtGxwAuN8G59pMElbL7dRVVXFwoUL+eUvf0lycuNaa8Ln5Dt69Cg333wzs2fP5vnnn693uYzGLINxzjnnkJKSQlxcHIMHD6agoICvv/6aSZMmkZaWht1uP+31nZoqagFKKZUKXAD8GUBr7dZal0brekEpvWvfg2qgiS/grIFd+fE5mYBRzfvTZ6E/cmG5m0NFMjZKiKg6d4FR42lJjmTjvA0lacPlNjweDwsXLmT27Nl873vfa/T5w1fOffjhh/nhD3/I2rVreeihh+pdLqMxy2CEp7Farc3+/C0pmm1Xg4BC4H+VUuOAbGCR1rrWpHdKqfnAfIA+ffqQk5PTpIsUFxfXOia5sJwhYTUoV0UxuY045/f7+Hkr0UpRtY9vjpaz53gRQ3p2B2Db3hIK87/FqjpWTapu2QiDlEv9WrNslFJUV5v/r46/2XhEQ3XkRQudTid+vz+YB6/Xi8vlorq6uta+cePG8dZbb3H77bezZcsWUlNTsVgsjB8/ntWrV3Prrbfy6aefUlZWRk1NDePHj+eee+7hmmuuoVu3bpSVlVFVVUXfvn3x+/3U1NRQVVXFkiVLGDBgANdcc02oHOpwOByUlJQE9/t8PpxOZ/B9WVkZqampVFdX89prr+Hz+aiursbtduP1eqmurq71ucAIoHU/Y3j6wHVcLhdDhgzh4Ycf5ujRoyQmJvL2228zdOjQWmVWX97r43a7G/1vLJoBygacCfxEa71JKbUMWAwsCU+ktX4OeA6M2cybOpPySbMvFybC56ECi1PuRs/O/JuU49z41y8A+NOmg/xmTnf8fvBjhS6ZjB+c2qS8tTWZtTsyKZf6tfZs5m05Q3h8fDwWiyWYB5vNRlxcHImJibX23XPPPdx///1cc801JCQk8MQTT5CYmMjdd9/Nfffdx1VXXcWECRPo27cvCQkJjBkzhnvvvZe77roLv9+P3W7nwQcfJDExEYvFQkJCArm5uaxbt45hw4Zx3XXXAcbyGxdeeGGtPM6ZM4clS5awatUqli9fjtVqJT4+PpjnhQsXcv/995OamsrZZ58dDCQOhwObzUZiYmKtzwXGD4O6nzE8PRg1qLi4ODIzM7njjjv40Y9+RGpqKllZWXTt2jWYrjmzmTscjkbPYh+15TaUUr2Bz7XWmeb784HFWuuZ9R1z2sttADjL4LEBcO5tYDF7zVz032Bt3EqPi/6xjTdyDgNw/uAeXDFmYHDf+aO60r1Ly64YGU3yRRyZlEv9ZLmNyDrzchtVVVUkJSXh9XpZsGAB8+bN45JLLgE68HIbWuujwCGl1HBz08XAN9G6XlBcF7DF157uyNP4KuiDs0bRNdEOwCd7Cyl1haZBkrFRQojO5plnnmHOnDnMmjWLjIwMpk2b1mrXjnb/6Z8ALyqlHEAecGOUrwdKGT35vE6IM2+6uqsgPq1Rh6cnx7Fk1ijufeVLAJZ9uJNfTx+H1sbYqG8PVzFSxkYJITqJ+++/v82uHdVu5lrrHK31RK31WK315VrrkmheLyi5V50aVNMWI5w7oR8XDDNmPC+t8fDR3qPBfd8WVFEh60YJIUTUxd5URwAp5rIbAU1o4gPjJuLSuaNJNGc8f/2rfFxml0u/hpx95TI2Sgghoiw2A1Ry79oByt305dwzuiby0+8Zt8808Oynu4L7iso9HJSxUUIIEVUxGqDMe1ABTWziC/jxuZmM72/cuzpYUs2OY6EWyu0HKnB5ZN0oIYSIltgMUCl17kG5m9bEFxCY8dxmMeZB+tvmffjN1XrdXs32g7JulBDCEI3lNppqxYoV/PnPfwZg2bJlbNy48aQ0mzZt4rbbbmvwPLm5uXz00UfB941ZviMaYjNAJde9B9W8GhTA8N4p3DllMABun5//27IvuO9goZPCMlk3SgjRNI1dbuN0LFq0iHPPPbdZx9YNUI1ZviMaYnOa7rq9+JpxDyrcXVOHsO7rI+wtrGJbQQnThlfRt0sSYHSYmDo2HatZyxJCdAztfbmNiooKLrvsMt577z0sFgvV1dVceumlbNiwgTVr1rBq1So8Hg8DBw7kiSeeqDVHH8DixYuDM6F//PHHLF26lISEBM4666xgmq+++opHHnkEl8tFfHw8S5cuJSMjg+XLl+N0OsnOzua2227D6XQGl+/Iz8/nl7/8JSUlJaSlpfH444/Tt2/fepf1OB2xGaBSete5B9W8Jr6AOJuVx+eN5co/GCuF/HHjHv57+lhAGWOjCqoY2V/GRgnRbAc+gbwN4GvBFgmrA7KmwcDz603SnpfbCKy/tHnzZiZPnsyHH37Ieeedh91u55JLLuHqq68G4Omnn+a1117j+uuvj3h+l8vFkiVLeOGFFxg4cCB33313cF9WVhYvvvgiNpuNjRs38vTTT7NixQoWLlwYDEhAcHkPMCaonTt3LnPnzuWll17i4Ycf5tlnnwUat6xHU8RmE19id/CEZoA4nSa+gImZ3bh+sjHtUbnTw9u5h4P7vj0sY6OEOC0HPmnZ4ATG+Q580mCS9r7cxowZM1i/fj0A69atCy61sXv3bn7wgx8we/Zs1q5dW+8yGwB5eXlkZGSQmZmJUorLLrssuK+iooJFixYxa9YsHn300QbPE7Bt2zZmzZoFwMyZM8nOzg7ua8yyHk0RmzUoq82Y7ijAXQVaE1z0qZl+Pn04G3KPcaTMybu7jnBeVg9S4hzBsVHnjeyKOs1rCNEpDTw/OjWoBmpPEN3lNu67774G0zVmuY2pU6fy9NNPU1payo4dO5g8eTJgNN89++yzjBgxgtWrV7N58+Zm5XXZsmWcffbZ/P73vyc/P58f/ehHzTpPQGOW9WiK2AxQAIndwOsGmwPQRpOfPeGUhzUkJd7Ow5eP5uYXtqCBP2zczc8uOgMwxkYdKnIyoMfpXUOITlI3LAAAACAASURBVGng+acMJm1l4sSJrF+/nmHDhrFp0ya6du1KcnIykyZNYu3atdx555189NFHlJWVAUYt68477+SGG24gPT2d0tJSqqqq6NevX/CcWmseeOABsrKyuPHG+meAS0pKYvTo0TzyyCNMmTIleB+rqqqKHj164PF4WLt2Lb169ar3HFlZWRQUFHDw4EEGDBjAunXrgvsqKiqCx65Zs6bWdauqIrc8TZgwgXXr1nH55Zfz9ttvM3HiSXO8tpjYbOIDo5nP2/zpjupz8chezBrbB4DDZTV8cShUjf1axkYJEXMWLFhAbm4us2fP5re//S2PPfYYAHfddRdbtmxh5syZvPvuu/Tt2xeAIUOGcPfdd3PTTTcxe/ZsbrrpJgoLC2udMzs7mzfeeIPPP/+cOXPmMGfOnFq95sLNmDGDN998s9ZKuosWLeKqq67iuuuuIysrq8H8x8XF8dBDDzF//nzmzp1Lt27dgvtuueUWnnrqKS6//HK83tBtirPPPps9e/YwZ86cYBNjwJIlS1i9ejWzZ89m3bp1PPDAA40oxeaJ2nIbzdEiy20ErPovcDiMMVEAE2+HtIEnp2uGwgoX0576iLIaDw6rhf+ZMQ6H+ctmQI94zmon60bJshKRSbnUT5bbiKwzL7fRkA673EabS+x+WvPxNaRHShz/b6ZRwG6fn799kRfcJ2OjhBCiZcRugErq3qJjoeq68qwMzhtiLAm/42gZecWhWSVy9sm6UUIIcbpiN0Aldm+R+fjqY8x4PoZ4u1GEf/siD7/ZXFrp9LH7cMteT4hY1J5uMYjoa+rfO3YDVFL0mvgCBqQnct8lxoznZU4Pb+7ID+7bJetGCdGg+Ph4iouLJUh1ElpriouLiY+PP3ViUwx3M0+PahNfwI3fzeTNLw/zdUEZH+85xvlZPUhPjJexUUKcQkZGBvn5+Sf1cGuP3G53i4/xiQVNLZf4+HgyMjIanT52A9RJNajoBCib1cJj88Zw2TP/wefX/GXTXn560SgUKrhu1EAZGyXESex2O4MGDWrrbDRKTk5Oh+lx2JqiXS6x28QXpXFQkZzRN5X5FxhjEQ6X1bBxX+gXoawbJYQQzRPDASq9zqq6LX8PKtyii4cyqLsxw/mb2/Op8Rj3n2TdKCGEaJ7YDVA2Byh76L27MqqXi7dbefSKMcalIqwbVVQuY6OEEKIpohqglFL7lVJfK6VylFJNmyKiJcSngLkCLj4X+Js3+WNjTc5K57rv9Afgm2Nl7DxeFty3LU/GRgkhRFO0Rg3qIq31+EjTWERdYnfwhi+7Ed1mPoDFl46kZ0ocAP/Yuh+f3wiQMjZKCCGaJnab+ODk+1BR7CgRkJpg56E5owFjbNTrX4fWgZGxUUII0XhRnSxWKbUPKAE08Eet9XMR0swH5gP06dPnrLoz555KcXEx6enpEff13/o46akOSDVmGd7T5SIqHT2bdP7meuzTYj7Pd6KAn100kj6pRgeKeIuL3vYTp7s0VaM0VDadmZRL/aRsIpNyiaylymXChAkRJ4uNdoDqp7UuUEr1BN4FfqK1/ri+9C06mznAu7+C4p3Q3ZyOfswPoNeYJp2/uY6VO5n21EdUOL30TU3gp1NGBQfsnjW4S6usGyWzdkcm5VI/KZvIpFwia6lyaZPZzLXWBebzcWAN8J1oXu8kSa03FqquXl3i+eUMYwDb4bIaPtp7LLhP1o0SQohTi1qAUkolKaVSAq+B7wHbo3W9iOouuRGl6Y7qc83E/pw9yFgc7O3cw1S6PEY2vJodMjZKCCEaFM0aVC/gU6XUl8BmYJ3W+l9RvN7J6i650Qq9+MJZLIrH5o3FYbPg9vl5eev+4L4DMjZKCCEaFLUApbXO01qPMx9naK0fida16nXSbBKt3817UPck7p42FDDGRn19pCS4T8ZGCSFE/WK7m3lSdNeEaqxbz89iZJ8uAPzzy4N4fDI2SgghTiW2A1QUl31vCrvVwuPzxmBR5rpR22VslBBCnEpsByhHojECKyDK8/E1ZGxGGjefZywt8J99hRSUGcEysG6ULNomhBC1xXaAAnAkhV67q6ANA8G9lwxnQLdENPDS1n3BJeKLyj0cKnI2fLAQQnQysR+gEtLAbzahaR/4PW2XFYeVpXONgcKHy2r4aI+MjRJCiPrEfoBq47FQdZ03tDtXnmUsefyvnYcpcxpdzWXdKCGEqC32A9RJS7+3TUeJcP9v5ki6J8fh9vl5ZduB4PaDhU4Ky2RslBBCQGcIUInptQfrtmFHiYC0RAf/fdkZgDE2KqfgRHBfzj4ZGyWEENAZAtRJY6HavgYFMGNMby4Z1QuANV8fwu01FlOsdPr4VsZGCSFEJwhQiXWnO2ofX/5KKf5nzmhS4myUOz28uSM/uO9bGRslhBCdIUC1/XRH9emdGs/iGSMA2LivkIMlRt5kbJQQQnSGAHXShLHtJ0ABXDdpAN8Z1A0NrMrZX2ts1MFCGRslhOi8Yj9AteMaFJgznl8xBofNwuGyGj4MGxu1/aCMjRJCdF6xH6BOWrSwfXSSCJfVI5lFFxsznv9752FKql2AOTbqgIyNEkJ0TrEfoOK6gDesw4GrfX7hz7/AmPHc7fPz6pcHg9sPFsnYKCFE5xT7AUopY9LYgHYwDiqS8BnPc+uMjdomY6OEEJ1Q7AcoAEdK6LXPBX5f2+WlAeEznq/56hBOj5HPKqePXQXt696ZEEJEW+cIUEnp7W66o/oEZjwvd3lYGz426nAV5TI2SgjRiXSOANVOB+tGkuCw8ugVxoznn+0vZP8Jo0lSa8jJk7FRQojOo3MEqLpjodpZV/O6vjukO1dPzEADr+QcCN5/Kq7wcEDGRgkhOomoByillFUptU0p9Va0r1Wvk5Z+b98BCuCBGaPokRLHkfIaPtxzNLh9u6wbJYToJFqjBrUIyG2F69Svg9WgAFIT7Txkznj+711HKK4yxkZ5fJqvZWyUEKITaFSAUkotUkp1UYY/K6W2KqW+14jjMoCZwPOnm9HTktSjw9yDCnfpmD58/4xeeHx+XvsytG7UoSInx0tdbZgzIYSIPlsj092ktV6mlPo+0BW4Hvg78M4pjvsd8HMgpb4ESqn5wHyAPn36kJOT08gsGYqLi095TGJxKcPCltwoPHyAgvKmXaetXDNE8cm3ip3Hy9maX8yZGekAbNpVRD9HIRZV/7GNKZvOSMqlflI2kUm5RBbtcmlsgAp8Dc4A/q613qGUauCrEZRSs4DjWutspdSU+tJprZ8DngOYOHGiHj9+fCOzZMjJyeGUxxSnwM7fBd/2SE2gx5imXactLbEe5Berv2bN14cY0TOVRIcNr7YRlz6YMwbUG/sbVzadkJRL/aRsIpNyiSza5dLYe1DZSql3MALUv5VSKcCp7tR/F7hMKbUf+AcwVSn1f83O6elI6tGuJ4w9lWsn9WdyVjcqXd5aY6N2H6mmrNrThjkTQojoaWyAuhlYDEzSWlcDduDGhg7QWv9Ca52htc4ErgXe11r/1+lkttniUsDX/ufjq49SiseuGEuczcKmA0XkFRn5N8ZGVcjYKCFETGpsgDoH2KW1LlVK/Rfw/4Cy6GWrhXWQ+fgaktk9iXsvGWaMjfryAF6/UYE9Uelh//Gahg8WQogOqLEBaiVQrZQaB9wH7AX+1tiLaK0/1FrPakb+Wk74fHxeJ+iON5bo5vMGMaZfKscqnLy/OzQ2asfBSpzu9jm/oBBCNFdjA5RXG+1Ic4BntNa/p4Geee1SUnfwBpat0ODteN20bVYLj88bi82ieHfXEY5XGvfVPD7NV/s7VrOlEEKcSmMDVIVS6hcY3cvXKaUsGPehOo66Y6E6YDMfwKi+Xbj9wsF4/ZpXc0JjowpOuDha0vGCrhBC1KexAeoawIUxHuookAH8Jmq5ioa6s0l0kMG6kSyYOoSsHknsKapg88Gi4PacfeV4fR2v6VIIISJpVIAyg9KLQKo5vsmptW70Pah2IamHce8poIN1NQ8Xb7fyxLyxKAVvbs+n0mV0Na9x+/nmUMf9XEIIEa6xUx1dDWwGrgKuBjYppa6MZsZaXAdacqMxJmZ240eTB1Ll9vLG9kPB7XuPVlNSKWOjhBAdX2Ob+B7AGAP1Y631j4DvAEuil60o6OCDdSP52fQR9EtLYMuhE+w6Xh7cvi2vHL+MjRJCdHCNDVAWrfXxsPfFTTi2feiAM5qfSnKcjUfmjgbgtS8P4DbvP5VVe9l7tP2uGiyEEI3R2CDzL6XUv5VSNyilbgDWAeujl60o6KAzmp/KlOE9uWJCP4qqXLyz83Bwe+6hSjx+axvmTAghTk9jO0n8DGNC17Hm4zmt9f3RzFiLS+oec018AUtmjaJ7soMP9hzjcJlRc/L5odjbRaZBEkJ0WI1uptNa/1Nrfa/5WBPNTEWFPYHQpOyAq7zepB1N1yQH/33ZaPxasyrnQPD+U40/nvxiWSJeCNExNRiglFIVSqnyCI8KpVTH+4a3d+z5+BoyY0xvvjeqFwdLqvg0L3S78OsDlbi9MjZKCNHxNBigtNYpWusuER4pWusurZXJFhMXPh9fjTEdeIxQSvE/l48mJd7G+twCSqqNaZ1cHr8sES+E6JA6Vk+805WYHlp2Q/vB5244fQfTq0s8S2aOwuX188+vQtMgHSx0UlgWW59VCBH7OleASupeezaJGOnJF+6qiRmcN6Q7O46WkVNwIrh9275yfP7YqTEKIWJfJwtQdSeMjb0ApZTi0SvGkOiwsuarQ9R4jBpjldPHzvzYuu8mhIhtnTtAxWANCqB/t0R+/v3hlLs8rN1eZ4n4KpkGSQjRMXTCABWbY6Hq+tE5mYzs7uDzA0XsDVsiflteuYyNEkJ0CJ0rQCWmd4oaFIDFoljwnTTsNgurcvYHl+EoqZJpkIQQHUPnClCd4B5UuH5d7Nx7yTAKK138e9eR4PZvDlVS5ZQl4oUQ7ZsEqBh3y3mDGJeRyge7j9aaBilnnzT1CSHat6gFKKVUvFJqs1LqS6XUDqXUf0frWo2WmF7nHlTs92qzWS08ceU4LBZqTYN0vMzNoSKZBkkI0X5FswblAqZqrccB44HpSqnJUbzeqVltYLGH3rvK2i4vrWh47xQWTh3KwZIqPqk1DVIFLo9MgySEaJ+iFqC0IVBFsZuPtm9TiuH5+Bpy+5TBnNG3C+u/KeBEtQsAt1fz1X6ZBkkI0T5F9R6UUsqqlMoBjgPvaq03RfN6jRKfGnrt6TxNXHarhd9cOQ6/1ryaE5oGKb/YydESVxvmTAghIrNF8+Raax8wXimVBqxRSo3WWm8PT6OUmg/MB+jTpw85OTlNukZxcXGTjsl0QZrfBxYraC9fbtuCVlEthjYTqWzmjUxm1Y5ythwqZmL/dAA27yomI64Qi2r7Cm5raOq/mc5EyiYyKZfIol0urfLNrLUuVUp9AEwHttfZ9xzGYohMnDhRjx8/vknnzsnJoUnHFAwFVyUkGDWpcUP7Q3KvJl2zo4hUNqNG+/nymU95/etDjOjZheQ4Oz6sWNMGMW5Qx5ugvjma/G+mE5GyiUzKJbJol0s0e/H1MGtOKKUSgEuAndG6XqMl9YCa0tD76qK2y0sbcNgsPHnVOJxeH2u+PhTcnneshqJymfFcCNF+RPMeVB/gA6XUV8AXGPeg3ori9RonqTs4w3rvdbIABTC6Xyp3ThnM1vwT7DgaCtbb8mTGcyFE+xG1Jj6t9VfAhGidv9k6eQ0qYMHUIbyz4xivfXmAwekpxNutVJoznp8xIOXUJxBCiCjrXDNJgBmgOncNCiDOZuU3V42lwuVl7Y6wGc8PV1MqM54LIdqBThqgpAYFMDYjjTsuHMxn+wvZE5jxHNi6txy/NPUJIdpY5wtQielGLz6/OVmqu7L2KrudzE8uHsLw3ims2rYftznjeVm1l91HZMZzIUTb6nwBKj7NGAMlzXyA0dT35FXjKKlx86/cguD2nfmVlNd42zBnQojOrvMFKIsFUvuDU5r5Akb3S+Wui4bw0d5jHCgxZnj3a6OpT2Y8F0K0lc4XoAC6Zcl9qDoWXDSE4b278I+t+/D6zcUNKz3skcUNhRBtpBMHKGniC+ewWfjtVeMornbxTvjihgcrqZSmPiFEG+jEAUpqUHWN6tuFRRcP5b1vj5JfatSc/Bq25klTnxCi9UmAAqguBvkCBuD2Cwczpl8X/rFtX3BWieIKD3nHak5xpBBCtKzOG6A8NeA1557zOsET+8u/N4bNauG3V4+nqMrFe7tDTX07DlZQ6ZSmPiFE6+mcAarrQEBJT756DOmZzM++P5x3dh7hcJnR1OfzS68+IUTr6pwByhZndDUP7yhRJQEq3E3fHcRZmV15eet+aeoTQrSJzhmgALoNqn0fqkYCVDiLRfHbq8ZRUiNNfUKIttGJA1SdjhJSgzpJ/26J/Gr2GbyzS5r6hBCtr5MHKBkLdSpXTczgouE9T2rqkwG8QohokwAVUFMM2t92+WmnlFI8Nm8MNV4v735bewCvzNUnhIimzh2gfC5wmzUBv7f2SrsiqHtyHI9eMYZ3dx0hvzRsrr49ZfilqU8IESWdN0B1zTSeay3/XtwmWekIvndGb66emMFLW/fjNZflKKny8m2BjB8TQkRH5w1QjkRI6VtnRonCtstPB7Bk1igcdsW/dh4ObttZUCUr8AohoqLzBig4+T5U+aG2y0sHkBRn4+lrxvNx3jH2n6gEjBmituwpC3agEEKIltLJA9QgKM0PvT/+jXEvStTrzAFdueuiIbyYvQ+X11iVuKLGx46DlW2cMyFErIlagFJK9VdKfaCU+kYptUMptSha12q2bllQeTxUi/K5oOjbts1TB7DgoiEMSE/gje2hGufeo9UUlrnbMFdCiFgTzRqUF7hPaz0KmAzcpZQaFcXrNV23LOO5aE9o27Ev2yYvHYjNamHZtRP4+kgp3xwN3cPL3luG2ytd9YUQLSNqAUprfURrvdV8XQHkAv2idb1mCQSowt2hbYW54JOawKn075bII3NHs2rbASpdRieJGrefL/dVtHHOhBCxQrXGlDVKqUzgY2C01rq8zr75wHyAPn36nLV+/fomnbu4uJj09PRm5cviqWbsuksB0Gdeh0rsCsD+lHMojRvQrHO2J6dTNo217PMTFLniuOnsIcFtPeylJFvb76SyrVEuHZWUTWRSLpG1VLlMmDAhW2s9se72qAcopVQy8BHwiNZ6dUNpJ06cqLds2dKk8+fk5DB+/PjmZ/A3Q6CqEPqfBQPPNrb1GAXjrm/+OduJ0y6bRqh0eZm5/BMmD+jB5IE9ALBZFFPHppMUb43qtZurNcqlo5KyiUzKJbKWKhelVMQAFdVefEopO/BP4MVTBac2E2zmC7sPVbTLWMRQnFJynI3l107grR35FFYaZeb1a7bILBNCiNMUzV58CvgzkKu1fipa1zltgQDlLANrnPFa++D4jrbLUwczrn8a91wyjL9vycPnNzpJnKj0sCtfZpkQQjRfNGtQ3wWuB6YqpXLMx4woXq95AgEKwBX2hXrsq9bPSwd283mDGNY7+aRZJorKpcOJEKJ5otmL71OttdJaj9VajzcfTesB0Rr6nx16/e2G0OsTe6SZrwmUUjx51Ti+OVbKnqJQT74vdpfh8kjXcyFE03XumSQABn4XEs1eKCfyIC7VeK39ULq/zbLVEXVNcrDsugm8vHVfsOu50+Nna16ZLHAohGgyCVBWG4ycHXpfHTZ5bEle6+eng5uU2Y1bL8ji5a37g9uOlrjJO9p+u50LIdonCVAAo+aEXh/cFHpdsq/18xIDbr8wi77d4vloz7Hgtq8PVFBSKbOeCyEaTwIUQOb5kGAM0uXo9tD28gK5D9UMSil+e/U4vjxygkPmAoca2PRtqUyFJIRoNAlQAFY7jJhlvPa5jW9TADSUHmirXHVoqQl2VvxgAi9v3U+Nx5ghvsbtJ3uP3I8SQjSOBKiAUZeHXheH3XuSZr5mG90vlXsuGVr7flSpm91HqtsuU0KIDkMCVEDWhRCfZrw+viu0XTpKnJarJ/Vn3MAufLDnaHDbjoOVMj5KCHFKEqACrHYYMdN4XX441MxXUQBeV5tlKxY8OOsMjlZWsa84tKjhZ7tKqXH72jBXQoj2TgJUuEAzn9cFTnPSde2HMrkPdTocNgu//+FZrM/Np8IcH+X1aTbuLJGl4oUQ9ZIAFS5rSmigbsn+0Ha5D3XaeqTE8dS143hp675gUCqv9rF1r3SaEEJEJgEqnM0RauYrC80pJwGqZYzNSOOOi7JqLRWfX+xi71HpNCGEOJkEqLrOmGs8hweo8kOyym4LmTO+H2dlpbLpQFFw21f7KzheJvf5hBC1SYCqK2uK0ZvP64SqYmOb9kPpwbbMVUxZdPFQXLg5cMLoNKGU4j+5JZRXe9s4Z0KI9kQCVF02B4w0B+2G16JKpbt5S1FK8egVY9lyuIjSmkDNVPHh9mKc0rNPCGGSABVJoJmv/EhoW1l+2+QlRsXbrSy7djzrd+bj8hpByeeHD7YXS88+IQQgASqyQRcac/NVHA9tq8gH6W3WotKT43jqmnG8uf1QcHl4p1vz8TfF0rNPCCEBKiKr3ViCw1UOHnOyWE8NOEvaNl8xqH+3RB68fCRv5xYEt5VW+vjPzhIJUkJ0chKg6nPGFcZzZWFoW7k080XDiN5duG3qID7JCy3PUVjmYdPu0gaOEkLEOglQ9ck831hptzKsma+8oP704rRMyuzGrDN7kX2oOLjtyAk3W/ZKkBKis5IAVR+rDUZeJjWoVjR1RC+mjk5nx9FQUDpU6CI7T4KUEJ1R1AKUUuovSqnjSqntp07dTo2cXbujRHmBMSZKRM3Ukb2YPCyNvUUVwW0Hj7v4z67iBo4SQsSiaNag/gpMj+L5oy/zPCMguc2peHwuqJYvymibNqoX47NSggN5AY6XeHlve2EDRwkhYk3UApTW+mPgRLTO3ypsccbMEuHNfBVyH6o1TBvVi4lDu7AvLEiVV/pZm31UevcJ0UmoaP7PrpTKBN7SWo9uIM18YD5Anz59zlq/fn2TrlFcXEx6evpp5LJh6fvfpP+Jj2HAJACOxw/jcPKEqF2vJUW7bFpDXomHfVVpZHXvEtx2vLyMs9KrsFlVs84ZC+USLVI2kUm5RNZS5TJhwoRsrfXEutttp33m06S1fg54DmDixIl6/PjxTTo+JyeHph7TJIO6wwuvBd/2dNTQM5rXa0FRL5tWMB44UFTF6s1HyOyWAkDPLqlsK43j8km96NElvsnnjIVyiRYpm8ikXCKLdrlIL75TSc2AuG6h9xXSUaK1DeyexI8u6M++E6GOE92T4lmbfZzP9sg9QSFilQSoxsi6EFzmvRDthyq5Wd/a0pPjWPj9LAprqoPTInVNiOPQMTe/fzePKnOlXiFE7IhmN/OXgc+A4UqpfKXUzdG6VtQN/V6dAbsyHqot2KwW5l80iPgEcPuMWqzdaqFvShJ/2HCAD3Yelw4UQsSQaPbiu05r3UdrbddaZ2it/xyta0Vd/+9ATah5iWNft11eBDPH9+asISmUOkOLSGalp1Bw3MODq7/hP3ukhitELJAmvsaw2o17UQEn9rZdXgQAQ3olc/35/fCr0PpRSQ4bZ/brTu6BGn76j69YtfkgJVWyErIQHVWb9+LrMAZ8F0p2Gq/9btjxTxj8PYhPadt8dWI2q4V5Z/dl5+EKtuWVE28z/jmnJ8Xx3aReVNR4+J83duLVPob2TiarRzKDuidR5fbj92ssluZ1UxdCtA4JUI01/FL44HNISANlgSNbjCB19BuoKQOfG/xe6DYYMiYazYIDzoH0wW2d85g3om8Kw3ons21/GXlHa7BZjIaBlDg752f1AqDG42XXoWo+/qaYoirNs9u+wK81SgEKrBaF3apIjLPSNdFB10Q7aYkOUhPspMTbSIk3npPibCQ5rCTG2UiwW7FKkBMiaiRANVZyT6guA4sN4pKNbd0GGg9PDbirwFUF1Sfg4H/gmzXgLIc+42D8D2H0lZAkA/2ixWJRnJWVxpgBXcjZV8ahIicWFWrBTrDbGJSezKD05AbP4/X7qXB6KHd5KSh0s8tVRZXbG3w4vT6cHh81Hh8urx+tNcoCVqWw2yzE2y3E263E26zEBV7brcTbAq+N5zibhbhAGvM5uM1mMd8brx3mduPZeG+zKJSS4ChimwSoppizEnJehhO7wB4HgS8Ie4LxSOpuBKwAjxPKD8OXL8J/nobU/tClL6T0hZQ+kNzLeKT0htR+ECfNhafLYbPwnaFdmTREU1juZmdBFUXlbhSN+zK3WSx0TYyja2Jck6/t1xq314/H58fj9xuv/eZ7n8bj8+N2+qmq8uL1G++9fo3Xb+z3+v14g9t02OuwZzOdXxtNlBbAalVYlMJmMYJkeCCLs1lxWI3XwYc1tN9uDW1z2CwcO1zFfl1Qa1vdNHarCr63h6WxWyVoipYlAaop4lNh8u3G67JDsGut2eW8nq7N9nhIzzIeddUcMR7HfOB1QU0JVJcCFuM6Sb2MWltyL0jqYQS/xHTjWQLZKSml6JkaR8/UOLTWOD1+Kmq8lFf7OJh/hK7pPXB5fDg9fpwePx6vH6/v1OdtiEWpYI2prfi1Edx8gaBmBjRfIOiZ231ejdPtp9Lvw+v34gum6cKhHcXGMYFz+TReHTqHL/Awt4VvV0qjUCiLUR4WpbBaFBaLxqoUFovCZrFgsyozqFlw2EKvA4HObjXSOKwWbBYLdlvgtRGE7eY5bFYLDmvonA6rBVudc9gsxnub1YLdYjzbrCp0DqmNtlsSoJortT98505ztvNKY3n4mhKoOGwsy1GRbzT9nYrFCo5E45HaL7Td64bS3XAsxzi/u9poRnRXAwocSRCfZgSzuC7GIz7VuEcWnwYJaSQVHYMCH9iTjPPHp4IjBSydq/OmUooEh5UEh5WeqVB5rILxHVL9iAAAC3ZJREFUWZHvDXp9GpfHj9MMXm6PH7fXqBm5PaGakNenQw/zS7w9jMCyKIXDqsDavv/GPjOI+nR4wDPK0h8IeFrj8/hx+TXVfj8+7auV1ufX+HXt95G2NSaNQoMCC8poGDFrp0oZzbcut4uEjRuxYPzvY1FGOpvVgtViBDlbWMCzmcE0ECADaULPobTWOtutFoz9YfsC+y2BdKruPgsWC+a1MGvUxjZrnfSBc9T6AaFol0FaAtTpUpZQgOiSAb3GGNu1hupCKNkPpfugdL9RU/L7jM4U2vy5Xt8/CpsDbN0gsVvk/XX5feA8BpUHjet4XQz1uuDIauO9zwU+r3Fti914WG1gjTOaJ20J4Eg2Ap/DDGiOZLAnmvvjwBZvvHYkhfbZ4kP7rPb6P08HYfwqt5IU3/RakD+8duHX+PzUea/x+8GntZnWOMb4sjRqP36/xqdD5zK2Yb4OTx++3djXkcYoG1+WbVfTbEl1A17g7xj4uwT/3oF9XuNv7NYav/aH/qZaozWhv695fOC9pva/geA+8/zavJbWtffXfR2eTge3A+ha//sqFOZ/xvvAazM4KwXxuHi8XyVZPRq+t9tcEqCiRSlI6mk8Mr5TfzqfBzxVUHoQCr8xApmrnHqbDetjsYLFvBd2OrQfvGXgKjJqcYHeiYHA6veGAp3fZwRav998Dg+6CpTNzJf5rCzGM1bjZ6jVYQZKu/HaFmdus4Kyho61xRmB1BZnvLeaAdYSlkZZw4Kj+X+PMq9jsZkP45iE0r1wzB52jbDrKQuE368KnEdZjHMF9ivzdeCzARavC4vXid3nNjrIOEvBWWb8QEjqbjTVpvQ0gnuUArmuE+yCX0z+sC+6QAA09+mwoHnoUD59+/XD58f8oqvzheiv/ax17WuE11ACwTjwJag7WBBtLKtFYUXRhi27babG4+WfWwr42aXDo3J+CVBtzWoHaxr0ToPeY41tWoOn2ghUzlJwVxivXRXGw11pbPPU1K6NtQRlMWtEcdD0fgItxAfaAz4/eDW4NOAn+A2n/WGPwLeeDuU/0HvP7wPtNQJoWMAfroFP14aOj0SHn0+Fzqn95nlrnxNlNcrMHm/UJlWdJrZAAA8G+kBwNz9b4JzBwGoNBd1AwEfX+czBn7RmHkBpsCkLDJsJZ8xtcsmXHalmSJ+kJh/XFHV/ues6QbTe92EBNXRshGc/Eff56wTL2sE5rLYR4fwejwer1WYUPbEZaJsjwW5jZM+0qJ1fAlR7pFSoqS2lz6nTa23WbFxG0PLWgKeG/Xt2ktmvJ3idxsPnNn7Ne8x7Wd7q0PbAF357ER5oRNPt29CsANUalNlEZGlkz8r2oL5lJQLBVhOqVUJ4jbF2ENZhr8PTBYJh8DcYEbZT+xzh28OvEzg2mIaT0wfOS9g+f4Q8RspL6BjA5+SCkX2jVu4SoGKBUmZNzG40H5lK82ugfxPWatHa+FXv85iBy2XeN/MY24LP3tD7QM0g/LhgQPQaafyesJ+cCvCH7fOG1YDkZ2mLie/a1jnoFALBFuiUg7ZzcnLokjggaueXACVCVOC+ke3072WdjrrNeNR5H3j4fWH7dKgpLtjE5w09wuzevZuhgwebxzXUPKrN81nDmvjCmunCKWV2KDEflsD/WmbTXHhefB6jpuupMn4ABJoRAzUK7TN/wvrC7udZwn4MmOfRgWY/M7AHyio+Dfqd1byyF6IdkQAl2p/AfRiic9e5yl4K3SKMTRNCtCvSyC+EEKJdkgAlhBD/v717jZWrKsM4/n9srVhKqAUk2iJtpUGLgYKkqRZIA34AbQQNXkEIkfCFhIsaBWPiJZpIQgSNBDEULbFBTLk1xhC1kqofKBRaubQaCCocUmiNUEXC/fHDWidMjjMYes5h75n9/JKTc/aa3b3XvHln3u61Z9aKVkqBioiIVkqBioiIVkqBioiIVkqBioiIVkqBioiIVpLdnm/vS9oN/P11/rMDgX9MQ3dGQWLTX+IyWGLTX+LS31TF5VDbB01sbFWB2huSttg+tul+tFFi01/iMlhi01/i0t90xyVDfBER0UopUBER0UqjUKB+3HQHWiyx6S9xGSyx6S9x6W9a4zL096AiImI0jcIVVEREjKAUqIiIaKWhLlCSTpb0F0kPS7qk6f40RdIhku6QtF3Sg5IurO3zJP1G0kP1dyeXWZU0Q9JWSb+s24skba55c6OkWU33sQmS5kpaL+nPknZI+kByBiRdXF9HD0i6QdI+Xc0ZSddJ2iXpgZ62vjmi4gc1RvdJOmay5x/aAiVpBnAVcAqwFPiMpKXN9qoxLwFftL0UWAGcX2NxCbDR9hJgY93uoguBHT3blwFX2D4MeAr4fCO9at73gdttvwc4ihKjTueMpPnABcCxtt9HWTXz03Q3Z34KnDyhbVCOnAIsqT/nAVdP9uRDW6CA5cDDth+x/QLwc+DUhvvUCNs7bd9b//435Y1mPiUea+tua4HTmulhcyQtAD4CXFu3BZwIrK+7dDUu+wMnAGsAbL9g+2mSM1BWGn+rpJnAbGAnHc0Z278H/jmheVCOnApc7+JOYK6kd0zm/MNcoOYDj/Vsj9W2TpO0EDga2AwcbHtnfegJ4OCGutWkK4EvA6/U7QOAp22/VLe7mjeLgN3AT+rw57WS9qXjOWP7ceBy4FFKYdoD3ENyptegHJny9+RhLlAxgaQ5wE3ARbb/1fuYy/cJOvWdAkmrgV2272m6Ly00EzgGuNr20cB/mDCc19GceRvlSmAR8E5gX/53iCuq6c6RYS5QjwOH9GwvqG2dJOnNlOK0zvbNtfnJ8Uvs+ntXU/1ryErgo5L+RhkCPpFy32VuHb6B7ubNGDBme3PdXk8pWF3PmQ8Bf7W92/aLwM2UPErOvGpQjkz5e/IwF6i7gSX10zWzKDcyNzTcp0bU+yprgB22v9fz0Abg7Pr32cBtb3TfmmT7UtsLbC+k5MfvbJ8B3AGcXnfrXFwAbD8BPCbp8Np0ErCdjucMZWhvhaTZ9XU1HpfO50yPQTmyATirfppvBbCnZyhwrwz1TBKSPky5xzADuM72dxruUiMkHQf8AbifV++1fJVyH+oXwLsoy5h80vbEG56dIGkV8CXbqyUtplxRzQO2Amfafr7J/jVB0jLKh0dmAY8A51D+09rpnJH0TeBTlE/HbgXOpdxL6VzOSLoBWEVZVuNJ4OvArfTJkVrQf0gZEn0WOMf2lkmdf5gLVEREjK5hHuKLiIgRlgIVERGtlAIVERGtlAIVERGtlAIVERGtlAIV0WKSVo3Pwh7RNSlQERHRSilQEVNA0pmS7pK0TdI1dQ2qZyRdUdcW2ijpoLrvMkl31jVzbulZT+cwSb+V9CdJ90p6dz38nJ51m9bVL0Qi6bsqa4DdJ+nyhp56xLRJgYqYJEnvpcw8sNL2MuBl4AzKRKNbbB8BbKJ8Cx/geuArto+kzP4x3r4OuMr2UcAHKbNpQ5md/iLKumeLgZWSDgA+BhxRj/Pt6X2WEW+8FKiIyTsJeD9wt6RtdXsxZdqpG+s+PwOOq+swzbW9qbavBU6QtB8w3/YtALafs/1s3ecu22O2XwG2AQspy0A8B6yR9HHK1DIRIyUFKmLyBKy1vaz+HG77G33229t5xXrnfHsZmFnXJlpOmYV8NXD7Xh47orVSoCImbyNwuqS3A0iaJ+lQyutrfAbszwJ/tL0HeErS8bX9c8CmuhLymKTT6jHeImn2oBPWtb/2t/0r4GLKku0RI2Xm/98lIl6L7e2Svgb8WtKbgBeB8ymLAC6vj+2i3KeCskTBj2oBGp9FHEqxukbSt+oxPvEap90PuE3SPpQruC9M8dOKaFxmM4+YJpKesT2n6X5EDKsM8UVERCvlCioiIlopV1AREdFKKVAREdFKKVAREdFKKVAREdFKKVAREdFK/wW79SlUUoo9XAAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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N8Gtd0kKuhdCoAReJzY2lqefftrhnnPl448/ZteuXaxfv57evXtjMBgYO3YsnZGEPyYmhsLCQsf++fPnm23bt29f4uLi2LFjB3fccUeTbQICAhxzRAAXLlxo1KahlTNx4kRKS0vJyMggLS2NVatWARAeHo6/vz/btm1Dr9c3uk9wcDArV65k5cqVnDx5kl/96lekpqZy+eWXt/h/HjZsGFdddZVjnq89fZ47dy5/+ctfKCgo4Msvv+T9999v8rVUKhVz5swhLS2NqKgopk6dSnBwMCCL3t13380999zT6Lr9+/dTWFiIJEmO187Pz7+kBwNB10S4+ASdisViwWQyOf6sVis33XQTf/vb3zh37hwgT5bXu4iqqqrQarWEh4dTU1PDiy++2Gl9nTNnDlu2bOH06dPU1NTw2muvNdtWoVCwcuVKXnvtNT788EOMRiN2u50DBw7w+9//HoBBgwbx448/kp+fj8Fg4B//+MdF+1DvwnruueeoqKjgiiuuAGQLc+nSpTz99NOOoILCwkL27t0LyAEGOTk5SJKETqdDpVI16eI7cOAAmzZtctzj9OnT7N69m+HDhzv6/M0331BeXk5xcTH//ve/L9rniIgIxo0bx6pVq+jduzf9+vVrtu3ChQvZvn07W7dudbO0li5dynvvvcehQ4eQJInq6mq+/vprjEYjI0aMQK1Ws2HDBiwWCzt27ODw4cMX7ZfA9xACJehU7rrrLoYNG+b4e/nll7n11luZPn06t99+OyNHjuT6668nPT0dgGuuuYb4+HgmTZrE/Pnzm5zL8BRTpkxh+fLl3HrrrcyaNcvxo63VaptsP2fOHF566SU+/PBDJk2axMSJE1m7dq0jxPyKK65g3rx5XH311SxZsoRp06a1qh8LFy7ku+++Y86cOW5h3I899hiJiYlcf/31jBo1il//+tecOXMGkMPHb7vtNkaOHMkNN9zATTfdxIQJExrdOyQkhN27d7Nw4UJGjhzJnXfeycyZM1mxYgUAixYtYuDAgY73Z968ea3q84IFC/juu++ade/VM3z4cIdb1TU6MDU1lT/96U+sWbOGsWPHctVVVzmCV7RaLS+//DIfffQR48aN47PPPmPWrFmt6pfAt1CIgoUCQes4ffo0CxYs4PDhw2K9j0DQCQgLSiBogS+//BKz2UxFRQV//etfmTZtmhAngaCTEAIl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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# retrieve training statistics\n", + "list_of_stats = []\n", + "list_of_models = []\n", + "\n", + "for model in [\"model1\", \"model2\"]:\n", + " experiment_model_dir = \"./results/multiple_experiment_\" + model\n", + " train_stats = load_json(os.path.join(experiment_model_dir, \"training_statistics.json\"))\n", + " list_of_stats.append(train_stats)\n", + " list_of_models.append(model)\n", + "\n", + "\n", + "# generating learning curves from training\n", + "learning_curves(\n", + " list_of_stats, \"Survived\", model_names=list_of_models, output_directory=\"./visualizations\", file_format=\"png\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate Annotated Learning Curves Using seaborn package\n", + "\n", + "### Helper function to collect training statistics" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# function to generate pandas data frame from training statistcs\n", + "# Parameter:\n", + "# experiment_model_dir: directory containing the training statistics for a specific model training experiment\n", + "#\n", + "# Returns: pandas dataframe containing the performance metric and loss\n", + "#\n", + "\n", + "\n", + "def extract_training_stats(experiment_model_dir):\n", + " list_of_splits = [\"training\", \"validation\", \"test\"]\n", + " list_of_df = []\n", + " for split in list_of_splits:\n", + " train_stats = load_json(os.path.join(experiment_model_dir, \"training_statistics.json\"))\n", + " df = pd.DataFrame(train_stats[split][\"combined\"])\n", + " df.columns = [split + \"_\" + c for c in df.columns]\n", + " list_of_df.append(df)\n", + "\n", + " df = pd.concat(list_of_df, axis=1)\n", + " df[\"epoch\"] = df.index + 1\n", + "\n", + " return df" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Retrieve training results" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "pycharm": { + "is_executing": false + } + }, + "outputs": [], + "source": [ + "model1 = extract_training_stats(\"./results/multiple_experiment_model1\")\n", + "model1.name = \"model1\"\n", + "model2 = extract_training_stats(\"./results/multiple_experiment_model2\")\n", + "model2.name = \"model2\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
training_lossvalidation_losstest_lossepoch
06.6467167.0696277.5412131
16.4955526.9090967.3704742
26.3157076.7180517.1673513
36.1354156.5265966.9636944
45.9549756.3349486.7598505
\n", + "
" + ], + "text/plain": [ + " training_loss validation_loss test_loss epoch\n", + "0 6.646716 7.069627 7.541213 1\n", + "1 6.495552 6.909096 7.370474 2\n", + "2 6.315707 6.718051 7.167351 3\n", + "3 6.135415 6.526596 6.963694 4\n", + "4 5.954975 6.334948 6.759850 5" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model1.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Helper function to generate plot ready data" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# create pandas dataframe suitable for plotting learning curves\n", + "# Parameters\n", + "# train_df_list: list of pandas datatframe containing training statistics\n", + "#\n", + "# Returns: plot ready pandas dataframe\n", + "\n", + "\n", + "def create_plot_ready_data(list_of_train_stats_df):\n", + " # holding ready for plot ready data\n", + " plot_ready_list = []\n", + "\n", + " # consolidate the multiple training statistics dataframes\n", + " for df in list_of_train_stats_df:\n", + " for col in [\"training\", \"validation\"]:\n", + " df2 = df[[\"epoch\", col + \"_loss\"]].copy()\n", + " df2.columns = [\"epoch\", \"loss\"]\n", + " df2[\"type\"] = col\n", + " df2[\"model\"] = df.name\n", + " plot_ready_list.append(df2)\n", + "\n", + " return pd.concat(plot_ready_list, axis=0, ignore_index=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot learning curves" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# create plot ready data\n", + "learning_curves = create_plot_ready_data([model1, model2])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Learning Curves')" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot learning curves for the different models\n", + "fig = plt.figure(figsize=(10, 6))\n", + "sns.set_style(style=\"dark\")\n", + "ax = sns.lineplot(x=\"epoch\", y=\"loss\", style=\"type\", hue=\"model\", data=learning_curves)\n", + "ax.set_title(\"Learning Curves\", fontdict={\"fontsize\": 16})" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "fig.savefig(\"./visualizations/custom_learning_curve.png\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/titanic/multiple_model_training.py b/examples/titanic/multiple_model_training.py new file mode 100644 index 0000000..4c3b47c --- /dev/null +++ b/examples/titanic/multiple_model_training.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# # Multiple Model Training Example +# +# This example trains multiple models and extracts training statistics + +import logging +import shutil + +# ## Import required libraries +from ludwig.api import LudwigModel +from ludwig.datasets import titanic +from ludwig.visualize import learning_curves + +# clean out old results +shutil.rmtree("./results", ignore_errors=True) +shutil.rmtree("./visualizations", ignore_errors=True) + +# list models to train +list_of_model_ids = ["model1", "model2"] +list_of_train_stats = [] + +training_set, _, _ = titanic.load(split=True) + +# ## Train models +for model_id in list_of_model_ids: + print(">>>> training: ", model_id) + + # Define Ludwig model object that drive model training + model = LudwigModel(config="./" + model_id + "_config.yaml", logging_level=logging.WARN) + + # initiate model training + train_stats, _, _ = model.train( + dataset=training_set, experiment_name="multiple_model_experiment", model_name=model_id + ) + + # save training stats for later use + list_of_train_stats.append(train_stats) + + print(">>>>>>> completed: ", model_id, "\n") + +# generating learning curves from training +learning_curves( + list_of_train_stats, + "Survived", + model_names=list_of_model_ids, + output_directory="./visualizations", + file_format="png", +) diff --git a/examples/titanic/simple_model_training.py b/examples/titanic/simple_model_training.py new file mode 100644 index 0000000..5567a8e --- /dev/null +++ b/examples/titanic/simple_model_training.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +# # Simple Model Training Example +# +# This example is the API example for this Ludwig command line example +# (https://ludwig-ai.github.io/ludwig-docs/latest/examples/titanic/). + +# Import required libraries +import logging +import os +import shutil + +import yaml + +from ludwig.api import LudwigModel +from ludwig.datasets import titanic + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +# Download and prepare the dataset +training_set, test_set, _ = titanic.load(split=True) + +config = yaml.safe_load(""" +input_features: + - name: Pclass + type: category + - name: Sex + type: category + - name: Age + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: SibSp + type: number + - name: Parch + type: number + - name: Fare + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: Embarked + type: category + +output_features: + - name: Survived + type: binary + +""") + +# Define Ludwig model object that drive model training +model = LudwigModel(config=config, logging_level=logging.INFO) + +# initiate model training +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train( + dataset=training_set, experiment_name="simple_experiment", model_name="simple_model", skip_save_processed_input=True +) + +# list contents of output directory +print("contents of output directory:", output_directory) +for item in os.listdir(output_directory): + print("\t", item) + +# batch prediction +model.predict(test_set, skip_save_predictions=False) diff --git a/examples/twitter_bots/README.md b/examples/twitter_bots/README.md new file mode 100644 index 0000000..c9ac56f --- /dev/null +++ b/examples/twitter_bots/README.md @@ -0,0 +1,40 @@ +# Twitter Bots Example + +We'll be using the twitter human-bots dataset which is composed of 37438 rows each corresponding to a Twitter user +account. Each row contains 20 feature columns collected via the Twitter API. These features contain multiple data +modalities, including the account description and the profile image. + +The target column account_type has two unique values: bot or human. 25013 user accounts were annotated as human +accounts, the remaining 12425 are bots. + +### Preparatory Steps + +Create and download your [Kaggle API Credentials](https://github.com/Kaggle/kaggle-api#api-credentials). + +The Twitter Bots dataset is hosted by Kaggle, Ludwig will need to authenticate you through the Kaggle API to download +the dataset. + +### Examples + +Run `python train_twitter_bots.py` to train a single model. + +For a faster, more lightweight model run `python train_twitter_bots_text_only.py`, which does not use image features. + +This will download the Twitter Bots dataset into the current +directory, train a model, and write results into the following directories: + +``` +./outputs/results/ + api_experiment_run/ +./outputs/visualizations/ + confusion_matrix__account_type_top2.png + confusion_matrix_entropy__account_type_top2.png + learning_curves_account_type_accuracy.png + learning_curves_account_type_loss.png +``` + +After training, the script will generate the following plots: + +![Account Type Accuracy](images/learning_curves_account_type_loss.png) +![Account Type Loss](images/learning_curves_account_type_accuracy.png) +![Account Type Confusion Matrix](images/confusion_matrix__account_type_top2.png) diff --git a/examples/twitter_bots/train_twitter_bots.py b/examples/twitter_bots/train_twitter_bots.py new file mode 100644 index 0000000..fb43bc2 --- /dev/null +++ b/examples/twitter_bots/train_twitter_bots.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +"""Trains model on Twitter Bots dataset using default settings.""" + +import logging +import os +import shutil + +import yaml + +from ludwig import datasets +from ludwig.api import LudwigModel +from ludwig.utils.fs_utils import rename +from ludwig.visualize import confusion_matrix, learning_curves + +if __name__ == "__main__": + # Cleans out prior results + results_dir = os.path.join("outputs", "results") + visualizations_dir = os.path.join("outputs", "visualizations") + shutil.rmtree(results_dir, ignore_errors=True) + shutil.rmtree(visualizations_dir, ignore_errors=True) + + # Loads the dataset + twitter_bots_dataset = datasets.get_dataset("twitter_bots", cache_dir="downloads") + training_set, val_set, test_set = twitter_bots_dataset.load(split=True) + + # Moves profile images into local directory, so relative paths in the dataset will be resolved. + if not os.path.exists("profile_images"): + rename(os.path.join(twitter_bots_dataset.processed_dataset_dir, "profile_images"), "profile_images") + + config = yaml.safe_load(""" + input_features: + - name: default_profile + type: binary + - name: default_profile_image + type: binary + - name: description + type: text + - name: favourites_count + type: number + - name: followers_count + type: number + - name: friends_count + type: number + - name: geo_enabled + type: binary + - name: lang + type: category + - name: location + type: category + - name: profile_background_image_path + type: category + - name: profile_image_path + type: image + preprocessing: + num_channels: 3 + - name: statuses_count + type: number + - name: verified + type: binary + - name: average_tweets_per_day + type: number + - name: account_age_days + type: number + output_features: + - name: account_type + type: binary + """) + + model = LudwigModel(config, logging_level=logging.INFO) + + train_stats, preprocessed_data, output_directory = model.train(dataset=training_set, output_directory=results_dir) + + # Generates predictions and performance statistics for the test set. + test_stats, predictions, output_directory = model.evaluate( + test_set, collect_predictions=True, collect_overall_stats=True, output_directory=results_dir + ) + + confusion_matrix( + [test_stats], + model.training_set_metadata, + "account_type", + top_n_classes=[2], + model_names=[""], + normalize=True, + output_directory=visualizations_dir, + file_format="png", + ) + + # Visualizes learning curves, which show how performance metrics changed over time during training. + learning_curves( + train_stats, output_feature_name="account_type", output_directory=visualizations_dir, file_format="png" + ) diff --git a/examples/twitter_bots/train_twitter_bots_text_only.py b/examples/twitter_bots/train_twitter_bots_text_only.py new file mode 100644 index 0000000..aa11854 --- /dev/null +++ b/examples/twitter_bots/train_twitter_bots_text_only.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +"""Trains twitter bots using tabular and text features only, no images.""" + +import logging +import os +import shutil + +import yaml + +from ludwig.api import LudwigModel +from ludwig.datasets import twitter_bots +from ludwig.visualize import confusion_matrix, learning_curves + +if __name__ == "__main__": + # Cleans out prior results + results_dir = os.path.join("outputs", "results") + visualizations_dir = os.path.join("outputs", "visualizations") + shutil.rmtree(results_dir, ignore_errors=True) + shutil.rmtree(visualizations_dir, ignore_errors=True) + + # Loads the dataset + training_set, val_set, test_set = twitter_bots.load(split=True) + + config = yaml.safe_load(""" +input_features: + - name: created_at + type: date + column: created_at + - name: default_profile + type: binary + column: default_profile + - name: description + type: text + column: description + - name: favourites_count + type: number + column: favourites_count + - name: followers_count + type: number + column: followers_count + - name: friends_count + type: number + column: friends_count + - name: geo_enabled + type: binary + column: geo_enabled + - name: lang + type: category + column: lang + - name: location + type: text + column: location + - name: screen_name + type: text + column: screen_name + - name: statuses_count + type: number + column: statuses_count + - name: verified + type: binary + column: verified + - name: average_tweets_per_day + type: number + column: average_tweets_per_day + - name: account_age_days + type: number + column: account_age_days +output_features: + - name: account_type + type: category + column: account_type +trainer: + batch_size: 16 +defaults: + text: + preprocessing: + tokenizer: space_punct + max_sequence_length: 16 +model_type: ecd + """) + + model = LudwigModel(config, logging_level=logging.INFO) + + train_stats, preprocessed_data, output_directory = model.train(dataset=training_set, output_directory=results_dir) + + # Generates predictions and performance statistics for the test set. + test_stats, predictions, output_directory = model.evaluate( + test_set, collect_predictions=True, collect_overall_stats=True, output_directory=results_dir + ) + + confusion_matrix( + [test_stats], + model.training_set_metadata, + "account_type", + top_n_classes=[2], + model_names=[""], + normalize=True, + output_directory=visualizations_dir, + file_format="png", + ) + + # Visualizes learning curves, which show how performance metrics changed over time during training. + learning_curves( + train_stats, output_feature_name="account_type", output_directory=visualizations_dir, file_format="png" + ) diff --git a/examples/uncertainty/README.md b/examples/uncertainty/README.md new file mode 100644 index 0000000..3a8dac6 --- /dev/null +++ b/examples/uncertainty/README.md @@ -0,0 +1,81 @@ +# Uncertainty Quantification: MC Dropout and Temperature Scaling + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/uncertainty/uncertainty.ipynb) + +## Overview + +This example demonstrates two practical techniques for quantifying and reducing model uncertainty in Ludwig: + +- **Temperature Scaling Calibration**: Post-hoc calibration that adjusts overconfident predicted probabilities to better match empirical frequencies. Based on [Guo et al., ICML 2017](https://arxiv.org/abs/1706.04599). +- **MC Dropout**: Monte Carlo Dropout runs multiple stochastic forward passes at inference time to produce per-sample uncertainty estimates. Based on [Gal & Ghahramani, ICML 2016](https://arxiv.org/abs/1506.02142). + +Both techniques are applied to a binary wine quality classifier (UCI Wine Quality dataset) to illustrate when each method is appropriate and how to configure them in Ludwig. + +## What You Will Learn + +1. Why deep learning models are often overconfident and why calibration matters +1. How to enable temperature scaling in a Ludwig config (one line change) +1. How to compute Expected Calibration Error (ECE) and plot reliability diagrams +1. How to enable MC Dropout for per-sample uncertainty estimates +1. How to interpret the `uncertainty` output alongside predictions + +## Prerequisites + +- Python 3.9+ +- Ludwig installed (`pip install ludwig`) +- Internet access to download the UCI Wine Quality dataset (~80 KB) + +Optional (for GPU training): + +``` +pip install ludwig[gpu] +``` + +## Quick Start + +### Run the notebook + +Click the Colab badge above, or open `uncertainty.ipynb` locally with Jupyter. + +### Run the standalone script + +```bash +pip install ludwig +python train.py +``` + +This will: + +1. Download the red wine quality dataset from UCI +1. Train three models: baseline, temperature-scaled, and MC Dropout +1. Print Expected Calibration Error for each model +1. Save reliability diagram plots to `./visualizations/` + +## Files + +| File | Description | +| ------------------------ | ----------------------------------------- | +| `uncertainty.ipynb` | Interactive Colab notebook walkthrough | +| `train.py` | Standalone training and evaluation script | +| `config_baseline.yaml` | Baseline config — no calibration | +| `config_calibrated.yaml` | Config with temperature scaling enabled | +| `config_mc_dropout.yaml` | Config with MC Dropout enabled | + +## Dataset + +UCI Wine Quality (red wine), 1,599 samples, 11 physicochemical features. +Binary target: quality score >= 7 is "good" (positive class), otherwise "bad". +Class imbalance (~14% positive) makes calibration especially important. + +## Key Results + +| Model | ECE | Notes | +| ------------------- | ----- | ---------------------------------------------------- | +| Baseline | ~0.12 | Overconfident — probabilities cluster near 0/1 | +| Temperature Scaling | ~0.04 | Better calibrated, same accuracy | +| MC Dropout | — | Outputs per-sample uncertainty alongside predictions | + +## References + +- Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. (2017). On calibration of modern neural networks. *ICML*. +- Gal, Y., & Ghahramani, Z. (2016). Dropout as a Bayesian approximation: Representing model uncertainty in deep learning. *ICML*. diff --git a/examples/uncertainty/config_baseline.yaml b/examples/uncertainty/config_baseline.yaml new file mode 100644 index 0000000..81eca20 --- /dev/null +++ b/examples/uncertainty/config_baseline.yaml @@ -0,0 +1,69 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality + type: binary + decoder: + type: mlp_classifier + num_fc_layers: 1 + output_size: 64 + dropout: 0.1 + loss: + type: binary_weighted_cross_entropy + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + dropout: 0.1 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 diff --git a/examples/uncertainty/config_calibrated.yaml b/examples/uncertainty/config_calibrated.yaml new file mode 100644 index 0000000..322c9e8 --- /dev/null +++ b/examples/uncertainty/config_calibrated.yaml @@ -0,0 +1,74 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality + type: binary + decoder: + type: mlp_classifier + num_fc_layers: 1 + output_size: 64 + dropout: 0.1 + # Temperature scaling learns a single scalar T on the validation set such that + # calibrated_logit = logit / T. It does not change argmax predictions but + # makes probabilities more faithful to empirical frequencies. + # Reference: Guo et al., "On Calibration of Modern Neural Networks", ICML 2017. + calibration: temperature_scaling + loss: + type: binary_weighted_cross_entropy + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + dropout: 0.1 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 diff --git a/examples/uncertainty/config_mc_dropout.yaml b/examples/uncertainty/config_mc_dropout.yaml new file mode 100644 index 0000000..f01557e --- /dev/null +++ b/examples/uncertainty/config_mc_dropout.yaml @@ -0,0 +1,77 @@ +model_type: ecd + +input_features: + - name: fixed_acidity + type: number + preprocessing: + normalization: zscore + - name: volatile_acidity + type: number + preprocessing: + normalization: zscore + - name: citric_acid + type: number + preprocessing: + normalization: zscore + - name: residual_sugar + type: number + preprocessing: + normalization: zscore + - name: chlorides + type: number + preprocessing: + normalization: zscore + - name: free_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: total_sulfur_dioxide + type: number + preprocessing: + normalization: zscore + - name: density + type: number + preprocessing: + normalization: zscore + - name: pH + type: number + preprocessing: + normalization: zscore + - name: sulphates + type: number + preprocessing: + normalization: zscore + - name: alcohol + type: number + preprocessing: + normalization: zscore + +output_features: + - name: quality + type: binary + decoder: + type: mlp_classifier + num_fc_layers: 1 + output_size: 64 + # Dropout must be > 0 for MC Dropout to produce meaningful variance. + dropout: 0.3 + # mc_dropout_samples > 0 enables MC Dropout at inference time. + # Ludwig runs mc_dropout_samples stochastic forward passes (dropout enabled), + # returns the mean probability as the prediction, and adds an 'uncertainty' + # column to the output — the variance of the probability across passes. + # Reference: Gal & Ghahramani, "Dropout as a Bayesian Approximation", ICML 2016. + mc_dropout_samples: 20 + loss: + type: binary_weighted_cross_entropy + +combiner: + type: concat + num_fc_layers: 2 + output_size: 128 + # Dropout in the combiner also contributes to stochastic variation in MC Dropout. + dropout: 0.2 + +trainer: + epochs: 30 + learning_rate: 0.001 + batch_size: 128 diff --git a/examples/uncertainty/train.py b/examples/uncertainty/train.py new file mode 100644 index 0000000..1e13093 --- /dev/null +++ b/examples/uncertainty/train.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python +""" +Uncertainty Quantification with Ludwig: MC Dropout and Temperature Scaling. + +Trains three models on the UCI Wine Quality dataset and compares: + 1. Baseline — no calibration + 2. Temperature Scaling — post-hoc calibration via a learned temperature scalar + 3. MC Dropout — per-sample uncertainty estimates via stochastic inference + +Usage: + python train.py +""" + +import logging +import os +import shutil + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from ludwig.api import LudwigModel + +logging.basicConfig(level=logging.WARNING) + + +# --------------------------------------------------------------------------- +# Dataset +# --------------------------------------------------------------------------- + +WINE_URL = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv" + +WINE_FEATURES = [ + "fixed_acidity", + "volatile_acidity", + "citric_acid", + "residual_sugar", + "chlorides", + "free_sulfur_dioxide", + "total_sulfur_dioxide", + "density", + "pH", + "sulphates", + "alcohol", +] + + +def load_dataset() -> pd.DataFrame: + """Download and prepare the wine quality dataset.""" + print("Downloading wine quality dataset...") + df = pd.read_csv(WINE_URL, sep=";") + # Rename columns: replace spaces with underscores + df.columns = [c.replace(" ", "_") for c in df.columns] + # Binarise: quality >= 7 is "good" (True), otherwise "bad" (False) + df["quality"] = (df["quality"] >= 7).astype(int) + print(f" {len(df)} rows | positive class (quality>=7): {df['quality'].mean():.1%}") + return df + + +# --------------------------------------------------------------------------- +# Ludwig configs +# --------------------------------------------------------------------------- + + +def _input_features() -> list: + return [{"name": feat, "type": "number", "preprocessing": {"normalization": "zscore"}} for feat in WINE_FEATURES] + + +BASE_CONFIG = { + "model_type": "ecd", + "input_features": _input_features(), + "output_features": [ + { + "name": "quality", + "type": "binary", + "decoder": { + "type": "mlp_classifier", + "num_fc_layers": 1, + "output_size": 64, + "dropout": 0.1, + }, + "loss": {"type": "binary_weighted_cross_entropy"}, + } + ], + "combiner": { + "type": "concat", + "num_fc_layers": 2, + "output_size": 128, + "dropout": 0.1, + }, + "trainer": {"epochs": 30, "learning_rate": 0.001, "batch_size": 128}, +} + +CALIBRATED_CONFIG = { + **BASE_CONFIG, + "output_features": [ + { + **BASE_CONFIG["output_features"][0], + "decoder": { + **BASE_CONFIG["output_features"][0]["decoder"], + "calibration": "temperature_scaling", + }, + } + ], +} + +MC_DROPOUT_CONFIG = { + **BASE_CONFIG, + "output_features": [ + { + **BASE_CONFIG["output_features"][0], + "decoder": { + **BASE_CONFIG["output_features"][0]["decoder"], + "dropout": 0.3, + "mc_dropout_samples": 20, + }, + } + ], + "combiner": { + **BASE_CONFIG["combiner"], + "dropout": 0.2, + }, +} + + +# --------------------------------------------------------------------------- +# Calibration metrics +# --------------------------------------------------------------------------- + + +def expected_calibration_error( + probabilities: np.ndarray, + labels: np.ndarray, + n_bins: int = 10, +) -> float: + """Compute Expected Calibration Error (ECE). + + Args: + probabilities: Predicted probability for the positive class, shape (N,). + labels: Ground-truth binary labels, shape (N,). + n_bins: Number of equally-spaced confidence bins. + + Returns: + ECE as a float in [0, 1]. + """ + bins = np.linspace(0.0, 1.0, n_bins + 1) + ece = 0.0 + n = len(probabilities) + for lo, hi in zip(bins[:-1], bins[1:]): + mask = (probabilities >= lo) & (probabilities < hi) + if mask.sum() == 0: + continue + conf = probabilities[mask].mean() + acc = labels[mask].mean() + ece += mask.sum() / n * abs(conf - acc) + return float(ece) + + +def reliability_diagram( + probabilities_dict: dict, + labels: np.ndarray, + n_bins: int = 10, + output_path: str | None = None, +) -> None: + """Plot reliability diagrams for one or more models. + + Args: + probabilities_dict: Mapping from model name to probability arrays. + labels: Ground-truth binary labels. + n_bins: Number of confidence bins. + output_path: If provided, save the figure to this path. + """ + bins = np.linspace(0.0, 1.0, n_bins + 1) + bin_centers = 0.5 * (bins[:-1] + bins[1:]) + + fig, ax = plt.subplots(figsize=(6, 6)) + ax.plot([0, 1], [0, 1], "k--", label="Perfect calibration", linewidth=1.5) + + colors = ["tab:red", "tab:blue", "tab:green"] + for (name, probs), color in zip(probabilities_dict.items(), colors): + accs = [] + for lo, hi in zip(bins[:-1], bins[1:]): + mask = (probs >= lo) & (probs < hi) + if mask.sum() == 0: + accs.append(float("nan")) + else: + accs.append(labels[mask].mean()) + ece = expected_calibration_error(probs, labels, n_bins) + ax.plot( + bin_centers, + accs, + marker="o", + label=f"{name} (ECE={ece:.3f})", + color=color, + ) + + ax.set_xlabel("Mean predicted probability") + ax.set_ylabel("Fraction of positives") + ax.set_title("Reliability Diagram") + ax.legend(loc="upper left") + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + plt.tight_layout() + + if output_path: + os.makedirs(os.path.dirname(output_path), exist_ok=True) + plt.savefig(output_path, dpi=150) + print(f" Saved reliability diagram to {output_path}") + else: + plt.show() + plt.close(fig) + + +def uncertainty_histogram( + uncertainty: np.ndarray, + output_path: str | None = None, +) -> None: + """Plot distribution of MC Dropout uncertainty estimates.""" + fig, ax = plt.subplots(figsize=(6, 4)) + ax.hist(uncertainty, bins=40, edgecolor="white", color="tab:green") + ax.set_xlabel("Uncertainty (variance across MC samples)") + ax.set_ylabel("Count") + ax.set_title("MC Dropout Uncertainty Distribution") + plt.tight_layout() + if output_path: + os.makedirs(os.path.dirname(output_path), exist_ok=True) + plt.savefig(output_path, dpi=150) + print(f" Saved uncertainty histogram to {output_path}") + else: + plt.show() + plt.close(fig) + + +# --------------------------------------------------------------------------- +# Training helpers +# --------------------------------------------------------------------------- + + +def train_and_evaluate( + name: str, + config: dict, + dataset: pd.DataFrame, + output_dir: str, +) -> tuple[LudwigModel, pd.DataFrame, np.ndarray]: + """Train a Ludwig model and return predictions on the test split. + + Returns: + (model, predictions_df, labels) + """ + result_dir = os.path.join(output_dir, name) + shutil.rmtree(result_dir, ignore_errors=True) + + print(f"\n--- Training: {name} ---") + model = LudwigModel(config=config, logging_level=logging.WARNING) + model.train( + dataset=dataset, + experiment_name="uncertainty", + model_name=name, + output_directory=result_dir, + ) + + print(f" Evaluating {name}...") + _, predictions, _ = model.predict(dataset=dataset) + return model, predictions + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + output_dir = "./results" + viz_dir = "./visualizations" + os.makedirs(viz_dir, exist_ok=True) + + # Load dataset + df = load_dataset() + labels = df["quality"].values.astype(int) + + # Train models + _, baseline_preds = train_and_evaluate("baseline", BASE_CONFIG, df, output_dir) + _, calibrated_preds = train_and_evaluate("calibrated", CALIBRATED_CONFIG, df, output_dir) + mc_model, mc_preds = train_and_evaluate("mc_dropout", MC_DROPOUT_CONFIG, df, output_dir) + + # Extract probabilities + baseline_probs = baseline_preds["quality_probability_True"].values + calibrated_probs = calibrated_preds["quality_probability_True"].values + mc_probs = mc_preds["quality_probability_True"].values + + # Compute ECE + baseline_ece = expected_calibration_error(baseline_probs, labels) + calibrated_ece = expected_calibration_error(calibrated_probs, labels) + mc_ece = expected_calibration_error(mc_probs, labels) + + print("\n=== Expected Calibration Error (ECE) ===") + print(f" Baseline: ECE = {baseline_ece:.4f}") + print(f" Temperature Scaling: ECE = {calibrated_ece:.4f}") + print(f" MC Dropout: ECE = {mc_ece:.4f}") + print() + + # Reliability diagram + reliability_diagram( + { + "Baseline": baseline_probs, + "Temperature Scaling": calibrated_probs, + }, + labels, + output_path=os.path.join(viz_dir, "reliability_diagram.png"), + ) + + # MC Dropout uncertainty + if "quality_uncertainty" in mc_preds.columns: + uncertainty = mc_preds["quality_uncertainty"].values + print("MC Dropout uncertainty stats:") + print(f" mean={uncertainty.mean():.4f}, std={uncertainty.std():.4f}, max={uncertainty.max():.4f}") + + uncertainty_histogram( + uncertainty, + output_path=os.path.join(viz_dir, "mc_dropout_uncertainty.png"), + ) + + # Show high-uncertainty predictions + threshold = np.percentile(uncertainty, 80) + high_unc_mask = uncertainty >= threshold + print(f"\nHigh-uncertainty samples (top 20%, threshold={threshold:.4f}):") + high_unc_preds = mc_preds["quality_predictions"].values[high_unc_mask].astype(bool) + high_unc_labels = labels[high_unc_mask].astype(bool) + high_unc_acc = (high_unc_preds == high_unc_labels).mean() + print(f" count={high_unc_mask.sum()}, accuracy on these samples: {high_unc_acc:.2%}") + else: + print("Note: 'quality_uncertainty' column not found in predictions.") + print("Make sure mc_dropout_samples > 0 and the decoder has dropout > 0.") + + print(f"\nDone. Plots saved to {viz_dir}/") + + +if __name__ == "__main__": + main() diff --git a/examples/uncertainty/uncertainty.ipynb b/examples/uncertainty/uncertainty.ipynb new file mode 100644 index 0000000..ce7d3db --- /dev/null +++ b/examples/uncertainty/uncertainty.ipynb @@ -0,0 +1,552 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Uncertainty Quantification: MC Dropout and Temperature Scaling\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ludwig-ai/ludwig/blob/main/examples/uncertainty/uncertainty.ipynb)\n", + "\n", + "Modern deep learning classifiers are often **overconfident** — they assign extreme probabilities (near 0 or 1) even when they should be uncertain. This notebook demonstrates two complementary techniques for addressing that:\n", + "\n", + "| Technique | What it does | When to use |\n", + "|---|---|---|\n", + "| **Temperature Scaling** | Post-hoc calibration: rescales logits by a learned scalar so probabilities match empirical frequencies | You want trustworthy probability outputs for ranking, risk scoring, or downstream decisions |\n", + "| **MC Dropout** | Runs multiple stochastic forward passes at inference time to produce per-sample uncertainty estimates | You want to flag high-uncertainty predictions for human review or active learning |\n", + "\n", + "**Dataset**: UCI Wine Quality (red wine) — 1,599 samples, 11 physicochemical features, binary target: quality ≥ 7 is \"good\".\n", + "\n", + "**References**:\n", + "- Guo et al., *On Calibration of Modern Neural Networks*, ICML 2017\n", + "- Gal & Ghahramani, *Dropout as a Bayesian Approximation*, ICML 2016" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install ludwig --quiet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dataset\n", + "\n", + "We use the [UCI Wine Quality dataset](https://archive.ics.uci.edu/ml/datasets/wine+quality) (red wine). The original quality score ranges from 3–8; we binarise it: **quality ≥ 7 → good (1), otherwise bad (0)**.\n", + "\n", + "The dataset has ~14% positive examples, making probability calibration especially important." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import shutil\n", + "import warnings\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "WINE_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\"\n", + "\n", + "df = pd.read_csv(WINE_URL, sep=\";\")\n", + "df.columns = [c.replace(\" \", \"_\") for c in df.columns]\n", + "df[\"quality\"] = (df[\"quality\"] >= 7).astype(int)\n", + "\n", + "print(f\"Dataset shape: {df.shape}\")\n", + "print(f\"Positive class (quality>=7): {df['quality'].mean():.1%}\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Calibration Utilities\n", + "\n", + "We implement Expected Calibration Error (ECE) and reliability diagrams from scratch so we can compare models without extra dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def expected_calibration_error(probabilities, labels, n_bins=10):\n", + " \"\"\"Compute ECE: weighted average of |confidence - accuracy| per bin.\"\"\"\n", + " bins = np.linspace(0.0, 1.0, n_bins + 1)\n", + " ece = 0.0\n", + " n = len(probabilities)\n", + " for lo, hi in zip(bins[:-1], bins[1:]):\n", + " mask = (probabilities >= lo) & (probabilities < hi)\n", + " if mask.sum() == 0:\n", + " continue\n", + " conf = probabilities[mask].mean()\n", + " acc = labels[mask].mean()\n", + " ece += mask.sum() / n * abs(conf - acc)\n", + " return float(ece)\n", + "\n", + "\n", + "def plot_reliability_diagram(probabilities_dict, labels, n_bins=10, title=\"Reliability Diagram\"):\n", + " \"\"\"Plot reliability diagrams for multiple models side by side.\"\"\"\n", + " bins = np.linspace(0.0, 1.0, n_bins + 1)\n", + " bin_centers = 0.5 * (bins[:-1] + bins[1:])\n", + "\n", + " fig, ax = plt.subplots(figsize=(7, 6))\n", + " ax.plot([0, 1], [0, 1], \"k--\", label=\"Perfect\", linewidth=1.5)\n", + "\n", + " colors = [\"tab:red\", \"tab:blue\", \"tab:green\", \"tab:orange\"]\n", + " for (name, probs), color in zip(probabilities_dict.items(), colors):\n", + " accs = []\n", + " for lo, hi in zip(bins[:-1], bins[1:]):\n", + " mask = (probs >= lo) & (probs < hi)\n", + " accs.append(labels[mask].mean() if mask.sum() > 0 else float(\"nan\"))\n", + " ece = expected_calibration_error(probs, labels, n_bins)\n", + " ax.plot(bin_centers, accs, marker=\"o\", label=f\"{name} (ECE={ece:.3f})\", color=color)\n", + "\n", + " ax.set_xlabel(\"Mean predicted probability\", fontsize=12)\n", + " ax.set_ylabel(\"Fraction of positives\", fontsize=12)\n", + " ax.set_title(title, fontsize=13)\n", + " ax.legend()\n", + " ax.set_xlim(0, 1)\n", + " ax.set_ylim(0, 1)\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Baseline: Uncalibrated Model\n", + "\n", + "We train a standard binary classifier with Ludwig's ECD architecture. The decoder is `mlp_classifier`, which supports calibration and MC Dropout. The baseline has no calibration enabled." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.api import LudwigModel\n", + "\n", + "WINE_FEATURES = [\n", + " \"fixed_acidity\",\n", + " \"volatile_acidity\",\n", + " \"citric_acid\",\n", + " \"residual_sugar\",\n", + " \"chlorides\",\n", + " \"free_sulfur_dioxide\",\n", + " \"total_sulfur_dioxide\",\n", + " \"density\",\n", + " \"pH\",\n", + " \"sulphates\",\n", + " \"alcohol\",\n", + "]\n", + "\n", + "\n", + "def make_input_features():\n", + " return [{\"name\": f, \"type\": \"number\", \"preprocessing\": {\"normalization\": \"zscore\"}} for f in WINE_FEATURES]\n", + "\n", + "\n", + "baseline_config = {\n", + " \"model_type\": \"ecd\",\n", + " \"input_features\": make_input_features(),\n", + " \"output_features\": [\n", + " {\n", + " \"name\": \"quality\",\n", + " \"type\": \"binary\",\n", + " \"decoder\": {\n", + " \"type\": \"mlp_classifier\",\n", + " \"num_fc_layers\": 1,\n", + " \"output_size\": 64,\n", + " \"dropout\": 0.1,\n", + " # calibration: null (default — no calibration)\n", + " },\n", + " \"loss\": {\"type\": \"binary_weighted_cross_entropy\"},\n", + " }\n", + " ],\n", + " \"combiner\": {\n", + " \"type\": \"concat\",\n", + " \"num_fc_layers\": 2,\n", + " \"output_size\": 128,\n", + " \"dropout\": 0.1,\n", + " },\n", + " \"trainer\": {\"epochs\": 30, \"learning_rate\": 0.001, \"batch_size\": 128},\n", + "}\n", + "\n", + "shutil.rmtree(\"./results/baseline\", ignore_errors=True)\n", + "\n", + "baseline_model = LudwigModel(config=baseline_config, logging_level=logging.WARNING)\n", + "baseline_model.train(\n", + " dataset=df,\n", + " experiment_name=\"uncertainty\",\n", + " model_name=\"baseline\",\n", + " output_directory=\"./results/baseline\",\n", + ")\n", + "print(\"Baseline training complete.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evaluate Calibration\n", + "\n", + "A well-calibrated model should have predicted probability ≈ empirical frequency within each confidence bin. The **reliability diagram** visualises this: a perfectly calibrated model lies on the diagonal. The **Expected Calibration Error (ECE)** summarises miscalibration as a weighted average gap between confidence and accuracy across bins." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_, baseline_preds, _ = baseline_model.predict(dataset=df)\n", + "\n", + "baseline_probs = baseline_preds[\"quality_probability_True\"].values\n", + "true_labels = df[\"quality\"].values\n", + "\n", + "baseline_ece = expected_calibration_error(baseline_probs, true_labels)\n", + "print(f\"Baseline ECE: {baseline_ece:.4f}\")\n", + "print(f\"Predicted probability range: [{baseline_probs.min():.3f}, {baseline_probs.max():.3f}]\")\n", + "print(f\"Mean predicted probability: {baseline_probs.mean():.3f} (base rate: {true_labels.mean():.3f})\")\n", + "\n", + "plot_reliability_diagram(\n", + " {\"Baseline\": baseline_probs},\n", + " true_labels,\n", + " title=\"Reliability Diagram — Baseline\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### What to look for\n", + "\n", + "- Points **above the diagonal**: the model is underconfident in that bin (true frequency > predicted probability)\n", + "- Points **below the diagonal**: the model is overconfident (true frequency < predicted probability)\n", + "- Most neural networks are overconfident, especially in the high-confidence region (right side of the plot)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Temperature Scaling\n", + "\n", + "Temperature scaling is the simplest and most effective post-hoc calibration method. It learns a single scalar **T** on the validation set such that:\n", + "\n", + "```\n", + "calibrated_logit = logit / T\n", + "```\n", + "\n", + "- **T > 1**: softens the distribution (reduces overconfidence)\n", + "- **T < 1**: sharpens the distribution\n", + "- **T = 1**: no change\n", + "\n", + "Critically, temperature scaling **never changes argmax predictions** — accuracy is unchanged. It only adjusts the probabilities.\n", + "\n", + "In Ludwig, enable it by setting `calibration: temperature_scaling` in the decoder config." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import copy\n", + "\n", + "calibrated_config = copy.deepcopy(baseline_config)\n", + "# Enable temperature scaling — this is the only change from the baseline config\n", + "calibrated_config[\"output_features\"][0][\"decoder\"][\"calibration\"] = \"temperature_scaling\"\n", + "\n", + "print(\"Calibrated decoder config:\")\n", + "import json\n", + "\n", + "print(json.dumps(calibrated_config[\"output_features\"][0][\"decoder\"], indent=2))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "shutil.rmtree(\"./results/calibrated\", ignore_errors=True)\n", + "\n", + "calibrated_model = LudwigModel(config=calibrated_config, logging_level=logging.WARNING)\n", + "calibrated_model.train(\n", + " dataset=df,\n", + " experiment_name=\"uncertainty\",\n", + " model_name=\"calibrated\",\n", + " output_directory=\"./results/calibrated\",\n", + ")\n", + "print(\"Temperature scaling training complete.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_, calibrated_preds, _ = calibrated_model.predict(dataset=df)\n", + "\n", + "calibrated_probs = calibrated_preds[\"quality_probability_True\"].values\n", + "calibrated_ece = expected_calibration_error(calibrated_probs, true_labels)\n", + "print(f\"Baseline ECE: {baseline_ece:.4f}\")\n", + "print(f\"Temperature Scaling ECE: {calibrated_ece:.4f}\")\n", + "\n", + "# Compare reliability diagrams\n", + "plot_reliability_diagram(\n", + " {\n", + " \"Baseline\": baseline_probs,\n", + " \"Temperature Scaling\": calibrated_probs,\n", + " },\n", + " true_labels,\n", + " title=\"Reliability Diagram — Baseline vs Temperature Scaling\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Temperature scaling should move the reliability curve closer to the diagonal without affecting classification accuracy. The ECE should decrease." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MC Dropout\n", + "\n", + "Monte Carlo Dropout treats dropout at **inference time** as approximate Bayesian inference. Instead of doing one deterministic forward pass, we do **T stochastic passes** (dropout enabled) and compute:\n", + "\n", + "- **Mean probability** → the prediction\n", + "- **Variance across passes** → the uncertainty estimate\n", + "\n", + "High-uncertainty samples are those where the model gives very different answers across passes — it genuinely does not know.\n", + "\n", + "In Ludwig, enable MC Dropout via `mc_dropout_samples` on the decoder. The `uncertainty` column is automatically added to prediction outputs.\n", + "\n", + "**Important**: You need `dropout > 0` in the decoder (and ideally in the combiner too) for MC Dropout to produce meaningful variance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mc_config = copy.deepcopy(baseline_config)\n", + "mc_config[\"output_features\"][0][\"decoder\"].update(\n", + " {\n", + " \"dropout\": 0.3, # higher dropout → more variance across MC passes\n", + " \"mc_dropout_samples\": 20, # run 20 stochastic forward passes at inference\n", + " }\n", + ")\n", + "mc_config[\"combiner\"][\"dropout\"] = 0.2 # combiner dropout also contributes to variance\n", + "\n", + "print(\"MC Dropout config:\")\n", + "print(\" decoder:\", json.dumps(mc_config[\"output_features\"][0][\"decoder\"], indent=4))\n", + "print(\" combiner dropout:\", mc_config[\"combiner\"][\"dropout\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "shutil.rmtree(\"./results/mc_dropout\", ignore_errors=True)\n", + "\n", + "mc_model = LudwigModel(config=mc_config, logging_level=logging.WARNING)\n", + "mc_model.train(\n", + " dataset=df,\n", + " experiment_name=\"uncertainty\",\n", + " model_name=\"mc_dropout\",\n", + " output_directory=\"./results/mc_dropout\",\n", + ")\n", + "print(\"MC Dropout training complete.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_, mc_preds, _ = mc_model.predict(dataset=df)\n", + "\n", + "print(\"MC Dropout output columns:\")\n", + "print([c for c in mc_preds.columns if \"quality\" in c])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if \"quality_uncertainty\" in mc_preds.columns:\n", + " uncertainty = mc_preds[\"quality_uncertainty\"].values\n", + " mc_probs = mc_preds[\"quality_probability_True\"].values\n", + "\n", + " print(\"Uncertainty statistics:\")\n", + " print(f\" mean = {uncertainty.mean():.5f}\")\n", + " print(f\" std = {uncertainty.std():.5f}\")\n", + " print(f\" max = {uncertainty.max():.5f}\")\n", + " print(f\" p80 = {np.percentile(uncertainty, 80):.5f}\")\n", + "\n", + " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))\n", + "\n", + " # Histogram of uncertainty\n", + " ax1.hist(uncertainty, bins=40, edgecolor=\"white\", color=\"tab:green\")\n", + " ax1.set_xlabel(\"Uncertainty (variance across MC samples)\")\n", + " ax1.set_ylabel(\"Count\")\n", + " ax1.set_title(\"MC Dropout Uncertainty Distribution\")\n", + "\n", + " # Uncertainty vs predicted probability\n", + " scatter = ax2.scatter(mc_probs, uncertainty, c=true_labels, cmap=\"RdYlGn\", alpha=0.4, s=10, vmin=0, vmax=1)\n", + " plt.colorbar(scatter, ax=ax2, label=\"True label\")\n", + " ax2.set_xlabel(\"Mean predicted probability\")\n", + " ax2.set_ylabel(\"Uncertainty\")\n", + " ax2.set_title(\"Uncertainty vs Probability (coloured by true label)\")\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print(\"'quality_uncertainty' not found in predictions.\")\n", + " print(\"Available columns:\", list(mc_preds.columns))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if \"quality_uncertainty\" in mc_preds.columns:\n", + " # Samples the model is most uncertain about\n", + " top_k = 10\n", + " top_uncertain_idx = np.argsort(uncertainty)[-top_k:][::-1]\n", + "\n", + " uncertain_df = df.iloc[top_uncertain_idx][[\"quality\"] + WINE_FEATURES[:4]].copy()\n", + " uncertain_df[\"predicted_prob\"] = mc_probs[top_uncertain_idx].round(3)\n", + " uncertain_df[\"uncertainty\"] = uncertainty[top_uncertain_idx].round(5)\n", + " print(f\"Top-{top_k} highest-uncertainty samples:\")\n", + " print(uncertain_df.to_string())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Interpreting the uncertainty plot\n", + "\n", + "- **High uncertainty near p=0.5**: expected — the model genuinely cannot decide\n", + "- **High uncertainty near p=0 or p=1**: concerning — the model is confident but inconsistent across MC passes\n", + "- **Low uncertainty throughout**: dropout rate may be too low, or `mc_dropout_samples` too small" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary Comparison" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "baseline_acc = (baseline_preds[\"quality_predictions\"].astype(bool) == true_labels.astype(bool)).mean()\n", + "calibrated_acc = (calibrated_preds[\"quality_predictions\"].astype(bool) == true_labels.astype(bool)).mean()\n", + "mc_acc = (mc_preds[\"quality_predictions\"].astype(bool) == true_labels.astype(bool)).mean()\n", + "\n", + "mc_ece = expected_calibration_error(mc_preds[\"quality_probability_True\"].values, true_labels)\n", + "\n", + "print(\"=\" * 60)\n", + "print(f\"{'Model':<25} {'Accuracy':>10} {'ECE':>10}\")\n", + "print(\"-\" * 60)\n", + "print(f\"{'Baseline':<25} {baseline_acc:>10.3%} {baseline_ece:>10.4f}\")\n", + "print(f\"{'Temperature Scaling':<25} {calibrated_acc:>10.3%} {calibrated_ece:>10.4f}\")\n", + "print(f\"{'MC Dropout':<25} {mc_acc:>10.3%} {mc_ece:>10.4f}\")\n", + "print(\"=\" * 60)\n", + "print()\n", + "print(\"Temperature scaling should preserve accuracy while reducing ECE.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## When to Use Each Method\n", + "\n", + "| | Temperature Scaling | MC Dropout |\n", + "|---|---|---|\n", + "| **Goal** | Better-calibrated probabilities | Per-sample uncertainty estimates |\n", + "| **Changes accuracy?** | No | Slightly (mean over passes) |\n", + "| **Inference cost** | None (just divides logits) | ~T× slower (T = mc_dropout_samples) |\n", + "| **Needs validation set?** | Yes (to learn T) | No |\n", + "| **Works without dropout?** | Yes | No — dropout must be > 0 |\n", + "| **Config key** | `decoder.calibration: temperature_scaling` | `decoder.mc_dropout_samples: N` |\n", + "| **Output columns** | `quality_probability_*` (rescaled) | + `quality_uncertainty` |\n", + "| **Good for** | Risk scoring, decision thresholds, ranking | Active learning, human-in-the-loop, safety-critical review |\n", + "\n", + "### Can you use both?\n", + "\n", + "Yes! Temperature scaling and MC Dropout are orthogonal. You can combine them:\n", + "\n", + "```python\n", + "decoder_config = {\n", + " \"type\": \"mlp_classifier\",\n", + " \"dropout\": 0.3,\n", + " \"calibration\": \"temperature_scaling\", # calibrated mean probability\n", + " \"mc_dropout_samples\": 20, # + uncertainty estimate\n", + "}\n", + "```\n", + "\n", + "This gives you both well-calibrated probabilities **and** per-sample uncertainty — useful when both the probability value and the model's confidence in that value matter." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/vlm_finetuning/README.md b/examples/vlm_finetuning/README.md new file mode 100644 index 0000000..0a905a3 --- /dev/null +++ b/examples/vlm_finetuning/README.md @@ -0,0 +1,65 @@ +# Vision-Language Model Fine-Tuning with Ludwig + +Fine-tune a vision-language model (VLM) on a visual-question-answering dataset using +Ludwig's `is_multimodal: true` flag. The example uses **Qwen2-VL-7B-Instruct** with +LoRA + 4-bit quantisation to fit on a single 24 GB GPU, but the same config works with +any HuggingFace `AutoModelForVision2Seq`-compatible model (LLaVA, InternVL, etc.). + +## Dataset format + +A CSV file with three columns: + +| column | description | +| ------------ | ----------------------------------------- | +| `image_path` | Path to image file (JPEG / PNG) | +| `question` | Natural-language question about the image | +| `answer` | Expected answer (fine-tuning target) | + +## Setup + +```bash +pip install "ludwig[llm]" # transformers, peft, bitsandbytes +# Authenticate with HuggingFace if using a gated model +huggingface-cli login +``` + +## Run + +```bash +python run.py --dataset /path/to/vqa.csv --output_dir ./results +``` + +Override the base model: + +```bash +python run.py \ + --dataset /path/to/vqa.csv \ + --base_model llava-hf/llava-1.5-7b-hf +``` + +## Config highlights + +```yaml +is_multimodal: true # use AutoModelForVision2Seq + AutoProcessor +trust_remote_code: true # required for Qwen2-VL custom architecture + +adapter: + type: lora + r: 16 + alpha: 32 + +quantization: + bits: 4 + quantization_type: nf4 + compute_dtype: bfloat16 +``` + +## Supported VLM architectures + +Any model loadable via `AutoModelForVision2Seq` works out of the box: + +- `Qwen/Qwen2-VL-*` +- `llava-hf/llava-1.5-*` +- `llava-hf/llava-v1.6-*` +- `OpenGVLab/InternVL2-*` +- `microsoft/phi-3-vision-*` (also needs `trust_remote_code: true`) diff --git a/examples/vlm_finetuning/run.py b/examples/vlm_finetuning/run.py new file mode 100644 index 0000000..2e2d0be --- /dev/null +++ b/examples/vlm_finetuning/run.py @@ -0,0 +1,71 @@ +"""Vision-Language Model fine-tuning with Ludwig. + +Fine-tunes Qwen2-VL-7B on a visual-question-answering dataset. The dataset is +expected to be a CSV with three columns: + + image_path — relative or absolute path to the image file + question — the question to ask about the image + answer — the expected answer (target for fine-tuning) + +Usage: + python run.py --dataset /path/to/vqa.csv --output_dir ./results +""" + +import argparse +import os +import sys + + +def parse_args(argv=None): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--dataset", required=True, help="Path to CSV with columns: image_path, question, answer") + parser.add_argument("--config", default=os.path.join(os.path.dirname(__file__), "vlm_config.yaml")) + parser.add_argument("--output_dir", default="./results") + parser.add_argument( + "--base_model", + default=None, + help="Override base_model in config (e.g. llava-hf/llava-1.5-7b-hf)", + ) + return parser.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + + from ludwig.api import LudwigModel + + overrides = {} + if args.base_model: + overrides["base_model"] = args.base_model + + model = LudwigModel(config=args.config, logging_level=20) # INFO + + train_stats, _, output_dir = model.train( + dataset=args.dataset, + output_directory=args.output_dir, + skip_save_processed_input=True, + ) + + print(f"\nFine-tuning complete. Model saved to: {output_dir}") + print("\nValidation metrics:") + for split, metrics in train_stats.items(): + if metrics: + print(f" {split}: {metrics}") + + # Quick inference test + print("\nRunning a quick inference test …") + test_row = { + "image_path": "test_image.jpg", + "question": "What is in this image?", + } + try: + preds, _ = model.predict(dataset=[test_row]) + print(" answer:", preds["answer_predictions"].iloc[0]) + except Exception as exc: + print(f" (inference test skipped — {exc})") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/vlm_finetuning/vlm_config.yaml b/examples/vlm_finetuning/vlm_config.yaml new file mode 100644 index 0000000..c3fe891 --- /dev/null +++ b/examples/vlm_finetuning/vlm_config.yaml @@ -0,0 +1,49 @@ +model_type: llm + +# Qwen2-VL-7B-Instruct — swap to any HuggingFace Vision2Seq model +base_model: Qwen/Qwen2-VL-7B-Instruct + +# Enable multimodal (VLM) mode. +# Ludwig loads the model with AutoModelForVision2Seq and uses AutoProcessor +# for joint tokenisation of text + image patches. +is_multimodal: true + +# Trust the custom code shipped alongside Qwen2-VL on HuggingFace. +trust_remote_code: true + +input_features: + - name: image_path + type: image + - name: question + type: text + +output_features: + - name: answer + type: text + +# LoRA adapter for parameter-efficient fine-tuning +adapter: + type: lora + r: 16 + alpha: 32 + target_modules: ["q_proj", "v_proj"] + +trainer: + type: finetune + epochs: 3 + batch_size: 4 + gradient_accumulation_steps: 8 + learning_rate: 2.0e-5 + learning_rate_scheduler: + decay: cosine + warmup_fraction: 0.03 + +# 4-bit NF4 quantisation to fit the 7B model on a single 24 GB GPU +quantization: + bits: 4 + quantization_type: nf4 + compute_dtype: bfloat16 + +generation: + max_new_tokens: 256 + temperature: 0.0 diff --git a/examples/wine_quality/README.md b/examples/wine_quality/README.md new file mode 100644 index 0000000..78f8039 --- /dev/null +++ b/examples/wine_quality/README.md @@ -0,0 +1,24 @@ +# Ludwig Defaults Config Section Example + +Demonstrates how to use Ludwig's defaults section introduced in v0.6. + +### Preparatory Steps + +- Create `data` directory +- Download [Kaggle wine quality data set](https://www.kaggle.com/rajyellow46/wine-quality) into the `data` directory. Directory should + appear as follows: + +``` +wine_quality/ + data/ + winequalityN.csv +``` + +### Description + +Jupyter notebook `model_defaults_example.ipynb` demonstrates how to use the defaults section of Ludwig. +Key features demonstrated in the notebook: + +- Training data is prepared for use +- Programmatically create Ludwig config dictionary from the training data dataframe +- How to define preprocessing, encoder, decoder and loss sub-sections under the defaults section diff --git a/examples/wine_quality/model_defaults_example.ipynb b/examples/wine_quality/model_defaults_example.ipynb new file mode 100644 index 0000000..874ea42 --- /dev/null +++ b/examples/wine_quality/model_defaults_example.ipynb @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import os\n", + "import shutil\n", + "from pprint import pprint\n", + "\n", + "import pandas as pd\n", + "\n", + "from ludwig.api import LudwigModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Receive data for training" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "train_df = pd.read_csv(\"./data/winequalityN.csv\")\n", + "train_df[\"quality\"] = train_df[\"quality\"].apply(str)\n", + "train_df.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Replace white space in column names with underscore\n", + "new_col = []\n", + "for i in range(len(train_df.columns)):\n", + " new_col.append(train_df.columns[i].replace(\" \", \"_\"))\n", + "\n", + "train_df.columns = new_col" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "train_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "train_df.describe().T" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "train_df.dtypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "train_df[\"quality\"].value_counts().sort_index()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cols = list(set(train_df.columns) - set([\"quality\"]))\n", + "features = train_df[cols]\n", + "\n", + "# extract categorical features\n", + "categorical_features = []\n", + "for p in features:\n", + " if train_df[p].dtype == \"object\":\n", + " categorical_features.append(p)\n", + "\n", + "print(\"categorical features:\", categorical_features, \"\\n\")\n", + "\n", + "# get numerical features\n", + "numerical_features = list(set(features) - set(categorical_features))\n", + "\n", + "print(\"numerical features:\", numerical_features, \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for feature in categorical_features:\n", + " print(f\"# of distinct values in categorical feature '{feature}' : {train_df[feature].nunique()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Ludwig Config" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# template for config\n", + "config = {\"input_features\": [], \"output_features\": [], \"trainer\": {}}\n", + "\n", + "# setup input features for categorical features\n", + "for p in categorical_features:\n", + " a_feature = {\"name\": p.replace(\" \", \"_\"), \"type\": \"category\"}\n", + " config[\"input_features\"].append(a_feature)\n", + "\n", + "# setup input features for numerical features\n", + "for p in numerical_features:\n", + " a_feature = {\"name\": p.replace(\" \", \"_\"), \"type\": \"number\"}\n", + " config[\"input_features\"].append(a_feature)\n", + "\n", + "# set up output variable\n", + "config[\"output_features\"].append({\"name\": \"quality\", \"type\": \"category\"})\n", + "\n", + "# set default preprocessing and encoder for numerical features\n", + "config[\"defaults\"] = {\n", + " \"number\": {\n", + " \"preprocessing\": {\"missing_value_strategy\": \"fill_with_mean\", \"normalization\": \"zscore\"},\n", + " \"encoder\": {\"type\": \"dense\", \"num_layers\": 2},\n", + " },\n", + " \"category\": {\"encoder\": {\"type\": \"sparse\"}, \"decoder\": {\"top_k\": 2}, \"loss\": {\"confidence_penalty\": 0.1}},\n", + "}\n", + "\n", + "# set up trainer\n", + "config[\"trainer\"] = {\"epochs\": 5}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pprint(config, indent=2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize and Train LudwigModel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = LudwigModel(config, backend=\"local\", logging_level=logging.INFO)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inspecting Config After Model Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pprint(model.config[\"input_features\"], indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pprint(model.config[\"output_features\"], indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eval_stats, train_stats, _, _ = model.experiment(dataset=train_df, experiment_name=\"wine_quality\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " shutil.rmtree(\"./results\")\n", + " items = os.listdir(\"./\")\n", + " for item in items:\n", + " if item.endswith(\".hdf5\") or item.endswith(\".json\") or item == \".lock_preprocessing\":\n", + " os.remove(os.path.join(\"./\", item))\n", + "except Exception:\n", + " pass" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "949777d72b0d2535278d3dc13498b2535136f6dfe0678499012e853ee9abcab1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/wmt15/config_large.yaml b/examples/wmt15/config_large.yaml new file mode 100644 index 0000000..4fa2107 --- /dev/null +++ b/examples/wmt15/config_large.yaml @@ -0,0 +1,10 @@ +input_features: + - name: en + type: text + encoder: bert + pretrained_model_name_or_path: bert-base-uncased + +output_features: + - name: fr + type: text + tokenizer: french_tokenize diff --git a/examples/wmt15/config_small.yaml b/examples/wmt15/config_small.yaml new file mode 100644 index 0000000..fc6746d --- /dev/null +++ b/examples/wmt15/config_small.yaml @@ -0,0 +1,8 @@ +input_features: + - name: en + type: text + encoder: embed + +output_features: + - name: fr + type: text diff --git a/examples/wmt15/train_nmt.py b/examples/wmt15/train_nmt.py new file mode 100644 index 0000000..48140f3 --- /dev/null +++ b/examples/wmt15/train_nmt.py @@ -0,0 +1,24 @@ +"""Sample ludwig training code for training an NMT model (en -> fr) on WMT15 (https://www.statmt.org/wmt15/). + +The dataset is rather large (8GB), which can take several minutes to preprocess. +""" + +import logging +import shutil + +from ludwig.api import LudwigModel +from ludwig.datasets import wmt15 + +# clean out prior results +shutil.rmtree("./results", ignore_errors=True) + +# Download and prepare the dataset +training_set = wmt15.load() + +model = LudwigModel(config="./config_small.yaml", logging_level=logging.INFO) + +( + train_stats, # dictionary containing training statistics + preprocessed_data, # tuple Ludwig Dataset objects of pre-processed training data + output_directory, # location of training results stored on disk +) = model.train(dataset=training_set, experiment_name="simple_experiment", model_name="simple_model") diff --git a/ludwig/__init__.py b/ludwig/__init__.py new file mode 100644 index 0000000..9a37e2f --- /dev/null +++ b/ludwig/__init__.py @@ -0,0 +1,38 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import sys + +from ludwig.globals import LUDWIG_VERSION as __version__ # noqa + +logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s") + +# Disable annoying message about NUMEXPR_MAX_THREADS +logging.getLogger("numexpr").setLevel(logging.WARNING) + +# Prevent Dask from converting object-dtype columns to PyArrow strings. +# Dask's default convert-string:True tries to decode every object column as +# UTF-8, which corrupts binary data (image bytes, numpy arrays, etc.) with a +# UnicodeDecodeError. This must be set at import time — before the caller +# creates any Dask DataFrame — because the _to_string_dtype expression node is +# baked into the task graph at dd.from_pandas() / dd.read_*() time. +# Setting it in RayBackend.initialize() (which happens after train() is called) +# is too late to help user-provided DataFrames. GitHub issue #4149. +try: + import dask + + dask.config.set({"dataframe.convert-string": False}) +except ImportError: + pass diff --git a/ludwig/accounting/__init__.py b/ludwig/accounting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/accounting/used_tokens.py b/ludwig/accounting/used_tokens.py new file mode 100644 index 0000000..4337b12 --- /dev/null +++ b/ludwig/accounting/used_tokens.py @@ -0,0 +1,35 @@ +import torch + + +def get_used_tokens_for_ecd(inputs: dict[str, torch.Tensor], targets: dict[str, torch.Tensor]) -> int: + """Returns the number of used tokens for an ECD model. + + The number of used tokens is the total size of the input and output tensors, which corresponds to 1 token for + binary, category, and number features, and variable number of tokens for text features, for each example in the + batch. + + Args: + inputs: The input tensors for one forward pass through ecd. + targets: The target tensors for one forward pass through ecd. + """ + used_tokens = 0 + for input_feature_tensor in inputs.values(): + used_tokens += torch.flatten(input_feature_tensor).shape[0] + if targets is not None: + # targets may be None for evaluation. + for output_feature_tensor in targets.values(): + used_tokens += torch.flatten(output_feature_tensor).shape[0] + return used_tokens + + +def get_used_tokens_for_llm(model_inputs: torch.Tensor, tokenizer) -> int: + """Returns the number of used tokens for an LLM model. + + Args: + model_inputs: torch.Tensor with the merged input and target IDs. + tokenizer: The tokenizer used to encode the inputs. + + Returns: + The total number of non-pad tokens, for all examples in the batch. + """ + return torch.sum(model_inputs != tokenizer.pad_token_id).item() diff --git a/ludwig/api.py b/ludwig/api.py new file mode 100644 index 0000000..c710cbd --- /dev/null +++ b/ludwig/api.py @@ -0,0 +1,2044 @@ +# !/usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +""" +File name: LudwigModel.py +Author: Piero Molino +Date created: 5/21/2019 +Python Version: 3+ +""" + +import copy +import dataclasses +import logging +import os +import tempfile +import time +import traceback +from pprint import pformat +from typing import Any + +import numpy as np +import pandas as pd +import torch +from tabulate import tabulate + +from ludwig.api_annotations import PublicAPI +from ludwig.api_types import EvaluationFrequency, PreprocessedDataset, TrainingResults, TrainingStats +from ludwig.backend import Backend, initialize_backend, provision_preprocessing_workers +from ludwig.callbacks import Callback +from ludwig.constants import ( + AUTO, + BATCH_SIZE, + EVAL_BATCH_SIZE, + FALLBACK_BATCH_SIZE, + FULL, + HYPEROPT, + HYPEROPT_WARNING, + MIN_DATASET_SPLIT_ROWS, + MODEL_ECD, + MODEL_LLM, + TEST, + TIMESERIES, + TRAINING, + VALIDATION, +) +from ludwig.data.cache.types import CacheableDataset +from ludwig.data.dataset.base import Dataset +from ludwig.data.postprocessing import convert_predictions, postprocess +from ludwig.data.preprocessing import load_metadata, preprocess_for_prediction, preprocess_for_training +from ludwig.datasets import load_dataset_uris +from ludwig.experiment_utils import get_experiment_description +from ludwig.features.feature_registries import update_config_with_metadata, update_config_with_model +from ludwig.features.timeseries_feature import incremental_time_delay_embedding +from ludwig.globals import ( + MODEL_FILE_NAME, + MODEL_HYPERPARAMETERS_FILE_NAME, + model_weights_exist, + MODEL_WEIGHTS_FILE_NAME, + set_disable_progressbar, + TRAIN_SET_METADATA_FILE_NAME, + TRAINING_CHECKPOINTS_DIR_PATH, +) +from ludwig.models.base import BaseModel +from ludwig.models.calibrator import Calibrator +from ludwig.models.predictor import ( + calculate_overall_stats, + print_evaluation_stats, + save_evaluation_stats, + save_prediction_outputs, +) +from ludwig.models.registry import model_type_registry +from ludwig.schema.model_config import ModelConfig +from ludwig.types import ModelConfigDict +from ludwig.upload import get_upload_registry +from ludwig.utils import metric_utils +from ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version +from ludwig.utils.config_utils import get_preprocessing_params +from ludwig.utils.data_utils import ( + figure_data_format, + generate_kfold_splits, + load_dataset, + load_json, + load_yaml, + save_json, +) +from ludwig.utils.dataset_utils import generate_dataset_statistics +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import makedirs, path_exists, upload_output_directory +from ludwig.utils.heuristics import get_auto_learning_rate +from ludwig.utils.llm_utils import create_text_streamer, TextStreamer +from ludwig.utils.misc_utils import ( + get_file_names, + get_from_registry, + get_output_directory, + set_saved_weights_in_checkpoint_flag, +) +from ludwig.utils.print_utils import print_boxed +from ludwig.utils.tokenizers import HFTokenizer +from ludwig.utils.trainer_utils import get_training_report +from ludwig.utils.types import DataFrame +from ludwig.utils.upload_utils import HuggingFaceHub + +logger = logging.getLogger(__name__) + + +@PublicAPI +class LudwigModel: + """High-level interface to Ludwig's train / predict / evaluate / experiment pipelines. + + Example: + Train a model:: + + config = {...} + model = LudwigModel(config) + train_stats, _, _ = model.train(dataset=file_path) + # or with a DataFrame: + train_stats, _, _ = model.train(dataset=dataframe) + + Load a previously trained model and predict:: + + model = LudwigModel.load(model_dir) + predictions, output_dir = model.predict(dataset=file_path) + # or: + predictions, output_dir = model.predict(dataset=dataframe) + + Evaluate:: + + eval_stats, _, _ = model.evaluate(dataset=file_path) + """ + + def __init__( + self, + config: str | dict, + logging_level: int = logging.ERROR, + backend: Backend | str | None = None, + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + ) -> None: + """Initialize a LudwigModel. + + Args: + config: In-memory config dict or path to a YAML config file. + logging_level: Log level sent to stderr (e.g., logging.INFO). + backend: Backend instance or string name (e.g., "local", "ray") used for + preprocessing and training. + gpus: GPUs to use; same syntax as CUDA_VISIBLE_DEVICES. + gpu_memory_limit: Maximum memory fraction [0, 1] allowed per GPU device. + allow_parallel_threads: Allow Torch to use multi-threading for performance + at the cost of determinism. + callbacks: List of `ludwig.callbacks.Callback` objects that provide hooks + into the Ludwig pipeline. + """ + # check if config is a path or a dict + if isinstance(config, str): # assume path + config_dict = load_yaml(config) + self.config_fp = config + else: + config_dict = copy.deepcopy(config) + self.config_fp = None + + self._user_config = upgrade_config_dict_to_latest_version(config_dict) + + # Initialize the config object + self.config_obj = ModelConfig.from_dict(self._user_config) + + # setup logging + self.set_logging_level(logging_level) + + # setup Backend + self.backend = initialize_backend(backend or self._user_config.get("backend")) + logger.info(f"Using backend: {self.backend.BACKEND_TYPE}") + self.callbacks = callbacks if callbacks is not None else [] + + # setup PyTorch env (GPU allocation, etc.) + self.backend.initialize_pytorch( + gpus=gpus, gpu_memory_limit=gpu_memory_limit, allow_parallel_threads=allow_parallel_threads + ) + + # setup model + self.model = None + self.training_set_metadata: dict[str, dict] | None = None + + # online training state + self._online_trainer = None + + # Zero-shot LLM usage. + if ( + self.config_obj.model_type == MODEL_LLM + and self.config_obj.trainer.type == "none" + # Category output features require a vocabulary. The LLM LudwigModel should be initialized with + # model.train(dataset). + and self.config_obj.output_features[0].type == "text" + ): + self._initialize_llm_for_zero_shot() + + def _get_or_create_model( + self, config_obj: ModelConfig | None = None, random_seed: int = default_random_seed + ) -> None: + """Single entry point for model instantiation. + + Creates self.model from config_obj (or self.config_obj) if it hasn't been created yet. Safe to call multiple + times — no-ops if model exists. + """ + if self.model is not None: + return + cfg = config_obj or self.config_obj + logger.info(f"Creating {cfg.model_type} model") + self.model = LudwigModel.create_model(cfg, random_seed=random_seed) + + def _initialize_llm_for_zero_shot(self, random_seed: int = default_random_seed): + """Initialize the LLM for zero-shot (InferenceOnlyTrainer) inference only.""" + self._get_or_create_model(random_seed=random_seed) + + if self.model.model.device.type == "cpu" and torch.cuda.is_available(): + logger.warning(f"LLM was initialized on {self.model.model.device}. Moving to GPU for inference.") + self.model.model.to(torch.device("cuda")) + + def train( + self, + dataset: str | dict | pd.DataFrame | None = None, + training_set: str | dict | pd.DataFrame | Dataset | None = None, + validation_set: str | dict | pd.DataFrame | Dataset | None = None, + test_set: str | dict | pd.DataFrame | Dataset | None = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + experiment_name: str = "api_experiment", + model_name: str = "run", + model_resume_path: str | None = None, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = False, + output_directory: str | None = "results", + random_seed: int = default_random_seed, + callbacks: list[Callback] | None = None, + **kwargs: Any, + ) -> TrainingResults: + """Train the model on the provided dataset. + + Results are saved to `[output_directory]/[experiment_name]_[model_name]_n`, + where `n` increments to differentiate repeated runs. + + Args: + dataset: Source containing the full dataset. If it has a split column (0=train, + 1=validation, 2=test) it is used for splitting; otherwise the dataset is + split randomly. Mutually exclusive with `training_set`. + training_set: Source containing training data only. + validation_set: Source containing validation data only. + test_set: Source containing test data only. + training_set_metadata: Pre-computed metadata dict or path to a `.meta.json` + file produced by a previous Ludwig run on the same dataset. + data_format: Format hint for data sources. Inferred automatically when + `None`. Valid values: `'auto'`, `'csv'`, `'df'`, `'dict'`, + `'excel'`, `'feather'`, `'fwf'`, `'hdf5'`, `'html'`, `'json'`, + `'jsonl'`, `'parquet'`, `'pickle'`, `'sas'`, `'spss'`, `'stata'`, + `'tsv'`. + experiment_name: Name used when creating the output directory. + model_name: Name used when creating the output directory. + model_resume_path: Resume training from this checkpoint directory. + Config, optimizer state, and training statistics are all restored. + skip_save_training_description: Skip saving the experiment description JSON. + skip_save_training_statistics: Skip saving training statistics JSON. + skip_save_model: Skip saving model weights after each improvement. + The returned model will have end-of-training weights rather than + best-validation weights, and the model cannot be reloaded later. + skip_save_progress: Skip saving per-epoch checkpoints used for resuming. + skip_save_log: Skip saving TensorBoard logs. + skip_save_processed_input: Skip caching the preprocessed HDF5/JSON files. + output_directory: Root directory for all saved outputs. + random_seed: Seed for data splitting, weight initialization, and shuffling. + callbacks: Additional callbacks for this specific ``train()`` call. These are + merged with any callbacks already attached to the model via + ``LudwigModel(callbacks=[...])``. Useful for per-run instrumentation + (e.g., attaching a WandB logger to one run without rebuilding the model). + Callbacks added here do not persist after this call returns. + **kwargs: Additional keyword arguments forwarded to preprocessing. + + Returns: + A `TrainingResults` namedtuple with fields: + - `training_set_metadata`: feature-level preprocessing metadata. + - `preprocessed_data`: `(training_set, validation_set, test_set)` datasets. + - `output_directory`: path where all outputs were saved. + """ + # Only reset the metadata if the model has not been trained before + if self.training_set_metadata: + logger.warning( + "This model has been trained before. Its architecture has been defined by the original training set " + "(for example, the number of possible categorical outputs). The current training data will be mapped " + "to this architecture. If you want to change the architecture of the model, please concatenate your " + "new training data with the original and train a new model from scratch." + ) + training_set_metadata = self.training_set_metadata + + if self._user_config.get(HYPEROPT): + print_boxed("WARNING") + logger.warning(HYPEROPT_WARNING) + + # setup directories and file names + if model_resume_path is not None: + if path_exists(model_resume_path): + output_directory = model_resume_path + if self.backend.is_coordinator(): + logger.info(f"Model resume path '{model_resume_path}' exists, trying to resume training.") + else: + if self.backend.is_coordinator(): + logger.info( + f"Model resume path '{model_resume_path}' does not exist, starting training from scratch" + ) + model_resume_path = None + + if model_resume_path is None: + if self.backend.is_coordinator(): + output_directory = get_output_directory(output_directory, experiment_name, model_name) + else: + output_directory = None + + # if we are skipping all saving, + # there is no need to create a directory that will remain empty + should_create_output_directory = not ( + skip_save_training_description + and skip_save_training_statistics + and skip_save_model + and skip_save_progress + and skip_save_log + and skip_save_processed_input + ) + + output_url = output_directory + with upload_output_directory(output_directory) as (output_directory, upload_fn): + # Merge per-call callbacks with the model's own callbacks for the duration of this train() call. + train_callbacks = self.callbacks + (callbacks or []) + if upload_fn is not None: + # Upload output files (checkpoints, etc.) to remote storage at the end of + # each epoch and evaluation, in case of failure in the middle of training. + class UploadOnEpochEndCallback(Callback): + def on_eval_end(self, trainer, progress_tracker, save_path): + upload_fn() + + def on_epoch_end(self, trainer, progress_tracker, save_path): + upload_fn() + + train_callbacks = train_callbacks + [UploadOnEpochEndCallback()] + + description_fn = training_stats_fn = model_dir = None + if self.backend.is_coordinator(): + if should_create_output_directory: + makedirs(output_directory, exist_ok=True) + description_fn, training_stats_fn, model_dir = get_file_names(output_directory) + + if isinstance(training_set, Dataset) and training_set_metadata is not None: + preprocessed_data = PreprocessedDataset(training_set, validation_set, test_set, training_set_metadata) + else: + # save description + if self.backend.is_coordinator(): + description = get_experiment_description( + self.config_obj.to_dict(), + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + backend=self.backend, + random_seed=random_seed, + ) + + if not skip_save_training_description: + save_json(description_fn, description) + + # print description + experiment_description = [ + ["Experiment name", experiment_name], + ["Model name", model_name], + ["Output directory", output_directory], + ] + for key, value in description.items(): + if key != "config": # Config is printed separately. + experiment_description.append([key, pformat(value, indent=4)]) + + if self.backend.is_coordinator(): + print_boxed("EXPERIMENT DESCRIPTION") + logger.info(tabulate(experiment_description, tablefmt="fancy_grid")) + + print_boxed("LUDWIG CONFIG") + logger.info("User-specified config (with upgrades):\n") + logger.info(pformat(self._user_config, indent=4)) + logger.info( + "\nFull config saved to:\n" + f"{output_directory}/{experiment_name}/model/model_hyperparameters.json" + ) + + preprocessed_data = self.preprocess( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + model_resume_path=model_resume_path, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + output_directory=output_directory, + random_seed=random_seed, + **kwargs, + ) + training_set = preprocessed_data.training_set + validation_set = preprocessed_data.validation_set + test_set = preprocessed_data.test_set + training_set_metadata = preprocessed_data.training_set_metadata + + self.training_set_metadata = training_set_metadata + + if self.backend.is_coordinator(): + dataset_statistics = generate_dataset_statistics(training_set, validation_set, test_set) + + if not skip_save_model: + # save train set metadata + assert model_dir is not None + os.makedirs(model_dir, exist_ok=True) + save_json(os.path.join(model_dir, TRAIN_SET_METADATA_FILE_NAME), training_set_metadata) + + logger.info("\nDataset Statistics") + logger.info(tabulate(dataset_statistics, headers="firstrow", tablefmt="fancy_grid")) + + for callback in train_callbacks: + callback.on_train_init( + base_config=self._user_config, + experiment_directory=output_directory, + experiment_name=experiment_name, + model_name=model_name, + output_directory=output_directory, + resume_directory=model_resume_path, + ) + + # Build model if not provided + # if it was provided it means it was already loaded + if not self.model: + if self.backend.is_coordinator(): + print_boxed("MODEL") + # update model config with metadata properties derived from training set + update_config_with_metadata(self.config_obj, training_set_metadata) + logger.info("Warnings and other logs:") + self.model = LudwigModel.create_model(self.config_obj, random_seed=random_seed) + # update config with properties determined during model instantiation + update_config_with_model(self.config_obj, self.model) + set_saved_weights_in_checkpoint_flag(self.config_obj) + + # auto tune learning rate + if hasattr(self.config_obj.trainer, "learning_rate") and self.config_obj.trainer.learning_rate == AUTO: + detected_learning_rate = get_auto_learning_rate(self.config_obj) + self.config_obj.trainer.learning_rate = detected_learning_rate + + with self.backend.create_trainer( + model=self.model, + config=self.config_obj.trainer, + resume=model_resume_path is not None, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + callbacks=train_callbacks, + random_seed=random_seed, + ) as trainer: + # auto tune batch size + self._tune_batch_size_and_grad_accum(trainer, training_set, random_seed=random_seed) + + if ( + self.config_obj.model_type == MODEL_LLM + and trainer.config.type == "none" + and self.config_obj.adapter is not None + and self.config_obj.adapter.pretrained_adapter_weights is not None + ): + trainer.model.initialize_adapter() # Load pre-trained adapter weights for inference only + + # train model + if self.backend.is_coordinator(): + print_boxed("TRAINING") + if not skip_save_model: + self.save_config(model_dir) + + for callback in train_callbacks: + callback.on_train_start( + model=self.model, + config=self.config_obj.to_dict(), + config_fp=self.config_fp, + ) + + try: + train_stats = trainer.train( + training_set, + validation_set=validation_set, + test_set=test_set, + save_path=model_dir, + ) + self.model, train_trainset_stats, train_valiset_stats, train_testset_stats = train_stats + + # Calibrate output probabilities and save model (coordinator-only). + # Must run after training completes, before final model parameters are saved. + if self.backend.is_coordinator(): + calibrator = Calibrator( + self.model, + self.backend, + batch_size=trainer.eval_batch_size, + ) + self._run_calibration(calibrator, validation_set, training_set, skip_save_model, model_dir) + + # Evaluation Frequency + if self.config_obj.model_type == MODEL_ECD and self.config_obj.trainer.steps_per_checkpoint: + evaluation_frequency = EvaluationFrequency( + self.config_obj.trainer.steps_per_checkpoint, EvaluationFrequency.STEP + ) + elif self.config_obj.model_type == MODEL_ECD and self.config_obj.trainer.checkpoints_per_epoch: + evaluation_frequency = EvaluationFrequency( + 1.0 / self.config_obj.trainer.checkpoints_per_epoch, EvaluationFrequency.EPOCH + ) + else: + evaluation_frequency = EvaluationFrequency(1, EvaluationFrequency.EPOCH) + + # Unpack train()'s return. + # The statistics are all nested dictionaries of TrainerMetrics: feature_name -> metric_name -> + # List[TrainerMetric], with one entry per training checkpoint, according to steps_per_checkpoint. + # We reduce the dictionary of TrainerMetrics to a simple list of floats for interfacing with Ray + # Tune. + train_stats = TrainingStats( + metric_utils.reduce_trainer_metrics_dict(train_trainset_stats), + metric_utils.reduce_trainer_metrics_dict(train_valiset_stats), + metric_utils.reduce_trainer_metrics_dict(train_testset_stats), + evaluation_frequency, + ) + + # save training statistics + if self.backend.is_coordinator(): + if not skip_save_training_statistics: + save_json(training_stats_fn, train_stats) + + # results of the model with highest validation test performance + if ( + self.backend.is_coordinator() + and validation_set is not None + and not self.config_obj.trainer.skip_all_evaluation + ): + print_boxed("TRAINING REPORT") + training_report = get_training_report( + trainer.validation_field, + trainer.validation_metric, + test_set is not None, + train_valiset_stats, + train_testset_stats, + ) + logger.info(tabulate(training_report, tablefmt="fancy_grid")) + logger.info(f"\nFinished: {experiment_name}_{model_name}") + logger.info(f"Saved to: {output_directory}") + finally: + for callback in train_callbacks: + callback.on_train_end(output_directory) + + self.training_set_metadata = training_set_metadata + + if self.is_merge_and_unload_set(): + # For an LLM model trained with a LoRA adapter, merge first, then save the full model. + self.model.merge_and_unload(progressbar=self.config_obj.adapter.postprocessor.progressbar) + + if self.backend.is_coordinator() and not skip_save_model: + self.model.save_base_model(model_dir) + elif self.backend.is_coordinator() and not skip_save_model: + self.model.save(model_dir) + + # Save model card alongside the model (always) + if self.backend.is_coordinator() and not skip_save_model: + try: + from ludwig.utils.model_card import save_model_card + + save_model_card( + output_directory=output_directory, + config=self.config_obj.to_dict(), + training_set_metadata=training_set_metadata, + train_stats=train_stats, + model_dir=model_dir, + ) + except Exception: + logger.warning("Failed to generate model card.", exc_info=True) + + # Save training report (always, alongside the model) + if self.backend.is_coordinator() and not skip_save_model: + try: + from ludwig.utils.training_report import save_training_report + + save_training_report( + output_directory=output_directory, + config=self.config_obj.to_dict(), + training_set_metadata=training_set_metadata, + train_stats=train_stats, + model_dir=model_dir, + random_seed=random_seed, + ) + except Exception: + logger.warning("Failed to generate training report.", exc_info=True) + + # Synchronize model weights between workers + self.backend.sync_model(self.model) + + print_boxed("FINISHED") + return TrainingResults(train_stats, preprocessed_data, output_url) + + def train_online( + self, + dataset: str | dict | pd.DataFrame, + training_set_metadata: str | dict | None = None, + data_format: str = "auto", + random_seed: int = default_random_seed, + ) -> None: + """Train the model for one epoch on `dataset` (online / incremental learning). + + Args: + dataset: Source containing the training data for this epoch. + training_set_metadata: Pre-computed metadata from a prior run. When + `None`, metadata is derived from the provided dataset. + data_format: Format hint for the data source. Inferred when `'auto'`. + random_seed: Seed for data splitting and parameter initialization. + """ + training_set_metadata = training_set_metadata or self.training_set_metadata + preprocessing_params = get_preprocessing_params(self.config_obj) + + with provision_preprocessing_workers(self.backend): + training_dataset, _, _, training_set_metadata = preprocess_for_training( + self.config_obj, + training_set=dataset, + training_set_metadata=training_set_metadata, + data_format=data_format, + skip_save_processed_input=True, + preprocessing_params=preprocessing_params, + backend=self.backend, + random_seed=random_seed, + callbacks=self.callbacks, + ) + + if not self.training_set_metadata: + self.training_set_metadata = training_set_metadata + + if not self.model: + update_config_with_metadata(self.config_obj, training_set_metadata) + self.model = LudwigModel.create_model(self.config_obj, random_seed=random_seed) + # update config with properties determined during model instantiation + update_config_with_model(self.config_obj, self.model) + set_saved_weights_in_checkpoint_flag(self.config_obj) + + if not self._online_trainer: + self._online_trainer = self.backend.create_trainer( + config=self.config_obj.trainer, model=self.model, random_seed=random_seed + ) + + self._tune_batch_size_and_grad_accum(self._online_trainer, dataset, random_seed=random_seed) + + self.model = self._online_trainer.train_online(training_dataset) + + def _run_calibration( + self, + calibrator: Calibrator, + validation_set: Dataset | None, + training_set: Dataset, + skip_save_model: bool, + model_dir: str, + ) -> None: + """Run post-training probability calibration and save the model. + + Must be called only on the coordinator node, after training completes and + before the final model is saved. + """ + if calibrator.calibration_enabled(): + if validation_set is None: + logger.warning( + "Calibration uses validation set, but no validation split specified. " + "Will use training set for calibration. " + "Recommend providing a validation set when using calibration." + ) + calibrator.train_calibration(training_set, TRAINING) + elif len(validation_set) < MIN_DATASET_SPLIT_ROWS: + logger.warning( + f"Validation set size ({len(validation_set)} rows) is too small for calibration. " + "Will use training set for calibration. " + f"Validation set must have at least {MIN_DATASET_SPLIT_ROWS} rows." + ) + calibrator.train_calibration(training_set, TRAINING) + else: + calibrator.train_calibration(validation_set, VALIDATION) + if not skip_save_model: + self.model.save(model_dir) + + def _tune_batch_size_and_grad_accum(self, trainer, dataset, random_seed: int = default_random_seed): + """Sets AUTO batch-size-related parameters based on the trainer, backend type, and number of workers. + + Batch-size related parameters that are set: + - trainer.batch_size + - trainer.eval_batch_size + - trainer.gradient_accumulation_steps + - trainer.effective_batch_size + + The final batch size selected may be non-deterministic even with a fixed random seed since throughput-based + heuristics may be affected by resources used by other processes running on the machine. + """ + if not self.config_obj.trainer.can_tune_batch_size(): + # Some model types don't have batch sizes to be tuned + return + + # Render the batch size and gradient accumulation steps prior to batch size tuning. This is needed in the event + # the effective_batch_size and gradient_accumulation_steps are set explicitly, but batch_size is AUTO. In this + # case, we can infer the batch_size directly without tuning. + num_workers = self.backend.num_training_workers + self.config_obj.trainer.update_batch_size_grad_accum(num_workers) + + if self.config_obj.trainer.batch_size == AUTO: + if self.backend.supports_batch_size_tuning(): + tuned_batch_size = trainer.tune_batch_size( + self.config_obj, dataset, random_seed=random_seed, tune_for_training=True + ) + else: + logger.warning( + f"Backend {self.backend.BACKEND_TYPE} does not support batch size tuning, " + f"using fallback training batch size {FALLBACK_BATCH_SIZE}." + ) + tuned_batch_size = FALLBACK_BATCH_SIZE + + self.config_obj.trainer.batch_size = tuned_batch_size + + # Re-render the gradient_accumulation_steps to account for the explicit batch size. + self.config_obj.trainer.update_batch_size_grad_accum(num_workers) + + if self.config_obj.trainer.eval_batch_size in {AUTO, None}: + if self.backend.supports_batch_size_tuning(): + tuned_batch_size = trainer.tune_batch_size( + self.config_obj, dataset, random_seed=random_seed, tune_for_training=False + ) + else: + logger.warning( + f"Backend {self.backend.BACKEND_TYPE} does not support batch size tuning, " + f"using fallback eval batch size {FALLBACK_BATCH_SIZE}." + ) + tuned_batch_size = FALLBACK_BATCH_SIZE + + self.config_obj.trainer.eval_batch_size = tuned_batch_size + + # Update trainer params separate to config params for backends with stateful trainers + trainer.batch_size = self.config_obj.trainer.batch_size + trainer.eval_batch_size = self.config_obj.trainer.eval_batch_size + trainer.gradient_accumulation_steps = self.config_obj.trainer.gradient_accumulation_steps + + def save_dequantized_base_model(self, save_path: str) -> None: + """Upscales quantized weights of a model to fp16 and saves the result in a specified folder. + + Args: + save_path (str): The path to the folder where the upscaled model weights will be saved. + + Raises: + ValueError: + If the model type is not 'llm' or if quantization is not enabled or the number of bits is not 4 or 8. + RuntimeError: + If no GPU is available, as GPU is required for quantized models. + + Returns: + None + """ + if self.config_obj.model_type != MODEL_LLM: + raise ValueError( + f"Model type {self.config_obj.model_type} is not supported by this method. Only `llm` model type is " + "supported." + ) + + if not self.config_obj.quantization: + raise ValueError( + "Quantization is not enabled in your Ludwig model config. " + "To enable quantization, set `quantization` to `{'bits': 4}` or `{'bits': 8}` in your model config." + ) + + if self.config_obj.quantization.bits != 4: + raise ValueError( + "This method only works with quantized models with 4 bits. " + "Support for 8-bit quantized models will be added in a future release." + ) + + if not torch.cuda.is_available(): + raise RuntimeError("GPU is required for quantized models but no GPU found.") + + # Create the LLM model class instance with the loaded LLM if it hasn't been initialized yet. + if not self.model: + self.model = LudwigModel.create_model(self.config_obj) + + self.model.save_dequantized_base_model(save_path) + + logger.info( + "If you want to upload this model to huggingface.co, run the following Python commands: \n" + "from ludwig.utils.hf_utils import upload_folder_to_hfhub; \n" + f"upload_folder_to_hfhub(repo_id='desired/huggingface/repo/name', folder_path='{save_path}')" + ) + + def generate( + self, + input_strings: str | list[str], + generation_config: dict | None = None, + streaming: bool | None = False, + callbacks: list[Callback] | None = None, + ) -> str | list[str]: + """A simple generate() method that directly uses the underlying transformers library to generate text. + + Args: + input_strings: Input text or list of texts to generate from. + generation_config: Configuration for text generation. + streaming: If True, enable streaming output. + callbacks: Optional callbacks for this generate call. + + Returns: + Union[str, List[str]]: Generated text or list of generated texts. + """ + if self.config_obj.model_type != MODEL_LLM: + raise ValueError( + f"Model type {self.config_obj.model_type} is not supported by this method. Only `llm` model type is " + "supported." + ) + if not torch.cuda.is_available(): + # GPU is required for loading quantized models. See https://github.com/ludwig-ai/ludwig/issues/3695. + raise ValueError( + "A CUDA GPU is required for generate() with quantized LLMs, but none was detected.\n" + "Either run on a GPU machine or disable quantization in your model config." + ) + + # Decoder-only models require left-padding for correct generation results (right-padding causes HF warnings). + padding_side = "left" if not getattr(self.model.model.config, "is_encoder_decoder", False) else "right" + tokenizer = HFTokenizer(self.config_obj.base_model, padding_side=padding_side) + + with self.model.use_generation_config(generation_config): + start_time = time.time() + tokenized_inputs = tokenizer.tokenizer(input_strings, return_tensors="pt", padding=True) + input_ids = tokenized_inputs["input_ids"].to("cuda") + attention_mask = tokenized_inputs["attention_mask"].to("cuda") + + if streaming: + streamer = create_text_streamer(tokenizer.tokenizer) + outputs = self._generate_streaming_outputs(input_strings, input_ids, attention_mask, streamer) + else: + outputs = self._generate_non_streaming_outputs(input_strings, input_ids, attention_mask) + + decoded_outputs = tokenizer.tokenizer.batch_decode(outputs, skip_special_tokens=True) + logger.info(f"Finished generating in: {(time.time() - start_time):.2f}s.") + + return decoded_outputs[0] if len(decoded_outputs) == 1 else decoded_outputs + + def _generate_streaming_outputs( + self, + input_strings: str | list[str], + input_ids: torch.Tensor, + attention_mask: torch.Tensor, + streamer: TextStreamer, + ) -> torch.Tensor: + """Generate streaming outputs for the given input. + + Args: + input_strings (Union[str, List[str]]): Input text or list of texts to generate from. + input_ids (torch.Tensor): Tensor containing input IDs. + attention_mask (torch.Tensor): Tensor containing attention masks. + streamer (Union[TextStreamer, None]): Text streamer instance for streaming output. + + Returns: + torch.Tensor: Concatenated tensor of generated outputs. + """ + outputs = [] + input_strings = input_strings if isinstance(input_strings, list) else [input_strings] + for i in range(len(input_ids)): + with torch.no_grad(): + logger.debug(f"Input: {input_strings[i]}\n") + # NOTE: self.model.model.generation_config is not used here because it is the default + # generation config that the CausalLM was initialized with, rather than the one set within the + # context manager. + generated_output = self.model.model.generate( + input_ids=input_ids[i].unsqueeze(0), + attention_mask=attention_mask[i].unsqueeze(0), + generation_config=self.model.generation, + streamer=streamer, + ) + logger.debug("----------------------") + outputs.append(generated_output) + return torch.cat(outputs, dim=0) + + def _generate_non_streaming_outputs( + self, + _input_strings: str | list[str], + input_ids: torch.Tensor, + attention_mask: torch.Tensor, + ) -> torch.Tensor: + """Generate non-streaming outputs for the given input. + + Args: + _input_strings (Union[str, List[str]]): Unused input parameter. + input_ids (torch.Tensor): Tensor containing input IDs. + attention_mask (torch.Tensor): Tensor containing attention masks. + streamer (Union[TextStreamer, None]): Text streamer instance for streaming output. + + Returns: + torch.Tensor: Tensor of generated outputs. + """ + with torch.no_grad(): + # NOTE: self.model.model.generation_config is not used here because it is the default + # generation config that the CausalLM was initialized with, rather than the one set within the + # context manager. + return self.model.model.generate( + input_ids=input_ids, + attention_mask=attention_mask, + generation_config=self.model.generation, + ) + + def predict( + self, + dataset: str | dict | pd.DataFrame | None = None, + data_format: str | None = None, + split: str = FULL, + batch_size: int = 128, + generation_config: dict | None = None, + skip_save_unprocessed_output: bool = True, + skip_save_predictions: bool = True, + output_directory: str = "results", + return_type: type = pd.DataFrame, + callbacks: list[Callback] | None = None, + **kwargs: Any, + ) -> tuple[dict | pd.DataFrame, str]: + """Make predictions from a trained model on the provided dataset. + + Args: + dataset: Source containing the dataset to predict on. + data_format: Format hint for the data source. Inferred automatically when `None`. + Valid values: `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`, + `'fwf'`, `'hdf5'`, `'html'`, `'json'`, `'jsonl'`, `'parquet'`, `'pickle'`, + `'sas'`, `'spss'`, `'stata'`, `'tsv'`. + split: Which split of the data to use when the dataset contains a split column. + One of `'full'`, `'training'`, `'validation'`, `'test'`. + batch_size: Number of rows per prediction batch. + generation_config: LLM-only generation parameters. When `None`, the config used + at training time is applied. Ignored for non-LLM models. + skip_save_unprocessed_output: When `False`, raw numpy tensors are saved alongside + the postprocessed CSV files. When `True` (default), only CSVs are written. + skip_save_predictions: Skip writing prediction CSV files. + output_directory: Root directory for saved prediction outputs. + return_type: Format of the returned predictions (`pd.DataFrame` or `dict`). + callbacks: Extra callbacks for this predict call; combined with any callbacks + already registered to the model. + **kwargs: Forwarded to the underlying predictor. + + Returns: + A tuple `(predictions, output_directory)` where `predictions` is a + `pd.DataFrame` (or `dict`) of model outputs and `output_directory` is + the path where results were saved. + """ + self._check_initialization() + + # preprocessing + start_time = time.time() + logger.debug(f"Preprocessing dataset for prediction (batch_size={batch_size})") + dataset, _ = self._preprocess_for_prediction( + dataset, + data_format=data_format, + split=split, + include_outputs=False, + callbacks=callbacks, + ) + + logger.debug(f"Running batch prediction (batch_size={batch_size})") + with self.backend.create_predictor(self.model, batch_size=batch_size) as predictor: + with self.model.use_generation_config(generation_config): + predictions = predictor.batch_predict( + dataset, + ) + + if self.backend.is_coordinator(): + # if we are skipping all saving, + # there is no need to create a directory that will remain empty + should_create_exp_dir = not (skip_save_unprocessed_output and skip_save_predictions) + if should_create_exp_dir: + makedirs(output_directory, exist_ok=True) + + logger.debug("Postprocessing predictions") + postproc_predictions = postprocess( + predictions, + self.model.output_features, + self.training_set_metadata, + output_directory=output_directory, + backend=self.backend, + skip_save_unprocessed_output=skip_save_unprocessed_output or not self.backend.is_coordinator(), + ) + converted_postproc_predictions = convert_predictions( + postproc_predictions, self.model.output_features, return_type=return_type, backend=self.backend + ) + if self.backend.is_coordinator(): + if not skip_save_predictions: + save_prediction_outputs( + postproc_predictions, self.model.output_features, output_directory, self.backend + ) + + logger.info(f"Saved to: {output_directory}") + + logger.info(f"Finished predicting in: {(time.time() - start_time):.2f}s.") + return converted_postproc_predictions, output_directory + + def evaluate( + self, + dataset: str | dict | pd.DataFrame | None = None, + data_format: str | None = None, + split: str = FULL, + batch_size: int | None = None, + skip_save_unprocessed_output: bool = True, + skip_save_predictions: bool = True, + skip_save_eval_stats: bool = True, + collect_predictions: bool = False, + collect_overall_stats: bool = False, + output_directory: str = "results", + return_type: type = pd.DataFrame, + **kwargs: Any, + ) -> tuple[dict, dict | pd.DataFrame, str]: + """Evaluate a trained model and compute performance statistics. + + Args: + dataset: Source containing the dataset to evaluate. + data_format: Format hint for the data source. Inferred automatically when `None`. + Valid values: `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`, + `'fwf'`, `'hdf5'`, `'html'`, `'json'`, `'jsonl'`, `'parquet'`, `'pickle'`, + `'sas'`, `'spss'`, `'stata'`, `'tsv'`. + split: Which split of the data to use when the dataset contains a split column. + One of `'full'`, `'training'`, `'validation'`, `'test'`. + batch_size: Number of rows per evaluation batch. Defaults to `eval_batch_size` + from the trainer config. + skip_save_unprocessed_output: When `False`, raw numpy tensors are saved alongside + postprocessed CSV files. When `True` (default), only CSVs are written. + skip_save_predictions: Skip writing prediction CSV files. + skip_save_eval_stats: Skip writing evaluation statistics JSON. + collect_predictions: Collect and return postprocessed predictions. + collect_overall_stats: Compute and include dataset-level aggregate metrics. + output_directory: Root directory for saved evaluation outputs. + return_type: Format for returned predictions (`pd.DataFrame` or `dict`). + **kwargs: Forwarded to preprocessing. + + Returns: + A tuple `(eval_stats, predictions, output_directory)` where `eval_stats` is a + nested dict of feature → metric → value, `predictions` is a `pd.DataFrame` or + `dict` of model outputs, and `output_directory` is the path where results were + saved. + """ + self._check_initialization() + + for callback in self.callbacks: + callback.on_evaluation_start() + + # preprocessing + logger.debug("Preprocessing dataset for evaluation") + dataset, training_set_metadata = self._preprocess_for_prediction( + dataset, + data_format=data_format, + split=split, + include_outputs=True, + ) + + # Fallback to use eval_batch_size or batch_size if not provided + if batch_size is None: + # Requires dictionary getter since some trainer configs may not have a batch_size param + trainer_dict = self.config_obj.trainer.to_dict() + batch_size = trainer_dict.get(EVAL_BATCH_SIZE) or trainer_dict.get(BATCH_SIZE) + if batch_size is None: + raise ValueError( + "batch_size not specified and no default found in trainer config. " + "Set batch_size or eval_batch_size in your trainer config." + ) + + logger.debug(f"Running batch evaluation (batch_size={batch_size})") + with self.backend.create_predictor(self.model, batch_size=batch_size) as predictor: + eval_stats, predictions = predictor.batch_evaluation( + dataset, + collect_predictions=collect_predictions or collect_overall_stats, + ) + + # calculate the overall metrics + if collect_overall_stats: + dataset = dataset.to_df() + + overall_stats = calculate_overall_stats( + self.model.output_features, predictions, dataset, training_set_metadata + ) + eval_stats = { + of_name: ( + {**eval_stats[of_name], **overall_stats[of_name]} + # account for presence of 'combined' key + if of_name in overall_stats + else {**eval_stats[of_name]} + ) + for of_name in eval_stats + } + + if self.backend.is_coordinator(): + # if we are skipping all saving, + # there is no need to create a directory that will remain empty + should_create_exp_dir = not ( + skip_save_unprocessed_output and skip_save_predictions and skip_save_eval_stats + ) + if should_create_exp_dir: + makedirs(output_directory, exist_ok=True) + + if collect_predictions: + logger.debug("Postprocessing predictions") + postproc_predictions = postprocess( + predictions, + self.model.output_features, + self.training_set_metadata, + output_directory=output_directory, + backend=self.backend, + skip_save_unprocessed_output=skip_save_unprocessed_output or not self.backend.is_coordinator(), + ) + else: + postproc_predictions = predictions # = {} + + if self.backend.is_coordinator(): + should_save_predictions = ( + collect_predictions and postproc_predictions is not None and not skip_save_predictions + ) + if should_save_predictions: + save_prediction_outputs( + postproc_predictions, self.model.output_features, output_directory, self.backend + ) + + print_evaluation_stats(eval_stats) + if not skip_save_eval_stats: + save_evaluation_stats(eval_stats, output_directory) + + if should_save_predictions or not skip_save_eval_stats: + logger.info(f"Saved to: {output_directory}") + + if collect_predictions: + postproc_predictions = convert_predictions( + postproc_predictions, self.model.output_features, return_type=return_type, backend=self.backend + ) + + for callback in self.callbacks: + callback.on_evaluation_end() + + return eval_stats, postproc_predictions, output_directory + + def forecast( + self, + dataset: DataFrame, + data_format: str | None = None, + horizon: int = 1, + output_directory: str | None = None, + output_format: str = "parquet", + callbacks: list[Callback] | None = None, + ) -> DataFrame: + """Forecast `horizon` steps ahead using an iterative single-pass strategy. + + Preprocessing is performed once for the initial lookback window. Each subsequent horizon step slides the window + by one position using incremental_time_delay_embedding, reducing preprocessing complexity from O(horizon × + window_size) to O(window_size + horizon). + """ + self._check_initialization() + + # Load raw DataFrame once + dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, self.backend) + if isinstance(dataset, CacheableDataset): + dataset = dataset.unwrap() + df = load_dataset(dataset, data_format=data_format, df_lib=self.backend.df_engine.df_lib) + + ts_input_features = [f for f in self.config_obj.input_features if f.type == TIMESERIES] + ts_output_features = [f for f in self.config_obj.output_features if f.type == TIMESERIES] + + if not ts_input_features: + raise ValueError("Forecasting requires at least one input feature of type `timeseries`.") + + if horizon <= 0: + return_cols = [f.column for f in ts_output_features] + return pd.DataFrame({col: pd.Series(dtype=float) for col in return_cols}) + + max_window_size = max(f.preprocessing.window_size for f in ts_input_features) + + # Build a mapping from ts output column name → ts output feature config + ts_output_by_col = {f.column: f for f in ts_output_features} + + # Step 1: Preprocess the initial lookback window once + initial_df = df.tail(max_window_size) + preprocessed, _ = self._preprocess_for_prediction( + initial_df, + include_outputs=False, + callbacks=callbacks, + ) + + # Collect the last preprocessed embedding for each input feature. + # Non-timeseries features stay constant; timeseries features are slid per step. + # Keyed by proc_column of the model's input features. + last_embeddings: dict[str, np.ndarray] = {} + for i_feat in self.model.input_features.values(): + pc = i_feat.proc_column + if pc in preprocessed.dataset: + last_embeddings[pc] = preprocessed.dataset[pc][-1].copy() + + # Build a mapping: ts_input_feature.column → (proc_column, window_size, padding_value) + ts_input_info: list[tuple[str, str, int, float]] = [] + for ts_feat in ts_input_features: + i_feat = self.model.input_features.get(ts_feat.name) + if i_feat is not None and i_feat.proc_column in last_embeddings: + ts_input_info.append( + ( + ts_feat.column, + i_feat.proc_column, + ts_feat.preprocessing.window_size, + ts_feat.preprocessing.padding_value, + ) + ) + + # Step 2: Incremental prediction loop — O(horizon) steps, each O(1) preprocessing + predicted_rows: list[pd.DataFrame] = [] + total_forecasted = 0 + + with self.backend.create_predictor(self.model, batch_size=1) as predictor: + while total_forecasted < horizon: + # Build a single-sample batch from the last embeddings + batch = {pc: emb[np.newaxis] for pc, emb in last_embeddings.items()} + + # Run model forward pass on one sample, then postprocess + raw_preds = predictor.predict_single(batch) + postproc_preds = postprocess( + raw_preds, + self.model.output_features, + self.training_set_metadata, + backend=self.backend, + skip_save_unprocessed_output=True, + ) + + # Extract predicted values for each timeseries output feature + next_series: dict[str, pd.Series] = {} + for feat in ts_output_features: + key = f"{feat.name}_predictions" + next_series[feat.column] = pd.Series(postproc_preds[key].iloc[0]) + + next_preds = pd.DataFrame(next_series) + predicted_rows.append(next_preds) + total_forecasted += len(next_preds) + + # Step 3: Update embeddings incrementally for the next step. + # For each timeseries input feature, slide the window by one position. + for ts_col, proc_col, window_size, padding_value in ts_input_info: + # Use the predicted value if this ts input is also an output, else padding_value + # (matches the NaN-fill behavior of the original full-reprocessing path). + new_val = float(next_preds[ts_col].iloc[-1]) if ts_col in ts_output_by_col else padding_value + last_embeddings[proc_col] = incremental_time_delay_embedding( + new_val, last_embeddings[proc_col], window_size, padding_value + ) + + results_df = pd.concat(predicted_rows, ignore_index=True).head(horizon) + return_cols = [f.column for f in ts_output_features] + results_df = results_df[return_cols] + + if output_directory is not None: + if self.backend.is_coordinator(): + if output_format == "parquet": + output_path = os.path.join(output_directory, "forecast.parquet") + results_df.to_parquet(output_path) + elif output_format == "csv": + output_path = os.path.join(output_directory, "forecast.csv") + results_df.to_csv(output_path) + else: + raise ValueError(f"`output_format` {output_format} not supported. Must be one of [parquet, csv]") + logger.info(f"Saved to: {output_path}") + + return results_df + + def experiment( + self, + dataset: str | dict | pd.DataFrame | None = None, + training_set: str | dict | pd.DataFrame | None = None, + validation_set: str | dict | pd.DataFrame | None = None, + test_set: str | dict | pd.DataFrame | None = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + experiment_name: str = "experiment", + model_name: str = "run", + model_resume_path: str | None = None, + eval_split: str = TEST, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = False, + skip_save_unprocessed_output: bool = False, + skip_save_predictions: bool = False, + skip_save_eval_stats: bool = False, + skip_collect_predictions: bool = False, + skip_collect_overall_stats: bool = False, + output_directory: str = "results", + random_seed: int = default_random_seed, + **kwargs: Any, + ) -> tuple[dict | None, TrainingStats, PreprocessedDataset, str]: + """Train a model and immediately evaluate it on a held-out split. + + Combines `train()` and `evaluate()` in one call. Saves the model, + training statistics, and evaluation results to `output_directory`. + + Args: + dataset: Source containing the full dataset. Mutually exclusive with + `training_set` / `validation_set` / `test_set`. + training_set: Source containing training data only. + validation_set: Source containing validation data only. + test_set: Source containing test data only. + training_set_metadata: Pre-computed metadata dict or path to a `.meta.json` + file from a prior Ludwig run on the same dataset. + data_format: Format hint for data sources. Inferred automatically when `None`. + Valid values: `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`, + `'fwf'`, `'hdf5'`, `'html'`, `'json'`, `'jsonl'`, `'parquet'`, `'pickle'`, + `'sas'`, `'spss'`, `'stata'`, `'tsv'`. + experiment_name: Name used when creating the output directory. + model_name: Name used when creating the output directory. + model_resume_path: Resume training from this checkpoint directory. + eval_split: Which split to evaluate after training. One of `'training'`, + `'validation'`, `'test'`. + skip_save_training_description: Skip saving the experiment description JSON. + skip_save_training_statistics: Skip saving training statistics JSON. + skip_save_model: Skip saving model weights after each improvement. + skip_save_progress: Skip saving per-epoch checkpoints for resuming. + skip_save_log: Skip saving TensorBoard logs. + skip_save_processed_input: Skip caching the preprocessed HDF5/JSON files. + skip_save_unprocessed_output: Skip saving raw numpy prediction tensors. + skip_save_predictions: Skip writing prediction CSV files. + skip_save_eval_stats: Skip writing evaluation statistics JSON. + skip_collect_predictions: Do not collect postprocessed predictions. + skip_collect_overall_stats: Do not compute dataset-level aggregate metrics. + output_directory: Root directory for all saved outputs. + random_seed: Seed for weight initialization, data splitting, and shuffling. + **kwargs: Forwarded to preprocessing. + + Returns: + A tuple `(eval_stats, train_stats, preprocessed_data, output_directory)` where + `eval_stats` is performance metrics on the eval split (or `None` if eval was + skipped), `train_stats` is per-epoch training metrics, `preprocessed_data` + holds the three split datasets, and `output_directory` is where results were + saved. + """ + if self._user_config.get(HYPEROPT): + print_boxed("WARNING") + logger.warning(HYPEROPT_WARNING) + + train_result = self.train( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + model_resume_path=model_resume_path, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_unprocessed_output=skip_save_unprocessed_output, + output_directory=output_directory, + random_seed=random_seed, + ) + train_stats = train_result.train_stats + preprocessed_data = train_result.preprocessed_data + output_directory = train_result.output_directory + + eval_set = preprocessed_data.validation_set + if eval_split == TRAINING: + eval_set = preprocessed_data.training_set + elif eval_split == VALIDATION: + eval_set = preprocessed_data.validation_set + elif eval_split == TEST: + eval_set = preprocessed_data.test_set + else: + logger.warning(f"Eval split {eval_split} not supported. Using validation set instead") + + if eval_set is not None: + trainer_dict = self.config_obj.trainer.to_dict() + batch_size = trainer_dict.get(EVAL_BATCH_SIZE, trainer_dict.get(BATCH_SIZE, None)) + + # predict + try: + eval_stats, _, _ = self.evaluate( + eval_set, + data_format=data_format, + batch_size=batch_size, + output_directory=output_directory, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + collect_predictions=not skip_collect_predictions, + collect_overall_stats=not skip_collect_overall_stats, + return_type="dict", + ) + except NotImplementedError: + logger.warning( + "Skipping evaluation as the necessary methods are not " + "supported. Full exception below:\n" + f"{traceback.format_exc()}" + ) + eval_stats = None + else: + logger.warning(f"The evaluation set {eval_set} was not provided. Skipping evaluation") + eval_stats = None + + return eval_stats, train_stats, preprocessed_data, output_directory + + def collect_weights(self, tensor_names: list[str] | None = None, **kwargs: Any) -> list: + """Return the named tensors (weight matrices) from the trained model. + + Args: + tensor_names: Names of tensors to retrieve. When `None`, all tensors + are returned. + **kwargs: Unused; accepted for forward-compatibility. + + Returns: + List of `(name, tensor)` tuples. + """ + self._check_initialization() + collected_tensors = self.model.collect_weights(tensor_names) + return collected_tensors + + def collect_activations( + self, + layer_names: list[str], + dataset: str | dict[str, list] | pd.DataFrame, + data_format: str | None = None, + split: str = FULL, + batch_size: int = 128, + **kwargs: Any, + ) -> list: + """Collect intermediate-layer activations for the given dataset. + + Args: + layer_names: Names of layers in the model to collect activations from. + dataset: Source containing the data to run through the model. + data_format: Format hint for the data source. Inferred when `None`. + split: Which data split to use when the dataset has a split column. + One of `'full'`, `'training'`, `'validation'`, `'test'`. + batch_size: Number of rows per inference batch. + **kwargs: Unused; accepted for forward-compatibility. + + Returns: + List of activation tensors, one per layer name. + """ + self._check_initialization() + + # preprocessing + logger.debug("Preprocessing dataset for activation collection") + dataset, training_set_metadata = self._preprocess_for_prediction( + dataset, + data_format=data_format, + split=split, + include_outputs=False, + ) + + logger.debug(f"Collecting activations for layers: {layer_names} (batch_size={batch_size})") + with self.backend.create_predictor(self.model, batch_size=batch_size) as predictor: + activations = predictor.batch_collect_activations( + layer_names, + dataset, + ) + + return activations + + def preprocess( + self, + dataset: str | dict | pd.DataFrame | None = None, + training_set: str | dict | pd.DataFrame | None = None, + validation_set: str | dict | pd.DataFrame | None = None, + test_set: str | dict | pd.DataFrame | None = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + skip_save_processed_input: bool = True, + random_seed: int = default_random_seed, + **kwargs: Any, + ) -> PreprocessedDataset: + """Preprocess a dataset and return it split into training / validation / test sets. + + Args: + dataset: Source containing the full dataset. Mutually exclusive with + `training_set` / `validation_set` / `test_set`. + training_set: Source containing training data only. + validation_set: Source containing validation data only. + test_set: Source containing test data only. + training_set_metadata: Pre-computed metadata dict or `.meta.json` path + from a prior Ludwig run on the same dataset. + data_format: Format hint for data sources. Inferred when `None`. + Valid values: `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, + `'feather'`, `'fwf'`, `'hdf5'`, `'html'`, `'json'`, `'jsonl'`, + `'parquet'`, `'pickle'`, `'sas'`, `'spss'`, `'stata'`, `'tsv'`. + skip_save_processed_input: Skip caching the preprocessed HDF5/JSON files. + random_seed: Seed for data splitting and shuffling. + **kwargs: Forwarded to the underlying preprocessing function. + + Returns: + A `PreprocessedDataset` namedtuple with fields `training_set`, + `validation_set`, `test_set`, and `training_set_metadata`. + + Raises: + RuntimeError: If preprocessing fails (e.g., empty training set after + filtering, or lazy loading incompatible with RayBackend). + """ + print_boxed("PREPROCESSING") + + for callback in self.callbacks: + callback.on_preprocess_start(self.config_obj.to_dict()) + + preprocessing_params = get_preprocessing_params(self.config_obj) + + proc_training_set = proc_validation_set = proc_test_set = None + try: + with provision_preprocessing_workers(self.backend): + preprocessed_data = preprocess_for_training( + self.config_obj, + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + skip_save_processed_input=skip_save_processed_input, + preprocessing_params=preprocessing_params, + backend=self.backend, + random_seed=random_seed, + callbacks=self.callbacks, + ) + + proc_training_set, proc_validation_set, proc_test_set, training_set_metadata = preprocessed_data + + return PreprocessedDataset(proc_training_set, proc_validation_set, proc_test_set, training_set_metadata) + except Exception: + logger.debug(traceback.format_exc()) + raise + finally: + for callback in self.callbacks: + callback.on_preprocess_end(proc_training_set, proc_validation_set, proc_test_set, training_set_metadata) + + @staticmethod + def load( + model_dir: str, + logging_level: int = logging.ERROR, + backend: Backend | str | None = None, + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + from_checkpoint: bool = False, + ) -> "LudwigModel": # return is an instance of ludwig.api.LudwigModel class + """Load a previously trained LudwigModel from disk. + + Args: + model_dir: Path to the saved model directory (typically + `results///model/`). + logging_level: Log level sent to stderr (e.g., `logging.INFO`). + backend: Backend instance or string name used for preprocessing. + gpus: GPUs to use; same syntax as CUDA_VISIBLE_DEVICES. + gpu_memory_limit: Maximum memory fraction [0, 1] allowed per GPU. + allow_parallel_threads: Allow Torch multi-threading for performance + at the cost of determinism. + callbacks: List of `Callback` objects providing hooks into the pipeline. + from_checkpoint: When `True`, load from the latest training checkpoint + in `training_checkpoints/` instead of the final model weights. + + Returns: + A fully initialized `LudwigModel` ready for inference. + + Example:: + + model = LudwigModel.load("results/experiment/run/model") + predictions, _ = model.predict(dataset=df) + """ + # Initialize PyTorch before calling `broadcast()` to prevent initializing + # Torch with default parameters + backend_param = backend + backend = initialize_backend(backend) + backend.initialize_pytorch( + gpus=gpus, gpu_memory_limit=gpu_memory_limit, allow_parallel_threads=allow_parallel_threads + ) + + logger.info(f"Loading model from {model_dir}") + config = backend.broadcast_return(lambda: load_json(os.path.join(model_dir, MODEL_HYPERPARAMETERS_FILE_NAME))) + + # Upgrades deprecated fields and adds new required fields in case the config loaded from disk is old. + config_obj = ModelConfig.from_dict(config) + + # Ensure that the original backend is used if it was specified in the config and user requests it + if backend_param is None and "backend" in config: + # Reset backend from config + backend = initialize_backend(config.get("backend")) + + # initialize model + ludwig_model = LudwigModel( + config_obj.to_dict(), + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + + # generate model from config + set_saved_weights_in_checkpoint_flag(config_obj) + ludwig_model._get_or_create_model(config_obj) + + # load model weights + logger.info(f"Loading model weights from {model_dir}") + ludwig_model.load_weights(model_dir, from_checkpoint) + + # If merge_and_unload was NOT performed before saving (i.e., adapter weights exist), + # we need to merge them now for inference. + if ludwig_model.is_merge_and_unload_set(): + weights_save_path = os.path.join(model_dir, MODEL_WEIGHTS_FILE_NAME) + adapter_config_path = os.path.join(weights_save_path, "adapter_config.json") + if os.path.exists(adapter_config_path): + ludwig_model.model.merge_and_unload(progressbar=config_obj.adapter.postprocessor.progressbar) + + # load train set metadata + ludwig_model.training_set_metadata = backend.broadcast_return( + lambda: load_metadata(os.path.join(model_dir, TRAIN_SET_METADATA_FILE_NAME)) + ) + + return ludwig_model + + def load_weights( + self, + model_dir: str, + from_checkpoint: bool = False, + ) -> None: + """Load model weights from a saved model directory. + + Args: + model_dir: Path to the saved model directory. + from_checkpoint: When `True`, load from the latest training checkpoint + instead of the final model weights. + """ + if self.backend.is_coordinator(): + if from_checkpoint: + with self.backend.create_trainer( + model=self.model, + config=self.config_obj.trainer, + ) as trainer: + checkpoint = trainer.create_checkpoint_handle() + training_checkpoints_path = os.path.join(model_dir, TRAINING_CHECKPOINTS_DIR_PATH) + trainer.resume_weights_and_optimizer(training_checkpoints_path, checkpoint) + else: + self.model.load(model_dir) + + self.backend.sync_model(self.model) + + def save(self, save_path: str) -> None: + """Save the model config, weights, and training metadata to `save_path`. + + Args: + save_path: Directory where the model will be saved. Created if it + does not exist. Contains `model_hyperparameters.json`, weight + files, and `training_set_metadata.json`. + """ + self._check_initialization() + + # save config + self.save_config(save_path) + + # save model weights + self.model.save(save_path) + + # save training set metadata + training_set_metadata_path = os.path.join(save_path, TRAIN_SET_METADATA_FILE_NAME) + save_json(training_set_metadata_path, self.training_set_metadata) + + @staticmethod + def upload_to_hf_hub( + repo_id: str, + model_path: str, + repo_type: str = "model", + private: bool = False, + commit_message: str = "Upload trained [Ludwig](https://ludwig.ai/latest/) model weights", + commit_description: str | None = None, + ) -> bool: + """Uploads trained model artifacts to the HuggingFace Hub. + + Args: + repo_id: A namespace (user or an organization) and a repo name separated by a `/`. + model_path: The path of the saved model. This is either (a) the folder where the 'model_weights' + folder and the 'model_hyperparameters.json' file are stored, or (b) the parent of that folder. + private: Whether the model repo should be private. Defaults to False. + repo_type: Set to `"dataset"` or `"space"` if uploading to a dataset or space, `None` or `"model"` + if uploading to a model. Default is `None`. + commit_message: The summary / title / first line of the generated commit. + commit_description: The description of the generated commit. + + Returns: + True for success, False for failure. + """ + if model_weights_exist(os.path.join(model_path, MODEL_FILE_NAME)) and os.path.exists( + os.path.join(model_path, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME) + ): + experiment_path = model_path + elif model_weights_exist(model_path) and os.path.exists( + os.path.join(model_path, MODEL_HYPERPARAMETERS_FILE_NAME) + ): + experiment_path = os.path.dirname(model_path) + else: + raise ValueError( + f"Can't find model weights and '{MODEL_HYPERPARAMETERS_FILE_NAME}' either at " + f"'{model_path}' or at '{model_path}/model'" + ) + model_service = get_upload_registry()["hf_hub"] + hub: HuggingFaceHub = model_service() + hub.login() + upload_status: bool = hub.upload( + repo_id=repo_id, + model_path=experiment_path, + repo_type=repo_type, + private=private, + commit_message=commit_message, + commit_description=commit_description, + ) + return upload_status + + def save_config(self, save_path: str) -> None: + """Save config to specified location. + + Args: + save_path: filepath string to save config as a JSON file. + """ + os.makedirs(save_path, exist_ok=True) + model_hyperparameters_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME) + save_json(model_hyperparameters_path, self.config_obj.to_dict()) + + def export_model(self, save_path: str, format: str = "safetensors", sample_input: dict | None = None) -> None: + """Export the model in various formats. + + Args: + save_path: Directory to save the exported model. + format: Export format. One of "safetensors", "torch_export", "onnx". + sample_input: Example input for tracing (required for torch_export and onnx). + """ + from ludwig.utils.model_export import ModelExporter + + exporter = ModelExporter(self.model) + + if format == "safetensors": + return exporter.export_safetensors(save_path) + elif format == "torch_export": + return exporter.export_torch(save_path, sample_input) + elif format == "onnx": + return exporter.export_onnx(save_path, sample_input) + else: + raise ValueError(f"Unknown export format: {format}. Options: safetensors, torch_export, onnx") + + def _preprocess_for_prediction( + self, + dataset: str | dict | pd.DataFrame | Dataset, + data_format: str | None = None, + split: str | None = None, + include_outputs: bool = False, + callbacks: list | None = None, + ): + """Shared preprocessing wrapper for predict, evaluate, and collect_activations.""" + return preprocess_for_prediction( + self.config_obj, + dataset=dataset, + training_set_metadata=self.training_set_metadata, + data_format=data_format, + split=split, + include_outputs=include_outputs, + backend=self.backend, + callbacks=self.callbacks + (callbacks or []), + ) + + def _check_initialization(self): + missing = [] + if self.model is None: + missing.append("model") + if self._user_config is None: + missing.append("config") + if self.training_set_metadata is None: + missing.append("training_set_metadata") + if missing: + raise ValueError( + f"Model is not initialized (missing: {', '.join(missing)}). " + "Call train() or load() before predict/evaluate." + ) + + def free_gpu_memory(self) -> None: + """Manually moves the model to CPU to force GPU memory to be freed. + + For more context: https://discuss.pytorch.org/t/how-can-we-release-gpu-memory-cache/14530/35 + """ + if torch.cuda.is_available(): + self.model.model.to(torch.device("cpu")) + torch.cuda.empty_cache() + + @staticmethod + def create_model(config_obj: ModelConfig | dict, random_seed: int = default_random_seed) -> BaseModel: + """Instantiates BaseModel object. + + Args: + config_obj: Ludwig config object. + random_seed: Random seed used for weights initialization, splits and any other random function. + + Returns: + Instance of the Ludwig model object. + """ + if isinstance(config_obj, dict): + config_obj = ModelConfig.from_dict(config_obj) + model_type = get_from_registry(config_obj.model_type, model_type_registry) + return model_type(config_obj, random_seed=random_seed) + + @staticmethod + def set_logging_level(logging_level: int) -> None: + """Sets level for log messages. + + Args: + logging_level: Set/Update the logging level. Use logging constants like `logging.DEBUG`, + `logging.INFO` and `logging.ERROR`. + """ + logging.getLogger("ludwig").setLevel(logging_level) + if logging_level in {logging.WARNING, logging.ERROR, logging.CRITICAL}: + set_disable_progressbar(True) + else: + set_disable_progressbar(False) + + @property + def config(self) -> ModelConfigDict: + """Returns the fully-rendered config of this model including default values.""" + return self.config_obj.to_dict() + + @config.setter + def config(self, user_config: ModelConfigDict): + """Updates the config of this model. + + WARNING: this can have unexpected results on an already trained model. + """ + self._user_config = user_config + self.config_obj = ModelConfig.from_dict(self._user_config) + + def is_merge_and_unload_set(self) -> bool: + """Return True if this model is an LLM configured to merge_and_unload QLoRA adapter weights.""" + return self.config_obj.model_type == MODEL_LLM and self.model.is_merge_and_unload_set() + + +@PublicAPI +def kfold_cross_validate( + num_folds: int, + config: dict | str, + dataset: str | None = None, + data_format: str | None = None, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = False, + skip_save_predictions: bool = False, + skip_save_eval_stats: bool = False, + skip_collect_predictions: bool = False, + skip_collect_overall_stats: bool = False, + output_directory: str = "results", + random_seed: int = default_random_seed, + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + backend: Backend | str | None = None, + logging_level: int = logging.INFO, + **kwargs: Any, +) -> tuple[dict, dict]: + """Perform k-fold cross-validation and return aggregated metrics. + + Args: + num_folds: Number of folds for cross-validation. + config: Model config dict or path to a YAML config file. + dataset: Source containing the full dataset. Note: `'hdf5'` format is + not supported for k-fold cross-validation. + data_format: Format hint for the data source. Inferred automatically when + `None`. Valid values: `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, + `'feather'`, `'fwf'`, `'html'`, `'json'`, `'jsonl'`, `'parquet'`, + `'pickle'`, `'sas'`, `'spss'`, `'stata'`, `'tsv'`. + skip_save_training_description: Skip saving the experiment description JSON. + skip_save_training_statistics: Skip saving training statistics JSON. + skip_save_model: Skip saving model weights after each improvement. + skip_save_progress: Skip saving per-epoch checkpoints for resuming. + skip_save_log: Skip saving TensorBoard logs. + skip_save_processed_input: Skip caching preprocessed HDF5/JSON files. + skip_save_predictions: Skip writing prediction CSV files. + skip_save_eval_stats: Skip writing evaluation statistics JSON. + skip_collect_predictions: Do not collect postprocessed predictions. + skip_collect_overall_stats: Do not compute dataset-level aggregate metrics. + output_directory: Root directory for saved outputs. + random_seed: Seed for weight initialization, data splitting, and shuffling. + gpus: GPUs to use; same syntax as CUDA_VISIBLE_DEVICES. + gpu_memory_limit: Maximum memory fraction [0, 1] allowed per GPU device. + allow_parallel_threads: Allow Torch multi-threading at the cost of determinism. + backend: Backend instance or string name for preprocessing and training. + logging_level: Log level sent to stderr. + **kwargs: Forwarded to each fold's `experiment()` call. + + Returns: + A tuple `(kfold_cv_statistics, kfold_split_indices)` where + `kfold_cv_statistics` maps fold name → training + eval metrics, and + `kfold_split_indices` maps fold name → training/test index arrays. + """ + # if config is a path, convert to dictionary + if isinstance(config, str): # assume path + config = load_yaml(config) + backend = initialize_backend(backend or config.get("backend")) + + # check for k_fold + if num_folds is None: + raise ValueError("k_fold parameter must be specified") + + logger.info(f"starting {num_folds:d}-fold cross validation") + + # create output_directory if not available + if not os.path.isdir(output_directory): + os.mkdir(output_directory) + + # prepare data for k-fold processing + # use Ludwig's utility to facilitate creating a dataframe + # that is used as the basis for creating folds + + dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, backend) + + # determine data format of provided dataset + if not data_format or data_format == "auto": + data_format = figure_data_format(dataset) + + data_df = load_dataset(dataset, data_format=data_format, df_lib=backend.df_engine.df_lib) + + kfold_cv_stats = {} + kfold_split_indices = {} + + for train_indices, test_indices, fold_num in generate_kfold_splits(data_df, num_folds, random_seed): + with tempfile.TemporaryDirectory() as temp_dir_name: + curr_train_df = data_df.iloc[train_indices] + curr_test_df = data_df.iloc[test_indices] + + kfold_split_indices["fold_" + str(fold_num)] = { + "training_indices": train_indices, + "test_indices": test_indices, + } + + # train and validate model on this fold + logger.info(f"training on fold {fold_num:d}") + + model = LudwigModel( + config=config, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + ) + eval_stats, train_stats, preprocessed_data, output_directory = model.experiment( + training_set=curr_train_df, + test_set=curr_test_df, + experiment_name="cross_validation", + model_name="fold_" + str(fold_num), + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + skip_collect_predictions=skip_collect_predictions, + skip_collect_overall_stats=skip_collect_overall_stats, + output_directory=os.path.join(temp_dir_name, "results"), + random_seed=random_seed, + ) + + # augment the training statistics with scoring metric from + # the hold out fold + if dataclasses.is_dataclass(train_stats): + train_stats_dict = dataclasses.asdict(train_stats) + elif hasattr(train_stats, "to_dict"): + train_stats_dict = train_stats.to_dict() + else: + train_stats_dict = vars(train_stats) + train_stats_dict["fold_eval_stats"] = eval_stats + + # collect training statistics for this fold + kfold_cv_stats["fold_" + str(fold_num)] = train_stats_dict + + # consolidate raw fold metrics across all folds + raw_kfold_stats = {} + for fold_name in kfold_cv_stats: + curr_fold_eval_stats = kfold_cv_stats[fold_name]["fold_eval_stats"] + for of_name in curr_fold_eval_stats: + if of_name not in raw_kfold_stats: + raw_kfold_stats[of_name] = {} + fold_eval_stats_of = curr_fold_eval_stats[of_name] + + for metric in fold_eval_stats_of: + if metric not in { + "predictions", + "probabilities", + "confusion_matrix", + "overall_stats", + "per_class_stats", + "roc_curve", + "precision_recall_curve", + }: + if metric not in raw_kfold_stats[of_name]: + raw_kfold_stats[of_name][metric] = [] + raw_kfold_stats[of_name][metric].append(fold_eval_stats_of[metric]) + + # calculate overall kfold statistics + overall_kfold_stats = {} + for of_name in raw_kfold_stats: + overall_kfold_stats[of_name] = {} + for metric in raw_kfold_stats[of_name]: + mean = np.mean(raw_kfold_stats[of_name][metric]) + std = np.std(raw_kfold_stats[of_name][metric]) + overall_kfold_stats[of_name][metric + "_mean"] = mean + overall_kfold_stats[of_name][metric + "_std"] = std + + kfold_cv_stats["overall"] = overall_kfold_stats + + logger.info(f"completed {num_folds:d}-fold cross validation") + + return kfold_cv_stats, kfold_split_indices diff --git a/ludwig/api_annotations.py b/ludwig/api_annotations.py new file mode 100644 index 0000000..3430325 --- /dev/null +++ b/ludwig/api_annotations.py @@ -0,0 +1,183 @@ +def PublicAPI(*args, **kwargs): + """Annotation for documenting public APIs. Public APIs are classes and methods exposed to end users of Ludwig. + + If stability="stable", the APIs will remain backwards compatible across minor Ludwig releases + (e.g., Ludwig 0.6 -> Ludwig 0.7). + + If stability="experimental", the APIs can be used by advanced users who are tolerant to and expect + breaking changes. This will likely be seen in the case of incremental new feature development. + + Args: + stability: One of {"stable", "experimental"} + + Examples: + >>> from api_annotations import PublicAPI + >>> @PublicAPI + ... def func1(x): + ... return x + >>> @PublicAPI(stability="experimental") + ... def func2(y): + ... return y + """ + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + return PublicAPI(stability="stable")(args[0]) + + if "stability" in kwargs: + stability = kwargs["stability"] + if stability not in ("stable", "experimental"): + raise ValueError( + f"Unknown stability level '{stability}'.\n" + f"Expected one of: 'stable', 'experimental'.\n" + f"Fix: use @PublicAPI(stability='stable') or @PublicAPI(stability='experimental')." + ) + elif kwargs: + raise ValueError(f"Unknown kwargs: {kwargs.keys()}") + else: + stability = "stable" + + def wrap(obj): + if stability == "experimental": + message = f"PublicAPI ({stability}): This API is {stability} and may change before becoming stable." + else: + message = "PublicAPI: This API is stable across Ludwig releases." + + _append_doc(obj, message=message) + _mark_annotated(obj) + return obj + + return wrap + + +def DeveloperAPI(*args, **kwargs): + """Annotation for documenting developer APIs. Developer APIs are lower-level methods explicitly exposed to + advanced Ludwig users and library developers. Their interfaces may change across minor Ludwig releases (for + e.g., Ludwig 0.6.1 and Ludwig 0.6.2). + + Examples: + >>> from api_annotations import DeveloperAPI + >>> @DeveloperAPI + ... def func(x): + ... return x + """ + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + return DeveloperAPI()(args[0]) + + def wrap(obj): + _append_doc(obj, message="DeveloperAPI: This API may change across minor Ludwig releases.") + _mark_annotated(obj) + return obj + + return wrap + + +def Deprecated(*args, **kwargs): + """Annotation for documenting a deprecated API. Deprecated APIs may be removed in future releases of Ludwig + (e.g., Ludwig 0.7 to Ludwig 0.8). + + Args: + message: A message to help users understand the reason for the deprecation, and provide a migration path. + + Examples: + >>> from api_annotations import Deprecated + >>> @Deprecated + ... def func(x): + ... return x + >>> @Deprecated(message="g() is deprecated because the API is error prone. Please call h() instead.") + ... def g(y): + ... return y + """ + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + return Deprecated()(args[0]) + + message = "**DEPRECATED:** This API is deprecated and may be removed in a future Ludwig release." + + if "message" in kwargs: + message += " " + kwargs["message"] + del kwargs["message"] + + if kwargs: + raise ValueError(f"Unknown kwargs: {kwargs.keys()}") + + def inner(obj): + _append_doc(obj, message=message, directive="warning") + _mark_annotated(obj) + return obj + + return inner + + +def _append_doc(obj, message: str, directive: str | None = None) -> str: + """ + Args: + message: An additional message to append to the end of docstring for a class + or method that uses one of the API annotations + directive: A shorter message that provides contexts for the message and indents it. + For example, this could be something like 'warning' or 'info'. + """ + if not obj.__doc__: + obj.__doc__ = "" + + obj.__doc__ = obj.__doc__.rstrip() + + indent = _get_indent(obj.__doc__) + obj.__doc__ += "\n\n" + if directive is not None: + obj.__doc__ += f"{' ' * indent}.. {directive}::\n" + obj.__doc__ += f"{' ' * (indent + 4)}{message}" + else: + obj.__doc__ += f"{' ' * indent}{message}" + obj.__doc__ += f"\n{' ' * indent}" + + +def _mark_annotated(obj) -> None: + # Set magic token for check_api_annotations linter. + if hasattr(obj, "__name__"): + obj._annotated = obj.__name__ + + +def _is_annotated(obj) -> bool: + # Check the magic token exists and applies to this class (not a subclass). + return hasattr(obj, "_annotated") and obj._annotated == obj.__name__ + + +def _get_indent(docstring: str) -> int: + """ + Example: + >>> def f(): + ... '''Docstring summary.''' + >>> f.__doc__ + 'Docstring summary.' + >>> _get_indent(f.__doc__) + 0 + >>> def g(foo): + ... '''Docstring summary. + ... + ... Args: + ... foo: Does bar. + ... ''' + >>> g.__doc__ + 'Docstring summary.\\n\\n Args:\\n foo: Does bar.\\n ' + >>> _get_indent(g.__doc__) + 4 + >>> class A: + ... def h(): + ... '''Docstring summary. + ... + ... Returns: + ... None. + ... ''' + >>> A.h.__doc__ + 'Docstring summary.\\n\\n Returns:\\n None.\\n ' + >>> _get_indent(A.h.__doc__) + 8 + """ + if not docstring: + return 0 + + non_empty_lines = list(filter(bool, docstring.splitlines())) + if len(non_empty_lines) == 1: + # Docstring contains summary only. + return 0 + + # The docstring summary isn't indented, so check the indentation of the second non-empty line. + return len(non_empty_lines[1]) - len(non_empty_lines[1].lstrip()) diff --git a/ludwig/api_types.py b/ludwig/api_types.py new file mode 100644 index 0000000..433df3e --- /dev/null +++ b/ludwig/api_types.py @@ -0,0 +1,141 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Lightweight data-class types shared across Ludwig's public API. + +These are intentionally kept in a separate module so callers can import +``EvaluationFrequency``, ``TrainingStats``, ``PreprocessedDataset``, and +``TrainingResults`` without pulling in the entire ``ludwig.api`` module +(which transitively imports PyTorch, the full model registry, etc.). + +All four types remain importable from ``ludwig.api`` for backward compatibility. +""" + +import dataclasses +from dataclasses import dataclass +from typing import Any, ClassVar + +from ludwig.api_annotations import PublicAPI +from ludwig.constants import TEST, TRAINING, VALIDATION +from ludwig.data.dataset.base import Dataset +from ludwig.types import TrainingSetMetadataDict + + +@PublicAPI +@dataclass +class EvaluationFrequency: + """Represents the frequency of periodic evaluation of a metric during training. For example: + + "every epoch" + frequency: 1, period: EPOCH + + "every 50 steps". + frequency: 50, period: STEP + """ + + frequency: float = 1.0 + period: str = "epoch" # One of "epoch" or "step". + + EPOCH: ClassVar[str] = "epoch" # One epoch is a single pass through the training set. + STEP: ClassVar[str] = "step" # One step is training on one mini-batch. + + +@PublicAPI +@dataclass +class TrainingStats: + """Training statistics for all splits (training, validation, test).""" + + training: dict[str, Any] + validation: dict[str, Any] + test: dict[str, Any] + evaluation_frequency: EvaluationFrequency = dataclasses.field(default_factory=EvaluationFrequency) + + def __contains__(self, key: object) -> bool: + return ( + (key == TRAINING and self.training) + or (key == VALIDATION and self.validation) + or (key == TEST and self.test) + ) + + def __getitem__(self, key: str) -> dict[str, Any]: + return {TRAINING: self.training, VALIDATION: self.validation, TEST: self.test}[key] + + # Make TrainingStats a proper Mapping so dict(ts) and generic helpers like + # ludwig.utils.numerical_test_utils.assert_all_finite treat it as a dict + # rather than falling back to integer-index iteration (KeyError(0)). + _KEYS = (TRAINING, VALIDATION, TEST) + + def keys(self) -> tuple[str, ...]: + return self._KEYS + + def __iter__(self): + return iter(self._KEYS) + + +@PublicAPI +@dataclass +class PreprocessedDataset: + training_set: Dataset + validation_set: Dataset + test_set: Dataset + training_set_metadata: TrainingSetMetadataDict + + def __iter__(self): + import warnings + + warnings.warn( + "Tuple unpacking of PreprocessedDataset is deprecated. Use attribute access instead.", + DeprecationWarning, + stacklevel=2, + ) + return iter((self.training_set, self.validation_set, self.test_set, self.training_set_metadata)) + + def __getitem__(self, index): + import warnings + + warnings.warn( + "Indexed access of PreprocessedDataset is deprecated. Use attribute access instead.", + DeprecationWarning, + stacklevel=2, + ) + return (self.training_set, self.validation_set, self.test_set, self.training_set_metadata)[index] + + +@PublicAPI +@dataclass +class TrainingResults: + train_stats: TrainingStats + preprocessed_data: PreprocessedDataset + output_directory: str + + def __iter__(self): + import warnings + + warnings.warn( + "Tuple unpacking of TrainingResults is deprecated. " + "Use attribute access instead: result.train_stats, result.preprocessed_data, result.output_directory", + DeprecationWarning, + stacklevel=2, + ) + return iter((self.train_stats, self.preprocessed_data, self.output_directory)) + + def __getitem__(self, index): + import warnings + + warnings.warn( + "Indexed access of TrainingResults is deprecated. Use attribute access instead.", + DeprecationWarning, + stacklevel=2, + ) + return (self.train_stats, self.preprocessed_data, self.output_directory)[index] diff --git a/ludwig/automl/__init__.py b/ludwig/automl/__init__.py new file mode 100644 index 0000000..1607808 --- /dev/null +++ b/ludwig/automl/__init__.py @@ -0,0 +1,32 @@ +"""Ludwig AutoML — automated config generation and training. + +Public API +---------- +auto_train(dataset, target, time_limit_s) + End-to-end: run dataset quality checks, sample configs from the YAML-driven + search space, train each, and return the best model. + +create_auto_config(dataset, target) + Return a single sampled Ludwig config dict without training. + +train_with_config(dataset, config, output_dir) + Train one Ludwig config and return the LudwigModel. + +cli_init_config(argv) + CLI entry-point for `ludwig init_config`. + +Config generation pipeline +-------------------------- +The search space is defined by YAML files in ``ludwig/automl/search_space/``. +``config_enumerator`` builds the full combination lattice, ``config_sampler`` +draws a random subset, and ``config_validator`` rejects invalid combinations +before any training starts. ``target_detection`` infers the task type and +target column when they are not explicitly specified. +""" + +from ludwig.automl.automl import ( + auto_train, # noqa: F401 + cli_init_config, # noqa: F401 + create_auto_config, # noqa: F401 + train_with_config, # noqa: F401 +) diff --git a/ludwig/automl/auto_tune_config.py b/ludwig/automl/auto_tune_config.py new file mode 100644 index 0000000..e085d6f --- /dev/null +++ b/ludwig/automl/auto_tune_config.py @@ -0,0 +1,286 @@ +import copy +import logging +import math +from collections import OrderedDict + +import psutil + +try: + import GPUtil +except ImportError: + raise ImportError("GPUtil is not installed. In order to use auto_train please run pip install ludwig[ray]") + +from ludwig.api import LudwigModel +from ludwig.backend import initialize_backend +from ludwig.constants import ( + AUTO, + AUTOML_DEFAULT_TEXT_ENCODER, + AUTOML_LARGE_TEXT_DATASET, + AUTOML_MAX_ROWS_PER_CHECKPOINT, + AUTOML_SMALLER_TEXT_ENCODER, + AUTOML_SMALLER_TEXT_LENGTH, + AUTOML_TEXT_ENCODER_MAX_TOKEN_LEN, + HYPEROPT, + MINIMUM_BATCH_SIZE, + PREPROCESSING, + SPACE, + TEXT, + TRAINER, +) +from ludwig.data.preprocessing import preprocess_for_training +from ludwig.features.feature_registries import update_config_with_metadata +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.automl.utils import get_model_type +from ludwig.utils.torch_utils import initialize_pytorch + +logger = logging.getLogger(__name__) + +# maps variable search space that can be modified to minimum permissible value for the range +RANKED_MODIFIABLE_PARAM_LIST = { + "tabnet": OrderedDict( + { + "trainer.batch_size": 32, + "combiner.size": 8, + "combiner.output_size": 8, + } + ), + "concat": OrderedDict( + { + "trainer.batch_size": 32, + "combiner.output_size": 64, + "combiner.num_fc_layers": 1, + } + ), + "tabtransformer": OrderedDict( + { + "trainer.batch_size": 32, + "combiner.num_heads:": 4, + "combiner.output_size": 8, + "combiner.num_layers": 4, + "combiner.num_fc_layers": 1, + } + ), + "text": OrderedDict( # for single input feature text models e.g. bert and its variants + { + "trainer.batch_size": 16, + } + ), +} + + +BYTES_PER_MiB = 1048576 +BYTES_PER_WEIGHT = 4 # assumes 32-bit precision = 4 bytes +BYTES_OPTIMIZER_PER_WEIGHT = 8 # for optimizer m and v vectors + + +def get_trainingset_metadata(config, dataset, backend): + _, _, _, training_set_metadata = preprocess_for_training( + config, dataset=dataset, preprocessing_params=config[PREPROCESSING], backend=backend + ) + return training_set_metadata + + +# Note: if run in Ray Cluster, this method is run remote with gpu resources requested if available +def _get_machine_memory(): + if GPUtil.getGPUs(): + machine_mem = GPUtil.getGPUs()[0].memoryTotal * BYTES_PER_MiB + else: + machine_mem = psutil.virtual_memory().total + return machine_mem + + +def _get_text_feature_max_length(config, training_set_metadata) -> int: + """Returns max sequence length over text features, subject to preprocessing limit.""" + max_length = 0 + for feature in config["input_features"]: + if feature["type"] == TEXT: + feature_max_len = training_set_metadata[feature["name"]]["max_sequence_length"] + if feature_max_len > max_length: + max_length = feature_max_len + if ( + ("preprocessing" in config) + and (TEXT in config["preprocessing"]) + and ("max_sequence_length" in config["preprocessing"][TEXT]) + ): + limit = config["preprocessing"][TEXT]["max_sequence_length"] + else: + limit = 256 # Preprocessing default max_sequence_length = 256 + if max_length > limit + 2: # For start and stop symbols. + max_length = limit + 2 + return max_length + + +def _get_text_model_memory_usage(config, training_set_metadata, memory_usage) -> int: + max_feature_token_length = _get_text_feature_max_length(config, training_set_metadata) + memory_usage = (memory_usage / AUTOML_TEXT_ENCODER_MAX_TOKEN_LEN) * max_feature_token_length + return memory_usage + + +def compute_memory_usage(config_obj, training_set_metadata, model_category) -> int: + update_config_with_metadata(config_obj, training_set_metadata) + lm = LudwigModel.create_model(config_obj) + model_size = lm.get_model_size() # number of parameters in model + batch_size = config_obj.trainer.batch_size + if batch_size == AUTO: + # Smallest valid batch size that will allow training to complete + batch_size = MINIMUM_BATCH_SIZE + memory_usage = model_size * (BYTES_PER_WEIGHT + BYTES_OPTIMIZER_PER_WEIGHT) * batch_size + if model_category == TEXT: + return _get_text_model_memory_usage(config_obj.to_dict(), training_set_metadata, memory_usage) + else: + return memory_usage + + +def sub_new_params(config: dict, new_param_vals: dict): + new_config = copy.deepcopy(config) + for param, val in new_param_vals.items(): + config_section = param.split(".")[0] + param_name = param.split(".")[1] + new_config[config_section][param_name] = val + return new_config + + +def get_new_params(current_param_values, hyperparam_search_space, params_to_modify): + for param, _ in params_to_modify.items(): + if param in hyperparam_search_space: + if hyperparam_search_space[param][SPACE] == "choice": + current_param_values[param] = hyperparam_search_space[param]["categories"][-1] + else: + current_param_values[param] = hyperparam_search_space[param]["upper"] + return current_param_values + + +def _update_text_encoder(input_features: list, old_text_encoder: str, new_text_encoder: str) -> None: + for feature in input_features: + if feature["type"] == TEXT and feature["encoder"] == old_text_encoder: + feature["encoder"] = new_text_encoder + + +def _get_text_feature_min_usable_length(input_features: list, training_set_metadata) -> int: + """Returns min of AUTOML_SMALLER_TEXT_LENGTH and lowest 99th percentile sequence length over text features.""" + min_usable_length = AUTOML_SMALLER_TEXT_LENGTH + for feature in input_features: + if feature["type"] == TEXT: + feature_99ptile_len = training_set_metadata[feature["name"]]["max_sequence_length_99ptile"] + if feature_99ptile_len < min_usable_length: + min_usable_length = feature_99ptile_len + return round(min_usable_length) + + +def reduce_text_feature_max_length(config, training_set_metadata) -> bool: + """Reduce max sequence length, when viable, to control its quadratic impact.""" + input_features = config["input_features"] + min_usable_length = _get_text_feature_min_usable_length(input_features, training_set_metadata) + seq_len_limit = {"max_sequence_length": min_usable_length} + if "preprocessing" not in config: + config["preprocessing"] = {TEXT: seq_len_limit} + elif ( + (TEXT not in config["preprocessing"]) + or ("max_sequence_length" not in config["preprocessing"][TEXT]) + or (min_usable_length < float(config["preprocessing"][TEXT]["max_sequence_length"])) + ): + config["preprocessing"][TEXT] = seq_len_limit + else: + return False + return True + + +# For hyperparam_search_space comprised solely of choice spaces, compute maximum number of +# combinations and return that value if it is less than num_samples; else return num_samples. +def _update_num_samples(num_samples, hyperparam_search_space): + max_num_samples = 1 + for param in hyperparam_search_space: + if hyperparam_search_space[param][SPACE] == "choice": + max_num_samples *= len(hyperparam_search_space[param]["categories"]) + else: + return num_samples + if max_num_samples < num_samples: + return max_num_samples + return num_samples + + +# Note: if run in Ray Cluster, this method is run remote with gpu resources requested if available +def memory_tune_config(config, dataset, model_category, row_count, backend): + backend = initialize_backend(backend) + + fits_in_memory = False + tried_reduce_seq_len = False + config_obj = ModelConfig.from_dict(config) + raw_config = config_obj.to_dict() + training_set_metadata = get_trainingset_metadata(raw_config, dataset, backend) + modified_hyperparam_search_space = copy.deepcopy(raw_config[HYPEROPT]["parameters"]) + current_param_values = {} + param_list = [] + model_type = get_model_type(raw_config) + if model_type in RANKED_MODIFIABLE_PARAM_LIST: + params_to_modify = RANKED_MODIFIABLE_PARAM_LIST[model_type] + if len(params_to_modify.keys()) > 0: + param_list = list(params_to_modify.keys()) + max_memory = _get_machine_memory() + initialize_pytorch() + + while param_list: + # compute memory utilization + current_param_values = get_new_params(current_param_values, modified_hyperparam_search_space, params_to_modify) + temp_config = sub_new_params(raw_config, current_param_values) + config_obj = ModelConfig.from_dict(temp_config) + mem_use = compute_memory_usage(config_obj, training_set_metadata, model_category) + if mem_use > max_memory and model_category == TEXT and not tried_reduce_seq_len: + tried_reduce_seq_len = True + if reduce_text_feature_max_length(config, training_set_metadata): + reduce_text_feature_max_length(temp_config, training_set_metadata) + config_obj = ModelConfig.from_dict(temp_config) + mem_use = compute_memory_usage(config_obj, training_set_metadata, model_category) + logger.info(f"Checking model estimated mem use {mem_use} against memory size {max_memory}") + if mem_use <= max_memory: + fits_in_memory = True + break + # check if we have exhausted tuning of current param (e.g. we can no longer reduce the param value) + param, min_value = param_list[0], params_to_modify[param_list[0]] + + if param in modified_hyperparam_search_space: + param_space = modified_hyperparam_search_space[param]["space"] + if param_space == "choice": + if ( + len(modified_hyperparam_search_space[param]["categories"]) >= 2 + and modified_hyperparam_search_space[param]["categories"][-2] >= min_value + ): + modified_hyperparam_search_space[param]["categories"] = modified_hyperparam_search_space[param][ + "categories" + ][:-1] + else: + param_list.pop(0) # exhausted reduction of this parameter + else: + # reduce by 10% + upper_bound, lower_bound = ( + modified_hyperparam_search_space[param]["upper"], + modified_hyperparam_search_space[param]["lower"], + ) + reduction_val = (upper_bound - lower_bound) * 0.1 + new_upper_bound = upper_bound - reduction_val + if (new_upper_bound) > lower_bound and new_upper_bound > min_value: + modified_hyperparam_search_space[param]["upper"] = new_upper_bound + else: + param_list.pop(0) # exhausted reduction of this parameter + else: + param_list.pop(0) # param not in hyperopt search space + + if model_category == TEXT and row_count > AUTOML_LARGE_TEXT_DATASET: + if "checkpoints_per_epoch" not in config[TRAINER] and "steps_per_checkpoint" not in config[TRAINER]: + checkpoints_per_epoch = max(2, math.floor(row_count / AUTOML_MAX_ROWS_PER_CHECKPOINT)) + config[TRAINER]["checkpoints_per_epoch"] = ( + checkpoints_per_epoch # decrease latency to get model accuracy signal + ) + if "evaluate_training_set" not in config[TRAINER]: + config[TRAINER]["evaluate_training_set"] = False # reduce overhead for increased evaluation frequency + if not fits_in_memory: + # Switch to smaller pre-trained model encoder for large datasets. + _update_text_encoder(config["input_features"], AUTOML_DEFAULT_TEXT_ENCODER, AUTOML_SMALLER_TEXT_ENCODER) + + modified_config = copy.deepcopy(config) + + modified_config[HYPEROPT]["parameters"] = modified_hyperparam_search_space + modified_config[HYPEROPT]["executor"]["num_samples"] = _update_num_samples( + modified_config[HYPEROPT]["executor"]["num_samples"], modified_hyperparam_search_space + ) + return modified_config, fits_in_memory diff --git a/ludwig/automl/automl.py b/ludwig/automl/automl.py new file mode 100644 index 0000000..ae0478f --- /dev/null +++ b/ludwig/automl/automl.py @@ -0,0 +1,521 @@ +"""automl.py. + +Driver script which: + +(1) Builds a base config by performing type inference and populating config + w/default combiner parameters, training parameters, and hyperopt search space +(2) Tunes config based on resource constraints +(3) Runs hyperparameter optimization experiment +""" + +import argparse +import copy +import logging +import os +import warnings +from typing import Any + +import numpy as np +import pandas as pd +import yaml + +from ludwig.api import LudwigModel +from ludwig.api_annotations import PublicAPI +from ludwig.automl.base_config import ( + create_default_config, + DatasetInfo, + get_dataset_info, + get_features_config, + get_reference_configs, +) +from ludwig.backend import Backend, initialize_backend +from ludwig.constants import ( + AUTO, + AUTOML_DEFAULT_IMAGE_ENCODER, + AUTOML_DEFAULT_TABULAR_MODEL, + AUTOML_DEFAULT_TEXT_ENCODER, + BINARY, + CATEGORY, + ENCODER, + HYPEROPT, + IMAGE, + INPUT_FEATURES, + NAME, + NUMBER, + OUTPUT_FEATURES, + TABULAR, + TEXT, + TRAINER, + TYPE, +) +from ludwig.contrib import add_contrib_callback_args +from ludwig.data.cache.types import CacheableDataset +from ludwig.datasets import load_dataset_uris +from ludwig.globals import LUDWIG_VERSION, MODEL_FILE_NAME +from ludwig.hyperopt.run import hyperopt +from ludwig.schema.model_config import ModelConfig +from ludwig.types import ModelConfigDict +from ludwig.utils.automl.ray_utils import _ray_init +from ludwig.utils.automl.utils import _add_transfer_config, get_model_type, set_output_feature_metric +from ludwig.utils.data_utils import load_dataset, use_credentials +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import open_file +from ludwig.utils.heuristics import get_auto_learning_rate +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.print_utils import print_ludwig + +try: + import dask.dataframe as dd + from ray.tune import ExperimentAnalysis +except ImportError as e: + raise RuntimeError("ray is not installed. In order to use auto_train please run pip install ludwig[ray]") from e + + +logger = logging.getLogger(__name__) + +OUTPUT_DIR = "." +TABULAR_TYPES = {CATEGORY, NUMBER, BINARY} + + +class AutoTrainResults: + def __init__(self, experiment_analysis: ExperimentAnalysis, creds: dict[str, Any] | None = None): + self._experiment_analysis = experiment_analysis + self._creds = creds + + @property + def experiment_analysis(self): + return self._experiment_analysis + + @property + def best_trial_id(self) -> str: + return self._experiment_analysis.best_trial.trial_id + + @property + def best_model(self) -> LudwigModel | None: + checkpoint = self._experiment_analysis.best_checkpoint + if checkpoint is None: + logger.warning("No best model found") + return None + + # Use credentials context for remote checkpoints that may need custom auth + with use_credentials(self._creds): + with checkpoint.as_directory() as ckpt_path: + model_dir = os.path.join(ckpt_path, MODEL_FILE_NAME) + if not os.path.isdir(model_dir): + logger.warning( + f"Best checkpoint does not contain model files at {model_dir}. " + "The trial may not have completed a full training epoch." + ) + return None + # Ray Tune checkpoints contain training_checkpoints/ (from + # mid-training saves) but not model_weights (only saved after + # training completes). Load from the training checkpoint. + return LudwigModel.load(model_dir, from_checkpoint=True) + + +@PublicAPI +def auto_train( + dataset: str | pd.DataFrame | dd.DataFrame, + target: str, + time_limit_s: int | float, + output_directory: str = OUTPUT_DIR, + tune_for_memory: bool = False, + user_config: dict | None = None, + random_seed: int = default_random_seed, + use_reference_config: bool = False, + **kwargs, +) -> AutoTrainResults: + """Select the best model type for the dataset and run hyperparameter optimization. + + Builds configs for each model type (concat, tabnet, transformer), selects + one based on dataset attributes, then runs a hyperopt experiment. Batch + size and learning rate tuning happen automatically at training time. + + Args: + dataset: Data source to train over. + target: Name of the target feature. + time_limit_s: Total wall-clock seconds allocated to auto-training. + output_directory: Directory into which to write results. + tune_for_memory: Deprecated. Has no effect; ``batch_size=auto`` is + used instead. + user_config: Override automatic selection of specified config items. + random_seed: Random seed for hyperparameter sampling, data splitting, + parameter initialization, and training-set shuffling. + use_reference_config: If ``True``, seed the first hyperopt search + point from a reference model config when one is available. + **kwargs: Additional keyword args forwarded to + ``ludwig.hyperopt.run.hyperopt``. + + Returns: + Results containing the hyperopt experiments and the best model. + """ + config = create_auto_config( + dataset, + target, + time_limit_s, + tune_for_memory, + user_config, + random_seed, + use_reference_config=use_reference_config, + ) + return train_with_config(dataset, config, output_directory=output_directory, random_seed=random_seed, **kwargs) + + +@PublicAPI +def create_auto_config( + dataset: str | pd.DataFrame | dd.DataFrame | DatasetInfo, + target: str | list[str], + time_limit_s: int | float, + tune_for_memory: bool = False, + user_config: dict | None = None, + random_seed: int = default_random_seed, + imbalance_threshold: float = 0.9, + use_reference_config: bool = False, + backend: Backend | str = None, +) -> ModelConfigDict: + """Return an auto-generated Ludwig config for the given dataset, target, and time budget. + + Args: + dataset: Data source to train over, or a pre-computed ``DatasetInfo``. + target: Name or list of names of the target feature(s). + time_limit_s: Total wall-clock seconds allocated to auto-training. + tune_for_memory: Deprecated. Has no effect; ``batch_size=auto`` is + used instead. + user_config: Override automatic selection of specified config items. + random_seed: Random seed for hyperparameter sampling, data splitting, + parameter initialization, and training-set shuffling. + imbalance_threshold: Maximum imbalance ratio (minority / majority) + below which stratified sampling is applied. + use_reference_config: If ``True``, seed the first hyperopt search + point from a reference model config when one is available. + backend: Backend or string name of the backend to use. + + Returns: + Selected Ludwig model configuration dict. + """ + backend = initialize_backend(backend) + + if not isinstance(dataset, DatasetInfo): + # preload ludwig datasets + dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, backend) + if isinstance(dataset, CacheableDataset): + dataset = dataset.unwrap() + dataset = load_dataset(dataset, df_lib=backend.df_engine.df_lib) + + dataset_info = get_dataset_info(dataset) if not isinstance(dataset, DatasetInfo) else dataset + features_config = create_features_config(dataset_info, target) + return create_automl_config_for_features( + features_config, + dataset_info, + target, + time_limit_s=time_limit_s, + user_config=user_config, + random_seed=random_seed, + imbalance_threshold=imbalance_threshold, + use_reference_config=use_reference_config, + backend=backend, + ) + + +@PublicAPI +def create_automl_config_for_features( + features_config: ModelConfigDict, + dataset_info: DatasetInfo, + target: str | list[str], + time_limit_s: int | float, + tune_for_memory: bool = False, + user_config: dict | None = None, + random_seed: int = default_random_seed, + imbalance_threshold: float = 0.9, + use_reference_config: bool = False, + backend: Backend | str = None, +) -> ModelConfigDict: + default_configs = create_default_config( + features_config, dataset_info, target, time_limit_s, random_seed, imbalance_threshold, backend + ) + model_config, _, _ = _model_select(dataset_info, default_configs, user_config, use_reference_config) + + if tune_for_memory: + warnings.warn("`tune_for_memory=True` is deprecated, `batch_size=auto` will be used instead") + + return model_config + + +@PublicAPI +def create_features_config( + dataset_info: DatasetInfo, + target_name: str | list[str] | None = None, +) -> ModelConfigDict: + return get_features_config(dataset_info.fields, dataset_info.row_count, target_name) + + +@PublicAPI +def train_with_config( + dataset: str | pd.DataFrame | dd.DataFrame, + config: dict, + output_directory: str = OUTPUT_DIR, + random_seed: int = default_random_seed, + **kwargs, +) -> AutoTrainResults: + """Run hyperparameter optimization with the given config and return the best model. + + Args: + dataset: Data source to train over (file path or DataFrame). + config: Ludwig config dict to use for training. Typically produced by + ``create_auto_config``. + output_directory: Directory into which to write results. + random_seed: Random seed for hyperparameter sampling, data splitting, + parameter initialization, and training-set shuffling. + **kwargs: Additional keyword args forwarded to + ``ludwig.hyperopt.run.hyperopt``. + + Returns: + Results containing the hyperopt experiments and the best model. + """ + _ray_init() + + model_type = get_model_type(config) + hyperopt_results = _train( + config, dataset, output_directory=output_directory, model_name=model_type, random_seed=random_seed, **kwargs + ) + # catch edge case where metric_score is nan + # TODO (ASN): Decide how we want to proceed if at least one trial has + # completed + for trial in hyperopt_results.ordered_trials: + if isinstance(trial.metric_score, str) or np.isnan(trial.metric_score): + warnings.warn( + "There was an error running the experiment. " + "A trial failed to start. " + "Consider increasing the time budget for experiment. " + ) + + # Extract credentials needed to pull artifacts, if provided + creds = None + backend: Backend = initialize_backend(kwargs.get("backend")) + if backend is not None: + creds = backend.storage.artifacts.credentials + + experiment_analysis = hyperopt_results.experiment_analysis + return AutoTrainResults(experiment_analysis, creds) + + +def _model_select( + dataset_info: DatasetInfo, + default_configs, + user_config, + use_reference_config: bool, +): + """Performs model selection based on dataset or user specified model. + + Note: Current implementation returns ft_transformer by default for tabular datasets. + """ + fields = dataset_info.fields + + base_config = copy.deepcopy(default_configs["base_config"]) + model_category = None + + input_features = default_configs["base_config"]["input_features"] + + # tabular dataset heuristics + if len(fields) > 3 and all(f[TYPE] in TABULAR_TYPES for f in input_features): + model_category = TABULAR + base_config = merge_dict(base_config, default_configs["combiner"][AUTOML_DEFAULT_TABULAR_MODEL]) + + # override combiner heuristic if explicitly provided by user + if user_config is not None: + if "combiner" in user_config: + model_type = user_config["combiner"]["type"] + base_config = merge_dict(base_config, default_configs["combiner"][model_type]) + else: + # text heuristics + for i, input_feature in enumerate(input_features): + base_config_input_feature = base_config["input_features"][i] + # default text encoder is bert + if input_feature[TYPE] == TEXT: + model_category = TEXT + if ENCODER in input_feature: + base_config_input_feature[ENCODER][TYPE] = AUTOML_DEFAULT_TEXT_ENCODER + else: + base_config_input_feature[ENCODER] = {TYPE: AUTOML_DEFAULT_TEXT_ENCODER} + # TODO(shreya): Should this hyperopt config param be set here? + base_config[HYPEROPT]["executor"]["num_samples"] = 5 # set for small hyperparameter search space + base_config = merge_dict(base_config, default_configs[TEXT][AUTOML_DEFAULT_TEXT_ENCODER]) + + # TODO (ASN): add image heuristics + if input_feature[TYPE] == IMAGE: + model_category = IMAGE + if ENCODER in input_feature: + base_config_input_feature[ENCODER][TYPE] = AUTOML_DEFAULT_IMAGE_ENCODER + else: + base_config_input_feature[ENCODER] = {TYPE: AUTOML_DEFAULT_IMAGE_ENCODER} + + # Merge combiner config + base_config = merge_dict(base_config, default_configs["combiner"]["concat"]) + + # Adjust learning rate based on other config settings + if base_config[TRAINER]["learning_rate"] == AUTO: + # Add a fake output feature to ensure we can load the ModelConfig, as we expect there to be at least + # one output feature in all cases + # TODO(travis): less hacky way to do this, we should probably allow ModelConfig to be created without output + # features + load_config = copy.deepcopy(base_config) + if not load_config.get(OUTPUT_FEATURES): + load_config[OUTPUT_FEATURES] = [{"name": "fake", "type": "binary"}] + base_config[TRAINER]["learning_rate"] = get_auto_learning_rate(ModelConfig.from_dict(load_config)) + + # override and constrain automl config based on user specified values + if user_config is not None: + base_config = merge_dict(base_config, user_config) + + # remove all parameters from hyperparameter search that user has + # provided explicit values for + hyperopt_params_copy = copy.deepcopy(base_config["hyperopt"]["parameters"]) + for param_key in hyperopt_params_copy: + config_section, param = param_key.split(".")[0], param_key.split(".")[1] + if config_section in user_config: + if param in user_config[config_section]: + del base_config["hyperopt"]["parameters"][param_key] + + # if single output feature, set relevant metric and goal if not already set + base_config = set_output_feature_metric(base_config) + + # add as initial trial in the automl search the hyperparameter settings from + # the best model for a similar dataset and matching model type, if any. + if use_reference_config: + ref_configs = get_reference_configs() + base_config = _add_transfer_config(base_config, ref_configs) + + return base_config, model_category, dataset_info.row_count + + +def _train( + config: dict, + dataset: str | pd.DataFrame | dd.DataFrame, + output_directory: str, + model_name: str, + random_seed: int, + **kwargs, +): + hyperopt_results = hyperopt( + config, + dataset=dataset, + output_directory=output_directory, + model_name=model_name, + random_seed=random_seed, + skip_save_log=True, # avoid per-step log overhead by default + **kwargs, + ) + return hyperopt_results + + +def init_config( + dataset: str, + target: str | list[str], + time_limit_s: int | float, + tune_for_memory: bool = False, + suggested: bool = False, + hyperopt: bool = False, + output: str | None = None, + random_seed: int = default_random_seed, + use_reference_config: bool = False, + **kwargs, +): + config = create_auto_config( + dataset=dataset, + target=target, + time_limit_s=time_limit_s, + random_seed=random_seed, + use_reference_config=use_reference_config, + tune_for_memory=tune_for_memory, + ) + + if HYPEROPT in config and not hyperopt: + del config[HYPEROPT] + + if not suggested: + # Only use inputs and outputs + minimal_config = { + INPUT_FEATURES: [{"name": f[NAME], "type": f[TYPE]} for f in config[INPUT_FEATURES]], + OUTPUT_FEATURES: [{"name": f[NAME], "type": f[TYPE]} for f in config[OUTPUT_FEATURES]], + } + if hyperopt: + minimal_config[HYPEROPT] = config[HYPEROPT] + config = minimal_config + + if output is None: + print(yaml.safe_dump(config, None, sort_keys=False)) + else: + with open_file(output, "w") as f: + yaml.safe_dump(config, f, sort_keys=False) + + +def cli_init_config(sys_argv): + parser = argparse.ArgumentParser( + description="This script initializes a valid config from a dataset.", + prog="ludwig init_config", + usage="%(prog)s [options]", + ) + parser.add_argument( + "-d", + "--dataset", + type=str, + help="input data file path", + ) + parser.add_argument( + "-t", + "--target", + type=str, + help="target(s) to predict as output features of the model", + action="append", + required=False, + ) + parser.add_argument( + "--time_limit_s", + type=int, + help="time limit to train the model in seconds when using hyperopt", + required=False, + ) + parser.add_argument( + "--suggested", + type=bool, + help="use suggested config from automl, otherwise only use inferred types and return a minimal config", + default=False, + required=False, + ) + parser.add_argument( + "--hyperopt", + type=bool, + help="include automl hyperopt config", + default=False, + required=False, + ) + parser.add_argument( + "--random_seed", + type=int, + help="seed for random number generators used in hyperopt to improve repeatability", + required=False, + ) + parser.add_argument( + "--use_reference_config", + type=bool, + help="refine hyperopt search space by setting first search point from stored reference model config", + default=False, + required=False, + ) + parser.add_argument( + "-o", + "--output", + type=str, + help="output initialized YAML config path", + required=False, + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("init_config", *sys_argv) + + print_ludwig("Init Config", LUDWIG_VERSION) + init_config(**vars(args)) diff --git a/ludwig/automl/base_config.py b/ludwig/automl/base_config.py new file mode 100644 index 0000000..e37938b --- /dev/null +++ b/ludwig/automl/base_config.py @@ -0,0 +1,439 @@ +"""Uses heuristics to build ludwig configuration file: + +(1) infer types based on dataset +(2) populate with + - default combiner parameters, + - preprocessing parameters, + - combiner specific default training parameters, + - combiner specific hyperopt space + - feature parameters +(3) add machineresources + (base implementation -- # CPU, # GPU) +""" + +import logging +import os +from dataclasses import dataclass +from typing import Any + +import dask.dataframe as dd +import numpy as np +import pandas as pd +import yaml +from dataclasses_json import dataclass_json, LetterCase +from tqdm import tqdm + +from ludwig.api_annotations import DeveloperAPI +from ludwig.backend import Backend +from ludwig.constants import ( + COLUMN, + COMBINER, + ENCODER, + EXECUTOR, + HYPEROPT, + INPUT_FEATURES, + PREPROCESSING, + SCHEDULER, + SEARCH_ALG, + SPLIT, + TEXT, + TYPE, +) +from ludwig.types import ModelConfigDict +from ludwig.utils.automl.data_source import DataSource, wrap_data_source +from ludwig.utils.automl.field_info import FieldConfig, FieldInfo, FieldMetadata +from ludwig.utils.automl.type_inference import infer_type, should_exclude +from ludwig.utils.data_utils import load_yaml +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.system_utils import Resources + +logger = logging.getLogger(__name__) + +PATH_HERE = os.path.abspath(os.path.dirname(__file__)) +CONFIG_DIR = os.path.join(PATH_HERE, "defaults") + +BASE_AUTOML_CONFIG = os.path.join(CONFIG_DIR, "base_automl_config.yaml") +REFERENCE_CONFIGS = os.path.join(CONFIG_DIR, "reference_configs.yaml") + +combiner_defaults = { + "concat": os.path.join(CONFIG_DIR, "combiner/concat_config.yaml"), + "tabnet": os.path.join(CONFIG_DIR, "combiner/tabnet_config.yaml"), + "transformer": os.path.join(CONFIG_DIR, "combiner/transformer_config.yaml"), + "ft_transformer": os.path.join(CONFIG_DIR, "combiner/ft_transformer_config.yaml"), + "tabtransformer": os.path.join(CONFIG_DIR, "combiner/tabtransformer_config.yaml"), +} + +encoder_defaults = {"text": {"bert": os.path.join(CONFIG_DIR, "text/bert_config.yaml")}} + +# Cap for number of distinct values to return. +MAX_DISTINCT_VALUES_TO_RETURN = 10 + + +@DeveloperAPI +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class DatasetInfo: + fields: list[FieldInfo] + row_count: int + size_bytes: int = -1 + + +def allocate_experiment_resources(resources: Resources) -> dict: + """Allocates ray trial resources based on available resources. + + Args: + resources: specifies all available GPUs, CPUs and associated metadata of the machines (i.e. memory). + + Returns: + gpu and cpu resources per trial. + """ + # TODO (ASN): + # (1) expand logic to support multiple GPUs per trial (multi-gpu training) + # (2) add support for kubernetes namespace (if applicable) + # (3) add support for smarter allocation based on size of GPU memory + experiment_resources = {"cpu_resources_per_trial": 1} + gpu_count, cpu_count = resources.gpus, resources.cpus + if gpu_count > 0: + experiment_resources.update({"gpu_resources_per_trial": 1}) + if cpu_count > 1: + cpus_per_trial = max(int(cpu_count / gpu_count), 1) + experiment_resources["cpu_resources_per_trial"] = cpus_per_trial + + return experiment_resources + + +def get_resource_aware_hyperopt_config( + experiment_resources: dict[str, Any], time_limit_s: int | float, random_seed: int +) -> dict[str, Any]: + """Returns a Ludwig config with the hyperopt section populated with appropriate parameters. + + Hyperopt parameters are intended to be appropriate for the given resources and time limit. + """ + executor = experiment_resources + executor.update({"time_budget_s": time_limit_s}) + if time_limit_s is not None: + executor.update({SCHEDULER: {"max_t": time_limit_s}}) + + return { + HYPEROPT: { + SEARCH_ALG: {"random_state_seed": random_seed}, + EXECUTOR: executor, + }, + } + + +def _get_stratify_split_config(field_meta: FieldMetadata) -> dict: + return { + PREPROCESSING: { + SPLIT: { + TYPE: "stratify", + COLUMN: field_meta.name, + } + } + } + + +def get_default_automl_hyperopt() -> dict[str, Any]: + """Returns general, default settings for hyperopt. + + For example: + - We set a random_state_seed for sample sequence repeatability + - We use an increased reduction_factor to get more pruning/exploration. + + TODO: If settings seem reasonable, consider building this into the hyperopt schema, directly. + """ + return yaml.safe_load(""" + search_alg: + type: variant_generator + executor: + type: ray + num_samples: 10 + time_budget_s: 3600 + scheduler: + type: async_hyperband + time_attr: time_total_s + max_t: 3600 + grace_period: 72 + reduction_factor: 5 +""") + + +def create_default_config( + features_config: ModelConfigDict, + dataset_info: DatasetInfo, + target_name: str | list[str], + time_limit_s: int | float, + random_seed: int, + imbalance_threshold: float = 0.9, + backend: Backend = None, +) -> dict: + """Returns auto_train configs for three available combiner models. Coordinates the following tasks: + + - extracts fields and generates list of FieldInfo objects + - gets field metadata (i.e avg. words, total non-null entries) + - builds input_features and output_features section of config + - for imbalanced datasets, a preprocessing section is added to perform stratified sampling if the imbalance ratio + is smaller than imbalance_threshold + - for each combiner, adds default training, hyperopt + - infers resource constraints and adds gpu and cpu resource allocation per + trial + + Args: + dataset_info: Dataset Info object. + target_name: name of target feature. + time_limit_s: total time allocated to auto_train. Acts as the stopping parameter. + random_seed: a random seed that will be used anywhere there is a call to a random number generator, + including hyperparameter search sampling, as well as data splitting, parameter initialization and + training set shuffling. + imbalance_threshold: maximum imbalance ratio (minority / majority) to perform stratified sampling. + backend: backend to use for training. + + Returns: + dictionaries containing auto train config files for all available combiner types. + """ + base_automl_config = load_yaml(BASE_AUTOML_CONFIG) + base_automl_config.update(features_config) + + targets = convert_targets(target_name) + features_metadata = get_field_metadata(dataset_info.fields, dataset_info.row_count, targets) + + # Handle expensive features for CPU + resources = backend.get_available_resources() + for ifeature in base_automl_config[INPUT_FEATURES]: + if resources.gpus == 0: + if ifeature[TYPE] == TEXT: + # When no GPUs are available, default to the embed encoder, which is fast enough for CPU + ifeature[ENCODER] = {"type": "embed"} + + # create set of all feature types appearing in the dataset + feature_types = [[feat[TYPE] for feat in features] for features in features_config.values()] + feature_types = set(sum(feature_types, [])) + + model_configs = {} + + # update hyperopt config + experiment_resources = allocate_experiment_resources(resources) + base_automl_config = merge_dict( + base_automl_config, get_resource_aware_hyperopt_config(experiment_resources, time_limit_s, random_seed) + ) + + # add preprocessing section if single output feature is imbalanced + outputs_metadata = [f for f in features_metadata if f.mode == "output"] + if len(outputs_metadata) == 1: + of_meta = outputs_metadata[0] + is_categorical = of_meta.config.type in ["category", "binary"] + is_imbalanced = of_meta.imbalance_ratio < imbalance_threshold + if is_categorical and is_imbalanced: + base_automl_config.update(_get_stratify_split_config(of_meta)) + + model_configs["base_config"] = base_automl_config + + # read in all encoder configs + for feat_type, default_configs in encoder_defaults.items(): + if feat_type in feature_types: + if feat_type not in model_configs: + model_configs[feat_type] = {} + for encoder_name, encoder_config_path in default_configs.items(): + model_configs[feat_type][encoder_name] = load_yaml(encoder_config_path) + + # read in all combiner configs + model_configs[COMBINER] = {} + for combiner_type, default_config in combiner_defaults.items(): + combiner_config = load_yaml(default_config) + model_configs[COMBINER][combiner_type] = combiner_config + + return model_configs + + +# Read in the score and configuration of a reference model trained by Ludwig for each dataset in a list. +def get_reference_configs() -> dict: + reference_configs = load_yaml(REFERENCE_CONFIGS) + return reference_configs + + +def get_dataset_info(df: pd.DataFrame | dd.DataFrame) -> DatasetInfo: + """Constructs FieldInfo objects for each feature in dataset. These objects are used for downstream type + inference. + + Args: + df: Pandas or Dask dataframe. + + Returns: + Structure containing list of FieldInfo objects. + """ + source = wrap_data_source(df) + return get_dataset_info_from_source(source) + + +def is_field_boolean(source: DataSource, field: str) -> bool: + """Returns a boolean indicating whether the object field should have a bool dtype. + + Columns with object dtype that have 3 distinct values of which one is Nan/None is a bool type column. + """ + unique_values = source.df[field].unique() + if len(unique_values) <= 3: + for entry in unique_values: + try: + if np.isnan(entry): + continue + except TypeError: + # For some field types such as object arrays, np.isnan throws a TypeError + # In this case, do nothing and proceed to checking if the entry is a bool object + pass + if isinstance(entry, bool): + continue + return False + return True + return False + + +@DeveloperAPI +def get_dataset_info_from_source(source: DataSource) -> DatasetInfo: + """Constructs FieldInfo objects for each feature in dataset. These objects are used for downstream type + inference. + + Args: + source: A wrapper around a data source, which may represent a pandas or Dask dataframe. + + Returns: + Structure containing list of FieldInfo objects. + """ + row_count = len(source) + fields = [] + for field in tqdm(source.columns, desc="Analyzing fields", total=len(source.columns)): + logger.info(f"Analyzing field: {field}") + dtype = source.get_dtype(field) + num_distinct_values, distinct_values, distinct_values_balance = source.get_distinct_values( + field, MAX_DISTINCT_VALUES_TO_RETURN + ) + nonnull_values = source.get_nonnull_values(field) + image_values = source.get_image_values(field) + audio_values = source.get_audio_values(field) + + if dtype == "object": + # Check if it is a nullboolean field. We do this since if you read a csv with + # pandas that has a column of booleans and some missing values, the column is + # interpreted as object dtype instead of bool + if is_field_boolean(source, field): + dtype = "bool" + + avg_words = None + if source.is_string_type(dtype): + try: + avg_words = source.get_avg_num_tokens(field) + except AttributeError: + # Series is not actually a string type despite being an object, e.g., Decimal, Datetime, etc. + avg_words = None + + fields.append( + FieldInfo( + name=field, + dtype=dtype, + distinct_values=distinct_values, + num_distinct_values=num_distinct_values, + distinct_values_balance=distinct_values_balance, + nonnull_values=nonnull_values, + image_values=image_values, + audio_values=audio_values, + avg_words=avg_words, + ) + ) + return DatasetInfo(fields=fields, row_count=row_count, size_bytes=source.size_bytes()) + + +def get_features_config( + fields: list[FieldInfo], + row_count: int, + target_name: str | list[str] | None = None, +) -> dict: + """Constructs FieldInfo objects for each feature in dataset. These objects are used for downstream type + inference. + + Args: + fields: FieldInfo objects for all fields in dataset. + row_count: total number of entries in original dataset. + target_name: name of target feature. + + Returns: + Section of auto_train config for input_features and output_features. + """ + targets = convert_targets(target_name) + metadata = get_field_metadata(fields, row_count, targets) + return get_config_from_metadata(metadata, targets) + + +def convert_targets(target_name: str | list[str] | None = None) -> set[str]: + targets = target_name + if isinstance(targets, str): + targets = [targets] + if targets is None: + targets = [] + return set(targets) + + +def get_config_from_metadata(metadata: list[FieldMetadata], targets: set[str] | None = None) -> dict: + """Builds input/output feature sections of auto-train config using field metadata. + + Args: + metadata: FieldMetadata objects describing each field. + targets: names of target features. + + Returns: + Section of auto_train config for input_features and output_features. + """ + config = { + "input_features": [], + "output_features": [], + } + + for field_meta in metadata: + if field_meta.name in targets: + config["output_features"].append(field_meta.config.to_dict()) + elif not field_meta.excluded and field_meta.mode == "input": + config["input_features"].append(field_meta.config.to_dict()) + + return config + + +@DeveloperAPI +def get_field_metadata(fields: list[FieldInfo], row_count: int, targets: set[str] | None = None) -> list[FieldMetadata]: + """Computes metadata for each field in dataset. + + Args: + fields: FieldInfo objects for all fields in dataset. + row_count: total number of entries in original dataset. + targets: names of target features. + + Returns: + List of objects containing metadata for each field. + """ + + metadata = [] + column_count = len(fields) + for idx, field in enumerate(fields): + missing_value_percent = 1 - float(field.nonnull_values) / row_count + dtype = infer_type(field, missing_value_percent, row_count) + metadata.append( + FieldMetadata( + name=field.name, + config=FieldConfig( + name=field.name, + column=field.name, + type=dtype, + ), + excluded=should_exclude(idx, field, dtype, column_count, row_count, targets), + mode=infer_mode(field, targets), + missing_values=missing_value_percent, + imbalance_ratio=field.distinct_values_balance, + ) + ) + + return metadata + + +def infer_mode(field: FieldInfo, targets: set[str] | None = None) -> str: + if field.name in targets: + return "output" + if field.name.lower() == "split": + return "split" + return "input" diff --git a/ludwig/automl/config_enumerator.py b/ludwig/automl/config_enumerator.py new file mode 100644 index 0000000..3530e32 --- /dev/null +++ b/ludwig/automl/config_enumerator.py @@ -0,0 +1,255 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Enumerates all valid (encoder, combiner, decoder) combinations for a given feature schema.""" + +from __future__ import annotations + +import itertools +import logging +from dataclasses import dataclass +from typing import NamedTuple + +from ludwig.api_annotations import DeveloperAPI +from ludwig.automl.search_space import _default_search_space, SearchSpace + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Backwards-compatible module-level registry aliases +# --------------------------------------------------------------------------- +# These are computed lazily from the default SearchSpace so that existing code +# that does ``from ludwig.automl.config_enumerator import ENCODER_REGISTRY`` +# continues to work without modification. + + +def __getattr__(name: str): + if name == "ENCODER_REGISTRY": + return _default_search_space().encoder_registry + if name == "DECODER_REGISTRY": + return _default_search_space().decoder_registry + if name == "ALL_COMBINERS": + return list(_default_search_space().combiners.keys()) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +# --------------------------------------------------------------------------- +# Public data structures +# --------------------------------------------------------------------------- + + +@DeveloperAPI +class FeatureSpec(NamedTuple): + """Minimal description of a feature: its column name and Ludwig feature type.""" + + name: str + type: str # Ludwig feature type constant + + +@DeveloperAPI +@dataclass +class ConfigSpec: + """A single valid (encoder assignments, combiner, decoder) combination.""" + + input_encoders: dict[str, str] # feature_name -> encoder_type + combiner: str + output_decoder: str + output_type: str + + +# --------------------------------------------------------------------------- +# Core query functions +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def get_valid_encoders(feature_type: str, search_space: SearchSpace | None = None) -> list[str]: + """Returns the list of valid encoder names for *feature_type*. + + # Inputs + :param feature_type: (str) A Ludwig feature type constant (e.g. ``"text"``, ``"image"``). + :param search_space: (:class:`~ludwig.automl.search_space.SearchSpace` | None) Optional + custom search space. Uses the built-in defaults when ``None``. + + # Return + :return: (list[str]) Encoder names registered for that feature type. Returns an empty + list for unknown feature types (with a warning logged). + """ + ss = search_space or _default_search_space() + encoders = ss.encoder_registry.get(feature_type) + if encoders is None: + logger.warning(f"No encoder registry entry for feature type '{feature_type}'; returning empty list.") + return [] + return list(encoders) + + +@DeveloperAPI +def get_valid_decoders(feature_type: str, search_space: SearchSpace | None = None) -> list[str]: + """Returns the list of valid decoder names for *feature_type*. + + # Inputs + :param feature_type: (str) A Ludwig feature type constant used as an output feature. + :param search_space: (:class:`~ludwig.automl.search_space.SearchSpace` | None) Optional + custom search space. Uses the built-in defaults when ``None``. + + # Return + :return: (list[str]) Decoder names registered for that feature type. Returns an empty + list for unknown/unsupported output feature types (with a warning logged). + """ + ss = search_space or _default_search_space() + decoders = ss.decoder_registry.get(feature_type) + if decoders is None: + logger.warning(f"No decoder registry entry for feature type '{feature_type}'; returning empty list.") + return [] + return list(decoders) + + +@DeveloperAPI +def get_valid_combiners( + input_features: list[FeatureSpec], + search_space: SearchSpace | None = None, +) -> list[str]: + """Returns the list of valid combiner names for the given input feature schema. + + Applies compatibility constraints stored in each :class:`~ludwig.automl.search_space.CombinerSpec`: + + - ``requires_all_tabular``: only when **all** input features are tabular + (BINARY, CATEGORY, or NUMBER). + - ``exact_n_inputs``: only when the number of input features equals that value. + - ``requires_sequential``: only when at least one input feature is + SEQUENCE, TEXT, or TIMESERIES. + + # Inputs + :param input_features: (list[FeatureSpec]) The input feature specifications. + :param search_space: (:class:`~ludwig.automl.search_space.SearchSpace` | None) Optional + custom search space. Uses the built-in defaults when ``None``. + + # Return + :return: (list[str]) Compatible combiner names. + """ + ss = search_space or _default_search_space() + + _tabular = frozenset({"binary", "category", "number"}) + _sequential = frozenset({"sequence", "text", "timeseries"}) + + input_types = {f.type for f in input_features} + n_inputs = len(input_features) + all_tabular = input_types.issubset(_tabular) + has_sequential = bool(input_types & _sequential) + + valid: list[str] = [] + for spec in ss.combiners.values(): + c = spec.constraints + if c.get("requires_all_tabular") and not all_tabular: + continue + exact_n = c.get("exact_n_inputs") + if exact_n is not None and n_inputs != exact_n: + continue + if c.get("requires_sequential") and not has_sequential: + continue + valid.append(spec.name) + + return valid + + +# --------------------------------------------------------------------------- +# Full enumeration +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def enumerate_config_specs( + input_features: list[FeatureSpec], + output_feature: FeatureSpec, + max_configs: int | None = None, + search_space: SearchSpace | None = None, +) -> list[ConfigSpec]: + """Enumerates all valid (encoder, combiner, decoder) combinations for the given schema. + + Each :class:`ConfigSpec` specifies one encoder per input feature, one combiner, and one + decoder for the output feature. The combinatorial space can be very large; *max_configs* + caps the output using deterministic sampling across all axes. + + # Inputs + :param input_features: (list[FeatureSpec]) The input feature specifications. + :param output_feature: (FeatureSpec) The single output feature specification. + :param max_configs: (int | None) Maximum number of ``ConfigSpec`` objects to return. + ``None`` means unlimited. + :param search_space: (:class:`~ludwig.automl.search_space.SearchSpace` | None) Optional + custom search space. Uses the built-in defaults when ``None``. + + # Return + :return: (list[ConfigSpec]) All (or up to *max_configs*) valid config specs. + """ + ss = search_space or _default_search_space() + + per_feature_encoders: list[list[str]] = [get_valid_encoders(f.type, ss) for f in input_features] + valid_combiners = get_valid_combiners(input_features, ss) + valid_decoders = get_valid_decoders(output_feature.type, ss) + + if not valid_combiners: + logger.warning("No valid combiners for the given input feature schema; returning empty list.") + return [] + if not valid_decoders: + logger.warning(f"No valid decoders for output feature type '{output_feature.type}'; returning empty list.") + return [] + + empty_features = [f.name for f, enc_list in zip(input_features, per_feature_encoders) if not enc_list] + if empty_features: + logger.warning( + f"No valid encoders found for feature(s) {empty_features}; enumerate_config_specs will return empty list." + ) + return [] + + total = 1 + for enc_list in per_feature_encoders: + total *= len(enc_list) + total *= len(valid_combiners) * len(valid_decoders) + + logger.debug( + f"enumerate_config_specs: {total} total combinations " + f"(features={len(input_features)}, combiners={len(valid_combiners)}, decoders={len(valid_decoders)})" + ) + + def _make_spec(enc_combo: tuple, combiner: str, decoder: str) -> ConfigSpec: + return ConfigSpec( + input_encoders={f.name: enc for f, enc in zip(input_features, enc_combo)}, + combiner=combiner, + output_decoder=decoder, + output_type=output_feature.type, + ) + + results: list[ConfigSpec] = [] + encoder_combos = itertools.product(*per_feature_encoders) + + if max_configs is not None and total > max_configs: + stride = max(1, total // max_configs) + flat_idx = 0 + next_sample = 0 + for enc_combo in encoder_combos: + for combiner in valid_combiners: + for decoder in valid_decoders: + if flat_idx == next_sample: + results.append(_make_spec(enc_combo, combiner, decoder)) + next_sample += stride + if len(results) >= max_configs: + return results + flat_idx += 1 + else: + for enc_combo in encoder_combos: + for combiner in valid_combiners: + for decoder in valid_decoders: + results.append(_make_spec(enc_combo, combiner, decoder)) + + return results diff --git a/ludwig/automl/config_sampler.py b/ludwig/automl/config_sampler.py new file mode 100644 index 0000000..ef99f97 --- /dev/null +++ b/ludwig/automl/config_sampler.py @@ -0,0 +1,394 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Samples diverse Ludwig configs for a given feature schema.""" + +from __future__ import annotations + +import hashlib +import json +import logging +import random +from dataclasses import dataclass + +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI +from ludwig.automl.config_enumerator import ( + ConfigSpec, + FeatureSpec, + get_valid_combiners, + get_valid_decoders, + get_valid_encoders, +) +from ludwig.automl.search_space import _default_search_space, SearchSpace +from ludwig.constants import ( + BATCH_SIZE, + COMBINER, + INPUT_FEATURES, + LEARNING_RATE, + OUTPUT_FEATURES, + TRAINER, + TYPE, +) + +logger = logging.getLogger(__name__) + +# Maximum number of encoder choices sampled per feature type to keep configs tractable. +_MAX_ENCODERS_PER_TYPE: int = 3 + + +# --------------------------------------------------------------------------- +# Public data structures +# --------------------------------------------------------------------------- + + +@DeveloperAPI +@dataclass +class SampledConfig: + """A complete Ludwig-style config dict ready for training.""" + + config_dict: dict # Full Ludwig config as dict + config_hash: str # SHA-256 of canonical JSON + spec: ConfigSpec + trainer_params: dict # LR, batch_size, etc. + seed: int + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _canonical_json(obj: dict) -> str: + """Returns a deterministic JSON string suitable for hashing.""" + return json.dumps(obj, sort_keys=True, ensure_ascii=True) + + +def _hash_config(config_dict: dict) -> str: + """Returns the SHA-256 hex digest of the canonical JSON representation of *config_dict*.""" + return hashlib.sha256(_canonical_json(config_dict).encode()).hexdigest() + + +def _build_config_dict( + input_features: list[FeatureSpec], + output_feature: FeatureSpec, + spec: ConfigSpec, + trainer_params: dict, + combiner_params: dict | None = None, + encoder_hyperparams: dict[str, dict] | None = None, +) -> dict: + """Builds a complete Ludwig config dict from a :class:`ConfigSpec` and trainer params. + + # Inputs + :param input_features: Input feature specs. + :param output_feature: Output feature spec. + :param spec: The :class:`ConfigSpec` (encoder assignments, combiner, decoder). + :param trainer_params: Trainer hyperparameters dict. + :param combiner_params: Optional combiner hyperparameter dict (includes ``type`` key). + :param encoder_hyperparams: Optional mapping of feature name -> encoder hyperparam dict + (merged into each input feature's ``encoder`` sub-dict). + """ + encoder_hyperparams = encoder_hyperparams or {} + input_feature_dicts = [] + for f in input_features: + enc_dict: dict = {TYPE: spec.input_encoders[f.name]} + extra = encoder_hyperparams.get(f.name, {}) + # extra already contains {type: ..., param: val, ...} — merge, type wins from spec + for k, v in extra.items(): + if k != TYPE: + enc_dict[k] = v + input_feature_dicts.append({"name": f.name, TYPE: f.type, "encoder": enc_dict}) + + output_feature_dict = { + "name": output_feature.name, + TYPE: output_feature.type, + "decoder": {TYPE: spec.output_decoder}, + } + combiner_dict = combiner_params if combiner_params is not None else {TYPE: spec.combiner} + return { + INPUT_FEATURES: input_feature_dicts, + OUTPUT_FEATURES: [output_feature_dict], + COMBINER: combiner_dict, + TRAINER: dict(trainer_params), + } + + +def _sample_trainer_params( + rng: random.Random, + max_epochs: int, + time_limit_s: int | None, + search_space: SearchSpace | None = None, + max_learning_rate: float | None = None, + max_batch_size: int | None = None, +) -> dict: + """Samples a single set of trainer hyperparameters from the search space grids.""" + ss = search_space or _default_search_space() + lr_candidates = ss.trainer.learning_rate_values + if max_learning_rate is not None: + lr_candidates = [lr for lr in lr_candidates if lr <= max_learning_rate] + if not lr_candidates: + lr_candidates = ss.trainer.learning_rate_values + bs_candidates = ss.trainer.batch_size_values + if max_batch_size is not None: + bs_candidates = [bs for bs in bs_candidates if bs <= max_batch_size] + if not bs_candidates: + bs_candidates = ss.trainer.batch_size_values + params: dict = { + LEARNING_RATE: rng.choice(lr_candidates), + BATCH_SIZE: rng.choice(bs_candidates), + "epochs": max_epochs, + } + if time_limit_s is not None: + params["time_limit_s"] = time_limit_s + return params + + +def _candidate_encoders( + feature_type: str, + rng: random.Random, + search_space: SearchSpace | None = None, +) -> list[str]: + """Returns up to *_MAX_ENCODERS_PER_TYPE* encoders for *feature_type*, sampled without replacement.""" + all_encoders = get_valid_encoders(feature_type, search_space) + if len(all_encoders) <= _MAX_ENCODERS_PER_TYPE: + return list(all_encoders) + return rng.sample(all_encoders, _MAX_ENCODERS_PER_TYPE) + + +# --------------------------------------------------------------------------- +# Core sampling +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def sample_configs( + input_features: list[FeatureSpec], + output_feature: FeatureSpec, + n: int = 100, + seed: int = 42, + max_epochs: int = 50, + time_limit_s: int | None = None, + search_space: SearchSpace | None = None, +) -> list[SampledConfig]: + """Samples *n* diverse Ludwig configs for the given feature schema. + + Uses stratified sampling across combiners to ensure diversity: + + - At least 1 config per valid combiner (up to *n*). + - Remaining configs distributed proportionally across combiners. + - Trainer hyperparams sampled from the search space grids (LR, batch size). + - Encoder and combiner hyperparams sampled from per-spec grids in the search space. + - Encoder choices sampled per feature type (up to 3 options per type). + - Configs are deduplicated by :attr:`SampledConfig.config_hash`. + + # Inputs + :param input_features: (list[FeatureSpec]) The input feature specifications. + :param output_feature: (FeatureSpec) The single output feature specification. + :param n: (int) Target number of sampled configs (after deduplication may be lower). + :param seed: (int) Random seed for reproducibility. + :param max_epochs: (int) Maximum training epochs written into each config's trainer section. + :param time_limit_s: (int | None) Optional wall-clock time limit passed to the trainer. + :param search_space: (:class:`~ludwig.automl.search_space.SearchSpace` | None) Optional + custom search space. Uses the built-in defaults when ``None``. + + # Return + :return: (list[SampledConfig]) Deduplicated sampled configs (up to *n*). + """ + ss = search_space or _default_search_space() + rng = random.Random(seed) + + valid_combiners = get_valid_combiners(input_features, ss) + valid_decoders = get_valid_decoders(output_feature.type, ss) + + if not valid_combiners: + logger.warning("sample_configs: no valid combiners for the given input feature schema.") + return [] + if not valid_decoders: + logger.warning(f"sample_configs: no valid decoders for output type '{output_feature.type}'.") + return [] + + # Build combiner budget: at least 1 per combiner, remainder distributed evenly. + n_combiners = len(valid_combiners) + base_per_combiner = max(1, n // n_combiners) + remainder = max(0, n - base_per_combiner * n_combiners) + + combiner_budget: dict[str, int] = {} + for i, c in enumerate(valid_combiners): + combiner_budget[c] = base_per_combiner + (1 if i < remainder else 0) + + seen_hashes: set[str] = set() + results: list[SampledConfig] = [] + + for combiner, budget in combiner_budget.items(): + combiner_spec = ss.combiners.get(combiner) + attempts = 0 + generated = 0 + max_attempts = budget * 20 # avoid infinite loop on very small spaces + + while generated < budget and attempts < max_attempts: + attempts += 1 + + # Sample one encoder per input feature (with hyperparams). + input_encoders: dict[str, str] = {} + encoder_hyperparams: dict[str, dict] = {} + for feat in input_features: + choices = _candidate_encoders(feat.type, rng, ss) + if not choices: + logger.warning(f"sample_configs: no encoders for feature type '{feat.type}'; skipping feature.") + choices = ["passthrough"] + enc_name = rng.choice(choices) + input_encoders[feat.name] = enc_name + enc_spec = ss.encoders.get(enc_name) + if enc_spec is not None: + encoder_hyperparams[feat.name] = ss.sample_hyperparams(enc_spec, rng) + + decoder = rng.choice(valid_decoders) + max_lr = combiner_spec.max_learning_rate if combiner_spec is not None else None + max_bs = combiner_spec.max_batch_size if combiner_spec is not None else None + trainer_params = _sample_trainer_params( + rng, max_epochs, time_limit_s, ss, max_learning_rate=max_lr, max_batch_size=max_bs + ) + + # Sample combiner hyperparams via the SearchSpace. + if combiner_spec is not None: + combiner_params = ss.sample_hyperparams(combiner_spec, rng) + else: + combiner_params = {TYPE: combiner} + + spec = ConfigSpec( + input_encoders=input_encoders, + combiner=combiner, + output_decoder=decoder, + output_type=output_feature.type, + ) + config_dict = _build_config_dict( + input_features, output_feature, spec, trainer_params, combiner_params, encoder_hyperparams + ) + config_hash = _hash_config(config_dict) + + if config_hash in seen_hashes: + continue + + seen_hashes.add(config_hash) + results.append( + SampledConfig( + config_dict=config_dict, + config_hash=config_hash, + spec=spec, + trainer_params=trainer_params, + seed=seed, + ) + ) + generated += 1 + + if generated < budget: + logger.debug( + f"sample_configs: combiner '{combiner}' only produced {generated}/{budget} unique configs " + f"after {attempts} attempts (search space may be small)." + ) + + logger.info(f"sample_configs: produced {len(results)} unique configs (target={n}).") + return results[:n] + + +# --------------------------------------------------------------------------- +# Convenience entry point +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def configs_from_dataframe( + df: pd.DataFrame, + target_column: str, + n: int = 100, + seed: int = 42, + search_space: SearchSpace | None = None, +) -> list[SampledConfig]: + """Convenience function: infers the feature schema from *df* and samples *n* configs. + + Uses Ludwig's existing type-inference logic (:func:`~ludwig.utils.automl.type_inference.infer_type`) + to determine feature types for every column. The *target_column* is treated as the + output feature; all other non-excluded columns become input features. + + # Inputs + :param df: (pd.DataFrame) The dataset to infer types from. + :param target_column: (str) Name of the output / target column. + :param n: (int) Target number of sampled configs. + :param seed: (int) Random seed for reproducibility. + :param search_space: (:class:`~ludwig.automl.search_space.SearchSpace` | None) Optional + custom search space. Uses the built-in defaults when ``None``. + + # Return + :return: (list[SampledConfig]) Sampled configs for the inferred schema. + """ + from ludwig.automl.base_config import convert_targets, get_dataset_info, get_field_metadata + + dataset_info = get_dataset_info(df) + targets = convert_targets(target_column) + metadata = get_field_metadata(dataset_info.fields, dataset_info.row_count, targets) + + input_features: list[FeatureSpec] = [] + output_feature: FeatureSpec | None = None + + for field_meta in metadata: + if field_meta.name == target_column: + output_feature = FeatureSpec(name=field_meta.name, type=field_meta.config.type) + elif not field_meta.excluded and field_meta.mode == "input": + input_features.append(FeatureSpec(name=field_meta.name, type=field_meta.config.type)) + + if output_feature is None: + raise ValueError(f"Target column '{target_column}' not found in dataframe.") + if not input_features: + raise ValueError("No valid input features could be inferred from the dataframe.") + + logger.info( + f"configs_from_dataframe: inferred {len(input_features)} input features, " + f"output='{output_feature.name}' (type={output_feature.type})." + ) + + # Scale down epochs and cap batch sizes for large datasets to avoid OOM and timeouts. + # These thresholds are conservative; they keep wall time and VRAM use manageable on + # a single consumer GPU (10–24 GiB) without sacrificing benchmark coverage. + n_rows = len(df) + max_epochs = 50 + if search_space is None and n_rows > 100_000: + from ludwig.automl.search_space import _build_default_search_space, SearchSpace, TrainerSpec + + base = _build_default_search_space() + if n_rows > 500_000: + max_epochs = 5 + batch_sizes = [256, 512, 1024] + else: + max_epochs = 10 + batch_sizes = [128, 256, 512] + search_space = SearchSpace._from_specs( + encoders=base.encoders, + combiners=base.combiners, + decoders=base.decoders, + trainer=TrainerSpec( + learning_rate_values=base.trainer.learning_rate_values, + batch_size_values=batch_sizes, + default_epochs=max_epochs, + ), + ) + + return sample_configs( + input_features=input_features, + output_feature=output_feature, + n=n, + seed=seed, + max_epochs=max_epochs, + search_space=search_space, + ) diff --git a/ludwig/automl/config_validator.py b/ludwig/automl/config_validator.py new file mode 100644 index 0000000..f04464a --- /dev/null +++ b/ludwig/automl/config_validator.py @@ -0,0 +1,300 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Pre-flight validation that a Ludwig config is compatible with a DataFrame.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field + +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI +from ludwig.automl.config_enumerator import ( + FeatureSpec, + get_valid_combiners, + get_valid_decoders, + get_valid_encoders, +) +from ludwig.constants import ( + BATCH_SIZE, + COMBINER, + INPUT_FEATURES, + OUTPUT_FEATURES, + TYPE, +) + +logger = logging.getLogger(__name__) + +# Fraction of the dataset assumed to go to the training split for the batch-size check. +_TRAIN_SPLIT_FRACTION: float = 0.7 + + +# --------------------------------------------------------------------------- +# Public data structures +# --------------------------------------------------------------------------- + + +@DeveloperAPI +@dataclass +class ValidationResult: + """Result of a pre-flight config validation. + + :attr is_valid: ``True`` when all checks passed (and, if ``strict=True``, no warnings either). + :attr failures: Hard errors that definitely make the config incompatible. + :attr warnings: Soft issues that *may* cause problems but are not necessarily fatal. + """ + + is_valid: bool + failures: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + + def __bool__(self) -> bool: + return self.is_valid + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _get_feature_names(features: list[dict]) -> list[str]: + """Extracts the ``name`` key from each feature dict.""" + return [f.get("name", "") for f in features] + + +def _get_feature_type(feature: dict) -> str | None: + """Returns the ``type`` field of a feature dict, or ``None`` if absent.""" + return feature.get(TYPE) + + +def _get_encoder_type(feature: dict) -> str | None: + """Returns the encoder type nested inside a feature dict, or ``None`` if absent.""" + encoder = feature.get("encoder") + if isinstance(encoder, dict): + return encoder.get(TYPE) + if isinstance(encoder, str): + return encoder + return None + + +def _get_decoder_type(feature: dict) -> str | None: + """Returns the decoder type nested inside a feature dict, or ``None`` if absent.""" + decoder = feature.get("decoder") + if isinstance(decoder, dict): + return decoder.get(TYPE) + if isinstance(decoder, str): + return decoder + return None + + +def _get_combiner_type(config_dict: dict) -> str | None: + """Returns the combiner type from a config dict, or ``None`` if absent.""" + combiner = config_dict.get(COMBINER) + if isinstance(combiner, dict): + return combiner.get(TYPE) + if isinstance(combiner, str): + return combiner + return None + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def validate_config_for_dataset( + config_dict: dict, + df: pd.DataFrame, + strict: bool = False, +) -> ValidationResult: + """Pre-flight validation that a Ludwig config is compatible with a DataFrame. + + Performs the following checks **without training**: + + 1. All ``input_features`` columns exist in *df*. + 2. All ``output_features`` columns exist in *df*. + 3. Encoder types are valid for their respective feature types. + 4. Decoder types are valid for their respective output feature types. + 5. Combiner type is valid for the input feature set. + 6. ``batch_size`` (if specified) is less than ``len(df) * 0.7`` so the training + split contains at least one full batch. + 7. No output feature column is also an input feature column. + 8. Each output feature column has more than 1 distinct non-null value. + + # Inputs + :param config_dict: (dict) A Ludwig config dict (as produced by + :func:`~ludwig.automl.config_sampler.sample_configs` or hand-crafted). + :param df: (pd.DataFrame) The dataset the config will be trained on. + :param strict: (bool) When ``True``, WARN-level issues also cause + :attr:`ValidationResult.is_valid` to be ``False``. + + # Return + :return: (ValidationResult) Populated with any failures and/or warnings found. + """ + failures: list[str] = [] + warnings: list[str] = [] + + df_columns: set[str] = set(df.columns) + input_features: list[dict] = config_dict.get(INPUT_FEATURES, []) + output_features: list[dict] = config_dict.get(OUTPUT_FEATURES, []) + + input_names = _get_feature_names(input_features) + output_names = _get_feature_names(output_features) + + # ------------------------------------------------------------------ + # Input/output columns present in dataframe + # ------------------------------------------------------------------ + for name in input_names: + if name not in df_columns: + failures.append(f"Input feature column '{name}' not found in dataframe (columns: {sorted(df_columns)}).") + + for name in output_names: + if name not in df_columns: + failures.append(f"Output feature column '{name}' not found in dataframe (columns: {sorted(df_columns)}).") + + # ------------------------------------------------------------------ + # Encoder types valid for their feature types + # ------------------------------------------------------------------ + for feat in input_features: + feat_name = feat.get("name", "") + feat_type = _get_feature_type(feat) + enc_type = _get_encoder_type(feat) + + if feat_type is None: + warnings.append(f"Input feature '{feat_name}' has no 'type' specified; skipping encoder check.") + continue + + if enc_type is None: + # No encoder specified — Ludwig will use the default, which is always valid. + continue + + valid_encoders = get_valid_encoders(feat_type) + if not valid_encoders: + warnings.append( + f"Input feature '{feat_name}' has unknown feature type '{feat_type}'; cannot validate encoder." + ) + elif enc_type not in valid_encoders: + failures.append( + f"Encoder '{enc_type}' is not valid for feature type '{feat_type}' " + f"(feature '{feat_name}'). Valid encoders: {valid_encoders}." + ) + + # ------------------------------------------------------------------ + # Decoder types valid for their output feature types + # ------------------------------------------------------------------ + for feat in output_features: + feat_name = feat.get("name", "") + feat_type = _get_feature_type(feat) + dec_type = _get_decoder_type(feat) + + if feat_type is None: + warnings.append(f"Output feature '{feat_name}' has no 'type' specified; skipping decoder check.") + continue + + if dec_type is None: + continue + + valid_decoders = get_valid_decoders(feat_type) + if not valid_decoders: + warnings.append( + f"Output feature '{feat_name}' has unknown feature type '{feat_type}'; cannot validate decoder." + ) + elif dec_type not in valid_decoders: + failures.append( + f"Decoder '{dec_type}' is not valid for output feature type '{feat_type}' " + f"(feature '{feat_name}'). Valid decoders: {valid_decoders}." + ) + + # ------------------------------------------------------------------ + # Combiner compatible with input feature set + # ------------------------------------------------------------------ + combiner_type = _get_combiner_type(config_dict) + if combiner_type is not None: + # Build FeatureSpec list for features whose types are known. + known_input_specs: list[FeatureSpec] = [] + for feat in input_features: + ft = _get_feature_type(feat) + fn = feat.get("name", "") + if ft: + known_input_specs.append(FeatureSpec(name=fn, type=ft)) + + if known_input_specs: + valid_combiners = get_valid_combiners(known_input_specs) + if combiner_type not in valid_combiners: + failures.append( + f"Combiner '{combiner_type}' is not compatible with the input feature schema " + f"(input types: {[s.type for s in known_input_specs]}). " + f"Valid combiners: {valid_combiners}." + ) + else: + warnings.append("No combiner type specified in config; skipping combiner compatibility check.") + + # ------------------------------------------------------------------ + # Batch size fits within training split + # ------------------------------------------------------------------ + trainer = config_dict.get("trainer", {}) + batch_size = trainer.get(BATCH_SIZE) if isinstance(trainer, dict) else None + + if batch_size is not None and isinstance(batch_size, int): + train_rows = len(df) * _TRAIN_SPLIT_FRACTION + if batch_size >= train_rows: + warnings.append( + f"batch_size={batch_size} >= estimated training rows ({train_rows:.0f}). " + "The training split may contain fewer than one full batch." + ) + + # ------------------------------------------------------------------ + # No column serves as both input and output + # ------------------------------------------------------------------ + overlap = set(input_names) & set(output_names) + if overlap: + failures.append( + f"The following column(s) appear in both input_features and output_features: {sorted(overlap)}." + ) + + # ------------------------------------------------------------------ + # Each output feature has enough distinct values to train on + # ------------------------------------------------------------------ + for name in output_names: + if name not in df_columns: + # Already reported in check 2; skip to avoid duplicate noise. + continue + series = df[name].dropna() + n_distinct = series.nunique() + if n_distinct <= 1: + failures.append( + f"Output feature column '{name}' has {n_distinct} distinct non-null value(s); " + "at least 2 are required for meaningful training." + ) + + # ------------------------------------------------------------------ + # Determine overall validity + # ------------------------------------------------------------------ + is_valid = len(failures) == 0 and (not strict or len(warnings) == 0) + + result = ValidationResult(is_valid=is_valid, failures=failures, warnings=warnings) + + if failures: + logger.warning(f"validate_config_for_dataset: {len(failures)} failure(s) found.") + for msg in failures: + logger.warning(f" FAIL: {msg}") + if warnings: + logger.debug(f"validate_config_for_dataset: {len(warnings)} warning(s) found.") + for msg in warnings: + logger.debug(f" WARN: {msg}") + + return result diff --git a/ludwig/automl/defaults/base_automl_config.yaml b/ludwig/automl/defaults/base_automl_config.yaml new file mode 100644 index 0000000..da611a7 --- /dev/null +++ b/ludwig/automl/defaults/base_automl_config.yaml @@ -0,0 +1,20 @@ +trainer: + batch_size: auto #256 + learning_rate: auto #.001 + # validation_metric: accuracy + +hyperopt: + search_alg: + # Gives results like default + supports random_state_seed for sample sequence repeatability + type: variant_generator + executor: + type: ray + num_samples: 10 + time_budget_s: 7200 + scheduler: + type: async_hyperband + time_attr: time_total_s + max_t: 7200 + grace_period: 72 + # Increased over default to get more pruning/exploration + reduction_factor: 5 diff --git a/ludwig/automl/defaults/combiner/concat_config.yaml b/ludwig/automl/defaults/combiner/concat_config.yaml new file mode 100644 index 0000000..1e9985e --- /dev/null +++ b/ludwig/automl/defaults/combiner/concat_config.yaml @@ -0,0 +1,26 @@ +combiner: + type: concat + +hyperopt: + # goal: maximize + parameters: + combiner.num_fc_layers: + space: randint + lower: 1 + upper: 4 + combiner.output_size: + space: choice + categories: [128, 256] + combiner.dropout: + space: uniform + lower: 0.0 + upper: 0.1 + # This needs to be loguniform due to invalid schemas created by merging with a choice parameter space. See the + # comment in ludwig/automl/defaults/text/bert_config.yaml for more information. + trainer.learning_rate: + space: loguniform + lower: 0.00002 + upper: 0.001 + trainer.batch_size: + space: choice + categories: [64, 128, 256, 512, 1024] diff --git a/ludwig/automl/defaults/combiner/ft_transformer_config.yaml b/ludwig/automl/defaults/combiner/ft_transformer_config.yaml new file mode 100644 index 0000000..c64a0a3 --- /dev/null +++ b/ludwig/automl/defaults/combiner/ft_transformer_config.yaml @@ -0,0 +1,39 @@ +combiner: + type: ft_transformer + num_layers: 3 + num_heads: 8 + hidden_size: 64 + transformer_output_size: 64 + dropout: 0.1 + fc_dropout: 0.1 + +trainer: + batch_size: auto + learning_rate: auto + optimizer: + type: adam + +hyperopt: + parameters: + trainer.learning_rate: + space: loguniform + lower: 0.0001 + upper: 0.001 + trainer.batch_size: + space: choice + categories: [32, 64, 128, 256] + combiner.num_layers: + space: choice + categories: [2, 3, 4] + combiner.hidden_size: + # Must be divisible by num_heads (8). Valid values: 32, 64, 128, 256. + space: choice + categories: [32, 64, 128, 256] + combiner.dropout: + space: uniform + lower: 0.0 + upper: 0.3 + combiner.fc_dropout: + space: uniform + lower: 0.0 + upper: 0.3 diff --git a/ludwig/automl/defaults/combiner/tabnet_config.yaml b/ludwig/automl/defaults/combiner/tabnet_config.yaml new file mode 100644 index 0000000..8054f1c --- /dev/null +++ b/ludwig/automl/defaults/combiner/tabnet_config.yaml @@ -0,0 +1,46 @@ +combiner: + type: tabnet + +trainer: + batch_size: auto + learning_rate_scaling: sqrt + learning_rate_scheduler: + decay: exponential + decay_steps: 20000 + decay_rate: 0.8 + optimizer: + type: adam + +hyperopt: + parameters: + trainer.learning_rate: + space: loguniform + lower: 0.00002 + upper: 0.001 + trainer.learning_rate_scheduler.decay_rate: + space: choice + categories: [0.8, 0.9, 0.95] + trainer.learning_rate_scheduler.decay_steps: + space: choice + categories: [500, 2000, 8000, 10000, 20000] + combiner.size: + space: choice + categories: [8, 16, 24, 32, 64] + combiner.output_size: + space: choice + categories: [8, 16, 24, 32, 64, 128] + combiner.num_steps: + space: choice + categories: [3, 4, 5, 6, 7, 8, 9, 10] + combiner.relaxation_factor: + space: choice + categories: [1.0, 1.2, 1.5, 2.0] + combiner.sparsity: + space: choice + categories: [0.0, 0.000001, 0.0001, 0.001, 0.01, 0.1] + combiner.bn_virtual_bs: + space: choice + categories: [256, 512, 1024, 2048, 4096] + combiner.bn_momentum: + space: choice + categories: [0.4, 0.3, 0.2, 0.1, 0.05, 0.02] diff --git a/ludwig/automl/defaults/combiner/tabtransformer_config.yaml b/ludwig/automl/defaults/combiner/tabtransformer_config.yaml new file mode 100644 index 0000000..c72fe9c --- /dev/null +++ b/ludwig/automl/defaults/combiner/tabtransformer_config.yaml @@ -0,0 +1,39 @@ +combiner: + type: tabtransformer + num_layers: 4 + num_heads: 8 + hidden_size: 64 + transformer_output_size: 64 + dropout: 0.1 + fc_dropout: 0.1 + +trainer: + batch_size: auto + learning_rate: auto + optimizer: + type: adam + +hyperopt: + parameters: + trainer.learning_rate: + space: loguniform + lower: 0.0001 + upper: 0.001 + trainer.batch_size: + space: choice + categories: [32, 64, 128, 256] + combiner.num_layers: + space: choice + categories: [2, 4, 6] + combiner.hidden_size: + # Must be divisible by num_heads (8). Valid values: 32, 64, 128, 256. + space: choice + categories: [32, 64, 128, 256] + combiner.dropout: + space: uniform + lower: 0.0 + upper: 0.3 + combiner.fc_dropout: + space: uniform + lower: 0.0 + upper: 0.3 diff --git a/ludwig/automl/defaults/combiner/transformer_config.yaml b/ludwig/automl/defaults/combiner/transformer_config.yaml new file mode 100644 index 0000000..f199f47 --- /dev/null +++ b/ludwig/automl/defaults/combiner/transformer_config.yaml @@ -0,0 +1,36 @@ +combiner: + type: transformer + +trainer: + batch_size: auto #256 + learning_rate: auto #0.0001 + # validation_metric: accuracy + +hyperopt: + # goal: maximize + parameters: + trainer.learning_rate: + space: loguniform + lower: 0.00002 + upper: 0.001 + trainer.batch_size: + space: choice + categories: [64, 128, 256] + combiner.num_heads: + space: choice + categories: [4] + combiner.dropout: + space: uniform + lower: 0.1 + upper: 0.3 + combiner.num_layers: + space: randint + lower: 3 + upper: 4 + combiner.num_fc_layers: + space: choice + categories: [1, 2] + combiner.fc_dropout: + space: uniform + lower: 0.1 + upper: 0.5 diff --git a/ludwig/automl/defaults/reference_configs.yaml b/ludwig/automl/defaults/reference_configs.yaml new file mode 100644 index 0000000..c962a2b --- /dev/null +++ b/ludwig/automl/defaults/reference_configs.yaml @@ -0,0 +1,4217 @@ +# Record the score and configuration of a reference model trained by Ludwig for specified datasets. +# This information is useful for Ludwig AutoML hyperparameter transfer learning or for manual experimentation. +datasets: + - name: adult_census_income + goal: maximize + metric: accuracy + validation_metric_score: 0.8682432174682617 + training_rows: 29305 + test_rows: 16281 + validation_rows: 3256 + config: + output_features: + - name: income + type: category + input_features: + - name: age + type: number + - name: workclass + type: category + - name: fnlwgt + type: number + - name: education + type: category + - name: education-num + type: number + - name: marital-status + type: category + - name: occupation + type: category + - name: relationship + type: category + - name: race + type: category + - name: sex + type: category + - name: capital-gain + type: number + - name: capital-loss + type: number + - name: hours-per-week + type: number + - name: native-country + type: category + combiner: + type: tabnet + size: 8 # N_a + output_size: 128 # N_d + sparsity: 0.0 # lambda_sparse + bn_momentum: 0.4 # m_B + num_steps: 3 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 4096 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.01 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 500 + decay_rate: 0.95 + validation_metric: accuracy + - name: allstate_claims_severity + goal: minimize + metric: root_mean_squared_error + validation_metric_score: 1915.5531005859375 + training_rows: 131726 + test_rows: 37750 + validation_rows: 18842 + config: + output_features: + - name: loss + type: number + input_features: + - column: cat1 + name: cat1 + type: category + - column: cat2 + name: cat2 + type: category + - column: cat3 + name: cat3 + type: category + - column: cat4 + name: cat4 + type: category + - column: cat5 + name: cat5 + type: category + - column: cat6 + name: cat6 + type: category + - column: cat7 + name: cat7 + type: category + - column: cat8 + name: cat8 + type: category + - column: cat9 + name: cat9 + type: category + - column: cat10 + name: cat10 + type: category + - column: cat11 + name: cat11 + type: category + - column: cat12 + name: cat12 + type: category + - column: cat13 + name: cat13 + type: category + - column: cat14 + name: cat14 + type: category + - column: cat15 + name: cat15 + type: category + - column: cat16 + name: cat16 + type: category + - column: cat17 + name: cat17 + type: category + - column: cat18 + name: cat18 + type: category + - column: cat19 + name: cat19 + type: category + - column: cat20 + name: cat20 + type: category + - column: cat21 + name: cat21 + type: category + - column: cat22 + name: cat22 + type: category + - column: cat23 + name: cat23 + type: category + - column: cat24 + name: cat24 + type: category + - column: cat25 + name: cat25 + type: category + - column: cat26 + name: cat26 + type: category + - column: cat27 + name: cat27 + type: category + - column: cat28 + name: cat28 + type: category + - column: cat29 + name: cat29 + type: category + - column: cat30 + name: cat30 + type: category + - column: cat31 + name: cat31 + type: category + - column: cat32 + name: cat32 + type: category + - column: cat33 + name: cat33 + type: category + - column: cat34 + name: cat34 + type: category + - column: cat35 + name: cat35 + type: category + - column: cat36 + name: cat36 + type: category + - column: cat37 + name: cat37 + type: category + - column: cat38 + name: cat38 + type: category + - column: cat39 + name: cat39 + type: category + - column: cat40 + name: cat40 + type: category + - column: cat41 + name: cat41 + type: category + - column: cat42 + name: cat42 + type: category + - column: cat43 + name: cat43 + type: category + - column: cat44 + name: cat44 + type: category + - column: cat45 + name: cat45 + type: category + - column: cat46 + name: cat46 + type: category + - column: cat47 + name: cat47 + type: category + - column: cat48 + name: cat48 + type: category + - column: cat49 + name: cat49 + type: category + - column: cat50 + name: cat50 + type: category + - column: cat51 + name: cat51 + type: category + - column: cat52 + name: cat52 + type: category + - column: cat53 + name: cat53 + type: category + - column: cat54 + name: cat54 + type: category + - column: cat55 + name: cat55 + type: category + - column: cat56 + name: cat56 + type: category + - column: cat57 + name: cat57 + type: category + - column: cat58 + name: cat58 + type: category + - column: cat59 + name: cat59 + type: category + - column: cat60 + name: cat60 + type: category + - column: cat61 + name: cat61 + type: category + - column: cat62 + name: cat62 + type: category + - column: cat63 + name: cat63 + type: category + - column: cat64 + name: cat64 + type: category + - column: cat65 + name: cat65 + type: category + - column: cat66 + name: cat66 + type: category + - column: cat67 + name: cat67 + type: category + - column: cat68 + name: cat68 + type: category + - column: cat69 + name: cat69 + type: category + - column: cat70 + name: cat70 + type: category + - column: cat71 + name: cat71 + type: category + - column: cat72 + name: cat72 + type: category + - column: cat73 + name: cat73 + type: category + - column: cat74 + name: cat74 + type: category + - column: cat75 + name: cat75 + type: category + - column: cat76 + name: cat76 + type: category + - column: cat77 + name: cat77 + type: category + - column: cat78 + name: cat78 + type: category + - column: cat79 + name: cat79 + type: category + - column: cat80 + name: cat80 + type: category + - column: cat81 + name: cat81 + type: category + - column: cat82 + name: cat82 + type: category + - column: cat83 + name: cat83 + type: category + - column: cat84 + name: cat84 + type: category + - column: cat85 + name: cat85 + type: category + - column: cat86 + name: cat86 + type: category + - column: cat87 + name: cat87 + type: category + - column: cat88 + name: cat88 + type: category + - column: cat89 + name: cat89 + type: category + - column: cat90 + name: cat90 + type: category + - column: cat91 + name: cat91 + type: category + - column: cat92 + name: cat92 + type: category + - column: cat93 + name: cat93 + type: category + - column: cat94 + name: cat94 + type: category + - column: cat95 + name: cat95 + type: category + - column: cat96 + name: cat96 + type: category + - column: cat97 + name: cat97 + type: category + - column: cat98 + name: cat98 + type: category + - column: cat99 + name: cat99 + type: category + - column: cat100 + name: cat100 + type: category + - column: cat101 + name: cat101 + type: category + - column: cat102 + name: cat102 + type: category + - column: cat103 + name: cat103 + type: category + - column: cat104 + name: cat104 + type: category + - column: cat105 + name: cat105 + type: category + - column: cat106 + name: cat106 + type: category + - column: cat107 + name: cat107 + type: category + - column: cat108 + name: cat108 + type: category + - column: cat109 + name: cat109 + type: category + - column: cat110 + name: cat110 + type: category + - column: cat111 + name: cat111 + type: category + - column: cat112 + name: cat112 + type: category + - column: cat113 + name: cat113 + type: category + - column: cat114 + name: cat114 + type: category + - column: cat115 + name: cat115 + type: category + - column: cat116 + name: cat116 + type: category + - column: cont1 + name: cont1 + type: number + - column: cont2 + name: cont2 + type: number + - column: cont3 + name: cont3 + type: number + - column: cont4 + name: cont4 + type: number + - column: cont5 + name: cont5 + type: number + - column: cont6 + name: cont6 + type: number + - column: cont7 + name: cont7 + type: number + - column: cont8 + name: cont8 + type: number + - column: cont9 + name: cont9 + type: number + - column: cont10 + name: cont10 + type: number + - column: cont11 + name: cont11 + type: number + - column: cont12 + name: cont12 + type: number + - column: cont13 + name: cont13 + type: number + - column: cont14 + name: cont14 + type: number + combiner: + type: tabnet + size: 128 # N_a + output_size: 8 # N_d + sparsity: 0.0 # lambda_sparse + bn_momentum: 0.02 # m_B + num_steps: 10 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 4096 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.01 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 10000 + decay_rate: 0.9 + validation_metric: root_mean_squared_error + - name: bnp_claims_management + goal: maximize + metric: accuracy + validation_metric_score: 0.7761691808700562 + training_rows: 80101 + test_rows: 22823 + validation_rows: 11397 + config: + output_features: + - name: target + type: binary + input_features: + - name: v1 + type: number + - name: v2 + type: number + - name: v3 + type: category + - name: v4 + type: number + - name: v5 + type: number + - name: v6 + type: number + - name: v7 + type: number + - name: v8 + type: number + - name: v9 + type: number + - name: v10 + type: number + - name: v11 + type: number + - name: v12 + type: number + - name: v13 + type: number + - name: v14 + type: number + - name: v15 + type: number + - name: v16 + type: number + - name: v17 + type: number + - name: v18 + type: number + - name: v19 + type: number + - name: v20 + type: number + - name: v21 + type: number + - name: v22 + type: category + - name: v23 + type: number + - name: v24 + type: category + - name: v25 + type: number + - name: v26 + type: number + - name: v27 + type: number + - name: v28 + type: number + - name: v29 + type: number + - name: v30 + type: category + - name: v31 + type: category + - name: v32 + type: number + - name: v33 + type: number + - name: v34 + type: number + - name: v35 + type: number + - name: v36 + type: number + - name: v37 + type: number + - name: v38 + type: number + - name: v39 + type: number + - name: v40 + type: number + - name: v41 + type: number + - name: v42 + type: number + - name: v43 + type: number + - name: v44 + type: number + - name: v45 + type: number + - name: v46 + type: number + - name: v47 + type: category + - name: v48 + type: number + - name: v49 + type: number + - name: v50 + type: number + - name: v51 + type: number + - name: v52 + type: category + - name: v53 + type: number + - name: v54 + type: number + - name: v55 + type: number + - name: v56 + type: category + - name: v57 + type: number + - name: v58 + type: number + - name: v59 + type: number + - name: v60 + type: number + - name: v61 + type: number + - name: v62 + type: category + - name: v63 + type: number + - name: v64 + type: number + - name: v65 + type: number + - name: v66 + type: category + - name: v67 + type: number + - name: v68 + type: number + - name: v69 + type: number + - name: v70 + type: number + - name: v71 + type: category + - name: v72 + type: category + - name: v73 + type: number + - name: v74 + type: category + - name: v75 + type: category + - name: v76 + type: number + - name: v77 + type: number + - name: v78 + type: number + - name: v79 + type: category + - name: v80 + type: number + - name: v81 + type: number + - name: v82 + type: number + - name: v83 + type: number + - name: v84 + type: number + - name: v85 + type: number + - name: v86 + type: number + - name: v87 + type: number + - name: v88 + type: number + - name: v89 + type: number + - name: v90 + type: number + - name: v91 + type: category + - name: v92 + type: number + - name: v93 + type: number + - name: v94 + type: number + - name: v95 + type: number + - name: v96 + type: number + - name: v97 + type: number + - name: v98 + type: number + - name: v99 + type: number + - name: v100 + type: number + - name: v101 + type: number + - name: v102 + type: number + - name: v103 + type: number + - name: v104 + type: number + - name: v105 + type: number + - name: v106 + type: number + - name: v107 + type: category + - name: v108 + type: number + - name: v109 + type: number + - name: v110 + type: category + - name: v111 + type: number + - name: v112 + type: category + - name: v113 + type: category + - name: v114 + type: number + - name: v115 + type: number + - name: v116 + type: number + - name: v117 + type: number + - name: v118 + type: number + - name: v119 + type: number + - name: v120 + type: number + - name: v121 + type: number + - name: v122 + type: number + - name: v123 + type: number + - name: v124 + type: number + - name: v125 + type: category + - name: v126 + type: number + - name: v127 + type: number + - name: v128 + type: number + - name: v129 + type: number + - name: v130 + type: number + - name: v131 + type: number + combiner: + type: tabnet + size: 32 # N_a + output_size: 8 # N_d + sparsity: 0.0 # lambda_sparse + bn_momentum: 0.02 # m_B + num_steps: 3 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 256 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.01 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 2000 + decay_rate: 0.4 + validation_metric: accuracy + - name: ieee_fraud + goal: maximize + metric: accuracy + validation_metric_score: 0.9836957454681396 + training_rows: 413498 + test_rows: 118039 + validation_rows: 59003 + config: + output_features: + - name: isFraud + type: binary + input_features: + - name: TransactionDT + type: number + - name: TransactionAmt + type: number + - name: ProductCD + type: category + - name: card1 + type: number + - name: card2 + type: number + - name: card3 + type: number + - name: card4 + type: category + - name: card5 + type: number + - name: card6 + type: category + - name: addr1 + type: number + - name: addr2 + type: number + - name: dist1 + type: number + - name: dist2 + type: number + - name: P_emaildomain + type: category + - name: R_emaildomain + type: category + - name: C1 + type: number + - name: C2 + type: number + - name: C3 + type: number + - name: C4 + type: number + - name: C5 + type: number + - name: C6 + type: number + - name: C7 + type: number + - name: C8 + type: number + - name: C9 + type: number + - name: C10 + type: number + - name: C11 + type: number + - name: C12 + type: number + - name: C13 + type: number + - name: C14 + type: number + - name: D1 + type: number + - name: D2 + type: number + - name: D3 + type: number + - name: D4 + type: number + - name: D5 + type: number + - name: D6 + type: number + - name: D7 + type: number + - name: D8 + type: number + - name: D9 + type: number + - name: D10 + type: number + - name: D11 + type: number + - name: D12 + type: number + - name: D13 + type: number + - name: D14 + type: number + - name: D15 + type: number + - name: M1 + type: category + - name: M2 + type: category + - name: M3 + type: category + - name: M4 + type: category + - name: M5 + type: category + - name: M6 + type: category + - name: M7 + type: category + - name: M8 + type: category + - name: M9 + type: category + - name: V1 + type: number + - name: V2 + type: number + - name: V3 + type: number + - name: V4 + type: number + - name: V5 + type: number + - name: V6 + type: number + - name: V7 + type: number + - name: V8 + type: number + - name: V9 + type: number + - name: V10 + type: number + - name: V11 + type: number + - name: V12 + type: number + - name: V13 + type: number + - name: V14 + type: number + - name: V15 + type: number + - name: V16 + type: number + - name: V17 + type: number + - name: V18 + type: number + - name: V19 + type: number + - name: V20 + type: number + - name: V21 + type: number + - name: V22 + type: number + - name: V23 + type: number + - name: V24 + type: number + - name: V25 + type: number + - name: V26 + type: number + - name: V27 + type: number + - name: V28 + type: number + - name: V29 + type: number + - name: V30 + type: number + - name: V31 + type: number + - name: V32 + type: number + - name: V33 + type: number + - name: V34 + type: number + - name: V35 + type: number + - name: V36 + type: number + - name: V37 + type: number + - name: V38 + type: number + - name: V39 + type: number + - name: V40 + type: number + - name: V41 + type: number + - name: V42 + type: number + - name: V43 + type: number + - name: V44 + type: number + - name: V45 + type: number + - name: V46 + type: number + - name: V47 + type: number + - name: V48 + type: number + - name: V49 + type: number + - name: V50 + type: number + - name: V51 + type: number + - name: V52 + type: number + - name: V53 + type: number + - name: V54 + type: number + - name: V55 + type: number + - name: V56 + type: number + - name: V57 + type: number + - name: V58 + type: number + - name: V59 + type: number + - name: V60 + type: number + - name: V61 + type: number + - name: V62 + type: number + - name: V63 + type: number + - name: V64 + type: number + - name: V65 + type: number + - name: V66 + type: number + - name: V67 + type: number + - name: V68 + type: number + - name: V69 + type: number + - name: V70 + type: number + - name: V71 + type: number + - name: V72 + type: number + - name: V73 + type: number + - name: V74 + type: number + - name: V75 + type: number + - name: V76 + type: number + - name: V77 + type: number + - name: V78 + type: number + - name: V79 + type: number + - name: V80 + type: number + - name: V81 + type: number + - name: V82 + type: number + - name: V83 + type: number + - name: V84 + type: number + - name: V85 + type: number + - name: V86 + type: number + - name: V87 + type: number + - name: V88 + type: number + - name: V89 + type: number + - name: V90 + type: number + - name: V91 + type: number + - name: V92 + type: number + - name: V93 + type: number + - name: V94 + type: number + - name: V95 + type: number + - name: V96 + type: number + - name: V97 + type: number + - name: V98 + type: number + - name: V99 + type: number + - name: V100 + type: number + - name: V101 + type: number + - name: V102 + type: number + - name: V103 + type: number + - name: V104 + type: number + - name: V105 + type: number + - name: V106 + type: number + - name: V107 + type: number + - name: V108 + type: number + - name: V109 + type: number + - name: V110 + type: number + - name: V111 + type: number + - name: V112 + type: number + - name: V113 + type: number + - name: V114 + type: number + - name: V115 + type: number + - name: V116 + type: number + - name: V117 + type: number + - name: V118 + type: number + - name: V119 + type: number + - name: V120 + type: number + - name: V121 + type: number + - name: V122 + type: number + - name: V123 + type: number + - name: V124 + type: number + - name: V125 + type: number + - name: V126 + type: number + - name: V127 + type: number + - name: V128 + type: number + - name: V129 + type: number + - name: V130 + type: number + - name: V131 + type: number + - name: V132 + type: number + - name: V133 + type: number + - name: V134 + type: number + - name: V135 + type: number + - name: V136 + type: number + - name: V137 + type: number + - name: V138 + type: number + - name: V139 + type: number + - name: V140 + type: number + - name: V141 + type: number + - name: V142 + type: number + - name: V143 + type: number + - name: V144 + type: number + - name: V145 + type: number + - name: V146 + type: number + - name: V147 + type: number + - name: V148 + type: number + - name: V149 + type: number + - name: V150 + type: number + - name: V151 + type: number + - name: V152 + type: number + - name: V153 + type: number + - name: V154 + type: number + - name: V155 + type: number + - name: V156 + type: number + - name: V157 + type: number + - name: V158 + type: number + - name: V159 + type: number + - name: V160 + type: number + - name: V161 + type: number + - name: V162 + type: number + - name: V163 + type: number + - name: V164 + type: number + - name: V165 + type: number + - name: V166 + type: number + - name: V167 + type: number + - name: V168 + type: number + - name: V169 + type: number + - name: V170 + type: number + - name: V171 + type: number + - name: V172 + type: number + - name: V173 + type: number + - name: V174 + type: number + - name: V175 + type: number + - name: V176 + type: number + - name: V177 + type: number + - name: V178 + type: number + - name: V179 + type: number + - name: V180 + type: number + - name: V181 + type: number + - name: V182 + type: number + - name: V183 + type: number + - name: V184 + type: number + - name: V185 + type: number + - name: V186 + type: number + - name: V187 + type: number + - name: V188 + type: number + - name: V189 + type: number + - name: V190 + type: number + - name: V191 + type: number + - name: V192 + type: number + - name: V193 + type: number + - name: V194 + type: number + - name: V195 + type: number + - name: V196 + type: number + - name: V197 + type: number + - name: V198 + type: number + - name: V199 + type: number + - name: V200 + type: number + - name: V201 + type: number + - name: V202 + type: number + - name: V203 + type: number + - name: V204 + type: number + - name: V205 + type: number + - name: V206 + type: number + - name: V207 + type: number + - name: V208 + type: number + - name: V209 + type: number + - name: V210 + type: number + - name: V211 + type: number + - name: V212 + type: number + - name: V213 + type: number + - name: V214 + type: number + - name: V215 + type: number + - name: V216 + type: number + - name: V217 + type: number + - name: V218 + type: number + - name: V219 + type: number + - name: V220 + type: number + - name: V221 + type: number + - name: V222 + type: number + - name: V223 + type: number + - name: V224 + type: number + - name: V225 + type: number + - name: V226 + type: number + - name: V227 + type: number + - name: V228 + type: number + - name: V229 + type: number + - name: V230 + type: number + - name: V231 + type: number + - name: V232 + type: number + - name: V233 + type: number + - name: V234 + type: number + - name: V235 + type: number + - name: V236 + type: number + - name: V237 + type: number + - name: V238 + type: number + - name: V239 + type: number + - name: V240 + type: number + - name: V241 + type: number + - name: V242 + type: number + - name: V243 + type: number + - name: V244 + type: number + - name: V245 + type: number + - name: V246 + type: number + - name: V247 + type: number + - name: V248 + type: number + - name: V249 + type: number + - name: V250 + type: number + - name: V251 + type: number + - name: V252 + type: number + - name: V253 + type: number + - name: V254 + type: number + - name: V255 + type: number + - name: V256 + type: number + - name: V257 + type: number + - name: V258 + type: number + - name: V259 + type: number + - name: V260 + type: number + - name: V261 + type: number + - name: V262 + type: number + - name: V263 + type: number + - name: V264 + type: number + - name: V265 + type: number + - name: V266 + type: number + - name: V267 + type: number + - name: V268 + type: number + - name: V269 + type: number + - name: V270 + type: number + - name: V271 + type: number + - name: V272 + type: number + - name: V273 + type: number + - name: V274 + type: number + - name: V275 + type: number + - name: V276 + type: number + - name: V277 + type: number + - name: V278 + type: number + - name: V279 + type: number + - name: V280 + type: number + - name: V281 + type: number + - name: V282 + type: number + - name: V283 + type: number + - name: V284 + type: number + - name: V285 + type: number + - name: V286 + type: number + - name: V287 + type: number + - name: V288 + type: number + - name: V289 + type: number + - name: V290 + type: number + - name: V291 + type: number + - name: V292 + type: number + - name: V293 + type: number + - name: V294 + type: number + - name: V295 + type: number + - name: V296 + type: number + - name: V297 + type: number + - name: V298 + type: number + - name: V299 + type: number + - name: V300 + type: number + - name: V301 + type: number + - name: V302 + type: number + - name: V303 + type: number + - name: V304 + type: number + - name: V305 + type: number + - name: V306 + type: number + - name: V307 + type: number + - name: V308 + type: number + - name: V309 + type: number + - name: V310 + type: number + - name: V311 + type: number + - name: V312 + type: number + - name: V313 + type: number + - name: V314 + type: number + - name: V315 + type: number + - name: V316 + type: number + - name: V317 + type: number + - name: V318 + type: number + - name: V319 + type: number + - name: V320 + type: number + - name: V321 + type: number + - name: V322 + type: number + - name: V323 + type: number + - name: V324 + type: number + - name: V325 + type: number + - name: V326 + type: number + - name: V327 + type: number + - name: V328 + type: number + - name: V329 + type: number + - name: V330 + type: number + - name: V331 + type: number + - name: V332 + type: number + - name: V333 + type: number + - name: V334 + type: number + - name: V335 + type: number + - name: V336 + type: number + - name: V337 + type: number + - name: V338 + type: number + - name: V339 + type: number + - name: id_01 + type: number + - name: id_02 + type: number + - name: id_03 + type: number + - name: id_04 + type: number + - name: id_05 + type: number + - name: id_06 + type: number + - name: id_07 + type: number + - name: id_08 + type: number + - name: id_09 + type: number + - name: id_10 + type: number + - name: id_11 + type: number + - name: id_12 + type: category + - name: id_13 + type: number + - name: id_14 + type: number + - name: id_15 + type: category + - name: id_16 + type: category + - name: id_17 + type: number + - name: id_18 + type: number + - name: id_19 + type: number + - name: id_20 + type: number + - name: id_21 + type: number + - name: id_22 + type: number + - name: id_23 + type: category + - name: id_24 + type: number + - name: id_25 + type: number + - name: id_26 + type: number + - name: id_27 + type: category + - name: id_28 + type: category + - name: id_29 + type: category + - name: id_30 + type: category + - name: id_31 + type: text + - name: id_32 + type: number + - name: id_33 + type: category + - name: id_34 + type: category + - name: id_35 + type: category + - name: id_36 + type: category + - name: id_37 + type: category + - name: id_38 + type: category + - name: DeviceType + type: category + - name: DeviceInfo + type: category + combiner: + type: tabnet + size: 128 # N_a + output_size: 24 # N_d + sparsity: 0.000001 # lambda_sparse + bn_momentum: 0.02 # m_B + num_steps: 10 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 2048 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.01 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 10000 + decay_rate: 0.95 + validation_metric: accuracy + - name: mercedes_benz_greener + goal: minimize + metric: root_mean_squared_error + validation_metric_score: 7.685836315155029 + training_rows: 2969 + test_rows: 840 + validation_rows: 400 + config: + output_features: + - name: y + type: number + input_features: + - name: X0 + type: category + - name: X1 + type: category + - name: X2 + type: category + - name: X3 + type: category + - name: X4 + type: category + - name: X5 + type: category + - name: X6 + type: category + - name: X8 + type: category + - name: X10 + type: binary + - name: X11 + type: binary + - name: X12 + type: binary + - name: X13 + type: binary + - name: X14 + type: binary + - name: X15 + type: binary + - name: X16 + type: binary + - name: X17 + type: binary + - name: X18 + type: binary + - name: X19 + type: binary + - name: X20 + type: binary + - name: X21 + type: binary + - name: X22 + type: binary + - name: X23 + type: binary + - name: X24 + type: binary + - name: X26 + type: binary + - name: X27 + type: binary + - name: X28 + type: binary + - name: X29 + type: binary + - name: X30 + type: binary + - name: X31 + type: binary + - name: X32 + type: binary + - name: X33 + type: binary + - name: X34 + type: binary + - name: X35 + type: binary + - name: X36 + type: binary + - name: X37 + type: binary + - name: X38 + type: binary + - name: X39 + type: binary + - name: X40 + type: binary + - name: X41 + type: binary + - name: X42 + type: binary + - name: X43 + type: binary + - name: X44 + type: binary + - name: X45 + type: binary + - name: X46 + type: binary + - name: X47 + type: binary + - name: X48 + type: binary + - name: X49 + type: binary + - name: X50 + type: binary + - name: X51 + type: binary + - name: X52 + type: binary + - name: X53 + type: binary + - name: X54 + type: binary + - name: X55 + type: binary + - name: X56 + type: binary + - name: X57 + type: binary + - name: X58 + type: binary + - name: X59 + type: binary + - name: X60 + type: binary + - name: X61 + type: binary + - name: X62 + type: binary + - name: X63 + type: binary + - name: X64 + type: binary + - name: X65 + type: binary + - name: X66 + type: binary + - name: X67 + type: binary + - name: X68 + type: binary + - name: X69 + type: binary + - name: X70 + type: binary + - name: X71 + type: binary + - name: X73 + type: binary + - name: X74 + type: binary + - name: X75 + type: binary + - name: X76 + type: binary + - name: X77 + type: binary + - name: X78 + type: binary + - name: X79 + type: binary + - name: X80 + type: binary + - name: X81 + type: binary + - name: X82 + type: binary + - name: X83 + type: binary + - name: X84 + type: binary + - name: X85 + type: binary + - name: X86 + type: binary + - name: X87 + type: binary + - name: X88 + type: binary + - name: X89 + type: binary + - name: X90 + type: binary + - name: X91 + type: binary + - name: X92 + type: binary + - name: X93 + type: binary + - name: X94 + type: binary + - name: X95 + type: binary + - name: X96 + type: binary + - name: X97 + type: binary + - name: X98 + type: binary + - name: X99 + type: binary + - name: X100 + type: binary + - name: X101 + type: binary + - name: X102 + type: binary + - name: X103 + type: binary + - name: X104 + type: binary + - name: X105 + type: binary + - name: X106 + type: binary + - name: X107 + type: binary + - name: X108 + type: binary + - name: X109 + type: binary + - name: X110 + type: binary + - name: X111 + type: binary + - name: X112 + type: binary + - name: X113 + type: binary + - name: X114 + type: binary + - name: X115 + type: binary + - name: X116 + type: binary + - name: X117 + type: binary + - name: X118 + type: binary + - name: X119 + type: binary + - name: X120 + type: binary + - name: X122 + type: binary + - name: X123 + type: binary + - name: X124 + type: binary + - name: X125 + type: binary + - name: X126 + type: binary + - name: X127 + type: binary + - name: X128 + type: binary + - name: X129 + type: binary + - name: X130 + type: binary + - name: X131 + type: binary + - name: X132 + type: binary + - name: X133 + type: binary + - name: X134 + type: binary + - name: X135 + type: binary + - name: X136 + type: binary + - name: X137 + type: binary + - name: X138 + type: binary + - name: X139 + type: binary + - name: X140 + type: binary + - name: X141 + type: binary + - name: X142 + type: binary + - name: X143 + type: binary + - name: X144 + type: binary + - name: X145 + type: binary + - name: X146 + type: binary + - name: X147 + type: binary + - name: X148 + type: binary + - name: X150 + type: binary + - name: X151 + type: binary + - name: X152 + type: binary + - name: X153 + type: binary + - name: X154 + type: binary + - name: X155 + type: binary + - name: X156 + type: binary + - name: X157 + type: binary + - name: X158 + type: binary + - name: X159 + type: binary + - name: X160 + type: binary + - name: X161 + type: binary + - name: X162 + type: binary + - name: X163 + type: binary + - name: X164 + type: binary + - name: X165 + type: binary + - name: X166 + type: binary + - name: X167 + type: binary + - name: X168 + type: binary + - name: X169 + type: binary + - name: X170 + type: binary + - name: X171 + type: binary + - name: X172 + type: binary + - name: X173 + type: binary + - name: X174 + type: binary + - name: X175 + type: binary + - name: X176 + type: binary + - name: X177 + type: binary + - name: X178 + type: binary + - name: X179 + type: binary + - name: X180 + type: binary + - name: X181 + type: binary + - name: X182 + type: binary + - name: X183 + type: binary + - name: X184 + type: binary + - name: X185 + type: binary + - name: X186 + type: binary + - name: X187 + type: binary + - name: X189 + type: binary + - name: X190 + type: binary + - name: X191 + type: binary + - name: X192 + type: binary + - name: X194 + type: binary + - name: X195 + type: binary + - name: X196 + type: binary + - name: X197 + type: binary + - name: X198 + type: binary + - name: X199 + type: binary + - name: X200 + type: binary + - name: X201 + type: binary + - name: X202 + type: binary + - name: X203 + type: binary + - name: X204 + type: binary + - name: X205 + type: binary + - name: X206 + type: binary + - name: X207 + type: binary + - name: X208 + type: binary + - name: X209 + type: binary + - name: X210 + type: binary + - name: X211 + type: binary + - name: X212 + type: binary + - name: X213 + type: binary + - name: X214 + type: binary + - name: X215 + type: binary + - name: X216 + type: binary + - name: X217 + type: binary + - name: X218 + type: binary + - name: X219 + type: binary + - name: X220 + type: binary + - name: X221 + type: binary + - name: X222 + type: binary + - name: X223 + type: binary + - name: X224 + type: binary + - name: X225 + type: binary + - name: X226 + type: binary + - name: X227 + type: binary + - name: X228 + type: binary + - name: X229 + type: binary + - name: X230 + type: binary + - name: X231 + type: binary + - name: X232 + type: binary + - name: X233 + type: binary + - name: X234 + type: binary + - name: X235 + type: binary + - name: X236 + type: binary + - name: X237 + type: binary + - name: X238 + type: binary + - name: X239 + type: binary + - name: X240 + type: binary + - name: X241 + type: binary + - name: X242 + type: binary + - name: X243 + type: binary + - name: X244 + type: binary + - name: X245 + type: binary + - name: X246 + type: binary + - name: X247 + type: binary + - name: X248 + type: binary + - name: X249 + type: binary + - name: X250 + type: binary + - name: X251 + type: binary + - name: X252 + type: binary + - name: X253 + type: binary + - name: X254 + type: binary + - name: X255 + type: binary + - name: X256 + type: binary + - name: X257 + type: binary + - name: X258 + type: binary + - name: X259 + type: binary + - name: X260 + type: binary + - name: X261 + type: binary + - name: X262 + type: binary + - name: X263 + type: binary + - name: X264 + type: binary + - name: X265 + type: binary + - name: X266 + type: binary + - name: X267 + type: binary + - name: X268 + type: binary + - name: X269 + type: binary + - name: X270 + type: binary + - name: X271 + type: binary + - name: X272 + type: binary + - name: X273 + type: binary + - name: X274 + type: binary + - name: X275 + type: binary + - name: X276 + type: binary + - name: X277 + type: binary + - name: X278 + type: binary + - name: X279 + type: binary + - name: X280 + type: binary + - name: X281 + type: binary + - name: X282 + type: binary + - name: X283 + type: binary + - name: X284 + type: binary + - name: X285 + type: binary + - name: X286 + type: binary + - name: X287 + type: binary + - name: X288 + type: binary + - name: X289 + type: binary + - name: X290 + type: binary + - name: X291 + type: binary + - name: X292 + type: binary + - name: X293 + type: binary + - name: X294 + type: binary + - name: X295 + type: binary + - name: X296 + type: binary + - name: X297 + type: binary + - name: X298 + type: binary + - name: X299 + type: binary + - name: X300 + type: binary + - name: X301 + type: binary + - name: X302 + type: binary + - name: X304 + type: binary + - name: X305 + type: binary + - name: X306 + type: binary + - name: X307 + type: binary + - name: X308 + type: binary + - name: X309 + type: binary + - name: X310 + type: binary + - name: X311 + type: binary + - name: X312 + type: binary + - name: X313 + type: binary + - name: X314 + type: binary + - name: X315 + type: binary + - name: X316 + type: binary + - name: X317 + type: binary + - name: X318 + type: binary + - name: X319 + type: binary + - name: X320 + type: binary + - name: X321 + type: binary + - name: X322 + type: binary + - name: X323 + type: binary + - name: X324 + type: binary + - name: X325 + type: binary + - name: X326 + type: binary + - name: X327 + type: binary + - name: X328 + type: binary + - name: X329 + type: binary + - name: X330 + type: binary + - name: X331 + type: binary + - name: X332 + type: binary + - name: X333 + type: binary + - name: X334 + type: binary + - name: X335 + type: binary + - name: X336 + type: binary + - name: X337 + type: binary + - name: X338 + type: binary + - name: X339 + type: binary + - name: X340 + type: binary + - name: X341 + type: binary + - name: X342 + type: binary + - name: X343 + type: binary + - name: X344 + type: binary + - name: X345 + type: binary + - name: X346 + type: binary + - name: X347 + type: binary + - name: X348 + type: binary + - name: X349 + type: binary + - name: X350 + type: binary + - name: X351 + type: binary + - name: X352 + type: binary + - name: X353 + type: binary + - name: X354 + type: binary + - name: X355 + type: binary + - name: X356 + type: binary + - name: X357 + type: binary + - name: X358 + type: binary + - name: X359 + type: binary + - name: X360 + type: binary + - name: X361 + type: binary + - name: X362 + type: binary + - name: X363 + type: binary + - name: X364 + type: binary + - name: X365 + type: binary + - name: X366 + type: binary + - name: X367 + type: binary + - name: X368 + type: binary + - name: X369 + type: binary + - name: X370 + type: binary + - name: X371 + type: binary + - name: X372 + type: binary + - name: X373 + type: binary + - name: X374 + type: binary + - name: X375 + type: binary + - name: X376 + type: binary + - name: X377 + type: binary + - name: X378 + type: binary + - name: X379 + type: binary + - name: X380 + type: binary + - name: X382 + type: binary + - name: X383 + type: binary + - name: X384 + type: binary + - name: X385 + type: binary + combiner: + type: tabnet + size: 128 # N_a + output_size: 8 # N_d + sparsity: 0.1 # lambda_sparse + bn_momentum: 0.1 # m_B + num_steps: 9 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 256 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.005 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 500 + decay_rate: 0.95 + validation_metric: root_mean_squared_error + - name: otto_group_product + goal: maximize + metric: accuracy + validation_metric_score: 0.7956883907318115 + training_rows: 43459 + test_rows: 12296 + validation_rows: 6123 + config: + output_features: + - name: target + type: category + input_features: + - name: feat_1 + type: number + - name: feat_2 + type: number + - name: feat_3 + type: number + - name: feat_4 + type: number + - name: feat_5 + type: number + - name: feat_6 + type: number + - name: feat_7 + type: number + - name: feat_8 + type: number + - name: feat_9 + type: number + - name: feat_10 + type: number + - name: feat_11 + type: number + - name: feat_12 + type: number + - name: feat_13 + type: number + - name: feat_14 + type: number + - name: feat_15 + type: number + - name: feat_16 + type: number + - name: feat_17 + type: number + - name: feat_18 + type: number + - name: feat_19 + type: number + - name: feat_20 + type: number + - name: feat_21 + type: category + - name: feat_22 + type: number + - name: feat_23 + type: number + - name: feat_24 + type: number + - name: feat_25 + type: number + - name: feat_26 + type: number + - name: feat_27 + type: number + - name: feat_28 + type: number + - name: feat_29 + type: number + - name: feat_30 + type: number + - name: feat_31 + type: number + - name: feat_32 + type: number + - name: feat_33 + type: number + - name: feat_34 + type: number + - name: feat_35 + type: number + - name: feat_36 + type: number + - name: feat_37 + type: number + - name: feat_38 + type: number + - name: feat_39 + type: number + - name: feat_40 + type: number + - name: feat_41 + type: number + - name: feat_42 + type: number + - name: feat_43 + type: number + - name: feat_44 + type: number + - name: feat_45 + type: number + - name: feat_46 + type: number + - name: feat_47 + type: number + - name: feat_48 + type: number + - name: feat_49 + type: number + - name: feat_50 + type: number + - name: feat_51 + type: number + - name: feat_52 + type: number + - name: feat_53 + type: number + - name: feat_54 + type: number + - name: feat_55 + type: number + - name: feat_56 + type: number + - name: feat_57 + type: number + - name: feat_58 + type: number + - name: feat_59 + type: number + - name: feat_60 + type: number + - name: feat_61 + type: number + - name: feat_62 + type: number + - name: feat_63 + type: number + - name: feat_64 + type: number + - name: feat_65 + type: number + - name: feat_66 + type: number + - name: feat_67 + type: number + - name: feat_68 + type: number + - name: feat_69 + type: number + - name: feat_70 + type: number + - name: feat_71 + type: number + - name: feat_72 + type: number + - name: feat_73 + type: number + - name: feat_74 + type: number + - name: feat_75 + type: number + - name: feat_76 + type: number + - name: feat_77 + type: number + - name: feat_78 + type: number + - name: feat_79 + type: number + - name: feat_80 + type: number + - name: feat_81 + type: number + - name: feat_82 + type: number + - name: feat_83 + type: number + - name: feat_84 + type: number + - name: feat_85 + type: number + - name: feat_86 + type: number + - name: feat_87 + type: number + - name: feat_88 + type: number + - name: feat_89 + type: number + - name: feat_90 + type: number + - name: feat_91 + type: number + - name: feat_92 + type: number + - name: feat_93 + type: number + combiner: + type: tabnet + size: 128 # N_a + output_size: 128 # N_d + sparsity: 0.0 # lambda_sparse + bn_momentum: 0.2 # m_B + num_steps: 3 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 512 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.005 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 20000 + decay_rate: 0.4 + validation_metric: accuracy + - name: poker_hand + goal: maximize + metric: accuracy + validation_metric_score: 0.9804078340530396 + training_rows: 22509 + test_rows: 0 + validation_rows: 2501 + config: + output_features: + - name: hand + type: category + input_features: + - name: S1 + type: number + - name: C1 + type: number + - name: S2 + type: number + - name: C2 + type: number + - name: S3 + type: number + - name: C3 + type: number + - name: S4 + type: number + - name: C4 + type: number + - name: S5 + type: number + - name: C5 + type: number + combiner: + type: tabnet + size: 16 # N_a + output_size: 128 # N_d + sparsity: 0.0 # lambda_sparse + bn_momentum: 0.02 # m_B + num_steps: 6 # N_steps + relaxation_factor: 1.0 # gamma + bn_virtual_bs: 512 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.01 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 8000 + decay_rate: 0.8 + validation_metric: accuracy + - name: porto_seguro_safe_driver + goal: maximize + metric: accuracy + validation_metric_score: 0.9630663394927979 + training_rows: 416779 + test_rows: 118948 + validation_rows: 59485 + config: + output_features: + - name: target + type: binary + input_features: + - name: ps_ind_01 + type: category + - name: ps_ind_02_cat + type: number + - name: ps_ind_03 + type: category + - name: ps_ind_04_cat + type: category + - name: ps_ind_05_cat + type: category + - name: ps_ind_06_bin + type: binary + - name: ps_ind_07_bin + type: binary + - name: ps_ind_08_bin + type: binary + - name: ps_ind_09_bin + type: binary + - name: ps_ind_10_bin + type: binary + - name: ps_ind_11_bin + type: binary + - name: ps_ind_12_bin + type: binary + - name: ps_ind_13_bin + type: binary + - name: ps_ind_14 + type: category + - name: ps_ind_15 + type: category + - name: ps_ind_16_bin + type: binary + - name: ps_ind_17_bin + type: binary + - name: ps_ind_18_bin + type: binary + - name: ps_reg_01 + type: number + - name: ps_reg_02 + type: number + - name: ps_reg_03 + type: number + - name: ps_car_01_cat + type: category + - name: ps_car_02_cat + type: category + - name: ps_car_03_cat + type: category + - name: ps_car_04_cat + type: category + - name: ps_car_05_cat + type: category + - name: ps_car_06_cat + type: category + - name: ps_car_07_cat + type: category + - name: ps_car_08_cat + type: binary + - name: ps_car_09_cat + type: category + - name: ps_car_10_cat + type: category + - name: ps_car_11_cat + type: number + - name: ps_car_11 + type: category + - name: ps_car_12 + type: number + - name: ps_car_13 + type: number + - name: ps_car_14 + type: number + - name: ps_car_15 + type: number + - name: ps_calc_01 + type: number + - name: ps_calc_02 + type: number + - name: ps_calc_03 + type: number + - name: ps_calc_04 + type: category + - name: ps_calc_05 + type: category + - name: ps_calc_06 + type: category + - name: ps_calc_07 + type: category + - name: ps_calc_08 + type: category + - name: ps_calc_09 + type: category + - name: ps_calc_10 + type: number + - name: ps_calc_11 + type: number + - name: ps_calc_12 + type: category + - name: ps_calc_13 + type: category + - name: ps_calc_14 + type: number + - name: ps_calc_15_bin + type: binary + - name: ps_calc_16_bin + type: binary + - name: ps_calc_17_bin + type: binary + - name: ps_calc_18_bin + type: binary + - name: ps_calc_19_bin + type: binary + - name: ps_calc_20_bin + type: binary + combiner: + type: tabnet + size: 32 # N_a + output_size: 32 # N_d + sparsity: 0.0001 # lambda_sparse + bn_momentum: 0.4 # m_B + num_steps: 5 # N_steps + relaxation_factor: 1.2 # gamma + bn_virtual_bs: 1024 # B_v + trainer: + batch_size: 1024 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.005 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 10000 + decay_rate: 0.9 + validation_metric: accuracy + - name: santander_customer_satisfaction + goal: maximize + metric: accuracy + validation_metric_score: 0.9611535668373108 + training_rows: 53298 + test_rows: 15128 + validation_rows: 7594 + config: + output_features: + - name: TARGET + type: binary + input_features: + - name: var3 + type: number + - name: var15 + type: number + - name: imp_ent_var16_ult1 + type: number + - name: imp_op_var39_comer_ult1 + type: number + - name: imp_op_var39_comer_ult3 + type: number + - name: imp_op_var40_comer_ult1 + type: number + - name: imp_op_var40_comer_ult3 + type: number + - name: imp_op_var40_efect_ult1 + type: number + - name: imp_op_var40_efect_ult3 + type: number + - name: imp_op_var40_ult1 + type: number + - name: imp_op_var41_comer_ult1 + type: number + - name: imp_op_var41_comer_ult3 + type: number + - name: imp_op_var41_efect_ult1 + type: number + - name: imp_op_var41_efect_ult3 + type: number + - name: imp_op_var41_ult1 + type: number + - name: imp_op_var39_efect_ult1 + type: number + - name: imp_op_var39_efect_ult3 + type: number + - name: imp_op_var39_ult1 + type: number + - name: imp_sal_var16_ult1 + type: number + - name: ind_var1_0 + type: binary + - name: ind_var1 + type: binary + - name: ind_var2_0 + type: binary + - name: ind_var2 + type: binary + - name: ind_var5_0 + type: binary + - name: ind_var5 + type: binary + - name: ind_var6_0 + type: binary + - name: ind_var6 + type: binary + - name: ind_var8_0 + type: binary + - name: ind_var8 + type: binary + - name: ind_var12_0 + type: binary + - name: ind_var12 + type: binary + - name: ind_var13_0 + type: binary + - name: ind_var13_corto_0 + type: binary + - name: ind_var13_corto + type: binary + - name: ind_var13_largo_0 + type: binary + - name: ind_var13_largo + type: binary + - name: ind_var13_medio_0 + type: binary + - name: ind_var13_medio + type: binary + - name: ind_var13 + type: binary + - name: ind_var14_0 + type: binary + - name: ind_var14 + type: binary + - name: ind_var17_0 + type: binary + - name: ind_var17 + type: binary + - name: ind_var18_0 + type: binary + - name: ind_var18 + type: binary + - name: ind_var19 + type: binary + - name: ind_var20_0 + type: binary + - name: ind_var20 + type: binary + - name: ind_var24_0 + type: binary + - name: ind_var24 + type: binary + - name: ind_var25_cte + type: binary + - name: ind_var26_0 + type: binary + - name: ind_var26_cte + type: binary + - name: ind_var26 + type: binary + - name: ind_var25_0 + type: binary + - name: ind_var25 + type: binary + - name: ind_var27_0 + type: binary + - name: ind_var28_0 + type: binary + - name: ind_var28 + type: binary + - name: ind_var27 + type: binary + - name: ind_var29_0 + type: binary + - name: ind_var29 + type: binary + - name: ind_var30_0 + type: binary + - name: ind_var30 + type: binary + - name: ind_var31_0 + type: binary + - name: ind_var31 + type: binary + - name: ind_var32_cte + type: binary + - name: ind_var32_0 + type: binary + - name: ind_var32 + type: binary + - name: ind_var33_0 + type: binary + - name: ind_var33 + type: binary + - name: ind_var34_0 + type: binary + - name: ind_var34 + type: binary + - name: ind_var37_cte + type: binary + - name: ind_var37_0 + type: binary + - name: ind_var37 + type: binary + - name: ind_var39_0 + type: binary + - name: ind_var40_0 + type: binary + - name: ind_var40 + type: binary + - name: ind_var41_0 + type: binary + - name: ind_var41 + type: binary + - name: ind_var39 + type: binary + - name: ind_var44_0 + type: binary + - name: ind_var44 + type: binary + - name: ind_var46_0 + type: binary + - name: ind_var46 + type: binary + - name: num_var1_0 + type: number + - name: num_var1 + type: number + - name: num_var4 + type: category + - name: num_var5_0 + type: number + - name: num_var5 + type: number + - name: num_var6_0 + type: number + - name: num_var6 + type: number + - name: num_var8_0 + type: number + - name: num_var8 + type: number + - name: num_var12_0 + type: number + - name: num_var12 + type: number + - name: num_var13_0 + type: number + - name: num_var13_corto_0 + type: number + - name: num_var13_corto + type: number + - name: num_var13_largo_0 + type: number + - name: num_var13_largo + type: number + - name: num_var13_medio_0 + type: number + - name: num_var13_medio + type: number + - name: num_var13 + type: number + - name: num_var14_0 + type: number + - name: num_var14 + type: number + - name: num_var17_0 + type: number + - name: num_var17 + type: number + - name: num_var18_0 + type: number + - name: num_var18 + type: number + - name: num_var20_0 + type: number + - name: num_var20 + type: number + - name: num_var24_0 + type: number + - name: num_var24 + type: number + - name: num_var26_0 + type: number + - name: num_var26 + type: number + - name: num_var25_0 + type: number + - name: num_var25 + type: number + - name: num_op_var40_hace2 + type: number + - name: num_op_var40_hace3 + type: number + - name: num_op_var40_ult1 + type: number + - name: num_op_var40_ult3 + type: number + - name: num_op_var41_hace2 + type: number + - name: num_op_var41_hace3 + type: number + - name: num_op_var41_ult1 + type: number + - name: num_op_var41_ult3 + type: number + - name: num_op_var39_hace2 + type: number + - name: num_op_var39_hace3 + type: number + - name: num_op_var39_ult1 + type: number + - name: num_op_var39_ult3 + type: number + - name: num_var27_0 + type: binary + - name: num_var28_0 + type: binary + - name: num_var28 + type: binary + - name: num_var27 + type: binary + - name: num_var29_0 + type: number + - name: num_var29 + type: number + - name: num_var30_0 + type: number + - name: num_var30 + type: number + - name: num_var31_0 + type: number + - name: num_var31 + type: number + - name: num_var32_0 + type: number + - name: num_var32 + type: number + - name: num_var33_0 + type: number + - name: num_var33 + type: number + - name: num_var34_0 + type: number + - name: num_var34 + type: number + - name: num_var35 + type: number + - name: num_var37_med_ult2 + type: number + - name: num_var37_0 + type: number + - name: num_var37 + type: number + - name: num_var39_0 + type: number + - name: num_var40_0 + type: number + - name: num_var40 + type: number + - name: num_var41_0 + type: number + - name: num_var41 + type: binary + - name: num_var39 + type: number + - name: num_var42_0 + type: number + - name: num_var42 + type: number + - name: num_var44_0 + type: number + - name: num_var44 + type: number + - name: num_var46_0 + type: binary + - name: num_var46 + type: binary + - name: saldo_var1 + type: number + - name: saldo_var5 + type: number + - name: saldo_var6 + type: number + - name: saldo_var8 + type: number + - name: saldo_var12 + type: number + - name: saldo_var13_corto + type: number + - name: saldo_var13_largo + type: number + - name: saldo_var13_medio + type: number + - name: saldo_var13 + type: number + - name: saldo_var14 + type: number + - name: saldo_var17 + type: number + - name: saldo_var18 + type: number + - name: saldo_var20 + type: number + - name: saldo_var24 + type: number + - name: saldo_var26 + type: number + - name: saldo_var25 + type: number + - name: saldo_var28 + type: binary + - name: saldo_var27 + type: binary + - name: saldo_var29 + type: number + - name: saldo_var30 + type: number + - name: saldo_var31 + type: number + - name: saldo_var32 + type: number + - name: saldo_var33 + type: number + - name: saldo_var34 + type: number + - name: saldo_var37 + type: number + - name: saldo_var40 + type: number + - name: saldo_var41 + type: binary + - name: saldo_var42 + type: number + - name: saldo_var44 + type: number + - name: saldo_var46 + type: binary + - name: var36 + type: number + - name: delta_imp_amort_var18_1y3 + type: number + - name: delta_imp_amort_var34_1y3 + type: number + - name: delta_imp_aport_var13_1y3 + type: number + - name: delta_imp_aport_var17_1y3 + type: number + - name: delta_imp_aport_var33_1y3 + type: number + - name: delta_imp_compra_var44_1y3 + type: number + - name: delta_imp_reemb_var13_1y3 + type: number + - name: delta_imp_reemb_var17_1y3 + type: number + - name: delta_imp_reemb_var33_1y3 + type: number + - name: delta_imp_trasp_var17_in_1y3 + type: number + - name: delta_imp_trasp_var17_out_1y3 + type: number + - name: delta_imp_trasp_var33_in_1y3 + type: number + - name: delta_imp_trasp_var33_out_1y3 + type: number + - name: delta_imp_venta_var44_1y3 + type: number + - name: delta_num_aport_var13_1y3 + type: number + - name: delta_num_aport_var17_1y3 + type: number + - name: delta_num_aport_var33_1y3 + type: number + - name: delta_num_compra_var44_1y3 + type: number + - name: delta_num_reemb_var13_1y3 + type: number + - name: delta_num_reemb_var17_1y3 + type: number + - name: delta_num_reemb_var33_1y3 + type: number + - name: delta_num_trasp_var17_in_1y3 + type: number + - name: delta_num_trasp_var17_out_1y3 + type: number + - name: delta_num_trasp_var33_in_1y3 + type: number + - name: delta_num_trasp_var33_out_1y3 + type: number + - name: delta_num_venta_var44_1y3 + type: number + - name: imp_amort_var18_hace3 + type: binary + - name: imp_amort_var18_ult1 + type: number + - name: imp_amort_var34_hace3 + type: binary + - name: imp_amort_var34_ult1 + type: number + - name: imp_aport_var13_hace3 + type: number + - name: imp_aport_var13_ult1 + type: number + - name: imp_aport_var17_hace3 + type: number + - name: imp_aport_var17_ult1 + type: number + - name: imp_aport_var33_hace3 + type: number + - name: imp_aport_var33_ult1 + type: number + - name: imp_var7_emit_ult1 + type: number + - name: imp_var7_recib_ult1 + type: number + - name: imp_compra_var44_hace3 + type: number + - name: imp_compra_var44_ult1 + type: number + - name: imp_reemb_var13_hace3 + type: binary + - name: imp_reemb_var13_ult1 + type: number + - name: imp_reemb_var17_hace3 + type: number + - name: imp_reemb_var17_ult1 + type: number + - name: imp_reemb_var33_hace3 + type: binary + - name: imp_reemb_var33_ult1 + type: number + - name: imp_var43_emit_ult1 + type: number + - name: imp_trans_var37_ult1 + type: number + - name: imp_trasp_var17_in_hace3 + type: number + - name: imp_trasp_var17_in_ult1 + type: number + - name: imp_trasp_var17_out_hace3 + type: binary + - name: imp_trasp_var17_out_ult1 + type: number + - name: imp_trasp_var33_in_hace3 + type: number + - name: imp_trasp_var33_in_ult1 + type: number + - name: imp_trasp_var33_out_hace3 + type: binary + - name: imp_trasp_var33_out_ult1 + type: number + - name: imp_venta_var44_hace3 + type: number + - name: imp_venta_var44_ult1 + type: number + - name: ind_var7_emit_ult1 + type: binary + - name: ind_var7_recib_ult1 + type: binary + - name: ind_var10_ult1 + type: binary + - name: ind_var10cte_ult1 + type: binary + - name: ind_var9_cte_ult1 + type: binary + - name: ind_var9_ult1 + type: binary + - name: ind_var43_emit_ult1 + type: binary + - name: ind_var43_recib_ult1 + type: binary + - name: var21 + type: number + - name: num_var2_0_ult1 + type: binary + - name: num_var2_ult1 + type: binary + - name: num_aport_var13_hace3 + type: number + - name: num_aport_var13_ult1 + type: number + - name: num_aport_var17_hace3 + type: number + - name: num_aport_var17_ult1 + type: number + - name: num_aport_var33_hace3 + type: number + - name: num_aport_var33_ult1 + type: number + - name: num_var7_emit_ult1 + type: number + - name: num_var7_recib_ult1 + type: number + - name: num_compra_var44_hace3 + type: number + - name: num_compra_var44_ult1 + type: number + - name: num_ent_var16_ult1 + type: number + - name: num_var22_hace2 + type: number + - name: num_var22_hace3 + type: number + - name: num_var22_ult1 + type: number + - name: num_var22_ult3 + type: number + - name: num_med_var22_ult3 + type: number + - name: num_med_var45_ult3 + type: number + - name: num_meses_var5_ult3 + type: category + - name: num_meses_var8_ult3 + type: category + - name: num_meses_var12_ult3 + type: category + - name: num_meses_var13_corto_ult3 + type: category + - name: num_meses_var13_largo_ult3 + type: category + - name: num_meses_var13_medio_ult3 + type: number + - name: num_meses_var17_ult3 + type: category + - name: num_meses_var29_ult3 + type: category + - name: num_meses_var33_ult3 + type: category + - name: num_meses_var39_vig_ult3 + type: category + - name: num_meses_var44_ult3 + type: category + - name: num_op_var39_comer_ult1 + type: number + - name: num_op_var39_comer_ult3 + type: number + - name: num_op_var40_comer_ult1 + type: number + - name: num_op_var40_comer_ult3 + type: number + - name: num_op_var40_efect_ult1 + type: number + - name: num_op_var40_efect_ult3 + type: number + - name: num_op_var41_comer_ult1 + type: number + - name: num_op_var41_comer_ult3 + type: number + - name: num_op_var41_efect_ult1 + type: number + - name: num_op_var41_efect_ult3 + type: number + - name: num_op_var39_efect_ult1 + type: number + - name: num_op_var39_efect_ult3 + type: number + - name: num_reemb_var13_hace3 + type: binary + - name: num_reemb_var13_ult1 + type: number + - name: num_reemb_var17_hace3 + type: number + - name: num_reemb_var17_ult1 + type: number + - name: num_reemb_var33_hace3 + type: binary + - name: num_reemb_var33_ult1 + type: number + - name: num_sal_var16_ult1 + type: number + - name: num_var43_emit_ult1 + type: number + - name: num_var43_recib_ult1 + type: number + - name: num_trasp_var11_ult1 + type: number + - name: num_trasp_var17_in_hace3 + type: number + - name: num_trasp_var17_in_ult1 + type: number + - name: num_trasp_var17_out_hace3 + type: binary + - name: num_trasp_var17_out_ult1 + type: number + - name: num_trasp_var33_in_hace3 + type: number + - name: num_trasp_var33_in_ult1 + type: number + - name: num_trasp_var33_out_hace3 + type: binary + - name: num_trasp_var33_out_ult1 + type: number + - name: num_venta_var44_hace3 + type: number + - name: num_venta_var44_ult1 + type: number + - name: num_var45_hace2 + type: number + - name: num_var45_hace3 + type: number + - name: num_var45_ult1 + type: number + - name: num_var45_ult3 + type: number + - name: saldo_var2_ult1 + type: binary + - name: saldo_medio_var5_hace2 + type: number + - name: saldo_medio_var5_hace3 + type: number + - name: saldo_medio_var5_ult1 + type: number + - name: saldo_medio_var5_ult3 + type: number + - name: saldo_medio_var8_hace2 + type: number + - name: saldo_medio_var8_hace3 + type: number + - name: saldo_medio_var8_ult1 + type: number + - name: saldo_medio_var8_ult3 + type: number + - name: saldo_medio_var12_hace2 + type: number + - name: saldo_medio_var12_hace3 + type: number + - name: saldo_medio_var12_ult1 + type: number + - name: saldo_medio_var12_ult3 + type: number + - name: saldo_medio_var13_corto_hace2 + type: number + - name: saldo_medio_var13_corto_hace3 + type: number + - name: saldo_medio_var13_corto_ult1 + type: number + - name: saldo_medio_var13_corto_ult3 + type: number + - name: saldo_medio_var13_largo_hace2 + type: number + - name: saldo_medio_var13_largo_hace3 + type: number + - name: saldo_medio_var13_largo_ult1 + type: number + - name: saldo_medio_var13_largo_ult3 + type: number + - name: saldo_medio_var13_medio_hace2 + type: number + - name: saldo_medio_var13_medio_hace3 + type: binary + - name: saldo_medio_var13_medio_ult1 + type: number + - name: saldo_medio_var13_medio_ult3 + type: number + - name: saldo_medio_var17_hace2 + type: number + - name: saldo_medio_var17_hace3 + type: number + - name: saldo_medio_var17_ult1 + type: number + - name: saldo_medio_var17_ult3 + type: number + - name: saldo_medio_var29_hace2 + type: number + - name: saldo_medio_var29_hace3 + type: number + - name: saldo_medio_var29_ult1 + type: number + - name: saldo_medio_var29_ult3 + type: number + - name: saldo_medio_var33_hace2 + type: number + - name: saldo_medio_var33_hace3 + type: number + - name: saldo_medio_var33_ult1 + type: number + - name: saldo_medio_var33_ult3 + type: number + - name: saldo_medio_var44_hace2 + type: number + - name: saldo_medio_var44_hace3 + type: number + - name: saldo_medio_var44_ult1 + type: number + - name: saldo_medio_var44_ult3 + type: number + - name: var38 + type: number + combiner: + type: tabnet + size: 24 # N_a + output_size: 128 # N_d + sparsity: 0.001 # lambda_sparse + bn_momentum: 0.2 # m_B + num_steps: 7 # N_steps + relaxation_factor: 1.2 # gamma + bn_virtual_bs: 256 # B_v + trainer: + batch_size: 4096 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.005 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 10000 + decay_rate: 0.8 + validation_metric: accuracy + - name: santander_customer_transaction + goal: maximize + metric: accuracy + validation_metric_score: 0.9150915145874023 + training_rows: 139904 + test_rows: 40098 + validation_rows: 19998 + config: + output_features: + - name: target + type: binary + input_features: + - name: var_0 + type: number + - name: var_1 + type: number + - name: var_2 + type: number + - name: var_3 + type: number + - name: var_4 + type: number + - name: var_5 + type: number + - name: var_6 + type: number + - name: var_7 + type: number + - name: var_8 + type: number + - name: var_9 + type: number + - name: var_10 + type: number + - name: var_11 + type: number + - name: var_12 + type: number + - name: var_13 + type: number + - name: var_14 + type: number + - name: var_15 + type: number + - name: var_16 + type: number + - name: var_17 + type: number + - name: var_18 + type: number + - name: var_19 + type: number + - name: var_20 + type: number + - name: var_21 + type: number + - name: var_22 + type: number + - name: var_23 + type: number + - name: var_24 + type: number + - name: var_25 + type: number + - name: var_26 + type: number + - name: var_27 + type: number + - name: var_28 + type: number + - name: var_29 + type: number + - name: var_30 + type: number + - name: var_31 + type: number + - name: var_32 + type: number + - name: var_33 + type: number + - name: var_34 + type: number + - name: var_35 + type: number + - name: var_36 + type: number + - name: var_37 + type: number + - name: var_38 + type: number + - name: var_39 + type: number + - name: var_40 + type: number + - name: var_41 + type: number + - name: var_42 + type: number + - name: var_43 + type: number + - name: var_44 + type: number + - name: var_45 + type: number + - name: var_46 + type: number + - name: var_47 + type: number + - name: var_48 + type: number + - name: var_49 + type: number + - name: var_50 + type: number + - name: var_51 + type: number + - name: var_52 + type: number + - name: var_53 + type: number + - name: var_54 + type: number + - name: var_55 + type: number + - name: var_56 + type: number + - name: var_57 + type: number + - name: var_58 + type: number + - name: var_59 + type: number + - name: var_60 + type: number + - name: var_61 + type: number + - name: var_62 + type: number + - name: var_63 + type: number + - name: var_64 + type: number + - name: var_65 + type: number + - name: var_66 + type: number + - name: var_67 + type: number + - name: var_68 + type: number + - name: var_69 + type: number + - name: var_70 + type: number + - name: var_71 + type: number + - name: var_72 + type: number + - name: var_73 + type: number + - name: var_74 + type: number + - name: var_75 + type: number + - name: var_76 + type: number + - name: var_77 + type: number + - name: var_78 + type: number + - name: var_79 + type: number + - name: var_80 + type: number + - name: var_81 + type: number + - name: var_82 + type: number + - name: var_83 + type: number + - name: var_84 + type: number + - name: var_85 + type: number + - name: var_86 + type: number + - name: var_87 + type: number + - name: var_88 + type: number + - name: var_89 + type: number + - name: var_90 + type: number + - name: var_91 + type: number + - name: var_92 + type: number + - name: var_93 + type: number + - name: var_94 + type: number + - name: var_95 + type: number + - name: var_96 + type: number + - name: var_97 + type: number + - name: var_98 + type: number + - name: var_99 + type: number + - name: var_100 + type: number + - name: var_101 + type: number + - name: var_102 + type: number + - name: var_103 + type: number + - name: var_104 + type: number + - name: var_105 + type: number + - name: var_106 + type: number + - name: var_107 + type: number + - name: var_108 + type: number + - name: var_109 + type: number + - name: var_110 + type: number + - name: var_111 + type: number + - name: var_112 + type: number + - name: var_113 + type: number + - name: var_114 + type: number + - name: var_115 + type: number + - name: var_116 + type: number + - name: var_117 + type: number + - name: var_118 + type: number + - name: var_119 + type: number + - name: var_120 + type: number + - name: var_121 + type: number + - name: var_122 + type: number + - name: var_123 + type: number + - name: var_124 + type: number + - name: var_125 + type: number + - name: var_126 + type: number + - name: var_127 + type: number + - name: var_128 + type: number + - name: var_129 + type: number + - name: var_130 + type: number + - name: var_131 + type: number + - name: var_132 + type: number + - name: var_133 + type: number + - name: var_134 + type: number + - name: var_135 + type: number + - name: var_136 + type: number + - name: var_137 + type: number + - name: var_138 + type: number + - name: var_139 + type: number + - name: var_140 + type: number + - name: var_141 + type: number + - name: var_142 + type: number + - name: var_143 + type: number + - name: var_144 + type: number + - name: var_145 + type: number + - name: var_146 + type: number + - name: var_147 + type: number + - name: var_148 + type: number + - name: var_149 + type: number + - name: var_150 + type: number + - name: var_151 + type: number + - name: var_152 + type: number + - name: var_153 + type: number + - name: var_154 + type: number + - name: var_155 + type: number + - name: var_156 + type: number + - name: var_157 + type: number + - name: var_158 + type: number + - name: var_159 + type: number + - name: var_160 + type: number + - name: var_161 + type: number + - name: var_162 + type: number + - name: var_163 + type: number + - name: var_164 + type: number + - name: var_165 + type: number + - name: var_166 + type: number + - name: var_167 + type: number + - name: var_168 + type: number + - name: var_169 + type: number + - name: var_170 + type: number + - name: var_171 + type: number + - name: var_172 + type: number + - name: var_173 + type: number + - name: var_174 + type: number + - name: var_175 + type: number + - name: var_176 + type: number + - name: var_177 + type: number + - name: var_178 + type: number + - name: var_179 + type: number + - name: var_180 + type: number + - name: var_181 + type: number + - name: var_182 + type: number + - name: var_183 + type: number + - name: var_184 + type: number + - name: var_185 + type: number + - name: var_186 + type: number + - name: var_187 + type: number + - name: var_188 + type: number + - name: var_189 + type: number + - name: var_190 + type: number + - name: var_191 + type: number + - name: var_192 + type: number + - name: var_193 + type: number + - name: var_194 + type: number + - name: var_195 + type: number + - name: var_196 + type: number + - name: var_197 + type: number + - name: var_198 + type: number + - name: var_199 + type: number + combiner: + type: tabnet + size: 8 # N_a + output_size: 8 # N_d + sparsity: 0.0 # lambda_sparse + bn_momentum: 0.4 # m_B + num_steps: 3 # N_steps + relaxation_factor: 2.0 # gamma + bn_virtual_bs: 256 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.005 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 20000 + decay_rate: 0.95 + validation_metric: accuracy + - name: sarcos + goal: minimize + metric: root_mean_squared_error + validation_metric_score: 2.0124664306640625 + training_rows: 40036 + test_rows: 0 + validation_rows: 4448 + config: + output_features: + - name: torque_1 + type: number + input_features: + - name: position_1 + type: number + - name: position_2 + type: number + - name: position_3 + type: number + - name: position_4 + type: number + - name: position_5 + type: number + - name: position_6 + type: number + - name: position_7 + type: number + - name: velocity_1 + type: number + - name: velocity_2 + type: number + - name: velocity_3 + type: number + - name: velocity_4 + type: number + - name: velocity_5 + type: number + - name: velocity_6 + type: number + - name: velocity_7 + type: number + - name: acceleration_1 + type: number + - name: acceleration_2 + type: number + - name: acceleration_3 + type: number + - name: acceleration_4 + type: number + - name: acceleration_5 + type: number + - name: acceleration_6 + type: number + - name: acceleration_7 + type: number + combiner: + type: tabnet + size: 128 # N_a + output_size: 8 # N_d + sparsity: 0.000001 # lambda_sparse + bn_momentum: 0.02 # m_B + num_steps: 4 # N_steps + relaxation_factor: 1.2 # gamma + bn_virtual_bs: 4096 # B_v + trainer: + batch_size: 256 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.005 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 20000 + decay_rate: 0.4 + validation_metric: root_mean_squared_error + - name: walmart_recruiting + goal: maximize + metric: accuracy + validation_metric_score: 0.31689465045928955 + training_rows: 453154 + test_rows: 129276 + validation_rows: 64624 + config: + output_features: + - name: TripType + type: category + input_features: + - name: VisitNumber + type: number + - name: Weekday + type: category + - name: Upc + type: number + - name: ScanCount + type: number + - name: FinelineNumber + type: number + combiner: + type: tabnet + size: 32 # N_a + output_size: 128 # N_d + sparsity: 0.000001 # lambda_sparse + bn_momentum: 0.4 # m_B + num_steps: 4 # N_steps + relaxation_factor: 1.2 # gamma + bn_virtual_bs: 4096 # B_v + trainer: + batch_size: 8192 # B + eval_batch_size: null # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + learning_rate: 0.01 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_steps: 20000 + decay_rate: 0.9 + validation_metric: accuracy diff --git a/ludwig/automl/defaults/text/bert_config.yaml b/ludwig/automl/defaults/text/bert_config.yaml new file mode 100644 index 0000000..20fa8c7 --- /dev/null +++ b/ludwig/automl/defaults/text/bert_config.yaml @@ -0,0 +1,29 @@ +trainer: + epochs: 10 + learning_rate_scheduler: + warmup_fraction: 0.1 + decay: linear + optimizer: + type: adamw + use_mixed_precision: true + +defaults: + text: + encoder: + type: bert + trainable: true + +hyperopt: + # goal: maximize + parameters: + # This parameter space was updated to be loguniform because of issues merging with the trainer.learning_rate + # parameter space in ludwig/automl/defaults/combiner/concat_config.yaml. Doing automl on a text feature would + # create an invalid combination of loguniform and choice paramters. + # TODO(jeffkinnison): Add a second pass `merge_dicts` to handle parameter spaces + trainer.learning_rate: + space: loguniform + lower: 0.00002 + upper: 0.00003 + trainer.batch_size: + space: choice + categories: [16, 32, 64, 128] diff --git a/ludwig/automl/search_space.py b/ludwig/automl/search_space.py new file mode 100644 index 0000000..a553d44 --- /dev/null +++ b/ludwig/automl/search_space.py @@ -0,0 +1,472 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""YAML-driven SearchSpace loader for AutoML hyperparameter grids.""" + +from __future__ import annotations + +import logging +import os +import random +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Iterator + +from ludwig.api_annotations import DeveloperAPI + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Spec dataclasses +# --------------------------------------------------------------------------- + + +@dataclass +class EncoderSpec: + """Specification for a single encoder type.""" + + name: str + feature_types: list[str] + preprocessing: dict = field(default_factory=dict) + hyperparameters: dict[str, list] = field(default_factory=dict) + + +@dataclass +class CombinerSpec: + """Specification for a single combiner type.""" + + name: str + constraints: dict = field(default_factory=dict) + hyperparameters: dict[str, list] = field(default_factory=dict) + # Attention/transformer combiners require lower LRs to stay numerically stable. + # When set, the sampler will only draw from trainer LR values <= this threshold. + max_learning_rate: float | None = None + # Attention combiners with many input features can OOM at large batch sizes. + # When set, the sampler will only draw from trainer batch_size values <= this threshold. + max_batch_size: int | None = None + + +@dataclass +class DecoderSpec: + """Specification for a single decoder type.""" + + name: str + feature_types: list[str] + hyperparameters: dict[str, list] = field(default_factory=dict) + + +@dataclass +class TrainerSpec: + """Trainer hyperparameter grids.""" + + learning_rate_values: list[float] + batch_size_values: list[int] + default_epochs: int + + +# --------------------------------------------------------------------------- +# Built-in defaults (must exactly match prior hardcoded values) +# --------------------------------------------------------------------------- + +_DEFAULT_ENCODER_REGISTRY: dict[str, list[str]] = { + "binary": ["passthrough", "dense"], + "category": ["onehot", "passthrough", "dense", "sparse", "target", "hash"], + "number": ["passthrough", "dense", "ple", "periodic", "bins"], + "vector": ["passthrough", "dense"], + "text": [ + "bert", + "distilbert", + "roberta", + "xlnet", + "albert", + "electra", + "longformer", + "auto_transformer", + "deberta", + "modernbert", + "camembert", + "gpt", + "gpt2", + "t5", + "mt5", + "xlm", + "xlmroberta", + "tf_idf", + "embed", + "rnn", + "parallel_cnn", + "stacked_cnn", + "transformer", + "mamba2", + ], + "image": [ + "stacked_cnn", + "resnet", + "efficientnet", + "vit", + "densenet", + "alexnet", + "vgg", + "googlenet", + "inceptionv3", + "mobilenetv2", + "regnet", + "convnext", + "dinov2", + "siglip", + "swin_transformer", + "maxvit", + ], + "audio": ["rnn", "stacked_cnn", "parallel_cnn", "wav2vec2", "hubert", "whisper", "mamba2"], + "timeseries": [ + "dense", + "rnn", + "stacked_cnn", + "parallel_cnn", + "patchtst", + "nbeats", + "mamba2", + "transformer", + "passthrough", + ], + "sequence": ["embed", "rnn", "stacked_cnn", "parallel_cnn", "transformer", "mamba2"], + "date": ["embed", "wave"], + "h3": ["embed", "rnn", "weighted_sum"], + "bag": ["embed"], + "set": ["embed"], +} + +_DEFAULT_DECODER_REGISTRY: dict[str, list[str]] = { + "binary": ["mlp_classifier", "regressor"], + "category": ["classifier", "mlp_classifier"], + "number": ["regressor"], + "text": ["generator", "tagger", "transformer_generator"], + "sequence": ["generator", "tagger", "transformer_generator"], + "image": ["fpn", "segformer", "unet"], + "set": ["classifier"], + "vector": ["projector"], + "timeseries": ["projector"], +} + +_DEFAULT_ALL_COMBINERS: list[str] = [ + "concat", + "tabnet", + "transformer", + "tabtransformer", + "ft_transformer", + "tabpfn_v2", + "project_aggregate", + "comparator", + "sequence", + "sequence_concat", + "cross_attention", + "perceiver", + "gated_fusion", + "hypernetwork", +] + +_DEFAULT_COMBINER_CONSTRAINTS: dict[str, dict] = { + "tabnet": {"requires_all_tabular": True}, + "tabpfn_v2": {"requires_all_tabular": True}, + "tabtransformer": {"requires_all_tabular": True}, + "ft_transformer": {"requires_all_tabular": True}, + "comparator": {"exact_n_inputs": 2}, + "sequence": {"requires_sequential": True}, + "sequence_concat": {"requires_sequential": True}, +} + +# Shared FC-layer hyperparam grid used by several combiners. +_FC_HYPERPARAMS: dict[str, list] = { + "num_fc_layers": [1, 2, 3], + "output_size": [64, 128, 256], + "dropout": [0.0, 0.1, 0.3], +} + +_DEFAULT_COMBINER_HYPERPARAMS: dict[str, dict[str, list]] = { + "concat": _FC_HYPERPARAMS, + "tabtransformer": _FC_HYPERPARAMS, + "ft_transformer": _FC_HYPERPARAMS, + "project_aggregate": _FC_HYPERPARAMS, + "gated_fusion": _FC_HYPERPARAMS, + "hypernetwork": _FC_HYPERPARAMS, + "tabnet": {"size": [8, 16, 32], "output_size": [8, 16, 32], "num_steps": [3, 5, 7]}, + "transformer": {"num_heads": [2, 4, 8], "num_layers": [1, 2, 3], "dropout": [0.0, 0.1, 0.3]}, +} + +# Attention/transformer combiners require lower learning rates to avoid NaN loss. +# Any combiner listed here will restrict LR sampling to values <= this threshold. +_TRANSFORMER_COMBINER_MAX_LR: float = 3e-3 +_DEFAULT_COMBINER_MAX_LR: dict[str, float] = { + "transformer": _TRANSFORMER_COMBINER_MAX_LR, + "tabtransformer": _TRANSFORMER_COMBINER_MAX_LR, + "ft_transformer": _TRANSFORMER_COMBINER_MAX_LR, + "cross_attention": _TRANSFORMER_COMBINER_MAX_LR, + "perceiver": _TRANSFORMER_COMBINER_MAX_LR, + "hypernetwork": _TRANSFORMER_COMBINER_MAX_LR, +} + +# cross_attention and perceiver scale quadratically with n_input_features and OOM at +# batch_size=512 on consumer GPUs (10-24 GiB) when there are many features. +# Cap these at 256 to keep VRAM use manageable. +_ATTENTION_COMBINER_MAX_BATCH_SIZE: int = 256 +_DEFAULT_COMBINER_MAX_BATCH_SIZE: dict[str, int] = { + "cross_attention": _ATTENTION_COMBINER_MAX_BATCH_SIZE, + "perceiver": _ATTENTION_COMBINER_MAX_BATCH_SIZE, +} + +_DEFAULT_TRAINER_SPEC = TrainerSpec( + learning_rate_values=[1e-4, 3e-4, 1e-3, 3e-3, 1e-2], + batch_size_values=[64, 128, 256, 512], + default_epochs=50, +) + + +def _build_default_search_space() -> SearchSpace: + """Constructs a SearchSpace from the hardcoded defaults (no YAML loading).""" + encoders: dict[str, EncoderSpec] = {} + for feat_type, enc_names in _DEFAULT_ENCODER_REGISTRY.items(): + for enc_name in enc_names: + if enc_name not in encoders: + encoders[enc_name] = EncoderSpec(name=enc_name, feature_types=[]) + if feat_type not in encoders[enc_name].feature_types: + encoders[enc_name].feature_types.append(feat_type) + + combiners: dict[str, CombinerSpec] = { + name: CombinerSpec( + name=name, + constraints=_DEFAULT_COMBINER_CONSTRAINTS.get(name, {}), + hyperparameters=_DEFAULT_COMBINER_HYPERPARAMS.get(name, {}), + max_learning_rate=_DEFAULT_COMBINER_MAX_LR.get(name), + max_batch_size=_DEFAULT_COMBINER_MAX_BATCH_SIZE.get(name), + ) + for name in _DEFAULT_ALL_COMBINERS + } + + decoders: dict[str, DecoderSpec] = {} + for feat_type, dec_names in _DEFAULT_DECODER_REGISTRY.items(): + for dec_name in dec_names: + if dec_name not in decoders: + decoders[dec_name] = DecoderSpec(name=dec_name, feature_types=[]) + if feat_type not in decoders[dec_name].feature_types: + decoders[dec_name].feature_types.append(feat_type) + + return SearchSpace._from_specs(encoders, combiners, decoders, _DEFAULT_TRAINER_SPEC) + + +# --------------------------------------------------------------------------- +# YAML loading helpers +# --------------------------------------------------------------------------- + + +def _extract_hyperparameters(raw: dict[str, Any]) -> dict[str, list]: + """Extracts only samplable (``values``-bearing) entries from a raw hyperparameters dict.""" + result: dict[str, list] = {} + for param_name, param_spec in raw.items(): + if isinstance(param_spec, dict) and "values" in param_spec: + result[param_name] = list(param_spec["values"]) + # entries with only ``default`` are intentionally omitted from the sampling pool + return result + + +def _iter_yaml_dir(spec_dir: Path) -> Iterator[tuple[Path, dict]]: + """Yields (path, data) for each valid named YAML file in *spec_dir*.""" + import yaml + + for yaml_path in sorted(spec_dir.glob("*.yaml")): + with open(yaml_path) as fh: + data = yaml.safe_load(fh) + if isinstance(data, dict) and "name" in data: + yield yaml_path, data + else: + logger.warning(f"Skipping YAML without 'name' key: {yaml_path}") + + +def _load_encoders_from_dir(enc_dir: Path) -> dict[str, EncoderSpec]: + """Loads all encoder YAML files from *enc_dir*.""" + return { + data["name"]: EncoderSpec( + name=data["name"], + feature_types=list(data.get("feature_types", [])), + preprocessing=dict(data.get("preprocessing", {})), + hyperparameters=_extract_hyperparameters(data.get("hyperparameters", {})), + ) + for _, data in _iter_yaml_dir(enc_dir) + } + + +def _load_combiners_from_dir(comb_dir: Path) -> dict[str, CombinerSpec]: + """Loads all combiner YAML files from *comb_dir*.""" + return { + data["name"]: CombinerSpec( + name=data["name"], + constraints=dict(data.get("constraints", {})), + hyperparameters=_extract_hyperparameters(data.get("hyperparameters", {})), + max_learning_rate=data.get("max_learning_rate"), + max_batch_size=data.get("max_batch_size"), + ) + for _, data in _iter_yaml_dir(comb_dir) + } + + +def _load_decoders_from_dir(dec_dir: Path) -> dict[str, DecoderSpec]: + """Loads all decoder YAML files from *dec_dir*.""" + return { + data["name"]: DecoderSpec( + name=data["name"], + feature_types=list(data.get("feature_types", [])), + hyperparameters=_extract_hyperparameters(data.get("hyperparameters", {})), + ) + for _, data in _iter_yaml_dir(dec_dir) + } + + +def _load_trainer_from_yaml(trainer_path: Path) -> TrainerSpec: + """Loads trainer hyperparameter grids from a YAML file.""" + import yaml + + with open(trainer_path) as fh: + data = yaml.safe_load(fh) + + lr_spec = data.get("learning_rate", {}) + bs_spec = data.get("batch_size", {}) + epochs_spec = data.get("epochs", {}) + + return TrainerSpec( + learning_rate_values=list(lr_spec.get("values", _DEFAULT_TRAINER_SPEC.learning_rate_values)), + batch_size_values=list(bs_spec.get("values", _DEFAULT_TRAINER_SPEC.batch_size_values)), + default_epochs=int(epochs_spec.get("default", _DEFAULT_TRAINER_SPEC.default_epochs)), + ) + + +# --------------------------------------------------------------------------- +# SearchSpace +# --------------------------------------------------------------------------- + + +@DeveloperAPI +class SearchSpace: + """Container for encoder, combiner, decoder, and trainer hyperparameter grids. + + Construct from YAML files in a directory hierarchy:: + + search_space/ + encoders/ *.yaml + combiners/ *.yaml + decoders/ *.yaml + trainer.yaml + + Or rely on the built-in defaults by passing ``search_space_dir=None``. + + # Inputs + :param search_space_dir: (str | os.PathLike | None) Root directory containing the + ``encoders/``, ``combiners/``, ``decoders/`` subdirectories and ``trainer.yaml``. + Pass ``None`` to use the built-in hardcoded defaults. + """ + + def __init__(self, search_space_dir: str | os.PathLike | None = None) -> None: + if search_space_dir is None: + _ss = _build_default_search_space() + self.encoders = _ss.encoders + self.combiners = _ss.combiners + self.decoders = _ss.decoders + self.trainer = _ss.trainer + else: + root = Path(search_space_dir) + self.encoders = _load_encoders_from_dir(root / "encoders") + self.combiners = _load_combiners_from_dir(root / "combiners") + self.decoders = _load_decoders_from_dir(root / "decoders") + trainer_yaml = root / "trainer.yaml" + self.trainer = _load_trainer_from_yaml(trainer_yaml) if trainer_yaml.exists() else _DEFAULT_TRAINER_SPEC + + self.encoder_registry: dict[str, list[str]] = self._build_encoder_registry() + self.decoder_registry: dict[str, list[str]] = self._build_decoder_registry() + + @classmethod + def _from_specs( + cls, + encoders: dict[str, EncoderSpec], + combiners: dict[str, CombinerSpec], + decoders: dict[str, DecoderSpec], + trainer: TrainerSpec, + ) -> SearchSpace: + """Internal constructor: build a SearchSpace directly from spec dicts.""" + instance = cls.__new__(cls) + instance.encoders = encoders + instance.combiners = combiners + instance.decoders = decoders + instance.trainer = trainer + instance.encoder_registry = instance._build_encoder_registry() + instance.decoder_registry = instance._build_decoder_registry() + return instance + + # ------------------------------------------------------------------ + # Registry builders + # ------------------------------------------------------------------ + + def _build_encoder_registry(self) -> dict[str, list[str]]: + registry: dict[str, list[str]] = {} + for spec in self.encoders.values(): + for ft in spec.feature_types: + registry.setdefault(ft, []).append(spec.name) + return registry + + def _build_decoder_registry(self) -> dict[str, list[str]]: + registry: dict[str, list[str]] = {} + for spec in self.decoders.values(): + for ft in spec.feature_types: + registry.setdefault(ft, []).append(spec.name) + return registry + + # ------------------------------------------------------------------ + # Sampling + # ------------------------------------------------------------------ + + def sample_hyperparams( + self, + spec: EncoderSpec | CombinerSpec | DecoderSpec, + rng: random.Random, + ) -> dict: + """Returns a dict of sampled hyperparameter values for *spec*. + + For each param in ``spec.hyperparameters``, one value is chosen uniformly at random + from its ``values`` list. The ``type`` key is always set to ``spec.name``. + + # Inputs + :param spec: An :class:`EncoderSpec`, :class:`CombinerSpec`, or :class:`DecoderSpec`. + :param rng: (:class:`random.Random`) Random number generator. + + # Return + :return: (dict) Sampled hyperparameter dict, always including ``{"type": spec.name}``. + """ + params: dict = {"type": spec.name} + for param_name, values in spec.hyperparameters.items(): + if values: + params[param_name] = rng.choice(values) + return params + + +# --------------------------------------------------------------------------- +# Module-level default instance (lazily created) +# --------------------------------------------------------------------------- + +_DEFAULT_SEARCH_SPACE: SearchSpace | None = None + + +def _default_search_space() -> SearchSpace: + """Returns the lazily-instantiated module-level default :class:`SearchSpace`.""" + global _DEFAULT_SEARCH_SPACE + if _DEFAULT_SEARCH_SPACE is None: + _DEFAULT_SEARCH_SPACE = SearchSpace() + return _DEFAULT_SEARCH_SPACE diff --git a/ludwig/automl/target_detection.py b/ludwig/automl/target_detection.py new file mode 100644 index 0000000..72944f1 --- /dev/null +++ b/ludwig/automl/target_detection.py @@ -0,0 +1,261 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from __future__ import annotations + +import logging +from dataclasses import dataclass +from enum import Enum + +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI + +logger = logging.getLogger(__name__) + +# Column names that strongly suggest a target / label role. +_TARGET_NAME_HINTS = { + "target", + "label", + "y", + "output", + "class", + "outcome", + "result", + "Target", + "Label", + "Class", + "Y", +} + +# Maximum number of distinct values for a column to be considered MULTICLASS +# when the column has numeric dtype. +_MULTICLASS_MAX_DISTINCT = 20 + +# Fraction of total rows below which a numeric column with few distinct values +# is still treated as MULTICLASS rather than REGRESSION. +_MULTICLASS_FRACTION_CUTOFF = 0.05 + +# Confidence values assigned by each heuristic tier. +_CONF_NAME_HINT = 0.95 +_CONF_LAST_COLUMN = 0.60 +_CONF_LOW_CARDINALITY_LAST_3 = 0.50 +_CONF_BALANCED_BINARY = 0.45 +_CONF_FEWEST_DISTINCT = 0.30 + +# Minimum confidence required to return a result. +_CONF_MIN_ACCEPTABLE = 0.25 + +# Balance thresholds for the balanced-binary heuristic. +_BALANCE_LOW = 0.30 +_BALANCE_HIGH = 0.70 + + +class TaskType(str, Enum): + BINARY = "binary" + MULTICLASS = "multiclass" + REGRESSION = "regression" + + +@DeveloperAPI +@dataclass +class TargetDetectionResult: + column: str + confidence: float # 0.0–1.0 + task_type: TaskType + reason: str # Human-readable explanation + + +def _is_id_column(series: pd.Series) -> bool: + """Returns True if a column looks like an ID (all-unique or sequential ints).""" + n = len(series.dropna()) + if n == 0: + return False + n_distinct = series.nunique(dropna=True) + if n_distinct == n: + # All-unique — strong ID signal. + return True + # Sequential integers: sorted unique values form a contiguous range. + if pd.api.types.is_integer_dtype(series): + unique_sorted = sorted(series.dropna().unique()) + if len(unique_sorted) >= 2: + lo, hi = int(unique_sorted[0]), int(unique_sorted[-1]) + if hi - lo + 1 == len(unique_sorted): + return True + return False + + +@DeveloperAPI +def infer_task_type(series: pd.Series) -> TaskType: + """Infers task type from a pandas Series (the target column values). + + # Inputs + :param series: (pd.Series) the candidate target column. + + # Return + :return: (TaskType) the inferred task type. + """ + n_distinct = series.nunique(dropna=True) + n_rows = len(series.dropna()) + + if n_distinct == 2: + return TaskType.BINARY + + if n_distinct <= _MULTICLASS_MAX_DISTINCT or (n_rows > 0 and n_distinct / n_rows <= _MULTICLASS_FRACTION_CUTOFF): + return TaskType.MULTICLASS + + if pd.api.types.is_numeric_dtype(series): + return TaskType.REGRESSION + + return TaskType.MULTICLASS + + +def _score_column(col: str, series: pd.Series, df: pd.DataFrame) -> tuple[float, str]: + """Returns (confidence, reason) for a single candidate column. + + Applies heuristics in priority order and returns as soon as a match is + found. Lower-priority heuristics are only evaluated when none of the + higher-priority ones fire. + """ + n_distinct = series.nunique(dropna=True) + n_rows = len(df) + col_index = list(df.columns).index(col) + last_index = len(df.columns) - 1 + + # Heuristic 1 — well-known target name. + if col in _TARGET_NAME_HINTS: + return _CONF_NAME_HINT, f"Column name '{col}' is a well-known target/label name" + + # Heuristic 2 — last column in the DataFrame. + if col_index == last_index: + return _CONF_LAST_COLUMN, f"Column '{col}' is the last column in the DataFrame" + + # Heuristic 3 — lowest-cardinality non-binary, non-constant among last 3 columns. + last_3 = list(df.columns[max(0, last_index - 2) :]) + if col in last_3 and 2 < n_distinct < n_rows: + min_distinct_in_last3 = ( + min(df[c].nunique(dropna=True) for c in last_3 if df[c].nunique(dropna=True) > 2) + if any(df[c].nunique(dropna=True) > 2 for c in last_3) + else None + ) + if min_distinct_in_last3 is not None and n_distinct == min_distinct_in_last3: + return ( + _CONF_LOW_CARDINALITY_LAST_3, + f"Column '{col}' has the lowest cardinality ({n_distinct}) among the last 3 columns", + ) + + # Heuristic 4 — binary column with near-50/50 balance. + if n_distinct == 2: + counts = series.value_counts(normalize=True, dropna=True) + minority_frac = counts.min() + if _BALANCE_LOW <= minority_frac <= _BALANCE_HIGH: + return ( + _CONF_BALANCED_BINARY, + f"Column '{col}' is binary with a balanced split (minority={minority_frac:.2f})", + ) + + # Heuristic 5 — not an ID column, fewest distinct values. + if not _is_id_column(series) and n_distinct > 1: + return ( + _CONF_FEWEST_DISTINCT, + f"Column '{col}' is a non-ID column with {n_distinct} distinct values", + ) + + return 0.0, f"Column '{col}' did not match any target heuristic" + + +@DeveloperAPI +def detect_all_target_candidates(df: pd.DataFrame) -> list[TargetDetectionResult]: + """Returns all plausible target columns sorted by confidence descending. + + # Inputs + :param df: (pd.DataFrame) the dataset to analyse. + + # Return + :return: (list[TargetDetectionResult]) candidates sorted by confidence. + """ + if df.empty or len(df.columns) == 0: + return [] + + results: list[TargetDetectionResult] = [] + for col in df.columns: + series = df[col] + confidence, reason = _score_column(col, series, df) + if confidence > 0.0: + task_type = infer_task_type(series) + results.append( + TargetDetectionResult( + column=col, + confidence=confidence, + task_type=task_type, + reason=reason, + ) + ) + + results.sort(key=lambda r: r.confidence, reverse=True) + return results + + +@DeveloperAPI +def detect_target_column(df: pd.DataFrame) -> TargetDetectionResult: + """Infers the most likely target column from a DataFrame. + + Heuristics (ordered by decreasing confidence): + + 1. Column named exactly: target, label, y, output, class, outcome, result, + Target, Label, Class, Y → confidence 0.95 + 2. Last column in the DataFrame → confidence 0.6 + 3. Lowest-cardinality non-binary, non-constant column among the last 3 + columns → confidence 0.5 + 4. Binary column with ~50/50 split (balance between 0.3–0.7) → confidence 0.45 + 5. Column excluded from ID candidates (not all-unique, not sequential int) + with fewest distinct values → confidence 0.3 + + Task type is inferred from the winning column: + + - 2 distinct values → BINARY + - ≤20 distinct values (or ≤5% of rows) → MULTICLASS + - all-numeric values → REGRESSION + + # Inputs + :param df: (pd.DataFrame) the dataset to analyse. + + # Return + :return: (TargetDetectionResult) the most likely target column. + + :raises ValueError: if no column scores above 0.25 confidence. + """ + if df.empty or len(df.columns) == 0: + raise ValueError("Cannot detect target column: the DataFrame is empty or has no columns.") + + candidates = detect_all_target_candidates(df) + if not candidates: + raise ValueError( + "Cannot detect target column: no columns in the DataFrame matched any target heuristic. " + "Please specify the target column explicitly." + ) + + best = candidates[0] + if best.confidence < _CONF_MIN_ACCEPTABLE: + raise ValueError( + f"Cannot detect target column with sufficient confidence " + f"(best candidate '{best.column}' scored {best.confidence:.2f} < {_CONF_MIN_ACCEPTABLE}). " + "Please specify the target column explicitly." + ) + + logger.info( + f"Auto-detected target column '{best.column}' " + f"(task={best.task_type.value}, confidence={best.confidence:.2f}): {best.reason}" + ) + return best diff --git a/ludwig/backend/__init__.py b/ludwig/backend/__init__.py new file mode 100644 index 0000000..d177283 --- /dev/null +++ b/ludwig/backend/__init__.py @@ -0,0 +1,103 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import contextlib +import logging +import os + +from ludwig.api_annotations import DeveloperAPI +from ludwig.backend.base import Backend, LocalBackend + +logger = logging.getLogger(__name__) + + +# TODO: remove LOCAL_BACKEND as a global constant, replace with singleton LocalBackend.shared_instance(). +LOCAL_BACKEND = LocalBackend.shared_instance() + + +LOCAL = "local" +DASK = "dask" +RAY = "ray" + +ALL_BACKENDS = [LOCAL, DASK, RAY] + + +def _has_ray(): + # Temporary workaround to prevent tests from automatically using the Ray backend. Taken from + # https://stackoverflow.com/questions/25188119/test-if-code-is-executed-from-within-a-py-test-session + if "PYTEST_CURRENT_TEST" in os.environ: + return False + + try: + import ray + except ImportError: + return False + + if ray.is_initialized(): + return True + + try: + ray.init("auto", ignore_reinit_error=True) + return True + except (ConnectionError, RuntimeError): + return False + + +def get_local_backend(**kwargs): + return LocalBackend(**kwargs) + + +def create_ray_backend(**kwargs): + from ludwig.backend.ray import RayBackend + + return RayBackend(**kwargs) + + +backend_registry = { + LOCAL: get_local_backend, + RAY: create_ray_backend, + None: get_local_backend, +} + + +@DeveloperAPI +def create_backend(type, **kwargs): + if isinstance(type, Backend): + return type + + if type is None and _has_ray(): + type = RAY + + return backend_registry[type](**kwargs) + + +@DeveloperAPI +def initialize_backend(backend): + if isinstance(backend, dict): + backend = create_backend(**backend) + else: + backend = create_backend(backend) + backend.initialize() + return backend + + +@contextlib.contextmanager +def provision_preprocessing_workers(backend): + if backend.BACKEND_TYPE == RAY: + with backend.provision_preprocessing_workers(): + yield + else: + yield diff --git a/ludwig/backend/base.py b/ludwig/backend/base.py new file mode 100644 index 0000000..6ecfac9 --- /dev/null +++ b/ludwig/backend/base.py @@ -0,0 +1,337 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable, Generator +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import dataclass +from typing import Any, TYPE_CHECKING + +import numpy as np +import pandas as pd +import psutil +import torch +from tqdm import tqdm + +from ludwig.api_annotations import DeveloperAPI +from ludwig.backend.utils.storage import StorageManager +from ludwig.constants import MODEL_LLM +from ludwig.data.cache.manager import PreprocessedDataCache +from ludwig.data.dataframe.base import DataFrameEngine +from ludwig.data.dataframe.pandas import PANDAS +from ludwig.data.dataset.base import DatasetManager +from ludwig.data.dataset.pandas import PandasDatasetManager +from ludwig.distributed import init_dist_strategy +from ludwig.models.base import BaseModel +from ludwig.schema.trainer import BaseTrainerConfig +from ludwig.types import HyperoptConfigDict +from ludwig.utils.audio_utils import read_audio_from_path +from ludwig.utils.batch_size_tuner import BatchSizeEvaluator +from ludwig.utils.dataframe_utils import from_batches, to_batches +from ludwig.utils.fs_utils import get_bytes_obj_from_path +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.system_utils import Resources +from ludwig.utils.torch_utils import initialize_pytorch +from ludwig.utils.types import DataFrame, Series + +if TYPE_CHECKING: + from ludwig.trainers.base import BaseTrainer + + +@DeveloperAPI +@dataclass(frozen=True) +class BackendCapabilities: + """Named feature flags that a :class:`Backend` can advertise. + + Use this instead of a raw ``dict[str, Any]`` so that callers get IDE + completion and type-safe access:: + + if backend.capabilities.distributed: + ... + + All flags default to ``False``; subclasses set them in the class body:: + + class RayBackend(Backend): + capabilities = BackendCapabilities(distributed=True, hyperopt=True) + """ + + distributed: bool = False + hyperopt: bool = False + async_execution: bool = False + cache_preprocessing: bool = True + + +@DeveloperAPI +class Backend(ABC): + """Abstract base class for Ludwig execution backends. + + Required abstract methods (must be implemented by every subclass): + initialize(), initialize_pytorch(), create_trainer(), sync_model(), + broadcast_return(), is_coordinator(), df_engine, + supports_multiprocessing, read_binary_files(), num_nodes, + num_training_workers, get_available_resources(), + max_concurrent_trials(), tune_batch_size(), batch_transform() + + Optional methods (have sensible defaults; override to add capability): + supports_batch_size_tuning() — returns True; set to False for + backends that cannot tune batch sizes (e.g. remote inference only). + + Set the ``capabilities`` class attribute to advertise named feature flags:: + + class MyBackend(Backend): + capabilities = BackendCapabilities(distributed=False) + """ + + capabilities: BackendCapabilities = BackendCapabilities() + + def __init__( + self, + dataset_manager: DatasetManager, + cache_dir: str | None = None, + credentials: dict[str, dict[str, Any]] | None = None, + ): + credentials = credentials or {} + self._dataset_manager = dataset_manager + self._storage_manager = StorageManager(**credentials) + self._cache_manager = PreprocessedDataCache(self._dataset_manager, cache_dir) + + @property + def storage(self) -> StorageManager: + return self._storage_manager + + @property + def cache(self) -> PreprocessedDataCache: + return self._cache_manager + + @property + def dataset_manager(self) -> DatasetManager: + return self._dataset_manager + + @abstractmethod + def initialize(self): + raise NotImplementedError() + + @abstractmethod + def initialize_pytorch(self, *args, **kwargs): + raise NotImplementedError() + + @contextmanager + @abstractmethod + def create_trainer(self, config: BaseTrainerConfig, model: BaseModel, **kwargs) -> Generator: + raise NotImplementedError() + + @abstractmethod + def sync_model(self, model): + raise NotImplementedError() + + @abstractmethod + def broadcast_return(self, fn): + raise NotImplementedError() + + @abstractmethod + def is_coordinator(self): + raise NotImplementedError() + + @property + @abstractmethod + def df_engine(self) -> DataFrameEngine: + raise NotImplementedError() + + @property + @abstractmethod + def supports_multiprocessing(self): + raise NotImplementedError() + + @abstractmethod + def read_binary_files(self, column: Series, map_fn: Callable | None = None) -> Series: + raise NotImplementedError() + + @property + @abstractmethod + def num_nodes(self) -> int: + raise NotImplementedError() + + @property + @abstractmethod + def num_training_workers(self) -> int: + raise NotImplementedError() + + @abstractmethod + def get_available_resources(self) -> Resources: + raise NotImplementedError() + + @abstractmethod + def max_concurrent_trials(self, hyperopt_config: HyperoptConfigDict) -> int | None: + raise NotImplementedError() + + @abstractmethod + def tune_batch_size(self, evaluator_cls: type[BatchSizeEvaluator], dataset_len: int) -> int: + """Returns best batch size (measured in samples / s) on the given evaluator. + + The evaluator class will need to be instantiated on each worker in the backend cluster, then call + `evaluator.select_best_batch_size(dataset_len)`. + """ + raise NotImplementedError() + + @abstractmethod + def batch_transform( + self, df: DataFrame, batch_size: int, transform_fn: Callable, name: str | None = None + ) -> DataFrame: + """Applies `transform_fn` to every `batch_size` length batch of `df` and returns the result.""" + raise NotImplementedError() + + def supports_batch_size_tuning(self) -> bool: + return True + + +class LocalDataProcessingMixin: + @property + def df_engine(self): + return PANDAS + + @property + def supports_multiprocessing(self): + return True + + @staticmethod + def read_binary_files(column: pd.Series, map_fn: Callable | None = None, file_size: int | None = None) -> pd.Series: + column = column.fillna(np.nan).replace([np.nan], [None]) # normalize NaNs to None + + sample_fname = column.head(1).values[0] + with ThreadPoolExecutor() as executor: # number of threads is inferred + if isinstance(sample_fname, str): + if map_fn is read_audio_from_path: # bypass torchaudio issue that no longer takes in file-like objects + result = executor.map( # type: ignore[misc] + lambda path: map_fn(path) if path is not None else path, column.values + ) + else: + result = executor.map( + lambda path: get_bytes_obj_from_path(path) if path is not None else path, column.values + ) + else: + # If the sample path is not a string, assume the paths has already been read in + result = column.values + + if map_fn is not None and map_fn is not read_audio_from_path: + result = executor.map(lambda x: map_fn(x) if x is not None else None, result) + + return pd.Series(result, index=column.index, name=column.name) + + @staticmethod + def batch_transform(df: DataFrame, batch_size: int, transform_fn: Callable, name: str | None = None) -> DataFrame: + name = name or "Batch Transform" + batches = to_batches(df, batch_size) + transform = transform_fn() + out_batches = [transform(batch.reset_index(drop=True)) for batch in tqdm(batches, desc=name)] + out_df = from_batches(out_batches).reset_index(drop=True) + return out_df + + +class LocalTrainingMixin: + @staticmethod + def initialize(): + init_dist_strategy("local") + + @staticmethod + def initialize_pytorch(*args, **kwargs): + initialize_pytorch(*args, **kwargs) + + @staticmethod + def create_predictor(model: BaseModel, **kwargs): + from ludwig.models.predictor import get_predictor_cls + + return get_predictor_cls(model.type())(model, **kwargs) # type: ignore[call-arg] + + def sync_model(self, model): + pass + + @staticmethod + def broadcast_return(fn): + return fn() + + @staticmethod + def is_coordinator() -> bool: + return True + + @staticmethod + def tune_batch_size(evaluator_cls: type[BatchSizeEvaluator], dataset_len: int) -> int: + evaluator = evaluator_cls() + return evaluator.select_best_batch_size(dataset_len) + + +class RemoteTrainingMixin: + def sync_model(self, model): + pass + + @staticmethod + def broadcast_return(fn): + return fn() + + @staticmethod + def is_coordinator() -> bool: + return True + + +@DeveloperAPI +class LocalBackend(LocalDataProcessingMixin, LocalTrainingMixin, Backend): + BACKEND_TYPE = "local" + + _shared_instance: LocalBackend + + @classmethod + def shared_instance(cls) -> LocalBackend: + """Returns a shared singleton LocalBackend instance.""" + if not hasattr(cls, "_shared_instance"): + cls._shared_instance = cls() + return cls._shared_instance + + def __init__(self, **kwargs) -> None: + super().__init__(dataset_manager=PandasDatasetManager(self), **kwargs) + + @property + def num_nodes(self) -> int: + return 1 + + @property + def num_training_workers(self) -> int: + return 1 + + def get_available_resources(self) -> Resources: + return Resources(cpus=psutil.cpu_count(), gpus=torch.cuda.device_count()) + + def max_concurrent_trials(self, hyperopt_config: HyperoptConfigDict) -> int | None: + # Every trial will be run with Pandas and NO Ray Datasets. Allow Ray Tune to use all the + # trial resources it wants, because there is no Ray Datasets process to compete with it for CPUs. + return None + + def create_trainer( + self, + config: BaseTrainerConfig, + model: BaseModel, + **kwargs, + ) -> BaseTrainer: # type: ignore[override] + from ludwig.trainers.registry import get_llm_trainers_registry, get_trainers_registry + + trainer_cls: type + if model.type() == MODEL_LLM: + trainer_cls = get_from_registry(config.type, get_llm_trainers_registry()) + else: + trainer_cls = get_from_registry(model.type(), get_trainers_registry()) + + return trainer_cls(config=config, model=model, **kwargs) diff --git a/ludwig/backend/datasource.py b/ludwig/backend/datasource.py new file mode 100644 index 0000000..ca49a44 --- /dev/null +++ b/ludwig/backend/datasource.py @@ -0,0 +1,67 @@ +"""Custom Ray datasource utilities for reading binary files with None handling.""" + +import logging +from typing import TYPE_CHECKING + +import pandas as pd +import ray +import urllib3 + +from ludwig.utils.fs_utils import get_bytes_obj_from_http_path, is_http + +if TYPE_CHECKING: + import pyarrow + +logger = logging.getLogger(__name__) + + +def read_binary_files_with_index( + paths_and_idxs: list[tuple[str | None, int]], + filesystem: "pyarrow.fs.FileSystem | None" = None, +) -> "ray.data.Dataset": + """Read binary files into a Ray Dataset, handling None paths and HTTP URLs. + + Each row in the resulting dataset has columns: + - "data": the raw bytes of the file (or None if path was None/failed) + - "idx": the original index for reordering + + Args: + paths_and_idxs: List of (path, index) tuples. Path can be None. + filesystem: PyArrow filesystem for reading non-HTTP files. + + Returns: + A ray.data.Dataset with "data" and "idx" columns. + """ + + def _read_file(path: str | None, idx: int) -> dict: + if path is None: + return {"data": None, "idx": idx} + elif is_http(path): + try: + data = get_bytes_obj_from_http_path(path) + except urllib3.exceptions.HTTPError as e: + logger.warning(e) + data = None + return {"data": data, "idx": idx} + else: + try: + with filesystem.open_input_stream(path) as f: + data = f.read() + except Exception as e: + logger.warning(f"Failed to read file {path}: {e}") + data = None + return {"data": data, "idx": idx} + + # Create a dataset from the paths and indices, then map to read files + records = [{"path": p, "idx": i} for p, i in paths_and_idxs] + ds = ray.data.from_items(records) + + def read_batch(batch: pd.DataFrame) -> pd.DataFrame: + results = [] + for _, row in batch.iterrows(): + result = _read_file(row["path"], row["idx"]) + results.append(result) + return pd.DataFrame(results) + + ds = ds.map_batches(read_batch, batch_format="pandas") + return ds diff --git a/ludwig/backend/ray.py b/ludwig/backend/ray.py new file mode 100644 index 0000000..a54c849 --- /dev/null +++ b/ludwig/backend/ray.py @@ -0,0 +1,1092 @@ +#! /usr/bin/env python +# Copyright (c) 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import contextlib +import copy +import logging +import os +import tempfile +from collections.abc import Callable +from functools import partial +from typing import Any, TYPE_CHECKING + +import dask +import numpy as np +import pandas as pd +import ray +import ray.train as rt +import torch +from fsspec.config import conf +from pyarrow.fs import FSSpecHandler, PyFileSystem +from ray import ObjectRef +from ray.train import Checkpoint, RunConfig, ScalingConfig +from ray.train.constants import TRAIN_ENABLE_WORKER_SPREAD_ENV +from ray.train.torch import TorchConfig, TorchTrainer +from ray.util.dask import ray_dask_get +from ray.util.placement_group import placement_group, remove_placement_group + +if TYPE_CHECKING: + from ludwig.api import LudwigModel + +from ludwig.backend.base import Backend, BackendCapabilities, RemoteTrainingMixin +from ludwig.backend.datasource import read_binary_files_with_index +from ludwig.constants import MODEL_ECD, MODEL_LLM, NAME, PREPROCESSING, PROC_COLUMN, TYPE +from ludwig.data.dataframe.base import DataFrameEngine + +try: + from ludwig.data.dataset.ray import ( + _SCALAR_TYPES, + cast_as_tensor_dtype, + RayDataset, + RayDatasetManager, + RayDatasetShard, + ) +except (ImportError, AttributeError): + _SCALAR_TYPES = cast_as_tensor_dtype = RayDataset = RayDatasetManager = RayDatasetShard = None +from ludwig.models.base import BaseModel +from ludwig.models.ecd import ECD +from ludwig.models.predictor import BasePredictor, get_output_columns, get_predictor_cls +from ludwig.schema.model_types.base import ModelConfig +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.trainers.registry import get_ray_trainers_registry, register_ray_trainer +from ludwig.trainers.trainer import BaseTrainer, RemoteTrainer +from ludwig.utils.data_utils import use_credentials +from ludwig.utils.fs_utils import get_fs_and_path +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.system_utils import Resources +from ludwig.utils.torch_utils import get_torch_device, initialize_pytorch +from ludwig.utils.types import Series + +logger = logging.getLogger(__name__) + +FIFTEEN_MINS_IN_S = 15 * 60 + + +def _num_nodes() -> int: + node_resources = [node["Resources"] for node in ray.nodes()] + return len(node_resources) + + +def get_trainer_kwargs(**kwargs) -> dict[str, Any]: + kwargs = copy.deepcopy(kwargs) + + # Our goal is to have a worker per resource used for training. + # The priority is GPUs, but can fall back to CPUs if there are no + # GPUs available. + use_gpu = kwargs.get("use_gpu", int(ray.cluster_resources().get("GPU", 0)) > 0) + if use_gpu: + num_workers = int(ray.cluster_resources().get("GPU", 0)) + else: + num_workers = _num_nodes() + + # Remove nics if present (legacy option) + kwargs.pop("nics", None) + + defaults = { + "backend": TorchConfig(), + "num_workers": num_workers, + "use_gpu": use_gpu, + "resources_per_worker": { + "CPU": 0 if use_gpu else 1, + "GPU": 1 if use_gpu else 0, + }, + } + return {**defaults, **kwargs} + + +def _create_dask_engine(**kwargs): + from ludwig.data.dataframe.dask import DaskEngine + + return DaskEngine(**kwargs) + + +def _create_modin_engine(**kwargs): + from ludwig.data.dataframe.modin import ModinEngine + + return ModinEngine(**kwargs) + + +def _create_pandas_engine(**kwargs): + from ludwig.data.dataframe.pandas import PandasEngine + + return PandasEngine(**kwargs) + + +_engine_registry = { + "dask": _create_dask_engine, + "modin": _create_modin_engine, + "pandas": _create_pandas_engine, +} + + +def _get_df_engine(processor): + logger.info(f"Ray processor params: {processor}") + if processor is None: + # TODO ray: find an informed way to set the parallelism, in practice + # it looks like Dask handles this well on its own most of the time + return _create_dask_engine() + + processor_kwargs = processor.copy() + + dtype = processor_kwargs.pop("type", "dask") + engine_cls = _engine_registry.get(dtype) + + return engine_cls(**processor_kwargs) + + +def _make_picklable(obj): + """Recursively convert defaultdicts (which contain unpicklable lambdas) to regular dicts.""" + from collections import defaultdict + + if isinstance(obj, defaultdict) or isinstance(obj, dict): + return {k: _make_picklable(v) for k, v in obj.items()} + elif isinstance(obj, tuple) and hasattr(obj, "_fields"): + # NamedTuple: reconstruct with the same field names + return type(obj)(**{f: _make_picklable(getattr(obj, f)) for f in obj._fields}) + elif isinstance(obj, list): + return [_make_picklable(item) for item in obj] + elif isinstance(obj, tuple): + return tuple(_make_picklable(item) for item in obj) + return obj + + +def train_fn( + executable_kwargs: dict[str, Any] | None = None, + model_ref: ObjectRef = None, + training_set_metadata: dict[str, Any] | None = None, + features: dict[str, dict] | None = None, + **kwargs, +): + """Ray Train worker function for distributed training. + + Runs inside each Ray worker process. Loads the model from an object ref, wraps dataset shards, trains, and saves + results to a Ray checkpoint so the driver can retrieve them (Ray Train 2.x requires a checkpoint for metrics). + """ + # Pin GPU before loading the model to prevent memory leaking onto other devices + initialize_pytorch() + + # Initialize a local distributed strategy so metric modules can sync. + from ludwig.distributed import init_dist_strategy + + init_dist_strategy("local") + + train_shard = RayDatasetShard( + rt.get_dataset_shard("train"), + features, + training_set_metadata, + ) + + try: + val_shard = rt.get_dataset_shard("val") + except KeyError: + val_shard = None + + if val_shard is not None: + val_shard = RayDatasetShard( + val_shard, + features, + training_set_metadata, + ) + + try: + test_shard = rt.get_dataset_shard("test") + except KeyError: + test_shard = None + + if test_shard is not None: + test_shard = RayDatasetShard( + test_shard, + features, + training_set_metadata, + ) + + model = ray.get(model_ref) + + # Use Ray Train's device assignment which respects use_gpu setting, + # rather than get_torch_device() which always picks CUDA if available. + from ray.train.torch import get_device as ray_get_device + + device = ray_get_device() + model = model.to(device) + + trainer = RemoteTrainer(model=model, device=str(device), report_tqdm_to_ray=True, **executable_kwargs) + results = trainer.train(train_shard, val_shard, test_shard, **kwargs) + + if results is not None: + # only return the model state dict back to the head node. + trained_model, *args = results + results = (trained_model.cpu().state_dict(), *args) + + torch.cuda.empty_cache() + + # Save results to a checkpoint so the driver can retrieve them. + # In Ray Train 2.x, result.metrics is only populated when a checkpoint is provided. + # + # We split the save into: + # - SafeTensors for model weights (secure, no pickle) + # - JSON for metadata (validation_field, validation_metric, non-tensor results) + train_results = results, trainer.validation_field, trainer.validation_metric + train_results = _make_picklable(train_results) + with tempfile.TemporaryDirectory() as tmpdir: + results_tuple, val_field, val_metric = train_results + state_dict, *other_results = results_tuple + + # Save model weights via SafeTensors + from safetensors.torch import save_file as st_save + + weights = {k: v.clone().contiguous() for k, v in state_dict.items()} + st_save(weights, os.path.join(tmpdir, "model_weights.safetensors")) + + # Save metadata + non-tensor results via JSON-safe torch.save + import json + + meta = { + "validation_field": val_field, + "validation_metric": val_metric, + } + with open(os.path.join(tmpdir, "train_meta.json"), "w") as f: + json.dump(meta, f) + + # Save remaining results (metrics dicts) that may contain non-serializable objects + torch.save(other_results, os.path.join(tmpdir, "train_other.pt")) + + rt.report(metrics={}, checkpoint=Checkpoint.from_directory(tmpdir)) + + +@ray.remote +def tune_batch_size_fn( + dataset: RayDataset = None, + data_loader_kwargs: dict[str, Any] | None = None, + executable_kwargs: dict[str, Any] | None = None, + model: ECD = None, + ludwig_config: ModelConfig | dict[str, Any] = None, + training_set_metadata: dict[str, Any] | None = None, + features: dict[str, dict] | None = None, + **kwargs, +) -> int: + # Pin GPU before loading the model to prevent memory leaking onto other devices + initialize_pytorch() + + # Initialize a local distributed strategy so metric modules can sync. + from ludwig.distributed import init_dist_strategy + + init_dist_strategy("local") + + try: + ds = dataset.to_ray_dataset(shuffle=False) + train_shard = RayDatasetShard( + ds, + features, + training_set_metadata, + ) + + device = get_torch_device() + model = model.to(device) + + trainer = RemoteTrainer(model=model, device=device, **executable_kwargs) + return trainer.tune_batch_size(ludwig_config, train_shard, **kwargs) + finally: + torch.cuda.empty_cache() + + +@ray.remote +def tune_learning_rate_fn( + dataset: RayDataset, + config: dict[str, Any], + data_loader_kwargs: dict[str, Any] | None = None, + executable_kwargs: dict[str, Any] | None = None, + model: ECD = None, + training_set_metadata: dict[str, Any] | None = None, + features: dict[str, dict] | None = None, + **kwargs, +) -> float: + # Pin GPU before loading the model to prevent memory leaking onto other devices + initialize_pytorch() + + # Initialize a local distributed strategy so metric modules can sync. + from ludwig.distributed import init_dist_strategy + + init_dist_strategy("local") + + try: + ds = dataset.to_ray_dataset(shuffle=False) + train_shard = RayDatasetShard( + ds, + features, + training_set_metadata, + ) + + device = get_torch_device() + model = model.to(device) + + trainer = RemoteTrainer(model=model, device=device, **executable_kwargs) + return trainer.tune_learning_rate(config, train_shard, **kwargs) + finally: + torch.cuda.empty_cache() + + +@contextlib.contextmanager +def spread_env(use_gpu: bool = False, num_workers: int = 1, **kwargs): + if TRAIN_ENABLE_WORKER_SPREAD_ENV in os.environ: + # User set this explicitly, so honor their selection + yield + return + + try: + if not use_gpu and num_workers > 1: + # When doing CPU-only training, default to a SPREAD policy to avoid + # packing too many workers on a single machine + os.environ[TRAIN_ENABLE_WORKER_SPREAD_ENV] = "1" + yield + finally: + if TRAIN_ENABLE_WORKER_SPREAD_ENV in os.environ: + del os.environ[TRAIN_ENABLE_WORKER_SPREAD_ENV] + + +def _build_scaling_config(trainer_kwargs: dict[str, Any]) -> ScalingConfig: + """Convert legacy trainer kwargs to a Ray ScalingConfig.""" + return ScalingConfig( + num_workers=trainer_kwargs.get("num_workers", 1), + use_gpu=trainer_kwargs.get("use_gpu", False), + resources_per_worker=trainer_kwargs.get("resources_per_worker"), + ) + + +def run_train_remote(train_loop, trainer_kwargs: dict[str, Any], callbacks=None, datasets=None, train_loop_config=None): + """Run a distributed training function using Ray TorchTrainer.""" + resolved_kwargs = get_trainer_kwargs(**trainer_kwargs) + + scaling_config = _build_scaling_config(resolved_kwargs) + torch_config = resolved_kwargs.get("backend", TorchConfig()) + + run_config_kwargs = {} + if callbacks: + run_config_kwargs["callbacks"] = callbacks + + with spread_env(**resolved_kwargs): + torch_trainer = TorchTrainer( + train_loop_per_worker=train_loop, + train_loop_config=train_loop_config, + torch_config=torch_config, + scaling_config=scaling_config, + run_config=RunConfig(**run_config_kwargs), + datasets=datasets, + ) + result = torch_trainer.fit() + return result + + +@register_ray_trainer(MODEL_ECD, default=True) +class RayTrainerV2(BaseTrainer): + def __init__( + self, + model: BaseModel, + trainer_kwargs: dict[str, Any], + data_loader_kwargs: dict[str, Any], + executable_kwargs: dict[str, Any], + **kwargs, + ): + self.model = model.cpu() + self.data_loader_kwargs = data_loader_kwargs + self.executable_kwargs = executable_kwargs + self.trainer_kwargs = trainer_kwargs + self._validation_field = None + self._validation_metric = None + + @staticmethod + def get_schema_cls(): + return ECDTrainerConfig + + def train( + self, + training_set: RayDataset, + validation_set: RayDataset | None = None, + test_set: RayDataset | None = None, + **kwargs, + ): + executable_kwargs = self.executable_kwargs + + kwargs = { + "training_set_metadata": training_set.training_set_metadata, + "features": training_set.features, + **kwargs, + } + + train_ds = training_set.to_ray_dataset(shuffle=True) + dataset = {"train": train_ds} + if validation_set is not None: + dataset["val"] = validation_set.to_ray_dataset(shuffle=False) + if test_set is not None: + dataset["test"] = test_set.to_ray_dataset(shuffle=False) + + train_loop_config = {"executable_kwargs": executable_kwargs, "model_ref": ray.put(self.model), **kwargs} + + def _train_loop(config): + train_fn(**config) + + result = run_train_remote( + _train_loop, + trainer_kwargs=self.trainer_kwargs, + datasets=dataset, + train_loop_config=train_loop_config, + ) + + with result.checkpoint.as_directory() as tmpdir: + safetensors_path = os.path.join(tmpdir, "model_weights.safetensors") + meta_path = os.path.join(tmpdir, "train_meta.json") + + if os.path.exists(safetensors_path): + # New format: SafeTensors weights + JSON metadata + from safetensors.torch import load_file as st_load + + state_dict = st_load(safetensors_path, device="cpu") + + import json + + with open(meta_path) as f: + meta = json.load(f) + self._validation_field = meta["validation_field"] + self._validation_metric = meta["validation_metric"] + + other_results = torch.load(os.path.join(tmpdir, "train_other.pt"), weights_only=False) + else: + # Legacy format: single torch.save file + train_results = torch.load(os.path.join(tmpdir, "train_results.pt"), weights_only=False) + results_tuple, self._validation_field, self._validation_metric = train_results + state_dict, *other_results = results_tuple + + # Load state dict back into the model + self.model.load_state_dict(state_dict) + results = (self.model, *other_results) + + return results + + def train_online(self, *args, **kwargs): + # TODO: When this is implemented we also need to update the + # Tqdm flow to report back the callback + raise NotImplementedError() + + def tune_batch_size( + self, + config: ModelConfig | dict[str, Any], + training_set: RayDataset, + **kwargs, + ) -> int: + return ray.get( + tune_batch_size_fn.options(num_cpus=self.num_cpus, num_gpus=self.num_gpus).remote( + dataset=training_set, + data_loader_kwargs=self.data_loader_kwargs, + executable_kwargs=self.executable_kwargs, + model=ray.put(self.model), + ludwig_config=config, + training_set_metadata=training_set.training_set_metadata, + features=training_set.features, + **kwargs, + ) + ) + + def tune_learning_rate(self, config, training_set: RayDataset, **kwargs) -> float: + return ray.get( + tune_learning_rate_fn.options(num_cpus=self.num_cpus, num_gpus=self.num_gpus).remote( + dataset=training_set, + config=config, + data_loader_kwargs=self.data_loader_kwargs, + executable_kwargs=self.executable_kwargs, + model=ray.put(self.model), + training_set_metadata=training_set.training_set_metadata, + features=training_set.features, + **kwargs, + ) + ) + + @property + def validation_field(self): + return self._validation_field + + @property + def validation_metric(self): + return self._validation_metric + + @property + def config(self) -> ECDTrainerConfig: + return self.executable_kwargs["config"] + + @property + def batch_size(self) -> int: + return self.config.batch_size + + @batch_size.setter + def batch_size(self, value: int): + self.config.batch_size = value + + @property + def eval_batch_size(self) -> int: + return self.config.eval_batch_size if self.config.eval_batch_size is not None else self.config.batch_size + + @eval_batch_size.setter + def eval_batch_size(self, value: int): + self.config.eval_batch_size = value + + @property + def resources_per_worker(self) -> dict[str, Any]: + trainer_kwargs = get_trainer_kwargs(**self.trainer_kwargs) + return trainer_kwargs.get("resources_per_worker", {}) + + @property + def num_cpus(self) -> int: + return self.resources_per_worker.get("CPU", 1) + + @property + def num_gpus(self) -> int: + return self.resources_per_worker.get("GPU", 0) + + def set_base_learning_rate(self, learning_rate: float): + self.config.learning_rate = learning_rate + + def shutdown(self): + pass + + +def eval_fn( + predictor_kwargs: dict[str, Any] | None = None, + model_ref: ObjectRef = None, + training_set_metadata: dict[str, Any] | None = None, + features: dict[str, dict] | None = None, + **kwargs, +): + """Ray Train worker function for distributed evaluation. + + Runs inside each Ray worker process (one per GPU/CPU resource slot). Each worker + receives a disjoint shard of the eval dataset from Ray Train's data-parallel split. + + ## Execution model + + This function is called by ``RayPredictor.batch_evaluation()`` via + ``run_train_remote()`` / ``TorchTrainer``. Before entering this function, Ray Train + has already called ``torch.distributed.init_process_group()``, so + ``torch.distributed.is_initialized()`` is ``True`` for the entire duration. + + ## Metric aggregation across workers + + Each worker calls ``predictor.batch_evaluation(eval_shard)``, which accumulates + torchmetrics state for that worker's shard and then calls ``model.get_metrics()`` + → ``metric_fn.compute()``. + + torchmetrics wraps ``compute()`` in its own ``sync_context()`` call. Ludwig + overrides ``LudwigMetric.sync_context()`` (``ludwig/modules/metric_modules.py``) so + that, when the registered Ludwig strategy provides no gather function *but* + ``torch.distributed`` is already initialized, it falls back to + ``torchmetrics.utilities.distributed.gather_all_tensors``. This all-gathers the + accumulator state across every worker before ``compute()`` runs, so the final metric + value reflects the **full** dataset rather than just one shard. + + We register ``LocalStrategy`` (not ``AccelerateStrategy``) here because we only + need metric sync — we do NOT want Accelerate to wrap the model or optimizer for a + pure-inference pass. + + ## Checkpoint / result handoff + + Only rank-0 writes the checkpoint that the driver reads back. If every worker called + ``rt.report(checkpoint=...)`` with the same filename, Ray Train (≥ 2.3) would merge + the checkpoint directories by *concatenating* files of the same name, producing + corrupted JSON (``json.decoder.JSONDecodeError: Extra data``). Non-zero workers still + call ``rt.report()`` (without a checkpoint) so Ray Train does not block waiting for + all workers to report. + """ + # Pin GPU before loading the model to prevent memory leaking onto other devices. + initialize_pytorch() + + # Register LocalStrategy so that get_current_dist_strategy() doesn't raise inside + # metric_modules. We use LocalStrategy rather than AccelerateStrategy because we + # don't want Accelerate to rewrap the model for this inference-only pass. + # Metric sync across workers is handled via the torch.distributed fallback in + # LudwigMetric.sync_context() — see the docstring there for details. + from ludwig.distributed import init_dist_strategy + + init_dist_strategy("local") + + try: + eval_shard = RayDatasetShard( + rt.get_dataset_shard("eval"), + features, + training_set_metadata, + ) + + model = ray.get(model_ref) + # Use Ray Train's device assignment which respects use_gpu setting + from ray.train.torch import get_device as ray_get_device + + device = ray_get_device() + model = model.to(device) + + predictor_cls = get_predictor_cls(model.type()) + predictor = predictor_cls(dist_model=model, model=model, report_tqdm_to_ray=True, **predictor_kwargs) + eval_results = predictor.batch_evaluation(eval_shard, **kwargs) + + # Save results to a checkpoint so the driver can retrieve them. + # Eval results are metrics dicts (no tensors), so we save as JSON where possible + # and fall back to torch.save for objects JSON can't handle. + # Only rank-0 writes and reports the checkpoint. If every worker calls + # rt.report(checkpoint=...) with a file of the same name, Ray Train + # merges the checkpoint directories and concatenates the files, producing + # invalid JSON. Non-zero workers still call rt.report() (without a + # checkpoint) so that Ray Train does not block waiting for all workers. + world_rank = rt.get_context().get_world_rank() + eval_results = _make_picklable(eval_results) + if world_rank == 0: + with tempfile.TemporaryDirectory() as tmpdir: + try: + import json + + # Try JSON first (no pickle, secure) + with open(os.path.join(tmpdir, "eval_results.json"), "w") as f: + json.dump(eval_results, f, default=str) + except (TypeError, ValueError): + # Fall back to torch.save for complex objects + torch.save(eval_results, os.path.join(tmpdir, "eval_results.pt")) + rt.report(metrics={}, checkpoint=Checkpoint.from_directory(tmpdir)) + else: + rt.report(metrics={}) + finally: + torch.cuda.empty_cache() + + +class RayPredictor(BasePredictor): + def __init__( + self, model: BaseModel, df_engine: DataFrameEngine, trainer_kwargs, data_loader_kwargs, **predictor_kwargs + ): + self.batch_size = predictor_kwargs["batch_size"] + self.trainer_kwargs = trainer_kwargs + self.data_loader_kwargs = data_loader_kwargs + self.predictor_kwargs = predictor_kwargs + self.actor_handles = [] + self.model = model.cpu() + self.df_engine = df_engine + + def get_trainer_kwargs(self) -> dict[str, Any]: + return get_trainer_kwargs(**self.trainer_kwargs) + + def get_resources_per_worker(self) -> tuple[int, int]: + trainer_kwargs = self.get_trainer_kwargs() + resources_per_worker = trainer_kwargs.get("resources_per_worker", {}) + num_gpus = resources_per_worker.get("GPU", 0) + num_cpus = resources_per_worker.get("CPU", (1 if num_gpus == 0 else 0)) + return num_cpus, num_gpus + + def batch_predict(self, dataset: RayDataset, *args, collect_logits: bool = False, **kwargs): + self._check_dataset(dataset) + + predictor_kwargs = self.predictor_kwargs + output_columns = get_output_columns(self.model.output_features, include_logits=collect_logits) + batch_predictor = self.get_batch_infer_model( + self.model, + predictor_kwargs, + output_columns, + dataset.features, + dataset.training_set_metadata, + *args, + collect_logits=collect_logits, + **kwargs, + ) + + columns = [f.proc_column for f in self.model.input_features.values()] + + def to_tensors(df: pd.DataFrame) -> pd.DataFrame: + for c in columns: + df[c] = cast_as_tensor_dtype(df[c]) + return df + + num_cpus, num_gpus = self.get_resources_per_worker() + + from ludwig.data.dataframe.dask import tensor_extension_casting + + with tensor_extension_casting(False): + # Apply lazy-decode transforms before casting to tensors. Without this, + # lazy audio/image columns still contain file-path strings at predict time, + # causing cast_as_tensor_dtype to raise a TypeError. + ds = dataset._with_lazy_decode(dataset.ds) + predictions = ds.map_batches(to_tensors, batch_format="pandas").map_batches( + batch_predictor, + batch_size=self.batch_size, + compute=ray.data.ActorPoolStrategy(), + batch_format="pandas", + num_cpus=num_cpus, + num_gpus=num_gpus, + ) + predictions = self.df_engine.from_ray_dataset(predictions) + + return predictions + + def predict_single(self, batch): + raise NotImplementedError("predict_single can only be called on a local predictor") + + def batch_evaluation( + self, + dataset: RayDataset, + collect_predictions: bool = False, + collect_logits=False, + **kwargs, + ): + # We need to be in a distributed context to collect the aggregated metrics, since it relies on collective + # communication ops. However, distributed training is not suitable for transforming one big dataset to another. + # For that we will use Ray Datasets. Therefore, we break this up into two separate steps, and two passes over + # the dataset. In the future, we can explore ways to combine these into a single step to reduce IO. + # Collect eval metrics by distributing work across nodes / gpus + datasets = {"eval": dataset.to_ray_dataset(shuffle=False)} + predictor_kwargs = { + **self.predictor_kwargs, + "collect_predictions": False, + } + eval_loop_config = { + "predictor_kwargs": predictor_kwargs, + "model_ref": ray.put(self.model), + "training_set_metadata": dataset.training_set_metadata, + "features": dataset.features, + **kwargs, + } + + def _eval_loop(config): + eval_fn(**config) + + result = run_train_remote( + _eval_loop, + trainer_kwargs=self.trainer_kwargs, + datasets=datasets, + train_loop_config=eval_loop_config, + ) + + # Load eval results from the checkpoint saved by eval_fn + with result.checkpoint.as_directory() as tmpdir: + json_path = os.path.join(tmpdir, "eval_results.json") + pt_path = os.path.join(tmpdir, "eval_results.pt") + if os.path.exists(json_path): + import json + + with open(json_path) as f: + eval_results = json.load(f) + eval_stats = eval_results[0] if isinstance(eval_results, (list, tuple)) else eval_results + else: + eval_stats, _ = torch.load(pt_path, weights_only=False) + + predictions = None + if collect_predictions: + # Collect eval predictions by using Ray Datasets to transform partitions of the data in parallel + predictions = self.batch_predict(dataset, collect_logits=collect_logits) + + return eval_stats, predictions + + def batch_collect_activations(self, model, *args, **kwargs): + raise NotImplementedError("Ray backend does not support collecting activations at this time.") + + def _check_dataset(self, dataset): + if not isinstance(dataset, RayDataset): + raise RuntimeError(f"Ray backend requires RayDataset for inference, found: {type(dataset)}") + + def shutdown(self): + for handle in self.actor_handles: + ray.kill(handle) + self.actor_handles.clear() + + def get_batch_infer_model( + self, + model: "LudwigModel", + predictor_kwargs: dict[str, Any], + output_columns: list[str], + features: dict[str, dict], + training_set_metadata: dict[str, Any], + *args, + **kwargs, + ): + model_ref = ray.put(model) + _, num_gpus = self.get_resources_per_worker() + + class BatchInferModel: + def __init__(self): + model = ray.get(model_ref) + # Respect the GPU setting from resources_per_worker. + # When num_gpus=0, force CPU even if CUDA is available on the machine, + # to avoid device mismatches between model outputs and targets. + if num_gpus > 0: + device = get_torch_device() + else: + device = "cpu" + self.model = model.to(device) + + self.output_columns = output_columns + self.features = features + self.training_set_metadata = training_set_metadata + self.reshape_map = { + f[PROC_COLUMN]: training_set_metadata[f[NAME]].get("reshape") for f in features.values() + } + predictor_cls = get_predictor_cls(self.model.type()) + predictor = predictor_cls(dist_model=self.model, model=self.model, **predictor_kwargs) + self.predict = partial(predictor.predict_single, *args, **kwargs) + + def __call__(self, df: pd.DataFrame) -> pd.DataFrame: + dataset = self._prepare_batch(df) + predictions = self.predict(batch=dataset).set_index(df.index) + ordered_predictions = predictions[self.output_columns] + return ordered_predictions + + def _prepare_batch(self, batch: pd.DataFrame) -> dict[str, np.ndarray]: + res = {} + for c in self.features: + if self.features[c][TYPE] not in _SCALAR_TYPES: + # Ensure columns stacked instead of turned into np.array([np.array, ...], dtype=object) objects + res[c] = np.stack(batch[c].values) + else: + res[c] = batch[c].to_numpy() + + for c in self.features: + reshape = self.reshape_map.get(c) + if reshape is not None: + res[c] = res[c].reshape((-1, *reshape)) + + return res + + return BatchInferModel + + +class RayBackend(RemoteTrainingMixin, Backend): + BACKEND_TYPE = "ray" + capabilities = BackendCapabilities(distributed=True, hyperopt=True, cache_preprocessing=True) + + def __init__(self, processor=None, trainer=None, loader=None, preprocessor_kwargs=None, **kwargs): + super().__init__(dataset_manager=RayDatasetManager(self), **kwargs) + self._preprocessor_kwargs = preprocessor_kwargs or {} + self._df_engine = _get_df_engine(processor) + self._distributed_kwargs = trainer or {} + self._pytorch_kwargs = {} + self._data_loader_kwargs = loader or {} + self._preprocessor_pg = None + + def initialize(self): + initialize_ray() + + dask.config.set(scheduler=ray_dask_get) + # Disable placement groups on dask + dask.config.set(annotations={"ray_remote_args": {"placement_group": None}}) + # Prevent Dask from converting object-dtype columns to PyArrow strings, + # which corrupts binary data, numpy arrays, and complex Python objects. + dask.config.set({"dataframe.convert-string": False}) + # Use in-memory task-based shuffle instead of disk (partd). partd creates + # a temp directory per shuffle and its __del__ rmtrees it; under Ray's + # parallel task execution multiple workers can race on the same temp dir, + # causing FileNotFoundError on the .lock file. + dask.config.set({"dataframe.shuffle.method": "tasks"}) + + def generate_bundles(self, num_cpu): + # Ray requires that each bundle be scheduleable on a single node. + # So a bundle of 320 cpus would never get scheduled. For now a simple heuristic + # to be used is to just request 1 cpu at a time. + return [{"CPU": 1} for _ in range(int(num_cpu))] + + @contextlib.contextmanager + def provision_preprocessing_workers(self): + num_cpu = self._preprocessor_kwargs.get("num_cpu") + if not num_cpu: + logger.info( + "Backend config has num_cpu not set. provision_preprocessing_workers() is a no-op in this case." + ) + yield + else: + bundles = self.generate_bundles(num_cpu) + logger.info("Requesting bundles of %s for preprocessing", bundles) + self._preprocessor_pg = placement_group(bundles) + ready = self._preprocessor_pg.wait(FIFTEEN_MINS_IN_S) + + if not ready: + remove_placement_group(self._preprocessor_pg) + raise TimeoutError( + "Ray timed out in provisioning the placement group for preprocessing." + f" {num_cpu} CPUs were requested but were unable to be provisioned." + ) + + logger.info("%s CPUs were requested and successfully provisioned", num_cpu) + try: + with dask.config.set(annotations={"ray_remote_args": {"placement_group": self._preprocessor_pg}}): + yield + finally: + self._release_preprocessing_workers() + + def _release_preprocessing_workers(self): + if self._preprocessor_pg is not None: + remove_placement_group(self._preprocessor_pg) + self._preprocessor_pg = None + + def initialize_pytorch(self, **kwargs): + # Make sure we don't claim any GPU resources on the head node + initialize_pytorch(gpus=-1) + self._pytorch_kwargs = kwargs + + def create_trainer(self, model: BaseModel, **kwargs) -> "BaseTrainer": + executable_kwargs = {**kwargs, **self._pytorch_kwargs} + if model.type() == MODEL_LLM: + from ludwig.trainers.registry import get_llm_ray_trainers_registry + + trainer_config = kwargs.get("config") + trainer_type = trainer_config.type if trainer_config else None + trainer_cls = get_from_registry(trainer_type, get_llm_ray_trainers_registry()) + else: + trainer_cls = get_from_registry(model.type(), get_ray_trainers_registry()) + + # Deep copy to workaround https://github.com/ray-project/ray/issues/24139 + all_kwargs = { + "model": model, + "trainer_kwargs": copy.deepcopy(self._distributed_kwargs), + "data_loader_kwargs": self._data_loader_kwargs, + "executable_kwargs": executable_kwargs, + } + all_kwargs.update(kwargs) + return trainer_cls(**all_kwargs) + + def create_predictor(self, model: BaseModel, **kwargs): + executable_kwargs = {**kwargs, **self._pytorch_kwargs} + return RayPredictor( + model, + self.df_engine, + copy.deepcopy(self._distributed_kwargs), + self._data_loader_kwargs, + **executable_kwargs, + ) + + def set_distributed_kwargs(self, **kwargs): + self._distributed_kwargs = kwargs + + @property + def df_engine(self): + return self._df_engine + + @property + def supports_multiprocessing(self): + return False + + def check_lazy_load_supported(self, feature): + if not feature[PREPROCESSING]["in_memory"]: + raise ValueError( + f"RayBackend does not support lazy loading of data files at train time. " + f"Set preprocessing config `in_memory: True` for feature {feature[NAME]}" + ) + + def read_binary_files(self, column: Series, map_fn: Callable | None = None, file_size: int | None = None) -> Series: + column = column.fillna(np.nan).replace([np.nan], [None]) # normalize NaNs to None + + # Assume that the list of filenames is small enough to fit in memory. Should be true unless there + # are literally billions of filenames. + # TODO(travis): determine if there is a performance penalty to passing in individual files instead of + # a directory. If so, we can do some preprocessing to determine if it makes sense to read the full directory + # then filter out files as a postprocessing step (depending on the ratio of included to excluded files in + # the directory). Based on a preliminary look at how Ray handles directory expansion to files, it looks like + # there should not be any difference between providing a directory versus a list of files. + pd_column = self.df_engine.compute(column) + fnames = pd_column.values.tolist() + idxs = pd_column.index.tolist() + + # Sample a filename to extract the filesystem info + sample_fname = fnames[0] + if isinstance(sample_fname, str): + fs, _ = get_fs_and_path(sample_fname) + filesystem = PyFileSystem(FSSpecHandler(fs)) + + paths_and_idxs = list(zip(fnames, idxs)) + ds = read_binary_files_with_index(paths_and_idxs, filesystem=filesystem) + # Rename "data" column to "value" for downstream compatibility + ds = ds.rename_columns({"data": "value"}) + else: + # Assume the path has already been read in, so just convert directly to a dataset + # Name the column "value" to match the behavior of the above + column_df = column.to_frame(name="value") + column_df["idx"] = column_df.index + ds = self.df_engine.to_ray_dataset(column_df) + + # Collect the Ray Dataset to pandas to avoid Arrow's string coercion + # for binary/object columns (to_dask() converts bytes to string[pyarrow], + # corrupting binary data and complex Python objects). + pdf = ds.to_pandas() + + if map_fn is not None: + with use_credentials(conf): + pdf["value"] = pdf["value"].map(map_fn) + + pdf = pdf.rename(columns={"value": column.name}) + if "idx" in pdf.columns: + pdf = pdf.set_index("idx", drop=True) + pdf.index.name = column.index.name + + # Convert to Dask for downstream compatibility. + # Note: dataframe.convert-string is disabled globally in RayBackend.initialize() + # to prevent object-dtype columns from being coerced to PyArrow strings. + df = self.df_engine.from_pandas(pdf) + return df[column.name] + + @property + def num_nodes(self) -> int: + if not ray.is_initialized(): + return 1 + return len(ray.nodes()) + + @property + def num_training_workers(self) -> int: + return self._distributed_kwargs.get("num_workers", 1) + + def max_concurrent_trials(self, hyperopt_config) -> int | None: + # Limit concurrency based on available resources to avoid deadlocks between + # Ray Tune trials and the Ray Datasets used internally for distributed training. + resources = self.get_available_resources() + num_cpus_per_trial = self._distributed_kwargs.get("resources_per_worker", {}).get("CPU", 1) + num_workers = self._distributed_kwargs.get("num_workers", 1) + cpus_per_trial = num_cpus_per_trial * num_workers + if cpus_per_trial > 0 and resources.cpus > 0: + return max(1, int(resources.cpus // cpus_per_trial)) + return None + + def tune_batch_size(self, evaluator_cls, dataset_len: int) -> int: + evaluator = evaluator_cls() + return evaluator.select_best_batch_size(dataset_len) + + def batch_transform(self, df, batch_size: int, transform_fn, name: str | None = None): + name = name or "Batch Transform" + import dask.dataframe as dd + + from ludwig.utils.dataframe_utils import from_batches, to_batches + + # Compute Dask DataFrame to pandas before batching, as Dask-expr + # doesn't support row slicing via integer indexing (df[i:j]). + npartitions = df.npartitions if hasattr(df, "npartitions") else 1 + df = self.df_engine.compute(df) + batches = to_batches(df, batch_size) + transform = transform_fn() + out_batches = [transform(batch.reset_index(drop=True)) for batch in batches] + out_df = from_batches(out_batches).reset_index(drop=True) + # Convert back to Dask so downstream code (split, etc.) still works + return dd.from_pandas(out_df, npartitions=max(1, npartitions)) + + def get_available_resources(self) -> Resources: + resources = ray.cluster_resources() + return Resources(cpus=resources.get("CPU", 0), gpus=resources.get("GPU", 0)) + + +def initialize_ray(): + if not ray.is_initialized(): + try: + ray.init("auto", ignore_reinit_error=True) + except ConnectionError: + init_ray_local() + + +def init_ray_local(): + logger.info("Initializing new Ray cluster...") + ray.init(ignore_reinit_error=True) diff --git a/ludwig/backend/utils/__init__.py b/ludwig/backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/backend/utils/storage.py b/ludwig/backend/utils/storage.py new file mode 100644 index 0000000..212a272 --- /dev/null +++ b/ludwig/backend/utils/storage.py @@ -0,0 +1,69 @@ +import contextlib +from typing import Any + +from ludwig.utils import data_utils + +CredInputs = str | dict[str, Any] | None + + +DEFAULTS = "defaults" +ARTIFACTS = "artifacts" +DATASETS = "datasets" +CACHE = "cache" + + +class Storage: + def __init__(self, creds: dict[str, Any] | None): + self._creds = creds + + @contextlib.contextmanager + def use_credentials(self): + with data_utils.use_credentials(self._creds): + yield + + @property + def credentials(self) -> dict[str, Any] | None: + return self._creds + + +class StorageManager: + def __init__( + self, + defaults: CredInputs = None, + artifacts: CredInputs = None, + datasets: CredInputs = None, + cache: CredInputs = None, + ): + defaults = load_creds(defaults) + cred_inputs = { + DEFAULTS: defaults, + ARTIFACTS: load_creds(artifacts), + DATASETS: load_creds(datasets), + CACHE: load_creds(cache), + } + + self.storages = {k: Storage(v if v is not None else defaults) for k, v in cred_inputs.items()} + + @property + def defaults(self) -> Storage: + return self.storages[DEFAULTS] + + @property + def artifacts(self) -> Storage: + """TODO(travis): Currently used for hyperopt, but should be used for all outputs.""" + return self.storages[ARTIFACTS] + + @property + def datasets(self) -> Storage: + """TODO(travis): Should be used to read in datasets.""" + return self.storages[DATASETS] + + @property + def cache(self) -> Storage: + return self.storages[CACHE] + + +def load_creds(cred: CredInputs) -> dict[str, Any]: + if isinstance(cred, str): + cred = data_utils.load_json(cred) + return cred diff --git a/ludwig/benchmarking/README.md b/ludwig/benchmarking/README.md new file mode 100644 index 0000000..6f10e0b --- /dev/null +++ b/ludwig/benchmarking/README.md @@ -0,0 +1,276 @@ +# Ludwig Benchmarking + +### Some use cases + +- Regression testing for ML experiments across releases and PRs. +- Model performance testing for experimenting with new features and hyperparameters. +- Resource usage tracking for the full ML pipeline. + +## Ludwig benchmarking CLI and API + +To run benchmarks, run the following command from the command line + +``` +ludwig benchmark --benchmarking_config path/to/benchmarking/config.yaml +``` + +To use the API + +``` +from ludwig.benchmarking.benchmark import benchmark + +benchmarking_config_path = "path/to/benchmarking/config.yaml" +benchmark(benchmarking_config_path) +``` + +In what follows, we describe what the benchmarking config looks for +multiple use cases. + +## The benchmarking config + +The benchmarking config is where you can specify + +1. The datasets you want to run the benchmarks on and their configs. +1. Whether these experiments are hyperopt or regular train and eval experiments. +1. The name of the experiment. +1. A python script to edit the specified Ludwig configs programmatically/on the fly. +1. The export path of these experiment's artifacts. (remotely or locally) +1. Whether to use `LudwigProfiler` to track resource + usage for preprocessing, training, and evaluation of the experiment. + +You can find an example of a benchmarking config in the `examples/` directory. + +## Basic Usage + +Say you implemented a new feature and would like to test it on several datasets. +In this case, this is what the benchmarking config could look like + +``` +experiment_name: SMOTE_test +hyperopt: false +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +experiments: + - dataset_name: ames_housing + config_path: /home/ray/configs/ames_housing_SMOTE.yaml + experiment_name: SMOTE_test_with_hyperopt + hyperopt: true + - dataset_name: protein + - ... + ... + - dataset_name: mercedes_benz_greener + config_path: /home/ray/configs/mercedes_benz_greener_SMOTE.yaml +``` + +For each experiment: + +- `dataset_name`: name of the dataset in `ludwig.datasets` to run the benchmark on. +- `config_path` (optional): path to Ludwig config. If not specified, this will load + the config corresponding to the dataset only containing `input_features` and + `output_features`. + +This will run `LudwigModel.experiment` on the datasets with their specified configs. +If these configs contain a hyperopt section and you'd like to run hyperopt, change +to `hyperopt: true`. +You can specify the same dataset multiple times with different configs. + +**Exporting artifacts** +By specifying `export_artifacts: true`, this will export the experiment artifacts +to the `export_base_path`. Once the model is trained and the artifacts are pushed +to the specified path, you will get a similar message to the following: + +``` +Uploaded metrics report and experiment config to + s3://benchmarking.us-west-2.ludwig.com/bench/ames_housing/SMOTE_test +``` + +This is the directory structure of the exported artifacts for one of the experiments. + +``` +s3://benchmarking.us-west-2.ludwig.com/bench/ +└── ames_housing + └── SMOTE_test + ├── config.yaml + └── experiment_run + ├── description.json + ├── model + │   ├── logs + │   │   ├── test + │   │   │   └── events.out.tfevents.1663320893.macbook-pro.lan.8043.2 + │   │   ├── training + │   │   │   └── events.out.tfevents.1663320893.macbook-pro.lan.8043.0 + │   │   └── validation + │   │   └── events.out.tfevents.1663320893.macbook-pro.lan.8043.1 + │   ├── model_hyperparameters.json + │   ├── training_progress.json + │   └── training_set_metadata.json + ├── test_statistics.json + └── training_statistics.json +``` + +Note that model checkpoints are not exported. Any other experiments on +the `ames_housing` dataset will also live under +`s3://benchmarking.us-west-2.ludwig.com/bench/ames_housing/` + +**Overriding parameters** +The benchmarking config's global parameters `experiment_name` and `hyperopt` can be overridden +if specified within an experiment. + +## Programmatically editing Ludwig configs + +To apply some changes to multiple Ludwig configs, you can specify a path to a python script +that does this without the need to do manual modifications across many configs. Example: + +``` +experiment_name: logistic_regression_hyperopt +hyperopt: true +process_config_file_path: /home/ray/process_config.py +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +experiments: + - dataset_name: ames_housing + config_path: /home/ray/configs/ames_housing_SMOTE.yaml + ... +``` + +In `/home/ray/process_config.py`, define the following function and add custom code to modify +ludwig configs + +``` +def process_config(ludwig_config: dict, experiment_dict: dict) -> dict: + """Modify a Ludwig config. + + :param ludwig_config: a Ludwig config. + :param experiment_dict: a benchmarking config experiment dictionary. + + returns: a modified Ludwig config. + """ + + # code to modify the Ludwig config. + + return ludwig_config +``` + +View the `examples/` folder for an example `process_config.py`. + +## Benchmarking the resource usage with `LudwigProfiler` + +To benchmark the resource usage of the preprocessing, training, and evaluation +steps of `LudwigModel.experiment`, you can specify in the benchmarking config +global parameters + +``` +profiler: + enable: true + use_torch_profiler: false + logging_interval: 0.1 +``` + +- `enable: true` will run benchmarking with `LudwigProfiler`. +- `use_torch_profiler: false` will skip using the torch profiler. +- `logging_interval: 0.1` will instruct `LudwigProfiler` to collect + resource usage information every 0.1 seconds. + +Note that profiling is only enabled in the case where `hyperopt: false`. +`LudwigProfiler` is passed in to `LudwigModel` callbacks. The specific +callbacks that will be called are: + +- `on_preprocess_(start/end)` +- `on_train_(start/end)` +- `on_evaluation_(start/end)` + +This is an example directory output when using the profiler: + +``` +full_bench_with_profiler_with_torch +├── config.yaml +├── experiment_run +├── system_resource_usage +│   ├── evaluation +│   │   └── run_0.json +│   ├── preprocessing +│   │   └── run_0.json +│   └── training +│   └── run_0.json +└── torch_ops_resource_usage + ├── evaluation + │   └── run_0.json + ├── preprocessing + │   └── run_0.json + └── training + └── run_0.json +``` + +The only difference is the `system_resource_usage` and `torch_ops_resource_usage`. +The difference between these two outputs can be found in the `LudwigProfiler` README. + +## Parameters and defaults + +Each of these parameters can also be specified in the experiments section to override the global value. +If not specified, the value of the global parameter will be propagated to the experiments. + +- `experiment_name` (required): name of the benchmarking run. +- `export` (required): dictionary specifying whether to export the experiment artifacts and the export path. +- `hyperopt` (optional): whether this is a hyperopt run or `LudwigModel.experiment`. +- `process_config_file_path` (optional): path to python script that will modify configs. +- `profiler` (optional): dictionary specifying whether to use the profiler and its parameters. + +## Comparing experiments + +You can summarize the exported artifacts of two experiments on multiple datasets. +For example, if you ran two experiments on the datasets `ames_housing` called +`small_batch_size` and `big_batch_size` where you varied the batch size, +you can create a diff summary of the model performance and resource usage of the two +experiments. This is how: + +``` +from ludwig.benchmarking.summarize import summarize_metrics + +dataset_list, metric_diffs, resource_usage_diffs = summarize_metrics( + bench_config_path = "path/to/benchmarking_config.yaml", + base_experiment = "small_batch_size", + experimental_experiment = "big_batch_size", + download_base_path = "s3://benchmarking.us-west-2.ludwig.com/bench/") +``` + +This will print + +``` +Model performance metrics for *small_batch_size* vs. *big_batch_size* on dataset *ames_housing* +Output Feature Name Metric Name small_batch_size big_batch_size Diff Diff Percentage +SalePrice mean_absolute_error 180551.609 180425.109 -126.5 -0.07 +SalePrice mean_squared_error 38668763136.0 38618021888.0 -50741248.0 -0.131 +SalePrice r2 -5.399 -5.391 0.008 -0.156 +SalePrice root_mean_squared_error 196643.75 196514.688 -129.062 -0.066 +SalePrice root_mean_squared_percentage_error 1.001 1.001 -0.001 -0.07 +Exported a CSV report to summarize_output/performance_metrics/ames_housing/small_batch_size-big_batch_size.csv + +Resource usage for *small_batch_size* vs. *big_batch_size* on *training* of dataset *ames_housing* +Metric Name small_batch_size big_batch_size Diff Diff Percentage +average_cpu_memory_usage 106.96 Mb 109.43 Mb 2.48 Mb 2.315 +average_cpu_utilization 1.2966666666666666 1.345 0.04833333333333334 3.728 +average_global_cpu_memory_available 3.46 Gb 3.46 Gb -1.10 Mb -0.031 +average_global_cpu_utilization 37.43333333333334 40.49 3.056666666666665 8.166 +disk_footprint 372736 413696 40960 10.989 +max_cpu_memory_usage 107.50 Mb 111.93 Mb 4.43 Mb 4.117 +max_cpu_utilization 1.44 1.67 0.22999999999999998 15.972 +max_global_cpu_utilization 54.1 60.9 6.799999999999997 12.569 +min_global_cpu_memory_available 3.46 Gb 3.46 Gb -712.00 Kb -0.02 +num_cpu 10 10 0 0.0 +num_oom_events 0 0 0 inf +num_runs 1 1 0 0.0 +torch_cpu_average_memory_used 81.44 Kb 381.15 Kb 299.70 Kb 367.992 +torch_cpu_max_memory_used 334.26 Kb 2.65 Mb 2.32 Mb 711.877 +torch_cpu_time 57.400ms 130.199ms 72.799ms 126.828 +torch_cuda_time 0.000us 0.000us 0.000us inf +total_cpu_memory_size 32.00 Gb 32.00 Gb 0 b 0.0 +total_execution_time 334.502ms 1.114s 779.024ms 232.891 +Exported a CSV report to summarize_output/resource_usage_metrics/ames_housing/training-small_batch_size-big_batch_size.csv + +Resource usage for *small_batch_size* vs. *big_batch_size* on *evaluation* of dataset *ames_housing* +... +Resource usage for *small_batch_size* vs. *big_batch_size* on *preprocessing* of dataset *ames_housing* +... +``` diff --git a/ludwig/benchmarking/__init__.py b/ludwig/benchmarking/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/benchmarking/artifacts.py b/ludwig/benchmarking/artifacts.py new file mode 100644 index 0000000..dcaa129 --- /dev/null +++ b/ludwig/benchmarking/artifacts.py @@ -0,0 +1,66 @@ +import os +from dataclasses import dataclass +from typing import Any + +from ludwig.globals import MODEL_FILE_NAME +from ludwig.types import ModelConfigDict, TrainingSetMetadataDict +from ludwig.utils.data_utils import load_json, load_yaml + + +@dataclass +class BenchmarkingResult: + # The Ludwig benchmarking config. + benchmarking_config: dict[str, Any] + + # The config for one experiment. + experiment_config: dict[str, Any] + + # The Ludwig config used to run the experiment. + ludwig_config: ModelConfigDict + + # The python script that is used to process the config before being used. + process_config_file: str + + # Loaded `description.json` file. + description: dict[str, Any] + + # Loaded `test_statistics.json` file. + test_statistics: dict[str, Any] + + # Loaded `training_statistics.json` file. + training_statistics: dict[str, Any] + + # Loaded `model_hyperparameters.json` file. + model_hyperparameters: dict[str, Any] + + # Loaded `training_progress.json` file. + training_progress: dict[str, Any] + + # Loaded `training_set_metadata.json` file. + training_set_metadata: TrainingSetMetadataDict + + +def build_benchmarking_result(benchmarking_config: dict, experiment_idx: int): + experiment_config = benchmarking_config["experiments"][experiment_idx] + process_config_file = "" + if experiment_config["process_config_file_path"]: + with open(experiment_config["process_config_file_path"]) as f: + process_config_file = "".join(f.readlines()) + experiment_run_path = os.path.join(experiment_config["experiment_name"], "experiment_run") + + return BenchmarkingResult( + benchmarking_config=benchmarking_config, + experiment_config=experiment_config, + ludwig_config=load_yaml(experiment_config["config_path"]), + process_config_file=process_config_file, + description=load_json(os.path.join(experiment_run_path, "description.json")), + test_statistics=load_json(os.path.join(experiment_run_path, "test_statistics.json")), + training_statistics=load_json(os.path.join(experiment_run_path, "training_statistics.json")), + model_hyperparameters=load_json( + os.path.join(experiment_run_path, MODEL_FILE_NAME, "model_hyperparameters.json") + ), + training_progress=load_json(os.path.join(experiment_run_path, MODEL_FILE_NAME, "training_progress.json")), + training_set_metadata=load_json( + os.path.join(experiment_run_path, MODEL_FILE_NAME, "training_set_metadata.json") + ), + ) diff --git a/ludwig/benchmarking/benchmark.py b/ludwig/benchmarking/benchmark.py new file mode 100644 index 0000000..d62b492 --- /dev/null +++ b/ludwig/benchmarking/benchmark.py @@ -0,0 +1,155 @@ +import argparse +import importlib +import logging +import os +import shutil +from typing import Any + +import ludwig.datasets +from ludwig.api import LudwigModel +from ludwig.benchmarking.artifacts import BenchmarkingResult, build_benchmarking_result +from ludwig.benchmarking.profiler_callbacks import LudwigProfilerCallback +from ludwig.benchmarking.utils import ( + create_default_config, + delete_hyperopt_outputs, + delete_model_checkpoints, + export_artifacts, + load_from_module, + populate_benchmarking_config_with_defaults, + propagate_global_parameters, + save_yaml, + validate_benchmarking_config, +) +from ludwig.contrib import add_contrib_callback_args +from ludwig.hyperopt.run import hyperopt +from ludwig.utils.data_utils import load_yaml + +logger = logging.getLogger() + + +def setup_experiment(experiment: dict[str, str]) -> dict[Any, Any]: + """Set up the backend and load the Ludwig config. + + Args: + experiment: dictionary containing the dataset name, config path, and experiment name. + Returns a Ludwig config. + """ + shutil.rmtree(os.path.join(experiment["experiment_name"]), ignore_errors=True) + if "config_path" not in experiment: + experiment["config_path"] = create_default_config(experiment) + model_config = load_yaml(experiment["config_path"]) + + if experiment["process_config_file_path"]: + process_config_spec = importlib.util.spec_from_file_location( + "process_config_file_path.py", experiment["process_config_file_path"] + ) + process_module = importlib.util.module_from_spec(process_config_spec) + process_config_spec.loader.exec_module(process_module) + model_config = process_module.process_config(model_config, experiment) + experiment["config_path"] = experiment["config_path"].replace( + ".yaml", "-" + experiment["experiment_name"] + "-modified.yaml" + ) + save_yaml(experiment["config_path"], model_config) + + return model_config + + +def benchmark_one(experiment: dict[str, str | dict[str, str]]) -> None: + """Run a Ludwig exepriment and track metrics given a dataset name. + + Args: + experiment: dictionary containing the dataset name, config path, and experiment name. + """ + logger.info(f"\nRunning experiment *{experiment['experiment_name']}* on dataset *{experiment['dataset_name']}*") + + # configuring backend and paths + model_config = setup_experiment(experiment) + + # loading dataset + # dataset_module = importlib.import_module(f"ludwig.datasets.{experiment['dataset_name']}") + dataset_module = ludwig.datasets.get_dataset(experiment["dataset_name"]) + dataset = load_from_module(dataset_module, model_config["output_features"][0]) + + if experiment["hyperopt"]: + # run hyperopt + hyperopt( + config=model_config, + dataset=dataset, + output_directory=experiment["experiment_name"], + skip_save_model=True, + skip_save_training_statistics=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + skip_save_unprocessed_output=True, + skip_save_predictions=True, + skip_save_training_description=True, + hyperopt_log_verbosity=0, + ) + delete_hyperopt_outputs(experiment["experiment_name"]) + else: + backend = None + ludwig_profiler_callbacks = None + if experiment["profiler"]["enable"]: + ludwig_profiler_callbacks = [LudwigProfilerCallback(experiment)] + # Currently, only local backend is supported with LudwigProfiler. + backend = "local" + logger.info("Currently, only local backend is supported with LudwigProfiler.") + # run model and capture metrics + model = LudwigModel( + config=model_config, callbacks=ludwig_profiler_callbacks, logging_level=logging.ERROR, backend=backend + ) + model.experiment( + dataset=dataset, + output_directory=experiment["experiment_name"], + skip_save_processed_input=True, + skip_save_unprocessed_output=True, + skip_save_predictions=True, + skip_collect_predictions=True, + ) + delete_model_checkpoints(experiment["experiment_name"]) + + +def benchmark(benchmarking_config: dict[str, Any] | str) -> dict[str, tuple[BenchmarkingResult, Exception]]: + """Launch benchmarking suite from a benchmarking config. + + Args: + benchmarking_config: config or config path for the benchmarking tool. Specifies datasets and their + corresponding Ludwig configs, as well as export options. + """ + if isinstance(benchmarking_config, str): + benchmarking_config = load_yaml(benchmarking_config) + validate_benchmarking_config(benchmarking_config) + benchmarking_config = populate_benchmarking_config_with_defaults(benchmarking_config) + benchmarking_config = propagate_global_parameters(benchmarking_config) + + experiment_artifacts = {} + for experiment_idx, experiment in enumerate(benchmarking_config["experiments"]): + dataset_name = experiment["dataset_name"] + try: + benchmark_one(experiment) + experiment_artifacts[dataset_name] = (build_benchmarking_result(benchmarking_config, experiment_idx), None) + except Exception as e: + logger.exception( + f"Experiment *{experiment['experiment_name']}* on dataset *{experiment['dataset_name']}* failed" + ) + experiment_artifacts[dataset_name] = (None, e) + finally: + if benchmarking_config["export"]["export_artifacts"]: + export_base_path = benchmarking_config["export"]["export_base_path"] + export_artifacts(experiment, experiment["experiment_name"], export_base_path) + return experiment_artifacts + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script runs a ludwig experiment on datasets specified in the benchmark config and exports " + "the experiment artifact for each of the datasets following the export parameters specified in" + "the benchmarking config.", + prog="ludwig benchmark", + usage="%(prog)s [options]", + ) + parser.add_argument("--benchmarking_config", type=str, help="The benchmarking config.") + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + benchmark(args.benchmarking_config) diff --git a/ludwig/benchmarking/examples/benchmarking_config.yaml b/ludwig/benchmarking/examples/benchmarking_config.yaml new file mode 100644 index 0000000..9e87580 --- /dev/null +++ b/ludwig/benchmarking/examples/benchmarking_config.yaml @@ -0,0 +1,45 @@ +experiment_name: example_benchmarking_run +hyperopt: false +process_config_file_path: /home/ray/process_config.py +profiler: + enable: true + use_torch_profiler: false + logging_interval: 0.1 +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +experiments: + - dataset_name: ames_housing + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/ames_housing.yaml + - dataset_name: protein + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/protein.yaml + - dataset_name: mercedes_benz_greener + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/mercedes_benz_greener.yaml + - dataset_name: santander_customer_satisfaction + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/santander_customer_satisfaction.yaml + - dataset_name: connect4 + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/connect4.yaml + - dataset_name: otto_group_product + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/otto_group_product.yaml + - dataset_name: bnp_claims_management + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/bnp_claims_management.yaml + - dataset_name: santander_customer_transaction + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/santander_customer_transaction.yaml + - dataset_name: allstate_claims_severity + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/allstate_claims_severity.yaml + - dataset_name: naval + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/naval.yaml + - dataset_name: sarcos + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/sarcos.yaml + - dataset_name: walmart_recruiting + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/walmart_recruiting.yaml + - dataset_name: numerai28pt6 + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/numerai28pt6.yaml + - dataset_name: adult_census_income + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/adult_census_income.yaml + - dataset_name: amazon_employee_access_challenge + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/amazon_employee_access_challenge.yaml + - dataset_name: forest_cover + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/forest_cover.yaml + - dataset_name: mushroom_edibility + config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/mushroom_edibility.yaml diff --git a/ludwig/benchmarking/examples/process_config.py b/ludwig/benchmarking/examples/process_config.py new file mode 100644 index 0000000..7182fe3 --- /dev/null +++ b/ludwig/benchmarking/examples/process_config.py @@ -0,0 +1,95 @@ +"""This function will take in a Ludwig config, strip away all its parameters except input and output featuresand +add some other parameters to run logistic regression hyperopt.""" + + +def process_config(ludwig_config: dict, experiment_dict: dict) -> dict: + """Modify a Ludwig config by programmatically adding elements to the config dictionary. + + The purpose is to apply changes for all datasets that are the same or are based on the + attributes of `experiment_dict` (e.g. dataset_name) removing the need to manually apply + small changes to configs on many datasets. + + Args: + ludwig_config: A Ludwig config. + experiment_dict: A benchmarking config experiment dictionary. + + Returns: + A modified Ludwig config. + """ + + # only keep input_features and output_features + main_config_keys = list(ludwig_config.keys()) + for key in main_config_keys: + if key not in ["input_features", "output_features"]: + del ludwig_config[key] + + temp = { + "preprocessing": {"split": {"type": "fixed"}}, + "trainer": {"epochs": 1024, "early_stop": 7, "eval_batch_size": 16384, "evaluate_training_set": False}, + "hyperopt": { + "goal": "maximize", + "output_feature": None, + "metric": None, + "split": "validation", + "parameters": { + "defaults.number.preprocessing.normalization": {"space": "choice", "categories": ["zscore", None]}, + "defaults.number.preprocessing.missing_value_strategy": { + "space": "choice", + "categories": ["fill_with_const", "fill_with_mean"], + }, + "combiner.type": {"space": "choice", "categories": ["tabnet", "concat"]}, + "trainer.learning_rate_scheduler.decay": {"space": "choice", "categories": [True, False]}, + "trainer.learning_rate": {"space": "loguniform", "lower": 0.0001, "upper": 0.1}, + "trainer.learning_rate_scheduler.decay_rate": {"space": "uniform", "lower": 0.4, "upper": 0.96}, + "trainer.batch_size": {"space": "randint", "lower": 32, "upper": 2048}, + }, + "search_alg": {"type": "variant_generator"}, + "executor": {"type": "ray", "num_samples": 1000}, + "scheduler": {"type": "bohb", "reduction_factor": 2}, + }, + } + + # add config parameters from temp + for key, value in temp.items(): + ludwig_config[key] = value + + dataset_name_to_metric = { + "ames_housing": "r2", + "mercedes_benz_greener": "r2", + "mushroom_edibility": "accuracy", + "amazon_employee_access_challenge": "roc_auc", + "naval": "r2", + "sarcos": "r2", + "protein": "r2", + "adult_census_income": "accuracy", + "otto_group_product": "accuracy", + "santander_customer_satisfaction": "accuracy", + "amazon_employee_access": "roc_auc", + "numerai28pt6": "accuracy", + "bnp_claims_management": "accuracy", + "allstate_claims_severity": "r2", + "santander_customer_transaction": "accuracy", + "connect4": "accuracy", + "forest_cover": "accuracy", + "ieee_fraud": "accuracy", + "porto_seguro_safe_driver": "accuracy", + "walmart_recruiting": "accuracy", + "poker_hand": "accuracy", + "higgs": "accuracy", + } + + # add hyperopt output feature and metric. + dataset_name = experiment_dict["dataset_name"] + ludwig_config["hyperopt"]["metric"] = dataset_name_to_metric[dataset_name] + ludwig_config["hyperopt"]["output_feature"] = ludwig_config["output_features"][0]["name"] + + # use sparse encoder for categorical features to mimic logistic regression. + for i, feature in enumerate(ludwig_config["input_features"]): + if feature["type"] == "category": + ludwig_config["input_features"][i]["encoder"] = "sparse" + for i, feature in enumerate(ludwig_config["output_features"]): + if feature["type"] == "category": + ludwig_config["output_features"][i]["encoder"] = "sparse" + + # make sure to return the ludwig_config + return ludwig_config diff --git a/ludwig/benchmarking/profiler.py b/ludwig/benchmarking/profiler.py new file mode 100644 index 0000000..a71e6f3 --- /dev/null +++ b/ludwig/benchmarking/profiler.py @@ -0,0 +1,268 @@ +import contextlib +import glob +import logging +import os +import shutil +import threading +import time +from queue import Empty as EmptyQueueException +from queue import Queue +from subprocess import PIPE, Popen +from typing import Any +from xml.etree.ElementTree import fromstring + +import psutil +import torch +from cpuinfo import get_cpu_info +from gpustat.core import GPUStatCollection + +from ludwig.benchmarking.profiler_dataclasses import profiler_dataclass_to_flat_dict, TorchProfilerMetrics +from ludwig.benchmarking.reporting import get_metrics_from_system_usage_profiler, get_metrics_from_torch_profiler +from ludwig.constants import LUDWIG_TAG +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.data_utils import save_json + +STOP_MESSAGE = "stop" +logger = logging.getLogger() + + +def get_gpu_info(): + """Gathers general hardware information about an nvidia GPU. + + This function was copied from `experiment_impact_tracker` to get around a Pandas 2.0 breaking change impacting the + package. https://github.com/Breakend/experiment-impact- + tracker/blob/master/experiment_impact_tracker/gpu/nvidia.py#L48-L73 + """ + p = Popen(["nvidia-smi", "-q", "-x"], stdout=PIPE) + outs, errors = p.communicate() + xml = fromstring(outs) + data = [] + driver_version = xml.findall("driver_version")[0].text + cuda_version = xml.findall("cuda_version")[0].text + + for _gpu_id, gpu in enumerate(xml.getiterator("gpu")): + gpu_data = {} + name = list(gpu.getiterator("product_name"))[0].text + memory_usage = gpu.findall("fb_memory_usage")[0] + total_memory = memory_usage.findall("total")[0].text + + gpu_data["name"] = name + gpu_data["total_memory"] = total_memory + gpu_data["driver_version"] = driver_version + gpu_data["cuda_version"] = cuda_version + data.append(gpu_data) + return data + + +def monitor(queue: Queue, info: dict[str, Any], logging_interval: int, cuda_is_available: bool) -> None: + """Monitors hardware resource use. + + Collects system specific metrics (CPU/CUDA, CPU/CUDA memory) at a `logging_interval` interval and pushes + results back to the parent process. + + Args: + queue: queue from which we can push and retrieve messages sent to the function targeted by the thread. + info: dictionary containing system resource usage information about the running process. + logging_interval: time interval at which we will poll the system for usage metrics. + cuda_is_available: stores torch.cuda.is_available(). + """ + info["global_cpu_memory_available"] = [psutil.virtual_memory().available] + info["global_cpu_utilization"] = [psutil.cpu_percent()] + # get the pid of the parent process. + tracked_process = psutil.Process(os.getpid()) + + # will return a meaningless 0 value on the first call because `interval` arg is set to None. + tracked_process.cpu_percent(interval=logging_interval) + with tracked_process.oneshot(): + info["cpu_utilization"] = [tracked_process.cpu_percent() / info["num_cpu"]] + info["cpu_memory_usage"] = [tracked_process.memory_full_info().uss] + try: + info["num_accessible_cpus"] = len(tracked_process.cpu_affinity()) + except (AttributeError, NotImplementedError): + pass + + while True: + try: + message = queue.get(block=False) + if isinstance(message, str): + if message == STOP_MESSAGE: + # synchronize CUDA to get accurate timing for jobs running on GPU. + if cuda_is_available: + torch.cuda.synchronize() + queue.put(info) + return + else: + queue.put(message) + except EmptyQueueException: + pass + if cuda_is_available: + gpu_infos = GPUStatCollection.new_query() + for i, gpu_info in enumerate(gpu_infos): + gpu_key = f"cuda_{i}" + info[f"{gpu_key}_memory_used"].append(gpu_info.memory_used) + with tracked_process.oneshot(): + info["cpu_utilization"].append(tracked_process.cpu_percent() / info["num_cpu"]) + info["cpu_memory_usage"].append(tracked_process.memory_full_info().uss) + info["global_cpu_memory_available"].append(psutil.virtual_memory().available) + info["global_cpu_utilization"].append(psutil.cpu_percent()) + time.sleep(logging_interval) + + +class LudwigProfiler(contextlib.ContextDecorator): + """Track system resource (hardware and software) usage. + + Warning: If `use_torch_profiler=True` while profiling on CUDA, it's not possible to benchmark DataLoaders + with `num_workers > 0` due to CUDA multiprocessing limitations. See warning under `profile` class + definition: https://github.com/pytorch/pytorch/blob/master/torch/autograd/profiler.py + + Attributes: + tag: a string tag describing the code block/function that we're tracking. + (e.g trainer.train, preprocessing, etc.) + output_dir: path where metrics are saved. + logging_interval: time interval in seconds at which system is polled for resource usage. + """ + + def __init__(self, tag: str, use_torch_profiler: bool, output_dir: str, logging_interval: float = 0.1) -> None: + self.tag = tag + self._tag = LUDWIG_TAG + self.tag + self.use_torch_profiler = use_torch_profiler + self.output_dir = output_dir + self.logging_interval = logging_interval + self.cuda_is_available = torch.cuda.is_available() + self.launched = False + if self.use_torch_profiler: + self.profiler_activities = [torch.profiler.ProfilerActivity.CPU] + if self.cuda_is_available: + self.profiler_activities.append(torch.profiler.ProfilerActivity.CUDA) + os.makedirs(os.path.join(self.output_dir), exist_ok=True) + + def _init_tracker_info(self): + """Initialize new self.info, self.torch_profiler, and self.torch_record_function instances. + + Important to call this in __enter__ if the user decides not to create a new class instance and therefore + __init__ wouldn't be called. + """ + self.info = {"code_block_tag": self.tag} + if self.use_torch_profiler: + self.torch_profiler = torch.profiler.profile(activities=self.profiler_activities, profile_memory=True) + self.torch_record_function = torch.profiler.record_function(self._tag) + + def _populate_static_information(self) -> None: + """Populate the report with static software and hardware information.""" + self.info["ludwig_version"] = LUDWIG_VERSION + self.info["start_disk_usage"] = shutil.disk_usage(os.path.expanduser("~")).used + + # CPU information + cpu_info = get_cpu_info() + self.info["cpu_architecture"] = cpu_info["arch"] + self.info["num_cpu"] = psutil.cpu_count() + self.info["cpu_name"] = cpu_info.get("brand_raw", "unknown") + self.info["total_cpu_memory_size"] = psutil.virtual_memory().total + + # GPU information + if self.cuda_is_available: + gpu_infos = get_gpu_info() + gpu_usage = GPUStatCollection.new_query() + for i, gpu_info in enumerate(gpu_infos): + gpu_key = f"cuda_{i}" + self.info[f"{gpu_key}_memory_used"] = [gpu_usage[i].memory_used] + self.info[f"{gpu_key}_name"] = gpu_info["name"] + self.info[f"{gpu_key}_total_memory"] = gpu_info["total_memory"] + self.info[f"{gpu_key}_driver_version"] = gpu_info["driver_version"] + self.info[f"{gpu_key}_cuda_version"] = gpu_info["cuda_version"] + + # recording in microseconds to be in line with torch profiler time recording. + self.info["start_time"] = time.perf_counter_ns() / 1000 + + def __enter__(self): + """Populate static information and monitors resource usage.""" + if self.launched: + raise RuntimeError("LudwigProfiler already launched. You can't use the same instance.") + + self._init_tracker_info() + self._populate_static_information() + + if self.use_torch_profiler: + # contextlib.ExitStack gracefully handles situations where __enter__ or __exit__ calls throw exceptions. + with contextlib.ExitStack() as ctx_exit_stack: + try: + # Launch torch.profiler to track PyTorch operators. + ctx_exit_stack.enter_context(self.torch_profiler) + except RuntimeError: + # PyTorch profiler is already enabled on this thread. + # Using the running PyTorch profiler to track events. + self.torch_profiler = None + + ctx_exit_stack.enter_context(self.torch_record_function) + self._ctx_exit_stack = ctx_exit_stack.pop_all() + try: + # Starting thread to monitor system resource usage. + self.queue = Queue() + self.t = threading.Thread( + target=monitor, + args=( + self.queue, + self.info, + self.logging_interval, + self.cuda_is_available, + ), + ) + self.t.start() + self.launched = True + except Exception: + self.launched = False + logger.exception("Encountered exception when launching tracker thread.") + + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + """Stop profiling, postprocess and export resource usage metrics.""" + try: + self.queue.put(STOP_MESSAGE) + self.t.join() + result = self.queue.get() + # If monitor thread crashed, result may be a string instead of dict + if isinstance(result, dict): + self.info = result + # recording in microseconds to be in line with torch profiler time recording. + self.info["end_time"] = time.perf_counter_ns() / 1000 + self.info["end_disk_usage"] = shutil.disk_usage(os.path.expanduser("~")).used + self.launched = False + except Exception: + logger.exception("Encountered exception when joining tracker thread.") + finally: + if self.use_torch_profiler: + self._ctx_exit_stack.close() + self._export_torch_metrics() + self._export_system_usage_metrics() + + def _export_system_usage_metrics(self): + """Export system resource usage metrics (no torch operators).""" + system_usage_metrics = get_metrics_from_system_usage_profiler(self.info) + output_subdir = os.path.join(self.output_dir, "system_resource_usage", system_usage_metrics.code_block_tag) + os.makedirs(output_subdir, exist_ok=True) + num_prev_runs = len(glob.glob(os.path.join(output_subdir, "run_*.json"))) + file_name = os.path.join(output_subdir, f"run_{num_prev_runs}.json") + save_json(file_name, profiler_dataclass_to_flat_dict(system_usage_metrics)) + + def _reformat_torch_usage_metrics_tags( + self, torch_usage_metrics: dict[str, Any] + ) -> dict[str, list[TorchProfilerMetrics]]: + reformatted_dict = {} + for key, value in torch_usage_metrics.items(): + assert key.startswith(LUDWIG_TAG) + reformatted_key = key[len(LUDWIG_TAG) :] + reformatted_dict[reformatted_key] = value + return reformatted_dict + + def _export_torch_metrics(self): + """Export resource usage metrics of torch operators.""" + if self.torch_profiler: + torch_usage_metrics = get_metrics_from_torch_profiler(self.torch_profiler) + torch_usage_metrics = self._reformat_torch_usage_metrics_tags(torch_usage_metrics) + for tag, runs in torch_usage_metrics.items(): + temp_dir = os.path.join(self.output_dir, "torch_ops_resource_usage", tag) + os.makedirs(temp_dir, exist_ok=True) + for run in runs: + num_prev_runs = len(glob.glob(os.path.join(temp_dir, "run_*.json"))) + save_json(os.path.join(temp_dir, f"run_{num_prev_runs}.json"), profiler_dataclass_to_flat_dict(run)) diff --git a/ludwig/benchmarking/profiler_callbacks.py b/ludwig/benchmarking/profiler_callbacks.py new file mode 100644 index 0000000..19fc276 --- /dev/null +++ b/ludwig/benchmarking/profiler_callbacks.py @@ -0,0 +1,59 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.benchmarking.profiler import LudwigProfiler +from ludwig.callbacks import Callback +from ludwig.constants import EVALUATION, PREPROCESSING, TRAINING + + +# TODO: Change annotation to PublicAPI once Ludwig 0.7 is released +@DeveloperAPI +class LudwigProfilerCallback(Callback): + """Class that defines the methods necessary to hook into process.""" + + def __init__(self, experiment: dict[str, Any]): + self.experiment_name = experiment["experiment_name"] + self.use_torch_profiler = experiment["profiler"]["use_torch_profiler"] + self.logging_interval = experiment["profiler"]["logging_interval"] + self.preprocess_profiler = None + self.train_profiler = None + self.evaluation_profiler = None + + def on_preprocess_start(self, *args, **kwargs): + self.preprocess_profiler = LudwigProfiler( + tag=PREPROCESSING, + output_dir=self.experiment_name, + use_torch_profiler=self.use_torch_profiler, + logging_interval=self.logging_interval, + ) + self.preprocess_profiler.__enter__() + + def on_preprocess_end(self, *args, **kwargs): + self.preprocess_profiler.__exit__(None, None, None) + del self.preprocess_profiler + + def on_train_start(self, *args, **kwargs): + self.train_profiler = LudwigProfiler( + tag=TRAINING, + output_dir=self.experiment_name, + use_torch_profiler=self.use_torch_profiler, + logging_interval=self.logging_interval, + ) + self.train_profiler.__enter__() + + def on_train_end(self, *args, **kwargs): + self.train_profiler.__exit__(None, None, None) + del self.train_profiler + + def on_evaluation_start(self): + self.evaluation_profiler = LudwigProfiler( + tag=EVALUATION, + output_dir=self.experiment_name, + use_torch_profiler=self.use_torch_profiler, + logging_interval=self.logging_interval, + ) + self.evaluation_profiler.__enter__() + + def on_evaluation_end(self): + self.evaluation_profiler.__exit__(None, None, None) + del self.evaluation_profiler diff --git a/ludwig/benchmarking/profiler_dataclasses.py b/ludwig/benchmarking/profiler_dataclasses.py new file mode 100644 index 0000000..2e2499c --- /dev/null +++ b/ludwig/benchmarking/profiler_dataclasses.py @@ -0,0 +1,89 @@ +import dataclasses +from dataclasses import dataclass + +from ludwig.utils.data_utils import flatten_dict + + +@dataclass +class DeviceUsageMetrics: + # Max CUDA memory utilization of the code block. + max_memory_used: float + + # Average CUDA memory utilization of the code block. + average_memory_used: float + + +@dataclass +class SystemResourceMetrics: + # Name of the code block/function to be profiled. + code_block_tag: str + + # Name of the CPU that the code ran on. + cpu_name: str + + # CPU architecture that the code ran on. + cpu_architecture: str + + # Number of CPUs on the machine. + num_cpu: int + + # Total CPU memory size. + total_cpu_memory_size: float + + # Ludwig version in the environment. + ludwig_version: str + + # Total execution time of the code block. + total_execution_time: float + + # The change in disk memory before and after the code block ran. + disk_footprint: float + + # Max CPU utilization of the code block. + max_cpu_utilization: float + + # Max CPU memory (RAM) utilization of the code block. + max_cpu_memory_usage: float + + # Min system-wide CPU memory available (how much physical memory is left). + min_global_cpu_memory_available: float + + # Max system-wide CPU utilization. + max_global_cpu_utilization: float + + # Average CPU utilization of the code block. + average_cpu_utilization: float + + # Average CPU memory (RAM) utilization of the code block. + average_cpu_memory_usage: float + + # Average system-wide CPU memory available (how much physical memory is left). + average_global_cpu_memory_available: float + + # Average system-wide CPU utilization. + average_global_cpu_utilization: float + + # Per device usage. Dictionary containing max and average memory used per device. + device_usage: dict[str, DeviceUsageMetrics] + + +@dataclass +class TorchProfilerMetrics: + # Time taken by torch ops to execute on the CPU. + torch_cpu_time: float + + # Time taken by torch ops to execute on CUDA devices. + torch_cuda_time: float + + # Number of out of memory events. + num_oom_events: int + + # Per device usage by torch ops. Dictionary containing max and average memory used per device. + device_usage: dict[str, DeviceUsageMetrics] + + +def profiler_dataclass_to_flat_dict(data: SystemResourceMetrics | TorchProfilerMetrics) -> dict: + """Returns a flat dictionary representation, with the device_usage key removed.""" + nested_dict = dataclasses.asdict(data) + nested_dict[""] = nested_dict.pop("device_usage") + return flatten_dict(nested_dict, sep="") diff --git a/ludwig/benchmarking/reporting.py b/ludwig/benchmarking/reporting.py new file mode 100644 index 0000000..17f509c --- /dev/null +++ b/ludwig/benchmarking/reporting.py @@ -0,0 +1,240 @@ +from collections import Counter, defaultdict +from statistics import mean +from typing import Any + +import torch +from torch._C._autograd import _KinetoEvent +from torch.autograd import DeviceType, profiler_util + +from ludwig.benchmarking.profiler_dataclasses import DeviceUsageMetrics, SystemResourceMetrics, TorchProfilerMetrics +from ludwig.constants import LUDWIG_TAG + + +def initialize_stats_dict(main_function_events: list[profiler_util.FunctionEvent]) -> dict[str, list]: + """Initialize dictionary which stores resource usage information per tagged code block. + + Args: + main_function_events: list of main function events. + """ + info = {} + for event_name in [evt.name for evt in main_function_events]: + info[event_name] = [] + return info + + +def get_memory_details(kineto_event: _KinetoEvent) -> tuple[str, int]: + """Get device name and number of bytes (de)allocated during an event. + + Args: + kineto_event: a Kineto event instance. + """ + if kineto_event.device_type() in [DeviceType.CPU, DeviceType.MKLDNN, DeviceType.IDEEP]: + return "cpu", kineto_event.nbytes() + elif kineto_event.device_type() in [DeviceType.CUDA, DeviceType.HIP]: + return f"cuda_{kineto_event.device_index()}", kineto_event.nbytes() + else: + raise ValueError(f"Device {kineto_event.device_type()} is not valid.") + + +def get_device_memory_usage( + kineto_event: _KinetoEvent, memory_events: list[list[_KinetoEvent | bool]] +) -> dict[str, DeviceUsageMetrics]: + """Get CPU and CUDA memory usage for an event. + + Args: + kineto_event: a Kineto event instance. + memory_events: list of memory events. + """ + mem_records_acc = profiler_util.MemRecordsAcc(memory_events) + start_us = kineto_event.start_ns() / 1000 + end_us = start_us + kineto_event.duration_ns() / 1000 + records_in_interval = mem_records_acc.in_interval(start_us, end_us) + memory_so_far = defaultdict(int) + count_so_far = defaultdict(int) + average_so_far = defaultdict(float) + max_so_far = defaultdict(int) + + for mem_record in records_in_interval: + device, nbytes = get_memory_details(mem_record[0]) + memory_so_far[device] += nbytes + max_so_far[device] = max(max_so_far[device], memory_so_far[device]) + average_so_far[device] = (memory_so_far[device] + (average_so_far[device] * count_so_far[device])) / ( + count_so_far[device] + 1 + ) + count_so_far[device] += 1 + memory_info_per_device = {} + for device in count_so_far: + memory_info_per_device[f"torch_{device}_"] = DeviceUsageMetrics( + max_memory_used=max_so_far[device], average_memory_used=average_so_far[device] + ) + return memory_info_per_device + + +def get_torch_op_time(events: list[profiler_util.FunctionEvent], attr: str) -> int | float: + """Get time torch operators spent executing for a list of events. + + Args: + events: list of events. + attr: a FunctionEvent attribute. Expecting one of "cpu_time_total", "device_time_total". + """ + if attr not in ["cpu_time_total", "device_time_total"]: + return -1 + + total = 0 + for e in events: + # Possible trace_names are torch ops, or tagged code blocks by LudwigProfiler (which are + # prepended with LUDWIG_TAG). + if LUDWIG_TAG not in e.trace_name: + total += getattr(e, attr) + else: + total += get_torch_op_time(e.cpu_children, attr) + return total + + +def get_device_run_durations(function_event: profiler_util.FunctionEvent) -> tuple[float, float]: + """Get CPU and device run durations for an event. + + Args: + function_event: a function event instance. + """ + torch_cpu_time = get_torch_op_time(function_event.cpu_children, "cpu_time_total") + torch_device_time = get_torch_op_time(function_event.cpu_children, "device_time_total") + return torch_cpu_time, torch_device_time + + +def get_num_oom_events(kineto_event: _KinetoEvent, out_of_memory_events: list[list[_KinetoEvent | bool]]) -> int: + oom_records_acc = profiler_util.MemRecordsAcc(out_of_memory_events) + start_us = kineto_event.start_ns() / 1000 + end_us = start_us + kineto_event.duration_ns() / 1000 + records_in_interval = oom_records_acc.in_interval(start_us, end_us) + return len(list(records_in_interval)) + + +def get_resource_usage_report( + main_kineto_events: list[_KinetoEvent], + main_function_events: list[profiler_util.FunctionEvent], + memory_events: list[list[_KinetoEvent | bool]], + out_of_memory_events: list[list[_KinetoEvent | bool]], + info: dict[str, Any], +) -> dict[str, list[TorchProfilerMetrics]]: + """Get relevant information from Kineto events and function events exported by the profiler. + + Args: + main_kineto_events: list of main Kineto events. + main_function_events: list of main function events. + memory_events: list of memory events. + out_of_memory_events: list of out of memory events. + info: dictionary used to record resource usage metrics. + """ + main_kineto_events = sorted( + (evt for evt in main_kineto_events if LUDWIG_TAG in evt.name()), key=lambda x: x.correlation_id() + ) + main_function_events = sorted((evt for evt in main_function_events if LUDWIG_TAG in evt.name), key=lambda x: x.id) + + for kineto_event, function_event in zip(main_kineto_events, main_function_events): + # Two different instances of `function_event` can have the same name if a the same + # tagged code block/function was executed more than once. + memory_info_per_device = get_device_memory_usage(kineto_event, memory_events) + torch_cpu_time, torch_cuda_time = get_device_run_durations(function_event) + num_oom_events = get_num_oom_events(kineto_event, out_of_memory_events) + torch_profiler_metrics = TorchProfilerMetrics( + torch_cpu_time=torch_cpu_time, + torch_cuda_time=torch_cuda_time, + num_oom_events=num_oom_events, + device_usage=memory_info_per_device, + ) + info[function_event.name].append(torch_profiler_metrics) + return info + + +def get_all_events( + kineto_events: list[_KinetoEvent], function_events: profiler_util.EventList +) -> tuple[ + list[_KinetoEvent], + list[profiler_util.FunctionEvent], + list[list[_KinetoEvent | bool]], + list[list[_KinetoEvent | bool]], +]: + """Return main Kineto and function events, memory and OOM events for functions/code blocks tagged in + LudwigProfiler. + + Args: + kineto_events: list of Kineto Events. + function_events: list of function events. + """ + # LUDWIG_TAG is prepended to LudwigProfiler tags. This edited tag is passed in to `torch.profiler.record_function` + # so we can easily retrieve events for code blocks wrapped with LudwigProfiler. + main_function_events = [evt for evt in function_events if LUDWIG_TAG in evt.name] + main_kineto_events = [event for event in kineto_events if LUDWIG_TAG in event.name()] + memory_events = [[event, False] for event in kineto_events if profiler_util.MEMORY_EVENT_NAME in event.name()] + # profiler_util.OUT_OF_MEMORY_EVENT_NAME seems to only be in newer versions of torch. + out_of_memory_events = [[event, False] for event in kineto_events if "[OutOfMemory]" in event.name()] + return main_kineto_events, main_function_events, memory_events, out_of_memory_events + + +def get_metrics_from_torch_profiler(profile: torch.profiler.profiler.profile) -> dict[str, list[TorchProfilerMetrics]]: + """Export time and resource usage metrics (CPU and CUDA) from a PyTorch profiler. + + The profiler keeps track of *torch operations* being executed in C++. It keeps track + of what device they're executed on, their execution time, and memory usage. + We only track the aforementioned metrics, but the torch profiler can keep track of + the stack trace, FLOPs, and torch modules. Tracking each additional item adds overhead. + + The torch profiler surfaces these metrics that are tracked under the hood by `libkineto`. + More on the Kineto project: https://github.com/pytorch/kineto + + Args: + profile: profiler object that contains all the events that were registered during the execution of the + wrapped code block. + """ + # events in both of these lists are in chronological order. + kineto_events = profile.profiler.kineto_results.events() + function_events = profile.profiler.function_events + main_kineto_events, main_function_events, memory_events, out_of_memory_events = get_all_events( + kineto_events, function_events + ) + + assert Counter([event.name for event in main_function_events]) == Counter( + [event.name() for event in main_kineto_events] + ) + info = initialize_stats_dict(main_function_events) + info = get_resource_usage_report( + main_kineto_events, main_function_events, memory_events, out_of_memory_events, info + ) + return info + + +def get_metrics_from_system_usage_profiler(system_usage_info: dict) -> SystemResourceMetrics: + """Package system resource usage metrics (no torch operators) in a dataclass. + + Args: + system_usage_info: dictionary containing resource usage information. + """ + device_usage_dict: dict[str, DeviceUsageMetrics] = {} + for key in system_usage_info: + if "cuda_" in key and "_memory_used" in key: + cuda_device_name = "_".join(key.split("_")[:2]) + "_" + max_memory_used = max(system_usage_info[key], default=0) + average_memory_used = mean(system_usage_info.get(key, [0])) + device_usage_dict[cuda_device_name] = DeviceUsageMetrics( + max_memory_used=max_memory_used, average_memory_used=average_memory_used + ) + return SystemResourceMetrics( + code_block_tag=system_usage_info["code_block_tag"], + cpu_name=system_usage_info.get("cpu_name", "unknown"), + cpu_architecture=system_usage_info["cpu_architecture"], + num_cpu=system_usage_info["num_cpu"], + total_cpu_memory_size=system_usage_info["total_cpu_memory_size"], + ludwig_version=system_usage_info["ludwig_version"], + total_execution_time=system_usage_info["end_time"] - system_usage_info["start_time"], + disk_footprint=system_usage_info["end_disk_usage"] - system_usage_info["start_disk_usage"], + max_cpu_utilization=max(system_usage_info["cpu_utilization"], default=0), + max_cpu_memory_usage=max(system_usage_info["cpu_memory_usage"], default=0), + min_global_cpu_memory_available=min(system_usage_info["global_cpu_memory_available"], default=0), + max_global_cpu_utilization=max(system_usage_info["global_cpu_utilization"], default=0), + average_cpu_utilization=mean(system_usage_info.get("cpu_utilization", [0])), + average_cpu_memory_usage=mean(system_usage_info.get("cpu_memory_usage", [0])), + average_global_cpu_memory_available=mean(system_usage_info.get("global_cpu_memory_available", [0])), + average_global_cpu_utilization=mean(system_usage_info.get("global_cpu_utilization", [0])), + device_usage=device_usage_dict, + ) diff --git a/ludwig/benchmarking/summarize.py b/ludwig/benchmarking/summarize.py new file mode 100644 index 0000000..e70b5e7 --- /dev/null +++ b/ludwig/benchmarking/summarize.py @@ -0,0 +1,108 @@ +import argparse +import logging +import os +import shutil + +from ludwig.benchmarking.summary_dataclasses import ( + build_metrics_diff, + build_resource_usage_diff, + export_metrics_diff_to_csv, + export_resource_usage_diff_to_csv, + MetricsDiff, + ResourceUsageDiff, +) +from ludwig.benchmarking.utils import download_artifacts + +logger = logging.getLogger() + + +def summarize_metrics( + bench_config_path: str, base_experiment: str, experimental_experiment: str, download_base_path: str +) -> tuple[list[str], list[MetricsDiff], list[list[ResourceUsageDiff]]]: + """Build metric and resource usage diffs from experiment artifacts. + + Args: + bench_config_path: Bench config file path. Can be the same one that was used to run + these experiments. + base_experiment: Name of the experiment we're comparing against. + experimental_experiment: Name of the experiment we're comparing. + download_base_path: Base path under which live the stored artifacts of + the benchmarking experiments. + """ + local_dir, dataset_list = download_artifacts( + bench_config_path, base_experiment, experimental_experiment, download_base_path + ) + metric_diffs, resource_usage_diffs = [], [] + for dataset_name in dataset_list: + try: + metric_diff = build_metrics_diff(dataset_name, base_experiment, experimental_experiment, local_dir) + metric_diffs.append(metric_diff) + + base_path = os.path.join(local_dir, dataset_name, base_experiment) + experimental_path = os.path.join(local_dir, dataset_name, experimental_experiment) + resource_usage_diff = build_resource_usage_diff( + base_path, experimental_path, base_experiment, experimental_experiment + ) + resource_usage_diffs.append(resource_usage_diff) + except Exception: + logger.exception(f"Exception encountered while creating diff summary for {dataset_name}.") + shutil.rmtree(local_dir, ignore_errors=True) + export_and_print(dataset_list, metric_diffs, resource_usage_diffs) + return dataset_list, metric_diffs, resource_usage_diffs + + +def export_and_print( + dataset_list: list[str], metric_diffs: list[MetricsDiff], resource_usage_diffs: list[list[ResourceUsageDiff]] +) -> None: + """Export to CSV and print a diff of performance and resource usage metrics of two experiments. + + Args: + dataset_list: List of datasets for which to print the diffs. + metric_diffs: Diffs for the performance metrics by dataset. + resource_usage_diffs: Diffs for the resource usage metrics per dataset per LudwigProfiler tag. + """ + for dataset_name, experiment_metric_diff in zip(dataset_list, metric_diffs): + output_path = os.path.join("summarize_output", "performance_metrics", dataset_name) + os.makedirs(output_path, exist_ok=True) + + logger.info( + f"Model performance metrics for *{experiment_metric_diff.base_experiment_name}* vs. *{experiment_metric_diff.experimental_experiment_name}* on dataset *{experiment_metric_diff.dataset_name}*" + ) + logger.info(experiment_metric_diff.to_string()) + filename = ( + "-".join([experiment_metric_diff.base_experiment_name, experiment_metric_diff.experimental_experiment_name]) + + ".csv" + ) + export_metrics_diff_to_csv(experiment_metric_diff, os.path.join(output_path, filename)) + + for dataset_name, experiment_resource_diff in zip(dataset_list, resource_usage_diffs): + output_path = os.path.join("summarize_output", "resource_usage_metrics", dataset_name) + os.makedirs(output_path, exist_ok=True) + for tag_diff in experiment_resource_diff: + logger.info( + f"Resource usage for *{tag_diff.base_experiment_name}* vs. *{tag_diff.experimental_experiment_name}* on *{tag_diff.code_block_tag}* of dataset *{dataset_name}*" + ) + logger.info(tag_diff.to_string()) + filename = ( + "-".join( + [tag_diff.code_block_tag, tag_diff.base_experiment_name, tag_diff.experimental_experiment_name] + ) + + ".csv" + ) + export_resource_usage_diff_to_csv(tag_diff, os.path.join(output_path, filename)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Summarize the model performance metrics and resource usage metrics of two experiments.", + prog="python summarize.py", + usage="%(prog)s [options]", + ) + parser.add_argument("--benchmarking_config", type=str, help="The benchmarking config.") + parser.add_argument("--base_experiment", type=str, help="The name of the first experiment.") + parser.add_argument("--experimental_experiment", type=str, help="The name of the second experiment.") + parser.add_argument("--download_base_path", type=str, help="The base path to download experiment artifacts from.") + args = parser.parse_args() + summarize_metrics( + args.benchmarking_config, args.base_experiment, args.experimental_experiment, args.download_base_path + ) diff --git a/ludwig/benchmarking/summary_dataclasses.py b/ludwig/benchmarking/summary_dataclasses.py new file mode 100644 index 0000000..b8f7d93 --- /dev/null +++ b/ludwig/benchmarking/summary_dataclasses.py @@ -0,0 +1,470 @@ +import csv +import logging +import os +from dataclasses import dataclass +from statistics import mean + +import ludwig.modules.metric_modules # noqa: F401 +from ludwig.benchmarking.utils import format_memory, format_time +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME +from ludwig.modules.metric_registry import get_metric_classes, metric_feature_type_registry # noqa: F401 +from ludwig.types import ModelConfigDict +from ludwig.utils.data_utils import load_json + +logger = logging.getLogger() + + +@dataclass +class MetricDiff: + """Diffs for a metric.""" + + # Name of the metric. + name: str + + # Value of the metric in base experiment (the one we benchmark against). + base_value: float + + # Value of the metric in the experimental experiment. + experimental_value: float + + # experimental_value - base_value. + diff: float + + # Percentage of change the metric with respect to base_value. + diff_percentage: float | str + + def __post_init__(self): + """Add human-readable string representations to the field.""" + + if "memory" in self.name: + self.base_value_str = format_memory(self.base_value) + self.experimental_value_str = format_memory(self.experimental_value) + self.diff_str = format_memory(self.diff) + elif "time" in self.name: + self.base_value_str = format_time(self.base_value) + self.experimental_value_str = format_time(self.experimental_value) + self.diff_str = format_time(self.diff) + else: + self.base_value_str = str(self.base_value) + self.experimental_value_str = str(self.experimental_value) + self.diff_str = str(self.diff) + + +def build_diff(name: str, base_value: float, experimental_value: float) -> MetricDiff: + """Build a diff between any type of metric. + + Args: + name: name assigned to the metric to be diff-ed. + base_value: base value of the metric. + experimental_value: experimental value of the metric. + """ + diff = experimental_value - base_value + diff_percentage = 100 * diff / base_value if base_value != 0 else "inf" + + return MetricDiff( + name=name, + base_value=base_value, + experimental_value=experimental_value, + diff=diff, + diff_percentage=diff_percentage, + ) + + +############################## +# Resource Usage Dataclasses # +############################## + + +@dataclass +class MetricsSummary: + """Summary of metrics from one experiment.""" + + # Path containing the artifacts for the experiment. + experiment_local_directory: str + + # Full Ludwig config. + config: ModelConfigDict + + # LudwigModel output feature type. + output_feature_type: str + + # LudwigModel output feature name. + output_feature_name: str + + # Dictionary that maps from metric name to their values. + metric_to_values: dict[str, float | int] + + # Names of metrics for the output feature. + metric_names: set[str] + + +@dataclass +class MetricsDiff: + """Store diffs for two experiments.""" + + # Dataset the two experiments are being compared on. + dataset_name: str + + # Name of the base experiment (the one we benchmark against). + base_experiment_name: str + + # Name of the experimental experiment. + experimental_experiment_name: str + + # Path under which all artifacts live on the local machine. + local_directory: str + + # `MetricsSummary` of the base_experiment. + base_summary: MetricsSummary + + # `MetricsSummary` of the experimental_experiment. + experimental_summary: MetricsSummary + + # `List[MetricDiff]` containing diffs for metric of the two experiments. + metrics: list[MetricDiff] + + def to_string(self): + ret = [] + spacing_str = "{:<20} {:<33} {:<13} {:<13} {:<13} {:<5}" + ret.append( + spacing_str.format( + "Output Feature Name", + "Metric Name", + self.base_experiment_name, + self.experimental_experiment_name, + "Diff", + "Diff Percentage", + ) + ) + for metric in sorted(self.metrics, key=lambda m: m.name): + output_feature_name = self.base_summary.output_feature_name + metric_name = metric.name + experiment1_val = round(metric.base_value, 3) + experiment2_val = round(metric.experimental_value, 3) + diff = round(metric.diff, 3) + diff_percentage = metric.diff_percentage + if isinstance(diff_percentage, float): + diff_percentage = round(metric.diff_percentage, 3) + ret.append( + spacing_str.format( + output_feature_name, + metric_name, + experiment1_val, + experiment2_val, + diff, + diff_percentage, + ) + ) + return "\n".join(ret) + + +def export_metrics_diff_to_csv(metrics_diff: MetricsDiff, path: str): + """Export metrics report to .csv. + + Args: + metrics_diff: MetricsDiff object containing the diff for two experiments on a dataset. + path: file name of the exported csv. + """ + with open(path, "w", newline="") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "Dataset Name", + "Output Feature Name", + "Metric Name", + metrics_diff.base_experiment_name, + metrics_diff.experimental_experiment_name, + "Diff", + "Diff Percentage", + ], + ) + writer.writeheader() + + for metric in sorted(metrics_diff.metrics, key=lambda m: m.name): + output_feature_name = metrics_diff.base_summary.output_feature_name + metric_name = metric.name + experiment1_val = round(metric.base_value, 3) + experiment2_val = round(metric.experimental_value, 3) + diff = round(metric.diff, 3) + diff_percentage = metric.diff_percentage + if isinstance(diff_percentage, float): + diff_percentage = round(metric.diff_percentage, 3) + writer.writerow( + { + "Dataset Name": metrics_diff.dataset_name, + "Output Feature Name": output_feature_name, + "Metric Name": metric_name, + metrics_diff.base_experiment_name: experiment1_val, + metrics_diff.experimental_experiment_name: experiment2_val, + "Diff": diff, + "Diff Percentage": diff_percentage, + } + ) + logger.info(f"Exported a CSV report to {path}\n") + + +def build_metrics_summary(experiment_local_directory: str) -> MetricsSummary: + """Build a metrics summary for an experiment. + + Args: + experiment_local_directory: directory where the experiment artifacts live. + e.g. local_experiment_repo/ames_housing/some_experiment/ + """ + config = load_json( + os.path.join(experiment_local_directory, "experiment_run", MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME) + ) + report = load_json(os.path.join(experiment_local_directory, "experiment_run", "test_statistics.json")) + output_feature_type: str = config["output_features"][0]["type"] + output_feature_name: str = config["output_features"][0]["name"] + metric_dict = report[output_feature_name] + full_metric_names = get_metric_classes(output_feature_type) + metric_to_values: dict[str, float | int] = { + metric_name: metric_dict[metric_name] for metric_name in full_metric_names if metric_name in metric_dict + } + metric_names: set[str] = set(metric_to_values) + + return MetricsSummary( + experiment_local_directory=experiment_local_directory, + config=config, + output_feature_name=output_feature_name, + output_feature_type=output_feature_type, + metric_to_values=metric_to_values, + metric_names=metric_names, + ) + + +def build_metrics_diff( + dataset_name: str, base_experiment_name: str, experimental_experiment_name: str, local_directory: str +) -> MetricsDiff: + """Build a MetricsDiff object between two experiments on a dataset. + + Args: + dataset_name: the name of the Ludwig dataset. + base_experiment_name: the name of the base experiment. + experimental_experiment_name: the name of the experimental experiment. + local_directory: the local directory where the experiment artifacts are downloaded. + """ + base_summary: MetricsSummary = build_metrics_summary( + os.path.join(local_directory, dataset_name, base_experiment_name) + ) + experimental_summary: MetricsSummary = build_metrics_summary( + os.path.join(local_directory, dataset_name, experimental_experiment_name) + ) + + metrics_in_common = set(base_summary.metric_names).intersection(set(experimental_summary.metric_names)) + + metrics: list[MetricDiff] = [ + build_diff(name, base_summary.metric_to_values[name], experimental_summary.metric_to_values[name]) + for name in metrics_in_common + ] + + return MetricsDiff( + dataset_name=dataset_name, + base_experiment_name=base_experiment_name, + experimental_experiment_name=experimental_experiment_name, + local_directory=local_directory, + base_summary=base_summary, + experimental_summary=experimental_summary, + metrics=metrics, + ) + + +############################## +# Resource Usage Dataclasses # +############################## + + +@dataclass +class ResourceUsageSummary: + """Summary of resource usage metrics from one experiment.""" + + # The tag with which the code block/function is labeled. + code_block_tag: str + + # Dictionary that maps from metric name to their values. + metric_to_values: dict[str, float | int] + + # Names of metrics for the output feature. + metric_names: set[str] + + +@dataclass +class ResourceUsageDiff: + """Store resource usage diffs for two experiments.""" + + # The tag with which the code block/function is labeled. + code_block_tag: str + + # Name of the base experiment (the one we benchmark against). + base_experiment_name: str + + # Name of the experimental experiment. + experimental_experiment_name: str + + # `List[Diff]` containing diffs for metric of the two experiments. + metrics: list[MetricDiff] + + def to_string(self): + ret = [] + spacing_str = "{:<36} {:<20} {:<20} {:<20} {:<5}" + ret.append( + spacing_str.format( + "Metric Name", + self.base_experiment_name, + self.experimental_experiment_name, + "Diff", + "Diff Percentage", + ) + ) + for metric in sorted(self.metrics, key=lambda m: m.name): + diff_percentage = metric.diff_percentage + if isinstance(metric.diff_percentage, float): + diff_percentage = round(metric.diff_percentage, 3) + ret.append( + spacing_str.format( + metric.name, + metric.base_value_str, + metric.experimental_value_str, + metric.diff_str, + diff_percentage, + ) + ) + return "\n".join(ret) + + +def export_resource_usage_diff_to_csv(resource_usage_diff: ResourceUsageDiff, path: str): + """Export resource usage metrics report to .csv. + + Args: + resource_usage_diff: ResourceUsageDiff object containing the diff for two experiments on a dataset. + path: file name of the exported csv. + """ + with open(path, "w", newline="") as f: + writer = csv.DictWriter( + f, + fieldnames=[ + "Code Block Tag", + "Metric Name", + resource_usage_diff.base_experiment_name, + resource_usage_diff.experimental_experiment_name, + "Diff", + "Diff Percentage", + ], + ) + writer.writeheader() + + for metric in sorted(resource_usage_diff.metrics, key=lambda m: m.name): + diff_percentage = metric.diff_percentage + if isinstance(metric.diff_percentage, float): + diff_percentage = round(metric.diff_percentage, 3) + writer.writerow( + { + "Code Block Tag": resource_usage_diff.code_block_tag, + "Metric Name": metric.name, + resource_usage_diff.base_experiment_name: metric.base_value_str, + resource_usage_diff.experimental_experiment_name: metric.experimental_value_str, + "Diff": metric.diff_str, + "Diff Percentage": diff_percentage, + } + ) + logger.info(f"Exported a CSV report to {path}\n") + + +def average_runs(path_to_runs_dir: str) -> dict[str, int | float]: + """Return average metrics from code blocks/function that ran more than once. + + Metrics for code blocks/functions that were executed exactly once will be returned as is. + + Args: + path_to_runs_dir: path to where metrics specific to a tag are stored. + e.g. resource_usage_out_dir/torch_ops_resource_usage/LudwigModel.evaluate/ + This directory will contain JSON files with the following pattern run_*.json + """ + runs = [load_json(os.path.join(path_to_runs_dir, run)) for run in os.listdir(path_to_runs_dir)] + # asserting that keys to each of the dictionaries are consistent throughout the runs. + assert len(runs) == 1 or all(runs[i].keys() == runs[i + 1].keys() for i in range(len(runs) - 1)) + runs_average = {"num_runs": len(runs)} + for key in runs[0]: + if isinstance(runs[0][key], (int, float)): + runs_average[key] = mean([run[key] for run in runs]) + return runs_average + + +def summarize_resource_usage(path: str, tags: list[str] | None = None) -> list[ResourceUsageSummary]: + """Create resource usage summaries for each code block/function that was decorated with ResourceUsageTracker. + + Each entry of the list corresponds to the metrics collected from a code block/function run. + Important: code blocks that ran more than once are averaged. + + Args: + path: corresponds to the `output_dir` argument in a ResourceUsageTracker run. + tags: optional list of tags to create summary for. If None, metrics from all tags will be summarized. + """ + summary = {} + # metric types: system_resource_usage, torch_ops_resource_usage. + all_metric_types = {"system_resource_usage", "torch_ops_resource_usage"} + for metric_type in all_metric_types.intersection(os.listdir(path)): + metric_type_path = os.path.join(path, metric_type) + # code block tags correspond to the `tag` argument in ResourceUsageTracker. + for code_block_tag in os.listdir(metric_type_path): + if tags and code_block_tag not in tags: + continue + if code_block_tag not in summary: + summary[code_block_tag] = {} + run_path = os.path.join(metric_type_path, code_block_tag) + # Metrics from code blocks/functions that ran more than once are averaged. + summary[code_block_tag][metric_type] = average_runs(run_path) + + summary_list = [] + for code_block_tag, metric_type_dicts in summary.items(): + merged_summary: dict[str, float | int] = {} + for metrics in metric_type_dicts.values(): + assert "num_runs" in metrics + assert "num_runs" not in merged_summary or metrics["num_runs"] == merged_summary["num_runs"] + merged_summary.update(metrics) + summary_list.append( + ResourceUsageSummary( + code_block_tag=code_block_tag, metric_to_values=merged_summary, metric_names=set(merged_summary) + ) + ) + return summary_list + + +def build_resource_usage_diff( + base_path: str, + experimental_path: str, + base_experiment_name: str | None = None, + experimental_experiment_name: str | None = None, +) -> list[ResourceUsageDiff]: + """Build and return a ResourceUsageDiff object to diff resource usage metrics between two experiments. + + Args: + base_path: corresponds to the `output_dir` argument in the base ResourceUsageTracker run. + experimental_path: corresponds to the `output_dir` argument in the experimental ResourceUsageTracker run. + """ + base_summary_list = summarize_resource_usage(base_path) + experimental_summary_list = summarize_resource_usage(experimental_path) + + summaries_list = [] + for base_summary in base_summary_list: + for experimental_summary in experimental_summary_list: + if base_summary.code_block_tag == experimental_summary.code_block_tag: + summaries_list.append((base_summary, experimental_summary)) + + diffs = [] + for base_summary, experimental_summary in summaries_list: + metrics_in_common = set(base_summary.metric_names).intersection(set(experimental_summary.metric_names)) + metrics: list[MetricDiff] = [ + build_diff(name, base_summary.metric_to_values[name], experimental_summary.metric_to_values[name]) + for name in metrics_in_common + ] + diff = ResourceUsageDiff( + code_block_tag=base_summary.code_block_tag, + base_experiment_name=base_experiment_name if base_experiment_name else "experiment_1", + experimental_experiment_name=( + experimental_experiment_name if experimental_experiment_name else "experiment_2" + ), + metrics=metrics, + ) + diffs.append(diff) + return diffs diff --git a/ludwig/benchmarking/utils.py b/ludwig/benchmarking/utils.py new file mode 100644 index 0000000..dbd718a --- /dev/null +++ b/ludwig/benchmarking/utils.py @@ -0,0 +1,304 @@ +import asyncio +import functools +import logging +import os +import shutil +import uuid +from concurrent.futures import ThreadPoolExecutor +from types import ModuleType +from typing import Any + +import fsspec +import pandas as pd +import yaml + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, CATEGORY +from ludwig.datasets import model_configs_for_dataset +from ludwig.datasets.loaders.dataset_loader import DatasetLoader +from ludwig.globals import CONFIG_YAML, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME, MODEL_WEIGHTS_SAFETENSORS_FILE_NAME +from ludwig.utils.data_utils import load_yaml +from ludwig.utils.dataset_utils import get_repeatable_train_val_test_split +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import get_fs_and_path + +HYPEROPT_OUTDIR_RETAINED_FILES = [ + "hyperopt_statistics.json", + "params.json", + "stderr", + "stdout", + "result.json", + "error.txt", +] +logger = logging.getLogger() + + +def load_from_module( + dataset_module: DatasetLoader | ModuleType, output_feature: dict[str, str], subsample_frac: float = 1 +) -> pd.DataFrame: + """Load the ludwig dataset, optionally subsamples it, and returns a repeatable split. A stratified split is + used for classification datasets. + + Args: + dataset_module: ludwig datasets module (e.g. ludwig.datasets.sst2, ludwig.datasets.ames_housing, etc.) + subsample_frac: percentage of the total dataset to load. + """ + dataset = dataset_module.load(split=False) + if subsample_frac < 1: + dataset = dataset.sample(frac=subsample_frac, replace=False, random_state=default_random_seed) + + if output_feature["type"] in [CATEGORY, BINARY]: + return get_repeatable_train_val_test_split( + dataset, + stratify_colname=output_feature["name"], + random_seed=default_random_seed, + ) + else: + return get_repeatable_train_val_test_split(dataset, random_seed=default_random_seed) + + +def export_artifacts(experiment: dict[str, str], experiment_output_directory: str, export_base_path: str): + """Save the experiment artifacts to the `bench_export_directory`. + + Args: + experiment: experiment dict that contains "dataset_name" (e.g. ames_housing), + "experiment_name" (specified by user), and "config_path" (path to experiment config. + Relative to ludwig/benchmarks/configs). + experiment_output_directory: path where the model, data, and logs of the experiment are saved. + export_base_path: remote or local path (directory) where artifacts are + exported. (e.g. s3://benchmarking.us-west-2.ludwig.com/bench/ or your/local/bench/) + """ + protocol, _ = fsspec.core.split_protocol(export_base_path) + fs, _ = get_fs_and_path(export_base_path) + try: + export_full_path = os.path.join(export_base_path, experiment["dataset_name"], experiment["experiment_name"]) + + # override previous experiment with the same name + if fs.exists(export_full_path): + fs.rm(export_full_path, recursive=True) + fs.put(experiment_output_directory, export_full_path, recursive=True) + fs.put( + os.path.join(experiment["config_path"]), + os.path.join(export_full_path, CONFIG_YAML), + ) + logger.info(f"Uploaded experiment artifact to\n\t{export_full_path}") + except Exception: + logger.exception( + f"Failed to upload experiment artifacts for experiment *{experiment['experiment_name']}* on " + f"dataset {experiment['dataset_name']}" + ) + + +def download_artifacts( + bench_config_path: str, + base_experiment: str, + experimental_experiment: str, + download_base_path: str, + local_dir: str = "benchmarking_summaries", +) -> tuple[str, list[str]]: + """Download benchmarking artifacts for two experiments. + + Args: + bench_config_path: bench config file path. Can be the same one that was used to run + these experiments. + base_experiment: name of the experiment we're comparing against. + experimental_experiment: name of the experiment we're comparing. + download_base_path: base path under which live the stored artifacts of + the benchmarking experiments. + """ + bench_config = load_yaml(bench_config_path) + protocol, _ = fsspec.core.split_protocol(download_base_path) + fs, _ = get_fs_and_path(download_base_path) + os.makedirs(local_dir, exist_ok=True) + + coroutines = [] + for experiment in bench_config["experiments"]: + dataset_name = experiment["dataset_name"] + for experiment_name in [base_experiment, experimental_experiment]: + coroutines.append(download_one(fs, download_base_path, dataset_name, experiment_name, local_dir)) + downloaded_names = asyncio.run(asyncio.gather(*coroutines, return_exceptions=True)) + + dataset_names = [experiment_tuple[0] for experiment_tuple in set(downloaded_names) if experiment_tuple[0]] + assert ( + len({experiment_tuple[1] for experiment_tuple in downloaded_names}) == 1 and downloaded_names[0][1] == local_dir + ), "Experiments not downloaded to the same path" + + return local_dir, dataset_names + + +@DeveloperAPI +async def download_one( + fs, download_base_path: str, dataset_name: str, experiment_name: str, local_dir: str +) -> tuple[str, str]: + """Download `config.yaml` and `report.json` for an experiment. + + Args: + fs: filesystem to use to download. + download_base_path: base path under which live the stored artifacts of + the benchmarking experiments. + dataset_name: name of the dataset we ran the experiments on. + experiment_name: name of the experiment (e.g. `v0.5.3_with_bert`) + local_dir: local directory under which the artifacts will be downloaded. + """ + loop = asyncio.get_running_loop() + local_experiment_dir = os.path.join(local_dir, dataset_name, experiment_name) + remote_experiment_directory = os.path.join(download_base_path, dataset_name, experiment_name) + os.makedirs(local_experiment_dir, exist_ok=True) + try: + with ThreadPoolExecutor() as pool: + func = functools.partial( + fs.get, + remote_experiment_directory, + local_experiment_dir, + recursive=True, + ) + await loop.run_in_executor(pool, func) + except Exception: + logger.exception(f"Couldn't download experiment *{experiment_name}* of dataset *{dataset_name}*.") + return "", local_dir + return dataset_name, local_dir + + +def validate_benchmarking_config(benchmarking_config: dict[str, Any]) -> None: + """Validates the parameters of the benchmarking config. + + Args: + benchmarking_config: benchmarking config dictionary. + + Raises: + ValueError if any of the expected parameters is not there. + """ + if "experiment_name" not in benchmarking_config and not all( + "experiment_name" in experiment for experiment in benchmarking_config["experiments"] + ): + raise ValueError("You must either specify a global experiment name or an experiment name for each experiment.") + if "export" not in benchmarking_config: + raise ValueError("""You must specify export parameters. Example: + export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. + """) + if "experiments" not in benchmarking_config: + raise ValueError("You must specify a list of experiments.") + for experiment in benchmarking_config["experiments"]: + if "dataset_name" not in experiment: + raise ValueError("A Ludwig dataset must be specified.") + + +def populate_benchmarking_config_with_defaults(benchmarking_config: dict[str, Any]) -> dict[str, Any]: + """Populates the parameters of the benchmarking config with defaults. + + Args: + benchmarking_config: benchmarking config dictionary. + """ + if "hyperopt" not in benchmarking_config: + benchmarking_config["hyperopt"] = False + if "process_config_file_path" not in benchmarking_config: + benchmarking_config["process_config_file_path"] = None + if "profiler" not in benchmarking_config: + benchmarking_config["profiler"] = {"enable": False, "use_torch_profiler": False, "logging_interval": None} + return benchmarking_config + + +def propagate_global_parameters(benchmarking_config: dict[str, Any]) -> dict[str, Any]: + """Propagate the global parameters of the benchmarking config to local experiments. + + Args: + benchmarking_config: benchmarking config dictionary. + """ + for experiment in benchmarking_config["experiments"]: + if "experiment_name" not in experiment: + experiment["experiment_name"] = benchmarking_config["experiment_name"] + if "export" not in experiment: + experiment["export"] = benchmarking_config["export"] + if "hyperopt" not in experiment: + experiment["hyperopt"] = benchmarking_config["hyperopt"] + if "process_config_file_path" not in experiment: + experiment["process_config_file_path"] = benchmarking_config["process_config_file_path"] + if "profiler" not in experiment: + experiment["profiler"] = benchmarking_config["profiler"] + return benchmarking_config + + +def create_default_config(experiment: dict[str, Any]) -> str: + """Create a Ludwig config that only contains input and output features. + + Args: + experiment: experiment dictionary. + + Returns: + path where the default config is saved. + """ + model_config = model_configs_for_dataset(experiment["dataset_name"])["default"] + + # only keep input_features and output_features + main_config_keys = list(model_config.keys()) + for key in main_config_keys: + if key not in ["input_features", "output_features"]: + del model_config[key] + config_path = f"{experiment['dataset_name']}-{uuid.uuid4().hex}.yaml" + save_yaml(config_path, model_config) + return config_path + + +def delete_model_checkpoints(output_directory: str): + """Deletes outputs of the experiment run that we don't want to save with the artifacts. + + Args: + output_directory: output directory of the hyperopt run. + """ + shutil.rmtree(os.path.join(output_directory, MODEL_FILE_NAME, "training_checkpoints"), ignore_errors=True) + for weights_name in (MODEL_WEIGHTS_SAFETENSORS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME): + weights_path = os.path.join(output_directory, MODEL_FILE_NAME, weights_name) + if os.path.isfile(weights_path): + os.remove(weights_path) + + +def delete_hyperopt_outputs(output_directory: str): + """Deletes outputs of the hyperopt run that we don't want to save with the artifacts. + + Args: + output_directory: output directory of the hyperopt run. + """ + for path, _currentDirectory, files in os.walk(output_directory): + for file in files: + filename = os.path.join(path, file) + if file not in HYPEROPT_OUTDIR_RETAINED_FILES: + os.remove(filename) + + +def save_yaml(filename, dictionary): + with open(filename, "w") as f: + yaml.dump(dictionary, f, default_flow_style=False) + + +def format_time(time_us): + """Defines how to format time in FunctionEvent. + + from https://github.com/pytorch/pytorch/blob/master/torch/autograd/profiler_util.py + """ + US_IN_SECOND = 1000.0 * 1000.0 + US_IN_MS = 1000.0 + if time_us >= US_IN_SECOND: + return f"{time_us / US_IN_SECOND:.3f}s" + if time_us >= US_IN_MS: + return f"{time_us / US_IN_MS:.3f}ms" + return f"{time_us:.3f}us" + + +def format_memory(nbytes): + """Returns a formatted memory size string. + + from https://github.com/pytorch/pytorch/blob/master/torch/autograd/profiler_util.py + """ + KB = 1024 + MB = 1024 * KB + GB = 1024 * MB + if abs(nbytes) >= GB: + return f"{nbytes * 1.0 / GB:.2f} Gb" + elif abs(nbytes) >= MB: + return f"{nbytes * 1.0 / MB:.2f} Mb" + elif abs(nbytes) >= KB: + return f"{nbytes * 1.0 / KB:.2f} Kb" + else: + return str(nbytes) + " b" diff --git a/ludwig/callbacks/__init__.py b/ludwig/callbacks/__init__.py new file mode 100644 index 0000000..161a2f3 --- /dev/null +++ b/ludwig/callbacks/__init__.py @@ -0,0 +1,404 @@ +# !/usr/bin/env python +# Copyright (c) 2021 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from abc import ABC +from collections.abc import Callable +from typing import Any + +from ludwig.api_annotations import PublicAPI +from ludwig.types import HyperoptConfigDict, ModelConfigDict, TrainingSetMetadataDict + + +@PublicAPI +class Callback(ABC): + """Base class for Ludwig lifecycle callbacks. + + Override any hook methods to execute custom logic at specific points in the + Ludwig pipeline (training, evaluation, hyperopt, etc.). All hook methods + are no-ops by default. + + Example:: + + class MyCallback(Callback): + def on_train_start(self, model, config, config_fp, **kwargs): + print(f"Training started with config: {config_fp}") + + def on_epoch_end(self, trainer, progress_tracker, save_path, **kwargs): + epoch = progress_tracker.epoch + loss = progress_tracker.train_metrics.get("combined", {}).get("loss", []) + if loss: + print(f"Epoch {epoch}: loss = {loss[-1]:.4f}") + + def on_preprocess_progress(self, progress: float, **kwargs): + print(f"Preprocessing: {progress:.0%}") + + model = LudwigModel(config, callbacks=[MyCallback()]) + + The ``on_preprocess_progress`` hook fires periodically while Ludwig preprocesses + features (during both training and prediction). ``progress`` is a ``float`` + in ``[0.0, 1.0]`` tracking completed partitions -- one increment per pandas + column, Dask partition, or Ray worker task. The final call is always + ``progress=1.0``. Works with all backends (pandas, Ray/Dask) with no extra + configuration required. + """ + + def on_cmdline(self, cmd: str, *args: list[str]): + """Called when Ludwig is run from the command line. + + Args: + cmd: The Ludwig subcommand being run (e.g., ``"train"``, ``"predict"``). + *args: The full list of command-line arguments (``sys.argv``). + """ + + def on_preprocess_start(self, config: ModelConfigDict, **kwargs): + """Called before preprocessing starts. + + Args: + config: The full Ludwig config dict. + """ + + def on_preprocess_end( + self, + training_set, + validation_set, + test_set, + training_set_metadata: TrainingSetMetadataDict, + **kwargs, + ): + """Called after preprocessing ends. + + Args: + training_set: Preprocessed training dataset. + validation_set: Preprocessed validation dataset. + test_set: Preprocessed test dataset. + training_set_metadata: Metadata inferred from the training set, + including vocabularies, feature statistics, and preprocessing + settings (same content as ``training_set_metadata.json``). + """ + + def on_hyperopt_init(self, experiment_name: str, **kwargs): + """Called to initialize state before hyperparameter optimization begins. + + Args: + experiment_name: Name of the current experiment. + """ + + def on_hyperopt_preprocessing_start(self, experiment_name: str, **kwargs): + """Called before data preprocessing for hyperparameter optimization begins. + + Args: + experiment_name: Name of the current experiment. + """ + + def on_hyperopt_preprocessing_end(self, experiment_name: str, **kwargs): + """Called after data preprocessing for hyperparameter optimization completes. + + Args: + experiment_name: Name of the current experiment. + """ + + def on_hyperopt_start(self, experiment_name: str, **kwargs): + """Called before any hyperparameter optimization trials are started. + + Args: + experiment_name: Name of the current experiment. + """ + + def on_hyperopt_end(self, experiment_name: str, **kwargs): + """Called after all hyperparameter optimization trials are completed. + + Args: + experiment_name: Name of the current experiment. + """ + + def on_hyperopt_finish(self, experiment_name: str, **kwargs): + """Deprecated — use ``on_hyperopt_end`` instead.""" + + def on_hyperopt_trial_start(self, parameters: HyperoptConfigDict, **kwargs): + """Called before the start of each hyperparameter optimization trial. + + Args: + parameters: The full parameter dict for this hyperopt trial. + """ + + def on_hyperopt_trial_end(self, parameters: HyperoptConfigDict, **kwargs): + """Called after the end of each hyperparameter optimization trial. + + Args: + parameters: The full parameter dict for this hyperopt trial. + """ + + def should_stop_hyperopt(self): + """Return ``True`` to stop the entire hyperopt run early. + + See `Ray Tune Stoppers `_. + """ + return False + + def on_resume_training(self, is_coordinator: bool, **kwargs): + """Called when training is resumed from a checkpoint. + + Args: + is_coordinator: Whether this worker is the coordinator. + """ + + def on_train_init( + self, + base_config: ModelConfigDict, + experiment_directory: str, + experiment_name: str, + model_name: str, + output_directory: str, + resume_directory: str | None, + **kwargs, + ): + """Called after preprocessing but before model and trainer objects are created. + + Args: + base_config: User-specified config before defaults/inferred values are added. + experiment_directory: Experiment directory (same as ``output_directory`` + when no experiment name is specified). + experiment_name: The experiment name. + model_name: The model name. + output_directory: Path where training results are stored. + resume_directory: Checkpoint directory to resume from, or ``None``. + """ + + def on_train_start( + self, + model, + config: ModelConfigDict, + config_fp: str | None, + **kwargs, + ): + """Called after the trainer is created, before training begins. + + Args: + model: The Ludwig model (``LudwigModule`` instance). + config: The full config dict. + config_fp: Path to the YAML config file, or ``None`` if config was + passed as a dict. + """ + + def on_train_end(self, output_directory: str, **kwargs): + """Called at the end of training, before the model is saved. + + Args: + output_directory: Path where training results are stored. + """ + + def on_trainer_train_setup(self, trainer, save_path: str, is_coordinator: bool, **kwargs): + """Called in every trainer (coordinator or worker) before training starts. + + Args: + trainer: The trainer instance. + save_path: Path to the directory where the model is saved. + is_coordinator: Whether this trainer is the coordinator. + """ + + def on_trainer_train_teardown(self, trainer, progress_tracker, save_path: str, is_coordinator: bool, **kwargs): + """Called in every trainer (coordinator or worker) after training completes. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress (epochs, metrics, etc.). + save_path: Path to the directory where the model is saved. + is_coordinator: Whether this trainer is the coordinator. + """ + + def on_batch_start(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator before each training batch. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_batch_end(self, trainer, progress_tracker, save_path: str, sync_step: bool = True, **kwargs): + """Called on the coordinator after each training batch. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + sync_step: Whether model params were updated and synced in this step. + """ + + def on_eval_start(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator at the start of evaluation. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_eval_end(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator at the end of evaluation. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_epoch_start(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator before the start of each epoch. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_epoch_end(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator after the end of each epoch. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_validation_start(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator before validation starts. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_validation_end(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator after validation completes. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_test_start(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator before test evaluation starts. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def on_test_end(self, trainer, progress_tracker, save_path: str, **kwargs): + """Called on the coordinator after test evaluation ends. + + Args: + trainer: The trainer instance. + progress_tracker: Object tracking training progress. + save_path: Path to the model save directory. + """ + + def should_early_stop(self, trainer, progress_tracker, is_coordinator, **kwargs): + """Return ``True`` to trigger early stopping on any worker. + + Ludwig ORs the return value across all workers, so any worker returning + ``True`` will stop training. + """ + return False + + def on_checkpoint(self, trainer, progress_tracker, **kwargs): + """Called after each checkpoint, regardless of whether the model was evaluated or saved.""" + + def on_save_best_checkpoint(self, trainer, progress_tracker, save_path, **kwargs): + """Called on every worker immediately after a new best model checkpoint is saved.""" + + def on_build_metadata_start(self, df, mode: str, **kwargs): + """Called before building dataset metadata. + + Args: + df: The dataset (``pd.DataFrame``). + mode: One of ``"prediction"``, ``"training"``, or ``None``. + """ + + def on_build_metadata_end(self, df, mode, **kwargs): + """Called after dataset metadata has been built. + + Args: + df: The dataset (``pd.DataFrame``). + mode: One of ``"prediction"``, ``"training"``, or ``None``. + """ + + def on_build_data_start(self, df, mode, **kwargs): + """Called before ``build_data`` (preprocessing, missing-value handling, metadata update). + + Args: + df: The dataset (``pd.DataFrame``). + mode: One of ``"prediction"``, ``"training"``, or ``None``. + """ + + def on_preprocess_progress(self, progress: float, **kwargs): + """Called periodically during ``build_data`` to report preprocessing progress. + + Progress is tracked at the partition level: each engine partition (pandas + column, Dask partition, or Ray task) increments the counter after it + completes, so the value reflects actual work done rather than an estimate. + + Args: + progress: Fraction of preprocessing completed, in the range ``[0.0, 1.0]``. + """ + + def on_build_data_end(self, df, mode, **kwargs): + """Called after ``build_data`` completes. + + Args: + df: The dataset (``pd.DataFrame``). + mode: One of ``"prediction"``, ``"training"``, or ``None``. + """ + + def on_evaluation_start(self, **kwargs): + """Called before preprocessing for standalone evaluation.""" + + def on_evaluation_end(self, **kwargs): + """Called after standalone evaluation completes.""" + + def on_visualize_figure(self, fig, **kwargs): + """Called after a visualization figure is generated. + + Args: + fig: The generated ``matplotlib.figure.Figure``. + """ + + def on_ludwig_end(self, **kwargs): + """Called at the very end of a Ludwig run for any cleanup.""" + + def prepare_ray_tune( + self, + train_fn: Callable, + tune_config: dict[str, Any], + tune_callbacks: list[Callable], + **kwargs, + ) -> tuple[Callable, dict[str, Any]]: + """Configure the Ray Tune training function and config before a hyperopt run. + + Args: + train_fn: The function that runs a single hyperopt trial. + tune_config: The Ray Tune configuration dict. + tune_callbacks: Additional Ray Tune callbacks (not yet used by Ludwig). + + Returns: + A tuple ``(train_fn, tune_config)`` — possibly modified — that is + passed directly to Ray Tune. + """ + return train_fn, tune_config diff --git a/ludwig/callbacks/studio.py b/ludwig/callbacks/studio.py new file mode 100644 index 0000000..c78e793 --- /dev/null +++ b/ludwig/callbacks/studio.py @@ -0,0 +1,251 @@ +"""StudioCallback — built-in Ludwig callback for Ludwig Studio integration. + +Writes newline-delimited JSON to ``/metrics.jsonl`` so that the +Ludwig Studio backend can pick up events incrementally and persist them to its +database / broadcast via WebSocket. + +Usage:: + + from ludwig.callbacks.studio import StudioCallback + + model = LudwigModel(config, callbacks=[StudioCallback(run_id="my-run", output_dir="/tmp/run")]) + model.train(dataset=df) + +Event schema +------------ +Each line is a JSON object with a ``"type"`` discriminator field. + +Metric event (emitted on every epoch end): + +.. code-block:: json + + { + "type": "metric", + "run_id": "...", + "epoch": 3, + "step": 1500, + "split": "train", + "feature": "label", + "metric": "accuracy", + "value": 0.93, + "progress_pct": 0.6, + "eta_seconds": 240.0, + "timestamp": 1718000000.0 + } + +Phase event (emitted at pipeline phase transitions): + +.. code-block:: json + + { + "type": "phase", + "run_id": "...", + "phase": "training", + "epoch": 0, + "total_epochs": 10, + "total_steps": 5000, + "steps_per_epoch": 500, + "timestamp": 1718000000.0 + } +""" + +import json +import time +from pathlib import Path +from typing import Any + +from ludwig.callbacks import Callback +from ludwig.utils.data_utils import NumpyEncoder + + +class _NDJSONChannel: + """Append-only newline-delimited JSON file handle, opened lazily.""" + + def __init__(self, path: Path) -> None: + self._path = path + self._fh = None + + def open(self) -> None: + if self._fh is None: + self._path.parent.mkdir(parents=True, exist_ok=True) + self._fh = open(self._path, "a", buffering=1) + + def emit(self, event: dict[str, Any]) -> None: + self.open() + self._fh.write(json.dumps(event, cls=NumpyEncoder) + "\n") + + def close(self) -> None: + if self._fh is not None: + self._fh.close() + self._fh = None + + def __del__(self) -> None: + self.close() + + +class StudioCallback(Callback): + """Streams training lifecycle events to a newline-delimited JSON file. + + Args: + run_id: Unique identifier for this training run (used to key events in + the Studio database). + output_dir: Directory where ``metrics.jsonl`` is written. Must be + writable. Created automatically if it does not exist. + group_id: Optional hyperopt group ID. When set, trial events are also + written to ``/trials.jsonl``. + group_output_dir: Root output directory for the hyperopt group. Required + when ``group_id`` is provided. + """ + + def __init__( + self, + run_id: str, + output_dir: str, + group_id: str | None = None, + group_output_dir: str | None = None, + ): + self.run_id = run_id + self.group_id = group_id + self._ch = _NDJSONChannel(Path(output_dir) / "metrics.jsonl") + self._trial_ch = _NDJSONChannel(Path(group_output_dir) / "trials.jsonl") if group_output_dir else None + # Hyperopt trial tracking + self._trial_idx: int = 0 + self._trial_best_metric_value: float | None = None + self._trial_best_metric_name: str | None = None + + # ── Internal helpers ────────────────────────────────────────────────────── + + def _emit(self, event: dict[str, Any]) -> None: + event.setdefault("run_id", self.run_id) + event.setdefault("timestamp", time.time()) + self._ch.emit(event) + + def _emit_trial(self, event: dict[str, Any]) -> None: + if self._trial_ch is None: + return + event.setdefault("group_id", self.group_id) + event.setdefault("timestamp", time.time()) + self._trial_ch.emit(event) + + def _progress(self, progress_tracker) -> dict[str, Any]: + """Extract ETA / progress fields from a ProgressTracker (if available).""" + out: dict[str, Any] = {} + if hasattr(progress_tracker, "progress_pct"): + out["progress_pct"] = round(progress_tracker.progress_pct, 4) + if hasattr(progress_tracker, "eta_seconds"): + eta = progress_tracker.eta_seconds + out["eta_seconds"] = round(eta, 1) if eta is not None else None + return out + + # ── Phase hooks ─────────────────────────────────────────────────────────── + + def on_preprocess_start(self, config, **kwargs) -> None: + self._emit({"type": "phase", "phase": "preprocessing", "epoch": None, "total_epochs": None}) + + def on_train_start(self, model, config, config_fp, **kwargs) -> None: + total = (config or {}).get("trainer", {}).get("epochs", None) + self._emit({"type": "phase", "phase": "training", "epoch": 0, "total_epochs": total}) + + def on_trainer_train_setup(self, trainer, save_path: str, is_coordinator: bool, **kwargs) -> None: + if not is_coordinator: + return + steps_per_epoch = getattr(trainer, "steps_per_epoch", 0) + total_steps = getattr(trainer, "total_steps", 0) + epochs = getattr(trainer, "epochs", None) + self._emit( + { + "type": "phase", + "phase": "training_setup", + "epoch": 0, + "total_epochs": epochs, + "total_steps": total_steps, + "steps_per_epoch": steps_per_epoch, + } + ) + + def on_eval_end(self, trainer, progress_tracker, save_path: str, **kwargs) -> None: + self._emit( + { + "type": "phase", + "phase": "evaluation", + "epoch": progress_tracker.epoch, + "total_epochs": None, + **self._progress(progress_tracker), + } + ) + + def on_train_end(self, output_directory: str, **kwargs) -> None: + self._emit({"type": "phase", "phase": "completed", "epoch": None, "total_epochs": None}) + self._ch.close() + + # ── Metric hook ─────────────────────────────────────────────────────────── + + def on_epoch_end(self, trainer, progress_tracker, save_path: str, **kwargs) -> None: + epoch = progress_tracker.epoch + step = getattr(progress_tracker, "steps", epoch) + progress = self._progress(progress_tracker) + + for split, metrics_dict in [ + ("train", progress_tracker.train_metrics), + ("validation", progress_tracker.validation_metrics), + ("test", progress_tracker.test_metrics), + ]: + for feature, feature_metrics in metrics_dict.items(): + for metric_name, values in feature_metrics.items(): + if not values: + continue + raw = values[-1] + # TrainerMetric namedtuple has .value; plain tuple uses index [-1] + value = getattr(raw, "value", raw[-1]) if hasattr(raw, "__len__") else raw + self._emit( + { + "type": "metric", + "epoch": epoch, + "step": step, + "split": split, + "feature": feature, + "metric": metric_name, + "value": float(value), + **progress, + } + ) + + # Track best validation metric for hyperopt trial reporting + if self.group_id is not None: + best_val = getattr(progress_tracker, "best_eval_metric_value", None) + if best_val is not None: + self._trial_best_metric_value = float(best_val) + vfield = getattr(trainer, "validation_field", None) + vmetric = getattr(trainer, "validation_metric", None) + if vfield and vmetric: + self._trial_best_metric_name = f"{vfield}/{vmetric}" + + # ── Hyperopt hooks ──────────────────────────────────────────────────────── + + def on_hyperopt_trial_start(self, parameters: dict[str, Any], **kwargs) -> None: + self._trial_best_metric_value = None + self._trial_best_metric_name = None + self._emit_trial( + { + "type": "trial_start", + "trial_idx": self._trial_idx, + "parameters": dict(parameters) if parameters else {}, + } + ) + + def on_hyperopt_trial_end(self, parameters: dict[str, Any], **kwargs) -> None: + self._emit_trial( + { + "type": "trial_end", + "trial_idx": self._trial_idx, + "parameters": dict(parameters) if parameters else {}, + "metric_score": self._trial_best_metric_value, + "metric_name": self._trial_best_metric_name, + } + ) + self._trial_idx += 1 + + def on_hyperopt_end(self, experiment_name: str, **kwargs) -> None: + self._emit_trial({"type": "hyperopt_end", "trial_count": self._trial_idx}) + if self._trial_ch: + self._trial_ch.close() diff --git a/ludwig/check.py b/ludwig/check.py new file mode 100644 index 0000000..a0869f7 --- /dev/null +++ b/ludwig/check.py @@ -0,0 +1,61 @@ +import argparse +import logging +import tempfile + +from ludwig.api import LudwigModel +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import INPUT_FEATURES, OUTPUT_FEATURES, TRAINER +from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logging_level_registry, print_boxed, print_ludwig + +NUM_EXAMPLES = 100 + + +@DeveloperAPI +def check_install(logging_level: int = logging.INFO, **kwargs): + config = { + INPUT_FEATURES: [ + {"name": "in1", "type": "text"}, + {"name": "in2", "type": "category"}, + {"name": "in3", "type": "number"}, + ], + OUTPUT_FEATURES: [{"name": "out1", "type": "binary"}], + TRAINER: {"epochs": 2, "batch_size": 8}, + } + + try: + df = build_synthetic_dataset_df(NUM_EXAMPLES, config) + model = LudwigModel(config, logging_level=logging_level) + with tempfile.TemporaryDirectory() as tmpdir: + model.train(dataset=df, output_directory=tmpdir) + except Exception: + print_boxed("CHECK INSTALL COMPLETE... FAILURE") + raise + + print_boxed("CHECK INSTALL COMPLETE... SUCCESS") + + +@DeveloperAPI +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This command checks Ludwig installation on a synthetic dataset.", + prog="ludwig check_install", + usage="%(prog)s [options]", + ) + + parser.add_argument( + "-l", + "--logging_level", + default="warning", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + args = parser.parse_args(sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + + print_ludwig("Check Install", LUDWIG_VERSION) + check_install(**vars(args)) diff --git a/ludwig/cli.py b/ludwig/cli.py new file mode 100644 index 0000000..ee92f62 --- /dev/null +++ b/ludwig/cli.py @@ -0,0 +1,201 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import sys + +import ludwig.contrib +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logo + + +class CLI: + """CLI describes a command line interface for interacting with Ludwig. + + Functions are described below. + """ + + def __init__(self): + parser = argparse.ArgumentParser( + description="ludwig cli runner", + usage=f"""\n{get_logo("ludwig cli", LUDWIG_VERSION)} +ludwig [] + +Available sub-commands: + train Trains a model + predict Predicts using a pretrained model + evaluate Evaluate a pretrained model's performance + forecast Forecast the next n data points in a timeseries using a pretrained model + experiment Runs a full experiment training a model and evaluating it + hyperopt Perform hyperparameter optimization + benchmark Run and track experiments on a number of datasets and configs, and export experiment artifacts. + serve Serves a pretrained model + visualize Visualizes experimental results + collect_summary Prints names of weights and layers activations to use with other collect commands + collect_weights Collects tensors containing a pretrained model weights + collect_activations Collects tensors for each datapoint using a pretrained model + datasets Downloads and lists Ludwig-ready datasets + export_mlflow Exports Ludwig models to MLflow + export_model Exports Ludwig models to safetensors, torch_export, or onnx + inspect Inspect a trained model (summary, weights, feature importance) + export_schema Exports the Ludwig config JSON schema + preprocess Preprocess data and saves it into Parquet and JSON format + synthesize_dataset Creates synthetic data for testing purposes + generate_config Generate a Ludwig config from a natural language task description using an LLM + init_config Initialize a user config from a dataset and targets + render_config Renders the fully populated config with all defaults set + check_install Runs a quick training run on synthetic data to verify installation status + upload Push trained model artifacts to HuggingFace Hub +""", + ) + parser.add_argument("command", help="Subcommand to run") + # parse_args defaults to [1:] for args, but you need to + # exclude the rest of the args too, or validation will fail + args = parser.parse_args(sys.argv[1:2]) + if not hasattr(self, args.command): + print("Unrecognized command") + parser.print_help() + exit(1) + # use dispatch pattern to invoke method with same name + getattr(self, args.command)() + + def train(self): + from ludwig import train + + train.cli(sys.argv[2:]) + + def predict(self): + from ludwig import predict + + predict.cli(sys.argv[2:]) + + def evaluate(self): + from ludwig import evaluate + + evaluate.cli(sys.argv[2:]) + + def forecast(self): + from ludwig import forecast + + forecast.cli(sys.argv[2:]) + + def experiment(self): + from ludwig import experiment + + experiment.cli(sys.argv[2:]) + + def hyperopt(self): + from ludwig import hyperopt_cli + + hyperopt_cli.cli(sys.argv[2:]) + + def benchmark(self): + from ludwig.benchmarking import benchmark + + benchmark.cli(sys.argv[2:]) + + def serve(self): + from ludwig import serve + + serve.cli(sys.argv[2:]) + + def visualize(self): + from ludwig import visualize + + visualize.cli(sys.argv[2:]) + + def collect_summary(self): + from ludwig import collect + + collect.cli_collect_summary(sys.argv[2:]) + + def collect_weights(self): + from ludwig import collect + + collect.cli_collect_weights(sys.argv[2:]) + + def collect_activations(self): + from ludwig import collect + + collect.cli_collect_activations(sys.argv[2:]) + + def export_mlflow(self): + from ludwig import export + + export.cli_export_mlflow(sys.argv[2:]) + + def export_model(self): + from ludwig import export + + export.cli_export_model(sys.argv[2:]) + + def inspect(self): + from ludwig import inspect_model + + inspect_model.cli(sys.argv[2:]) + + def export_schema(self): + from ludwig.schema.export_schema import main as export_schema_main + + export_schema_main(sys.argv[2:]) + + def preprocess(self): + from ludwig import preprocess + + preprocess.cli(sys.argv[2:]) + + def synthesize_dataset(self): + from ludwig.data import dataset_synthesizer + + dataset_synthesizer.cli(sys.argv[2:]) + + def generate_config(self): + from ludwig.config_generation import cli_generate_config + + cli_generate_config(sys.argv[2:]) + + def init_config(self): + from ludwig import automl + + automl.cli_init_config(sys.argv[2:]) + + def render_config(self): + from ludwig.utils import defaults + + defaults.cli_render_config(sys.argv[2:]) + + def check_install(self): + from ludwig import check + + check.cli(sys.argv[2:]) + + def datasets(self): + from ludwig import datasets + + datasets.cli(sys.argv[2:]) + + def upload(self): + from ludwig import upload + + upload.cli(sys.argv[2:]) + + +def main(): + ludwig.contrib.preload(sys.argv) + CLI() + + +if __name__ == "__main__": + main() diff --git a/ludwig/collect.py b/ludwig/collect.py new file mode 100644 index 0000000..219c8e5 --- /dev/null +++ b/ludwig/collect.py @@ -0,0 +1,457 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import importlib +import logging +import os +import sys + +import numpy as np +import torch +import torchinfo + +from ludwig.api import LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend +from ludwig.callbacks import Callback +from ludwig.constants import FULL, TEST, TRAINING, VALIDATION +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logging_level_registry, print_boxed, print_ludwig +from ludwig.utils.strings_utils import make_safe_filename + +logger = logging.getLogger(__name__) + + +def collect_activations( + model_path: str, + layers: list[str], + dataset: str, + data_format: str | None = None, + split: str = FULL, + batch_size: int = 128, + output_directory: str = "results", + gpus: list[str] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + **kwargs, +) -> list[str]: + """Uses the pretrained model to collect the tensors corresponding to a datapoint in the dataset. Saves the + tensors to the experiment directory. + + Args: + model_path: Filepath to pre-trained model. + layers: List of strings for layer names in the model to collect activations. + dataset: Source containing the data to make predictions. + data_format: Format to interpret data sources. Will be inferred automatically if not specified. + Valid formats are 'auto', 'csv', 'excel', 'feather', 'fwf', 'hdf5' (cache file produced + during previous training), 'html' (file containing a single HTML table), 'json', 'jsonl', + 'parquet', 'pickle' (pickled Pandas DataFrame), 'sas', 'spss', 'stata', 'tsv'. + split: Split on which to perform predictions. Valid values are 'training', 'validation', + 'test' and 'full'. + batch_size: Size of batches for processing. + output_directory: The directory that will contain the training statistics, TensorBoard logs, + the saved model and the training progress files. + gpus: List of GPUs that are available for training. + gpu_memory_limit: Maximum memory fraction [0, 1] allowed to allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading parallelism to improve performance + at the cost of determinism. + callbacks: A list of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline. + backend: Backend or string name of backend to use to execute preprocessing / training steps. + + Returns: + List of filepath to *.npy files containing the activations. + """ + logger.info(f"Dataset path: {dataset}") + logger.info(f"Model path: {model_path}") + logger.info(f"Output path: {output_directory}") + logger.info("\n") + + model = LudwigModel.load( + model_path, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + backend=backend, + ) + + # collect activations + print_boxed("COLLECT ACTIVATIONS") + collected_tensors = model.collect_activations( + layers, dataset, data_format=data_format, split=split, batch_size=batch_size + ) + + # saving + os.makedirs(output_directory, exist_ok=True) + saved_filenames = save_tensors(collected_tensors, output_directory) + + logger.info(f"Saved to: {output_directory}") + return saved_filenames + + +def collect_weights(model_path: str, tensors: list[str], output_directory: str = "results", **kwargs) -> list[str]: + """Loads a pretrained model and collects weights. + + Args: + model_path: Filepath to pre-trained model. + tensors: List of tensor names to collect weights. + output_directory: The directory where collected weights will be stored. + + Returns: + List of filepath to *.npy files containing the weights. + """ + logger.info(f"Model path: {model_path}") + logger.info(f"Output path: {output_directory}") + logger.info("\n") + + model = LudwigModel.load(model_path) + + # collect weights + print_boxed("COLLECT WEIGHTS") + collected_tensors = model.collect_weights(tensors) + + # saving + os.makedirs(output_directory, exist_ok=True) + saved_filenames = save_tensors(collected_tensors, output_directory) + + logger.info(f"Saved to: {output_directory}") + return saved_filenames + + +def save_tensors(collected_tensors, output_directory): + filenames = [] + for tensor_name, tensor_value in collected_tensors: + np_filename = os.path.join(output_directory, make_safe_filename(tensor_name) + ".npy") + if isinstance(tensor_value, torch.Tensor): + # Skip non-tensor collected artifacts, e.g. used_tokens. + np.save(np_filename, tensor_value.detach().cpu().numpy()) + filenames.append(np_filename) + return filenames + + +def print_model_summary(model_path: str, **kwargs) -> None: + """Loads a pretrained model and prints names of weights and layers activations. + + Args: + model_path: Filepath to pre-trained model. + """ + model = LudwigModel.load(model_path) + # Move model to CPU for torchinfo summary to avoid device mismatch issues. + model.model.cpu() + logger.info(torchinfo.summary(model.model, input_data=[model.model.get_model_inputs()], depth=20)) + + logger.info("\nModules:\n") + for name, _ in model.model.named_children(): + logger.info(name) + + logger.info("\nParameters:\n") + for name, _ in model.model.named_parameters(): + logger.info(name) + + +def pretrained_summary(pretrained_model: str, **kwargs) -> None: + """Loads a pretrained model from Huggingface or Torchvision models and prints names of layers. + + Args: + pretrained_model: Name of model to load (case sensitive). + """ + from transformers import AutoConfig, AutoModel + + model = None + # get access token if available + token = os.getenv("HUGGING_FACE_HUB_TOKEN") + if token is None: + logger.info("No token provided. Continuing loading without token access.") + elif not token: + raise ValueError("Invalid token provided. Exiting.") + else: + logger.info("Valid token provided. Proceeding with token access.") + + # from_config creates a random-weight model without downloading weights — fast and avoids OOM. + try: + config = AutoConfig.from_pretrained(pretrained_model, token=token) + model = AutoModel.from_config(config=config) + logger.info(f"Loaded {pretrained_model} from Hugging Face Transformers.") + except (OSError, ValueError, RuntimeError, TypeError) as e: + logger.error(f"Failed to load {pretrained_model} from Hugging Face Transformers: {e}") + + # Try and load from torchvision-models + if model is None: + try: + module = importlib.import_module("torchvision.models") + model = getattr(module, pretrained_model)(weights=None) + except AttributeError: + logger.error(f"{pretrained_model} is not a valid torchvision model.") + + if model: + for name, _ in model.named_parameters(): + logger.info(name) + else: + logger.error(f"Unable to load the model {pretrained_model} from any known source.") + + +def cli_collect_activations(sys_argv): + """Command Line Interface to communicate with the collection of tensors and there are several options that can + specified when calling this function: + + --data_csv: Filepath for the input csv + --data_hdf5: Filepath for the input hdf5 file, if there is a csv file, this + is not read + --d: Refers to the dataset type of the file being read, by default is + *generic* + --s: Refers to the split of the data, can be one of: train, test, + validation, full + --m: Input model that is necessary to collect to the tensors, this is a + required *option* + --t: Tensors to collect + --od: Output directory of the model, defaults to results + --bs: Batch size + --g: Number of gpus that are to be used + --gf: Fraction of each GPUs memory to use. + --v: Verbose: Defines the logging level that the user will be exposed to + """ + parser = argparse.ArgumentParser( + description="This script loads a pretrained model and uses it collect " + "tensors for each datapoint in the dataset.", + prog="ludwig collect_activations", + usage="%(prog)s [options]", + ) + + # --------------- + # Data parameters + # --------------- + parser.add_argument("--dataset", help="input data file path", required=True) + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "htmltables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + parser.add_argument( + "-s", + "--split", + default=FULL, + choices=[TRAINING, VALIDATION, TEST, FULL], + help="the split to obtain the model activations from", + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=True) + parser.add_argument("-lyr", "--layers", help="tensors to collect", nargs="+", required=True) + + # ------------------------- + # Output results parameters + # ------------------------- + parser.add_argument( + "-od", "--output_directory", type=str, default="results", help="directory that contains the results" + ) + + # ------------------ + # Generic parameters + # ------------------ + parser.add_argument("-bs", "--batch_size", type=int, default=128, help="size of batches") + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument("-g", "--gpus", type=int, default=0, help="list of gpu to use") + parser.add_argument( + "-gml", + "--gpu_memory_limit", + type=float, + default=None, + help="maximum memory fraction [0, 1] allowed to allocate per GPU device", + ) + parser.add_argument( + "-dpt", + "--disable_parallel_threads", + action="store_false", + dest="allow_parallel_threads", + help="disable PyTorch from using multithreading for reproducibility", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("collect_activations", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.collect") + + print_ludwig("Collect Activations", LUDWIG_VERSION) + + collect_activations(**vars(args)) + + +def cli_collect_weights(sys_argv): + """Command Line Interface to collecting the weights for the model. + + --m: Input model that is necessary to collect to the tensors, this is a + required *option* + --t: Tensors to collect + --od: Output directory of the model, defaults to results + --v: Verbose: Defines the logging level that the user will be exposed to + """ + parser = argparse.ArgumentParser( + description="This script loads a pretrained model and uses it collect weights.", + prog="ludwig collect_weights", + usage="%(prog)s [options]", + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=True) + parser.add_argument("-t", "--tensors", help="tensors to collect", nargs="+", required=True) + + # ------------------------- + # Output results parameters + # ------------------------- + parser.add_argument( + "-od", "--output_directory", type=str, default="results", help="directory that contains the results" + ) + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("collect_weights", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.collect") + + print_ludwig("Collect Weights", LUDWIG_VERSION) + + collect_weights(**vars(args)) + + +def cli_collect_summary(sys_argv): + """Command Line Interface to collecting a summary of the model layers and weights. + + --m: Input model that is necessary to collect to the tensors + --pm: Model name in order to fetch from Huggingface or Torchvision + --v: Verbose: Defines the logging level that the user will be exposed to + """ + parser = argparse.ArgumentParser( + description="This script loads a pretrained model " + "and prints names of weights and layers activations " + "to use with other collect commands", + prog="ludwig collect_summary", + usage="%(prog)s [options]", + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=False) + parser.add_argument( + "-pm", "--pretrained_model", help="pretrained model to summarize (torchvision and huggingface)", required=False + ) + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("collect_summary", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.collect") + + print_ludwig("Collect Summary", LUDWIG_VERSION) + + if args.model_path: + print_model_summary(**vars(args)) + elif args.pretrained_model and not args.model_path: + pretrained_summary(**vars(args)) + + +if __name__ == "__main__": + if len(sys.argv) > 1: + if sys.argv[1] == "activations": + cli_collect_activations(sys.argv[2:]) + elif sys.argv[1] == "weights": + cli_collect_weights(sys.argv[2:]) + elif sys.argv[1] == "names": + cli_collect_summary(sys.argv[2:]) + else: + print("Unrecognized command") + else: + print("Unrecognized command") diff --git a/ludwig/combiners/__init__.py b/ludwig/combiners/__init__.py new file mode 100644 index 0000000..4f7b5e2 --- /dev/null +++ b/ludwig/combiners/__init__.py @@ -0,0 +1,2 @@ +# Trigger @register_combiner decorators for combiners defined in separate modules. +import ludwig.combiners.tabpfn_v2_combiner # noqa: F401 diff --git a/ludwig/combiners/combiners.py b/ludwig/combiners/combiners.py new file mode 100644 index 0000000..70f4ee3 --- /dev/null +++ b/ludwig/combiners/combiners.py @@ -0,0 +1,1428 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from abc import ABC +from dataclasses import dataclass +from functools import lru_cache + +import torch +from torch.nn import Linear, ModuleList + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, ENCODER_OUTPUT, NUMBER +from ludwig.encoders.registry import get_sequence_encoder_registry +from ludwig.features.base_feature import InputFeature +from ludwig.modules.attention_modules import TransformerStack +from ludwig.modules.embedding_modules import Embed +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.modules.tabnet_modules import TabNet +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.comparator import ComparatorCombinerConfig +from ludwig.schema.combiners.concat import ConcatCombinerConfig +from ludwig.schema.combiners.cross_attention import CrossAttentionCombinerConfig +from ludwig.schema.combiners.ft_transformer import FTTransformerCombinerConfig +from ludwig.schema.combiners.gated_fusion import GatedFusionCombinerConfig +from ludwig.schema.combiners.hypernetwork import HyperNetworkCombinerConfig +from ludwig.schema.combiners.perceiver import PerceiverCombinerConfig +from ludwig.schema.combiners.project_aggregate import ProjectAggregateCombinerConfig +from ludwig.schema.combiners.sequence import SequenceCombinerConfig +from ludwig.schema.combiners.sequence_concat import SequenceConcatCombinerConfig +from ludwig.schema.combiners.tab_transformer import TabTransformerCombinerConfig +from ludwig.schema.combiners.tabnet import TabNetCombinerConfig +from ludwig.schema.combiners.transformer import TransformerCombinerConfig +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.registry import Registry +from ludwig.utils.torch_utils import LudwigModule, sequence_length_3D +from ludwig.utils.torch_utils import sequence_mask as torch_sequence_mask + +logger = logging.getLogger(__name__) + + +@dataclass +class Handle: + """This class provides an opaque handle to the input features, preventing them from being registered as state. + + This is important because we already reference the `input_features` as an attribute of ECD, so we don't need it to + appear twice in the state_dict. Some distributed strategies may get confused if the input features are set as an + attribute of the combiner, leading to shape mismatch errors when loading a saved checkpoint. + """ + + input_features: dict[str, "InputFeature"] + + +@DeveloperAPI +class Combiner(LudwigModule, ABC): + """Base class for combiners, which implements common properties. + + Subclasses will usually override: __init__() to set properties and allocate resources. Should call + super().__init__(input_features). forward() performs the forward pass given a dictionary of encoder + outputs. get_schema_cls() must returns the class of the corresponding schema for the combiner type. + """ + + def __init__(self, input_features: dict[str, "InputFeature"]): + super().__init__() + self.handle = Handle(input_features) + + @property + def concatenated_shape(self) -> torch.Size: + # compute the size of the last dimension for the incoming encoder outputs + # this is required to setup the fully connected layer + shapes = [ + torch.prod(torch.Tensor([*self.handle.input_features.get(k).output_shape])) + for k in self.handle.input_features + ] + return torch.Size([torch.sum(torch.Tensor(shapes)).type(torch.int32)]) + + @property + def input_shape(self) -> dict: + # input to combiner is a dictionary of the input features encoder + # outputs, this property returns dictionary of output shapes for each + # input feature's encoder output shapes. + return {k: self.handle.input_features.get(k).output_shape for k in self.handle.input_features} + + @property + @lru_cache(maxsize=1) + def output_shape(self) -> torch.Size: + pseudo_input = {} + for k in self.handle.input_features: + pseudo_input[k] = { + ENCODER_OUTPUT: torch.rand( + 2, *self.handle.input_features.get(k).output_shape, dtype=self.input_dtype, device=self.device + ) + } + output_tensor = self.forward(pseudo_input) + return output_tensor["combiner_output"].size()[1:] + + +combiner_impl_registry = Registry[type[Combiner]]() + + +def register_combiner(config_cls: type[BaseCombinerConfig]): + def wrap(cls: type[Combiner]): + combiner_impl_registry[config_cls] = cls + return cls + + return wrap + + +def create_combiner(config: BaseCombinerConfig, **kwargs) -> Combiner: + return combiner_impl_registry[type(config)](config=config, **kwargs) + + +@register_combiner(ConcatCombinerConfig) +class ConcatCombiner(Combiner): + def __init__( + self, input_features: dict[str, "InputFeature"] | None = None, config: ConcatCombinerConfig = None, **kwargs + ): + super().__init__(input_features) + self.name = "ConcatCombiner" + logger.debug(f" {self.name}") + + self.flatten_inputs = config.flatten_inputs + self.fc_stack = None + + # todo future: this may be redundant, check + fc_layers = config.fc_layers + if fc_layers is None: + fc_layers = [] + for _i in range(config.num_fc_layers): + fc_layers.append({"output_size": config.output_size}) + self.fc_layers = fc_layers + + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=self.concatenated_shape[-1], + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + residual=config.residual, + ) + + # Optional BatchEnsemble layer after FC stack + self.batch_ensemble_layer = None + if getattr(config, "batch_ensemble", False): + from ludwig.modules.batch_ensemble import BatchEnsembleLinear + + fc_output_size = config.output_size + num_members = getattr(config, "num_ensemble_members", 4) + self.batch_ensemble_layer = BatchEnsembleLinear( + in_features=fc_output_size, + out_features=fc_output_size, + num_members=num_members, + ) + logger.debug(f" BatchEnsemble: {num_members} members") + + if input_features and len(input_features) == 1 and self.fc_layers is None: + self.supports_masking = True + + def forward(self, inputs: dict) -> dict: # encoder outputs + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + + # ================ Flatten ================ + if self.flatten_inputs: + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + + # ================ Concat ================ + if len(encoder_outputs) > 1: + hidden = torch.cat(encoder_outputs, 1) + else: + hidden = list(encoder_outputs)[0] + + # ================ Fully Connected ================ + hidden = self.fc_stack(hidden) + + # ================ BatchEnsemble ================ + if self.batch_ensemble_layer is not None: + hidden = self.batch_ensemble_layer(hidden) + + return_data = {"combiner_output": hidden} + + if len(inputs) == 1: + # Workaround for including additional tensors from output of input encoders for + # potential use in decoders, e.g. LSTM state for seq2seq. + # TODO(Justin): Think about how to make this communication work for multi-sequence + # features. Other combiners. + for key, value in list(inputs.values())[0].items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + +@register_combiner(SequenceConcatCombinerConfig) +class SequenceConcatCombiner(Combiner): + def __init__( + self, input_features: dict[str, "InputFeature"], config: SequenceConcatCombinerConfig = None, **kwargs + ): + super().__init__(input_features) + self.name = "SequenceConcatCombiner" + logger.debug(f" {self.name}") + + self.reduce_output = config.reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=config.reduce_output, + max_sequence_length=self.concatenated_shape[0], + encoding_size=self.concatenated_shape[1], + ) + if self.reduce_output is None: + self.supports_masking = True + self.main_sequence_feature = config.main_sequence_feature + + @property + def concatenated_shape(self) -> torch.Size: + # computes the effective shape of the input tensor after combining + # all the encoder outputs + # determine max sequence length by finding the first sequence tensor + # assume all the sequences are of the same size, if not true + # this will be caught during processing + seq_size = None + for k in self.handle.input_features: + # dim-2 output_shape implies a sequence [seq_size, hidden] + if len(self.handle.input_features.get(k).output_shape) == 2: + seq_size = self.handle.input_features.get(k).output_shape[0] + break + + # collect the size of the last dimension for all input feature + # encoder outputs + shapes = [ + self.handle.input_features.get(k).output_shape[-1] for k in self.handle.input_features + ] # output shape not input shape + return torch.Size([seq_size, sum(shapes)]) + + def forward(self, inputs: dict) -> dict: # encoder outputs + if self.main_sequence_feature is None or self.main_sequence_feature not in inputs: + for if_name, if_outputs in inputs.items(): + # todo: when https://github.com/ludwig-ai/ludwig/issues/810 is closed + # convert following test from using shape to use explicit + # if_outputs[TYPE] values for sequence features + if len(if_outputs[ENCODER_OUTPUT].shape) == 3: + self.main_sequence_feature = if_name + break + + if self.main_sequence_feature is None: + raise Exception("No sequence feature available for sequence combiner") + + main_sequence_feature_encoding = inputs[self.main_sequence_feature] + + representation = main_sequence_feature_encoding[ENCODER_OUTPUT] + representations = [representation] + + sequence_max_length = representation.shape[1] + sequence_length = sequence_length_3D(representation) + + # ================ Concat ================ + for if_name, if_outputs in inputs.items(): + if if_name != self.main_sequence_feature: + if_representation = if_outputs[ENCODER_OUTPUT] + if len(if_representation.shape) == 3: + # The following check makes sense when + # both representations have a specified + # sequence length dimension. If they do not, + # then this check is simply checking if None == None + # and will not catch discrepancies in the different + # feature length dimension. Those errors will show up + # at training time. Possible solutions to this is + # to enforce a length second dimension in + # sequential feature placeholders, but that + # does not work with BucketedBatcher that requires + # the second dimension to be undefined in order to be + # able to trim the data points and speed up computation. + # So for now we are keeping things like this, make sure + # to write in the documentation that training time + # dimensions mismatch may occur if the sequential + # features have different lengths for some data points. + if if_representation.shape[1] != representation.shape[1]: + raise ValueError( + f"The sequence length of the input feature {if_name} " + f"is {if_representation.shape[1]} and is different from the sequence " + f"length of the main sequence feature {self.main_sequence_feature} which " + f"is {representation.shape[1]}.\n Shape of {if_name}: {if_representation.shape}, shape of {if_name}: {representation.shape}.\n" + "Sequence lengths of all sequential features " + "must be the same in order to be concatenated " + "by the sequence concat combiner. " + "Try to impose the same max sequence length " + "as a preprocessing parameter to both features " + f"or to reduce the output of {if_name}." + ) + # this assumes all sequence representations have the + # same sequence length, 2nd dimension + representations.append(if_representation) + + elif len(if_representation.shape) == 2: + multipliers = (1, sequence_max_length, 1) + tiled_representation = torch.tile(torch.unsqueeze(if_representation, 1), multipliers) + representations.append(tiled_representation) + + else: + raise ValueError( + f"The representation of {if_name} has rank {len(if_representation.shape)} and cannot be" + " concatenated by a sequence concat combiner. " + "Only rank 2 and rank 3 tensors are supported." + ) + + hidden = torch.cat(representations, 2) + logger.debug(f" concat_hidden: {hidden}") + + # ================ Mask ================ + sequence_mask = torch_sequence_mask(sequence_length, sequence_max_length) + hidden = torch.multiply(hidden, torch.unsqueeze(sequence_mask, -1).type(torch.float32)) + + # ================ Reduce ================ + hidden = self.reduce_sequence(hidden) + + return_data = {"combiner_output": hidden} + + if len(inputs) == 1: + for key, value in list(inputs.values())[0].items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + +@register_combiner(SequenceCombinerConfig) +class SequenceCombiner(Combiner): + def __init__(self, input_features: dict[str, "InputFeature"], config: SequenceCombinerConfig = None, **kwargs): + super().__init__(input_features) + self.name = "SequenceCombiner" + logger.debug(f" {self.name}") + + self.combiner = SequenceConcatCombiner( + input_features, + config=SequenceConcatCombinerConfig(reduce_output=None, main_sequence_feature=config.main_sequence_feature), + ) + + logger.debug( + f"combiner input shape {self.combiner.concatenated_shape}, output shape {self.combiner.output_shape}" + ) + + self.encoder_obj = get_from_registry(config.encoder.type, get_sequence_encoder_registry())( + should_embed=False, + reduce_output=config.reduce_output, + embedding_size=self.combiner.output_shape[1], + max_sequence_length=self.combiner.output_shape[0], + **kwargs, + ) + + if hasattr(self.encoder_obj, "supports_masking") and self.encoder_obj.supports_masking: + self.supports_masking = True + + @property + def concatenated_shape(self) -> torch.Size: + # computes the effective shape of the input tensor after combining + # all the encoder outputs + # determine max sequence length by finding the first sequence tensor + # assume all the sequences are of the same size, if not true + # this will be caught during processing + seq_size = None + for k in self.handle.input_features: + # dim-2 output_shape implies a sequence [seq_size, hidden] + if len(self.handle.input_features.get(k).output_shape) == 2: + seq_size = self.handle.input_features.get(k).output_shape[0] + break + + # collect the size of the last dimension for all input feature + # encoder outputs + shapes = [ + self.handle.input_features.get(k).output_shape[-1] for k in self.handle.input_features + ] # output shape not input shape + return torch.Size([seq_size, sum(shapes)]) + + def forward(self, inputs: dict) -> dict: # encoder outputs + # ================ Concat ================ + hidden = self.combiner(inputs) + + # ================ Sequence encoding ================ + hidden = self.encoder_obj(hidden["combiner_output"]) + + return_data = {"combiner_output": hidden[ENCODER_OUTPUT]} + for key, value in hidden.items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + +@register_combiner(TabNetCombinerConfig) +class TabNetCombiner(Combiner): + def __init__( + self, input_features: dict[str, "InputFeature"], config: TabNetCombinerConfig = None, **kwargs + ) -> None: + super().__init__(input_features) + self.name = "TabNetCombiner" + logger.debug(f" {self.name}") + + self.tabnet = TabNet( + self.concatenated_shape[-1], + config.size, + config.output_size, + num_steps=config.num_steps, + num_total_blocks=config.num_total_blocks, + num_shared_blocks=config.num_shared_blocks, + relaxation_factor=config.relaxation_factor, + bn_epsilon=config.bn_epsilon, + bn_momentum=config.bn_momentum, + bn_virtual_bs=config.bn_virtual_bs, + sparsity=config.sparsity, + entmax_mode=config.entmax_mode, + entmax_alpha=config.entmax_alpha, + ) + + if config.dropout > 0: + self.dropout = torch.nn.Dropout(config.dropout) + else: + self.dropout = None + + @property + def concatenated_shape(self) -> torch.Size: + # compute the size of the last dimension for the incoming encoder outputs + # this is required to setup + shapes = [ + torch.prod(torch.Tensor([*self.handle.input_features.get(k).output_shape])) + for k in self.handle.input_features + ] + return torch.Size([torch.sum(torch.Tensor(shapes)).type(torch.int32)]) + + def forward( + self, + inputs: torch.Tensor, # encoder outputs + ) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + + # ================ Flatten ================ + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + + # ================ Concat ================ + if len(encoder_outputs) > 1: + hidden = torch.cat(encoder_outputs, 1) + else: + hidden = list(encoder_outputs)[0] + + # ================ TabNet ================ + hidden, aggregated_mask, masks = self.tabnet(hidden) + if self.dropout: + hidden = self.dropout(hidden) + + return_data = { + "combiner_output": hidden, + "aggregated_attention_masks": aggregated_mask, + "attention_masks": masks, + } + + if len(inputs) == 1: + for key, value in list(inputs.values())[0].items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + @property + def output_shape(self) -> torch.Size: + return self.tabnet.output_shape + + +@register_combiner(TransformerCombinerConfig) +class TransformerCombiner(Combiner): + def __init__( + self, + input_features: dict[str, "InputFeature"] | None = None, + config: TransformerCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "TransformerCombiner" + logger.debug(f" {self.name}") + + self.reduce_output = config.reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=config.reduce_output, + max_sequence_length=len(input_features), + encoding_size=config.hidden_size, + ) + if self.reduce_output is None: + self.supports_masking = True + + # max sequence length for Transformer layer is number of input features + self.max_sequence_length = len(input_features) + + logger.debug(" Projectors") + self.projectors = ModuleList( + # regardless of rank-2 or rank-3 input, torch.prod() calculates size + # after flattening the encoder output tensor + [ + Linear( + torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32), + config.hidden_size, + ) + for inp in input_features + ] + ) + + logger.debug(" TransformerStack") + self.transformer_stack = TransformerStack( + input_size=config.hidden_size, + max_sequence_length=self.max_sequence_length, + hidden_size=config.hidden_size, + num_heads=config.num_heads, + output_size=config.transformer_output_size, + num_layers=config.num_layers, + dropout=config.dropout, + ) + + if self.reduce_output is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.transformer_stack.output_shape[-1], + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.fc_activation, + default_dropout=config.fc_dropout, + fc_residual=config.fc_residual, + ) + + def forward( + self, + inputs, # encoder outputs + ) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + + # ================ Flatten ================ + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + + # ================ Project & Concat ================ + projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)] + hidden = torch.stack(projected) # shape [num_eo, bs, h] + hidden = torch.permute(hidden, (1, 0, 2)) # shape [bs, num_eo, h] + + # ================ Transformer Layers ================ + hidden = self.transformer_stack(hidden) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden) + + return_data = {"combiner_output": hidden} + + if len(inputs) == 1: + for key, value in list(inputs.values())[0].items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + +@register_combiner(TabTransformerCombinerConfig) +class TabTransformerCombiner(Combiner): + def __init__( + self, + input_features: dict[str, "InputFeature"] | None = None, + config: TabTransformerCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "TabTransformerCombiner" + logger.debug(f"Initializing {self.name}") + + self.reduce_output = config.reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=config.reduce_output, max_sequence_length=len(input_features), encoding_size=config.hidden_size + ) + self.supports_masking = True + + self.embed_input_feature_name = config.embed_input_feature_name + if self.embed_input_feature_name: + vocab = [ + i_f + for i_f in input_features + if input_features.get(i_f).type() != NUMBER or input_features.get(i_f).type() != BINARY + ] + if self.embed_input_feature_name == "add": + self.embed_i_f_name_layer = Embed(vocab, config.hidden_size, force_embedding_size=True) + projector_size = config.hidden_size + elif isinstance(self.embed_input_feature_name, int): + if self.embed_input_feature_name > config.hidden_size: + raise ValueError( + "TabTransformer parameter " + "`embed_input_feature_name` " + f"specified integer value ({self.embed_input_feature_name}) " + "needs to be smaller than " + f"`hidden_size` ({config.hidden_size})." + ) + self.embed_i_f_name_layer = Embed( + vocab, + self.embed_input_feature_name, + force_embedding_size=True, + ) + projector_size = config.hidden_size - self.embed_input_feature_name + else: + raise ValueError( + "TabTransformer parameter " + "`embed_input_feature_name` " + "should be either None, an integer or `add`, " + "the current value is " + f"{self.embed_input_feature_name}" + ) + else: + projector_size = config.hidden_size + + logger.debug(" Projectors") + self.unembeddable_features = [] + self.embeddable_features = [] + for i_f in input_features: + if input_features.get(i_f).type() in {NUMBER, BINARY}: + self.unembeddable_features.append(i_f) + else: + self.embeddable_features.append(i_f) + + self.projectors = ModuleList() + for i_f in self.embeddable_features: + flatten_size = self.get_flatten_size(input_features.get(i_f).output_shape) + self.projectors.append(Linear(flatten_size[0], projector_size)) + + # input to layer_norm are the encoder outputs for unembeddable features, + # which are number or binary features. These should be 2-dim + # tensors. Size should be concatenation of these tensors. + concatenated_unembeddable_encoders_size = 0 + for i_f in self.unembeddable_features: + concatenated_unembeddable_encoders_size += input_features.get(i_f).output_shape[0] + + # Skip LayerNorm when normalizing a single value — LayerNorm(1) always + # outputs zero which kills gradients for all downstream parameters. + if concatenated_unembeddable_encoders_size > 1: + self.layer_norm = torch.nn.LayerNorm(concatenated_unembeddable_encoders_size) + else: + self.layer_norm = torch.nn.Identity() + + logger.debug(" TransformerStack") + self.transformer_stack = TransformerStack( + input_size=config.hidden_size, + max_sequence_length=len(self.embeddable_features), + hidden_size=config.hidden_size, + # todo: can we just use projector_size? # hidden_size, + num_heads=config.num_heads, + output_size=config.transformer_output_size, + num_layers=config.num_layers, + dropout=config.dropout, + ) + + logger.debug(" FCStack") + + # determine input size to fully connected layer based on reducer + if config.reduce_output == "concat": + fc_input_size = len(self.embeddable_features) * config.hidden_size + else: + fc_input_size = self.reduce_sequence.output_shape[-1] if len(self.embeddable_features) > 0 else 0 + self.fc_stack = FCStack( + fc_input_size + concatenated_unembeddable_encoders_size, + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.fc_activation, + default_dropout=config.fc_dropout, + fc_residual=config.fc_residual, + ) + + self._empty_hidden = torch.empty([1, 0]) + self._embeddable_features_indices = torch.arange(0, len(self.embeddable_features)) + + # Create empty tensor of shape [1, 0] to use as hidden in case there are no category or numeric/binary features. + self.register_buffer("empty_hidden", self._empty_hidden) + self.register_buffer("embeddable_features_indices", self._embeddable_features_indices) + + @staticmethod + def get_flatten_size(output_shape: torch.Size) -> torch.Size: + size = torch.prod(torch.Tensor([*output_shape])) + return torch.Size([size.type(torch.int32)]) + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape + + def forward( + self, + inputs: dict, # encoder outputs + ) -> dict: + unembeddable_encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs if k in self.unembeddable_features] + embeddable_encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs if k in self.embeddable_features] + + batch_size = ( + embeddable_encoder_outputs[0].shape[0] + if len(embeddable_encoder_outputs) > 0 + else unembeddable_encoder_outputs[0].shape[0] + ) + + # ================ Project & Concat embeddables ================ + if len(embeddable_encoder_outputs) > 0: + # ============== Flatten ================= + embeddable_encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in embeddable_encoder_outputs] + + projected = [self.projectors[i](eo) for i, eo in enumerate(embeddable_encoder_outputs)] + hidden = torch.stack(projected) # num_eo, bs, h + hidden = torch.permute(hidden, (1, 0, 2)) # bs, num_eo, h + + if self.embed_input_feature_name: + i_f_names_idcs = torch.reshape( + torch.arange(0, len(embeddable_encoder_outputs), device=self.device), [-1, 1] + ) + embedded_i_f_names = self.embed_i_f_name_layer(i_f_names_idcs) + embedded_i_f_names = torch.unsqueeze(embedded_i_f_names, dim=0) + embedded_i_f_names = torch.tile(embedded_i_f_names, [batch_size, 1, 1]) + if self.embed_input_feature_name == "add": + hidden = hidden + embedded_i_f_names + else: + hidden = torch.cat([hidden, embedded_i_f_names], -1) + + # ================ Transformer Layers ================ + hidden = self.transformer_stack(hidden) + + # ================ Sequence Reduction ================ + hidden = self.reduce_sequence(hidden) + else: + # create empty tensor because there are no category features + hidden = torch.empty([batch_size, 0], device=self.device) + + # ================ Concat Skipped ================ + if len(unembeddable_encoder_outputs) > 0: + unembeddable_encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in unembeddable_encoder_outputs] + # ================ Flatten ================ + if len(unembeddable_encoder_outputs) > 1: + unembeddable_hidden = torch.cat(unembeddable_encoder_outputs, -1) # tf.keras.layers.concatenate + else: + unembeddable_hidden = list(unembeddable_encoder_outputs)[0] + unembeddable_hidden = self.layer_norm(unembeddable_hidden) + + else: + # create empty tensor because there are not numeric/binary features + unembeddable_hidden = torch.tile(self.empty_hidden, [batch_size, 0]) + + # ================ Concat Skipped and Others ================ + # When reduce_output is None, hidden is 3D [batch, seq, dim] but + # unembeddable_hidden is 2D [batch, dim]. Expand to match. + if hidden.dim() == 3 and unembeddable_hidden.dim() == 2: + unembeddable_hidden = unembeddable_hidden.unsqueeze(1).expand(-1, hidden.size(1), -1) + hidden = torch.cat([hidden, unembeddable_hidden], -1) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden) + + return_data = {"combiner_output": hidden} + + if len(inputs) == 1: + for key, value in list(inputs.values())[0].items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + +@register_combiner(ComparatorCombinerConfig) +class ComparatorCombiner(Combiner): + def __init__( + self, + input_features: dict[str, "InputFeature"], + config: ComparatorCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "ComparatorCombiner" + logger.debug(f"Entering {self.name}") + + self.entity_1 = config.entity_1 + self.entity_2 = config.entity_2 + self.required_inputs = set(config.entity_1 + config.entity_2) + self.output_size = config.output_size + + self.fc_stack = None + + # todo future: this may be redundant, check + fc_layers = config.fc_layers + if fc_layers is None and config.num_fc_layers is not None: + fc_layers = [] + for _ in range(config.num_fc_layers): + fc_layers.append({"output_size": config.output_size}) + + if fc_layers is not None: + logger.debug("Setting up FCStack") + self.e1_fc_stack = FCStack( + self.get_entity_shape(config.entity_1)[-1], + layers=fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + ) + self.e2_fc_stack = FCStack( + self.get_entity_shape(config.entity_2)[-1], + layers=fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + ) + + self.last_fc_layer_output_size = fc_layers[-1]["output_size"] + + # todo: set initializer and regularization + self.register_buffer( + "bilinear_weights", + torch.randn([self.last_fc_layer_output_size, self.last_fc_layer_output_size], dtype=torch.float32), + ) + + def get_entity_shape(self, entity: list) -> torch.Size: + sizes = [torch.prod(torch.Tensor([*self.handle.input_features.get(k).output_shape])) for k in entity] + return torch.Size([torch.sum(torch.Tensor(sizes)).type(torch.int32)]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([2 * self.last_fc_layer_output_size + 2]) + + def forward( + self, + inputs: dict, # encoder outputs + ) -> dict[str, torch.Tensor]: # encoder outputs + if inputs.keys() != self.required_inputs: + raise ValueError(f"Missing inputs {self.required_inputs - set(inputs.keys())}") + + ############ + # Entity 1 # + ############ + e1_enc_outputs = [inputs[k][ENCODER_OUTPUT] for k in self.entity_1] + + # ================ Flatten ================ + batch_size = e1_enc_outputs[0].shape[0] + e1_enc_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in e1_enc_outputs] + + # ================ Concat ================ + if len(e1_enc_outputs) > 1: + e1_hidden = torch.cat(e1_enc_outputs, 1) + else: + e1_hidden = list(e1_enc_outputs)[0] + + # ================ Fully Connected ================ + e1_hidden = self.e1_fc_stack(e1_hidden) # [bs, output_size] + + ############ + # Entity 2 # + ############ + e2_enc_outputs = [inputs[k][ENCODER_OUTPUT] for k in self.entity_2] + + # ================ Flatten ================ + batch_size = e2_enc_outputs[0].shape[0] + e2_enc_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in e2_enc_outputs] + + # ================ Concat ================ + if len(e2_enc_outputs) > 1: + e2_hidden = torch.cat(e2_enc_outputs, 1) + else: + e2_hidden = list(e2_enc_outputs)[0] + + # ================ Fully Connected ================ + e2_hidden = self.e2_fc_stack(e2_hidden) # [bs, output_size] + + ########### + # Compare # + ########### + if e1_hidden.shape != e2_hidden.shape: + raise ValueError( + f"Mismatching shapes among dimensions! " + f"entity1 shape: {e1_hidden.shape} " + f"entity2 shape: {e2_hidden.shape}" + ) + + element_wise_mul = e1_hidden * e2_hidden # [bs, output_size] + dot_product = torch.sum(element_wise_mul, 1, keepdim=True) # [bs, 1] + abs_diff = torch.abs(e1_hidden - e2_hidden) # [bs, output_size] + bilinear_prod = torch.sum( + torch.mm(e1_hidden, self.bilinear_weights) * e2_hidden, dim=1, keepdim=True + ) # [bs, 1] + + logger.debug( + "preparing combiner output by concatenating these tensors: " + f"dot_product: {dot_product.shape}, element_size_mul: {element_wise_mul.shape}" + f", abs_diff: {abs_diff.shape}, bilinear_prod {bilinear_prod.shape}" + ) + hidden = torch.cat([dot_product, element_wise_mul, abs_diff, bilinear_prod], 1) # [bs, 2 * output_size + 2] + + return {"combiner_output": hidden} + + +@register_combiner(ProjectAggregateCombinerConfig) +class ProjectAggregateCombiner(Combiner): + def __init__( + self, + input_features: dict[str, "InputFeature"] | None = None, + config: ProjectAggregateCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "ProjectAggregateCombiner" + logger.debug(f" {self.name}") + + logger.debug(" Projectors") + self.projectors = ModuleList( + # regardless of rank-2 or rank-3 input, torch.prod() calculates size + # after flattening the encoder output tensor + [ + Linear( + torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32), + config.projection_size, + ) + for inp in input_features + ] + ) + + self.fc_stack = None + + # todo future: this may be redundant, check + fc_layers = config.fc_layers + if fc_layers is None and config.num_fc_layers is not None: + fc_layers = [] + for _i in range(config.num_fc_layers): + fc_layers.append({"output_size": config.output_size}) + + self.fc_layers = fc_layers + if self.fc_layers is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=config.projection_size, + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + residual=config.residual, + ) + + if input_features and len(input_features) == 1 and self.fc_layers is None: + self.supports_masking = True + + def forward(self, inputs: dict) -> dict: # encoder outputs + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + + # ================ Flatten ================ + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + + # ================ Project ================ + projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)] + hidden = torch.stack(projected) + hidden = torch.permute(hidden, (1, 0, 2)) # shape [bs, num_eo, h] + + # ================ Aggregate ================ + hidden = torch.mean(hidden, dim=1) + + # ================ Fully Connected ================ + if self.fc_stack is not None: + hidden = self.fc_stack(hidden) + + return_data = {"combiner_output": hidden} + + if len(inputs) == 1: + # Workaround for including additional tensors from output of input encoders for + # potential use in decoders, e.g. LSTM state for seq2seq. + # TODO(Justin): Think about how to make this communication work for multi-sequence + # features. Other combiners. + for key, value in list(inputs.values())[0].items(): + if key != ENCODER_OUTPUT: + return_data[key] = value + + return return_data + + +@register_combiner(FTTransformerCombinerConfig) +class FTTransformerCombiner(Combiner): + """FT-Transformer: project each encoder output to a token, prepend [CLS], apply Transformer self-attention. + + Output is the [CLS] token embedding followed by optional FC layers. + Based on Gorishniy et al., "Revisiting Deep Learning Models for Tabular Data", NeurIPS 2021. + """ + + def __init__( + self, + input_features: dict[str, "InputFeature"] | None = None, + config: FTTransformerCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "FTTransformerCombiner" + logger.debug(f" {self.name}") + + self.cls_token = torch.nn.Parameter(torch.randn(1, 1, config.hidden_size)) + + logger.debug(" Projectors") + self.projectors = ModuleList( + [ + Linear( + torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32), + config.hidden_size, + ) + for inp in input_features + ] + ) + + logger.debug(" TransformerStack") + self.transformer_stack = TransformerStack( + input_size=config.hidden_size, + max_sequence_length=len(input_features) + 1, + hidden_size=config.hidden_size, + num_heads=config.num_heads, + output_size=config.transformer_output_size, + num_layers=config.num_layers, + dropout=config.dropout, + ) + + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.transformer_stack.output_shape[-1], + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.fc_activation, + default_dropout=config.fc_dropout, + fc_residual=config.fc_residual, + ) + + def forward(self, inputs) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + + projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)] + tokens = torch.stack(projected, dim=1) + + cls_token = self.cls_token.expand(batch_size, -1, -1) + tokens = torch.cat([cls_token, tokens], dim=1) + + hidden = self.transformer_stack(tokens) + hidden = hidden[:, 0, :] # [CLS] token output + hidden = self.fc_stack(hidden) + + return {"combiner_output": hidden} + + +@register_combiner(CrossAttentionCombinerConfig) +class CrossAttentionCombiner(Combiner): + """Pairwise cross-attention between all feature pairs. + + Each feature selectively queries relevant information from all other features via multi-head cross-attention. + Research consistently shows 2-10% improvement over concatenation across modalities. + """ + + def __init__( + self, + input_features: dict[str, "InputFeature"] | None = None, + config: CrossAttentionCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "CrossAttentionCombiner" + logger.debug(f" {self.name}") + self.num_features = len(input_features) + self.hidden_size = config.hidden_size + + if self.num_features < 2: + logger.warning( + "CrossAttentionCombiner with a single input feature has no cross-modal pairs to attend over. " + "Consider using ConcatCombiner or adding more input features." + ) + + logger.debug(" Projectors") + self.projectors = ModuleList( + [ + Linear( + torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32), + config.hidden_size, + ) + for inp in input_features + ] + ) + + self.cross_attn_layers = ModuleList( + [ + torch.nn.MultiheadAttention( + config.hidden_size, config.num_heads, dropout=config.dropout, batch_first=True + ) + for _ in range(self.num_features) + ] + ) + self.layer_norms = ModuleList([torch.nn.LayerNorm(config.hidden_size) for _ in range(self.num_features)]) + + logger.debug(" FCStack") + self.fc_stack = FCStack( + config.hidden_size * self.num_features, + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + ) + + def forward(self, inputs) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)] + + fused = [] + for i in range(self.num_features): + query = projected[i].unsqueeze(1) + others = [projected[j] for j in range(self.num_features) if j != i] + if others: + kv = torch.stack(others, dim=1) + attended, _ = self.cross_attn_layers[i](query, kv, kv) + attended = attended.squeeze(1) + else: + attended = torch.zeros_like(projected[i]) + fused.append(self.layer_norms[i](projected[i] + attended)) + + hidden = torch.cat(fused, dim=-1) + hidden = self.fc_stack(hidden) + return {"combiner_output": hidden} + + +@register_combiner(PerceiverCombinerConfig) +class PerceiverCombiner(Combiner): + """Perceiver IO-style combiner with learnable latent bottleneck tokens. + + Learnable latent tokens cross-attend to all encoder outputs, then self-attend. Based on Jaegle et al., "Perceiver + IO", ICML 2022. + """ + + def __init__( + self, input_features: dict[str, "InputFeature"] | None = None, config: PerceiverCombinerConfig = None, **kwargs + ): + super().__init__(input_features) + self.name = "PerceiverCombiner" + logger.debug(f" {self.name}") + self.latent_dim = config.latent_dim + self.reduce_output = config.reduce_output + + self.latents = torch.nn.Parameter(torch.randn(config.num_latents, config.latent_dim) * 0.02) + + logger.debug(" Projectors") + self.projectors = ModuleList( + [ + Linear( + torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32), + config.latent_dim, + ) + for inp in input_features + ] + ) + + self.cross_attn = torch.nn.MultiheadAttention( + config.latent_dim, config.num_heads, dropout=config.dropout, batch_first=True + ) + self.cross_attn_norm = torch.nn.LayerNorm(config.latent_dim) + + self.self_attn_stack = TransformerStack( + input_size=config.latent_dim, + max_sequence_length=config.num_latents, + hidden_size=config.latent_dim, + num_heads=config.num_heads, + output_size=config.latent_dim, + num_layers=config.num_self_attention_layers, + dropout=config.dropout, + ) + + self.reduce_sequence = SequenceReducer( + reduce_mode=config.reduce_output, + max_sequence_length=config.num_latents, + encoding_size=config.latent_dim, + ) + + if config.reduce_output is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + config.latent_dim, + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + ) + else: + self.fc_stack = None + + def forward(self, inputs) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + projected = [self.projectors[i](eo).unsqueeze(1) for i, eo in enumerate(encoder_outputs)] + + all_tokens = torch.cat(projected, dim=1) + latents = self.latents.unsqueeze(0).expand(batch_size, -1, -1) + + attended, _ = self.cross_attn(latents, all_tokens, all_tokens) + latents = self.cross_attn_norm(latents + attended) + + hidden = self.self_attn_stack(latents) + + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + if self.fc_stack is not None: + hidden = self.fc_stack(hidden) + + return {"combiner_output": hidden} + + +@register_combiner(GatedFusionCombinerConfig) +class GatedFusionCombiner(Combiner): + """Gated cross-modal fusion inspired by Flamingo's gated cross-attention. + + Per-feature gates are initialized near zero for stable training, allowing cross-modal residuals to be added + gradually as training progresses. + """ + + def __init__( + self, + input_features: dict[str, "InputFeature"] | None = None, + config: GatedFusionCombinerConfig = None, + **kwargs, + ): + super().__init__(input_features) + self.name = "GatedFusionCombiner" + logger.debug(f" {self.name}") + self.num_features = len(input_features) + self.hidden_size = config.hidden_size + + logger.debug(" Projectors") + self.projectors = ModuleList( + [ + Linear( + torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32), + config.hidden_size, + ) + for inp in input_features + ] + ) + + self.gates = torch.nn.ParameterList( + [torch.nn.Parameter(torch.zeros(config.hidden_size) * 0.01) for _ in range(self.num_features)] + ) + + if self.num_features > 1: + self.cross_projections = ModuleList( + [ + Linear(config.hidden_size * (self.num_features - 1), config.hidden_size) + for _ in range(self.num_features) + ] + ) + else: + self.cross_projections = None + + logger.debug(" FCStack") + self.fc_stack = FCStack( + config.hidden_size * self.num_features, + layers=config.fc_layers, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_use_bias=config.use_bias, + default_weights_initializer=config.weights_initializer, + default_bias_initializer=config.bias_initializer, + default_norm=config.norm, + default_norm_params=config.norm_params, + default_activation=config.activation, + default_dropout=config.dropout, + ) + + def forward(self, inputs) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)] + + fused = [] + for i in range(self.num_features): + if self.cross_projections is not None: + others = [projected[j] for j in range(self.num_features) if j != i] + cross_input = torch.cat(others, dim=-1) + cross_modal = self.cross_projections[i](cross_input) + fused_i = projected[i] + torch.tanh(self.gates[i]) * cross_modal + else: + fused_i = projected[i] + fused.append(fused_i) + + hidden = torch.cat(fused, dim=-1) + hidden = self.fc_stack(hidden) + return {"combiner_output": hidden} + + +@register_combiner(HyperNetworkCombinerConfig) +class HyperNetworkCombiner(Combiner): + """HyperNetwork combiner: one modality generates weights for processing another. + + Instead of combining features additively, a hypernetwork generates the weights + of processing layers based on conditioning features. This means metadata + fundamentally changes how other features are processed. + + Unique Ludwig differentiator. Based on HyperFusion (arXiv 2403.13319, 2024). + """ + + def __init__(self, input_features: dict[str, "InputFeature"] | None = None, config=None, **kwargs): + super().__init__(input_features) + self.name = "HyperNetworkCombiner" + logger.debug(f" {self.name}") + self.num_features = len(input_features) + hidden_size = config.hidden_size + hyper_hidden = config.hyper_hidden_size + + # Project each feature to hidden_size + self.projectors = ModuleList( + [ + Linear( + int(torch.prod(torch.Tensor([*input_features.get(inp).output_shape]))), + hidden_size, + ) + for inp in input_features + ] + ) + + # Hypernetwork: first feature generates weights for processing others + # Weight generation: hidden_size -> (hidden_size * hidden_size) for a linear layer + self.hyper_net = torch.nn.Sequential( + Linear(hidden_size, hyper_hidden), + torch.nn.ReLU(), + Linear(hyper_hidden, hidden_size * hidden_size), + ) + + # FC stack on output + self.fc_stack = FCStack( + hidden_size * self.num_features, + num_layers=config.num_fc_layers, + default_output_size=config.output_size, + default_activation=config.activation, + default_dropout=config.dropout, + ) + + def forward(self, inputs) -> dict: + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + batch_size = encoder_outputs[0].shape[0] + encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs] + projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)] + + hidden_size = projected[0].shape[-1] + + # Use first feature as conditioning signal for the hypernetwork + conditioning = projected[0] + generated_weights = self.hyper_net(conditioning) # [batch, hidden*hidden] + generated_weights = generated_weights.view(batch_size, hidden_size, hidden_size) + + # Apply generated weights to all other features + transformed = [projected[0]] # conditioning feature passes through + for i in range(1, self.num_features): + # Batch matrix multiply: [batch, hidden, hidden] x [batch, hidden, 1] -> [batch, hidden, 1] + t = torch.bmm(generated_weights, projected[i].unsqueeze(-1)).squeeze(-1) + transformed.append(t) + + hidden = torch.cat(transformed, dim=-1) + hidden = self.fc_stack(hidden) + return {"combiner_output": hidden} diff --git a/ludwig/combiners/tabpfn_v2_combiner.py b/ludwig/combiners/tabpfn_v2_combiner.py new file mode 100644 index 0000000..a1e3902 --- /dev/null +++ b/ludwig/combiners/tabpfn_v2_combiner.py @@ -0,0 +1,105 @@ +"""TabPFN v2 combiner — pretrained-foundation-model fusion for tabular data. + +TabPFN v2 (Hollmann et al., 2022 / Hollmann et al., 2025) is a prior-fitted transformer +trained on synthetic tabular datasets that performs strong in-context learning on small +(<=10k rows) tabular problems without gradient-based fine-tuning. + +This combiner wraps a pretrained TabPFN v2 model as a fusion block inside Ludwig's ECD +architecture. It concatenates the per-feature encoder outputs into a single tabular +row representation and passes it through the frozen or LoRA-adapted TabPFN encoder, using +TabPFN's internal hidden states as the combined representation fed to the output decoders. + +Requires the optional ``tabpfn`` package (v2+). Install with ``pip install tabpfn``. + +Note: TabPFN's public API is sklearn-style (fit_predict on numpy arrays) and is not +differentiable. The current implementation uses a learnable linear projection on top of +the concatenated encoder outputs as a differentiable proxy. Full integration of TabPFN's +contextual embeddings into the gradient path is future work. +""" + +from __future__ import annotations + +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.combiners.combiners import Combiner, register_combiner +from ludwig.schema.combiners.tabpfn_v2 import TabPFNV2CombinerConfig + +logger = logging.getLogger(__name__) + + +@register_combiner(TabPFNV2CombinerConfig) +@DeveloperAPI +class TabPFNV2Combiner(Combiner): + """Combiner backed by a pretrained TabPFN v2 model. + + Concatenates per-feature encoder outputs along the feature dim, treats the resulting + ``(batch, concatenated_hidden)`` vector as a pseudo-tabular row, and projects it to + ``output_size`` via a learnable linear head so downstream decoders see a fixed-width + vector regardless of the number of input features. + """ + + def __init__( + self, + input_features: dict | None = None, + config: TabPFNV2CombinerConfig | None = None, + **kwargs, + ) -> None: + super().__init__(input_features) + if config is None: + config = TabPFNV2CombinerConfig() + self.config = config + self.name = "TabPFNV2Combiner" + + self.output_size = config.output_size + # LazyLinear defers the in_features inference to the first forward pass, + # so __init__ can be called with input_features=None for schema inspection. + self.projection = torch.nn.LazyLinear(config.output_size) + + # Check tabpfn availability eagerly so users get a clear pip install message + # immediately rather than a cryptic error at forward-pass time. + try: + import tabpfn # noqa: F401 + except ImportError as exc: + raise ImportError( + "The tabpfn_v2 combiner requires the optional 'tabpfn' package. Install with: pip install tabpfn" + ) from exc + + # Defer heavy TabPFN loading until _lazy_load_tabpfn() is explicitly called. + self._tabpfn_model = None + + def _lazy_load_tabpfn(self) -> None: + if self._tabpfn_model is not None: + return + try: + from tabpfn import TabPFNRegressor + except ImportError as exc: + raise ImportError( + "The tabpfn_v2 combiner requires the optional 'tabpfn' package. Install with: pip install tabpfn" + ) from exc + self._tabpfn_model = TabPFNRegressor( + device=self.config.device, + n_estimators=self.config.n_estimators, + ignore_pretraining_limits=True, + ) + logger.info("Loaded TabPFN v2 (%s, n_estimators=%d)", self.config.device, self.config.n_estimators) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.output_size]) + + def forward(self, inputs: dict[str, dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]: + # Concatenate per-feature encoder outputs along the last dim. + encoder_outputs = [feat["encoder_output"] for feat in inputs.values()] + if not encoder_outputs: + raise RuntimeError("TabPFNV2Combiner received no input features.") + hidden = torch.cat(encoder_outputs, dim=-1) + + # Project concatenated encodings to output_size. TabPFN's non-differentiable + # sklearn fit_predict API cannot be called in-loop during gradient training; + # using it as a pre-training feature extractor is future work. + embedding = self.projection(hidden) + + return {"combiner_output": embedding} diff --git a/ludwig/config_generation.py b/ludwig/config_generation.py new file mode 100644 index 0000000..aa34014 --- /dev/null +++ b/ludwig/config_generation.py @@ -0,0 +1,265 @@ +"""LLM-driven config generation: describe your ML task in English, get a Ludwig config. + +Uses the model's JSON Schema as context for an LLM to generate valid Ludwig configs. +The generated config is validated with strict Pydantic validation before returning. + +Usage: + from ludwig.config_generation import generate_config + + config = generate_config( + "I have a CSV with columns: age (number), income (number), education (text), " + "and I want to predict whether someone will default on a loan (binary)." + ) + # Returns a valid Ludwig config dict +""" + +import json +import logging + +logger = logging.getLogger(__name__) + + +def get_ludwig_schema_context() -> str: + """Get a compact JSON Schema description of Ludwig's config for LLM context.""" + try: + from ludwig.schema.model_types.ecd import ECDModelConfig + + ECDModelConfig.model_json_schema() # Validate schema is available + # Extract just the key parts to fit in context window + return json.dumps( + { + "description": "Ludwig declarative ML config. " + "Specify input_features, output_features, combiner, trainer.", + "input_feature_types": [ + "number", + "category", + "binary", + "text", + "image", + "audio", + "sequence", + "set", + "vector", + "timeseries", + "date", + "h3", + "bag", + ], + "output_feature_types": ["number", "category", "binary", "text", "sequence", "set", "vector"], + "combiner_types": [ + "concat", + "transformer", + "ft_transformer", + "cross_attention", + "perceiver", + "gated_fusion", + "tabnet", + "tabtransformer", + "comparator", + "project_aggregate", + "sequence", + "sequence_concat", + ], + "encoder_types_number": ["passthrough", "dense", "ple", "periodic"], + "encoder_types_category": ["dense", "sparse", "onehot", "passthrough"], + "encoder_types_text": [ + "auto_transformer", + "bert", + "gpt2", + "parallel_cnn", + "stacked_cnn", + "stacked_parallel_cnn", + "rnn", + "cnnrnn", + "transformer", + ], + "loss_balancing": [ + "none", + "log_transform", + "uncertainty", + "famo", + "gradnorm", + "nash_mtl", + "pareto_mtl", + ], + "trainer_type_ecd": "trainer (epochs, batch_size, " + "learning_rate, optimizer, early_stop, loss_balancing)", + "trainer_type_llm": "finetune, dpo, kto, orpo, grpo, none", + "presets": ["medium_quality", "high_quality", "best_quality"], + "example_config": { + "input_features": [ + {"name": "text_col", "type": "text", "encoder": {"type": "auto_transformer"}}, + {"name": "num_col", "type": "number"}, + ], + "output_features": [{"name": "target", "type": "category"}], + "combiner": {"type": "concat"}, + "trainer": {"epochs": 50, "batch_size": 128}, + }, + }, + indent=2, + ) + except Exception as exc: + logger.warning("Failed to build Ludwig schema context for config generation: %s", exc) + return "{}" + + +def generate_config( + task_description: str, + model: str = "claude-sonnet-4-20250514", + api_key: str | None = None, + validate: bool = True, +) -> dict: + """Generate a Ludwig config from a natural language task description. + + Uses an LLM to translate the description into a valid Ludwig YAML config. + The generated config is validated against Ludwig's Pydantic schema. + + Args: + task_description: Natural language description of the ML task. + Example: "I have customer data with age, income, and purchase history. + I want to predict churn (binary) and lifetime value (number)." + model: LLM model to use for generation. + api_key: API key for the LLM provider. If None, reads from environment. + validate: If True, validate the generated config against Ludwig's schema. + + Returns: + Dict with a valid Ludwig config. + + Raises: + ValueError if the generated config is invalid and validate=True. + """ + schema_context = get_ludwig_schema_context() + + prompt = f"""You are a Ludwig ML framework expert. Generate a valid Ludwig YAML config for this task. + +Ludwig Config Schema Context: +{schema_context} + +Task Description: +{task_description} + +Generate ONLY a valid JSON config (no markdown, no explanation). The config must have: +- input_features: list of dicts with name, type, and optional encoder +- output_features: list of dicts with name and type +- combiner: dict with type and parameters +- trainer: dict with epochs, batch_size, learning_rate + +Choose appropriate feature types, encoders, and combiner based on the task.""" + + try: + import anthropic + + client = anthropic.Anthropic(api_key=api_key) + response = client.messages.create( + model=model, + max_tokens=2048, + messages=[{"role": "user", "content": prompt}], + ) + config_str = response.content[0].text + except ImportError: + try: + from openai import OpenAI + + client = OpenAI(api_key=api_key) + response = client.chat.completions.create( + model=model if "gpt" in model else "gpt-4", + messages=[{"role": "user", "content": prompt}], + max_tokens=2048, + ) + config_str = response.choices[0].message.content + except ImportError: + raise ImportError( + "Either 'anthropic' or 'openai' package is required for config generation. " + "Install with: pip install anthropic OR pip install openai" + ) + + # Parse JSON from response (handle markdown code blocks) + config_str = config_str.strip() + if config_str.startswith("```"): + lines = config_str.split("\n") + config_str = "\n".join(lines[1:-1]) + + try: + config = json.loads(config_str) + except json.JSONDecodeError as exc: + raise ValueError( + f"LLM returned non-JSON response (JSONDecodeError: {exc}). Raw response:\n{config_str}" + ) from exc + + if validate: + try: + from ludwig.schema.model_types.base import ModelConfig + + validated = ModelConfig.from_dict(config) + logger.info("Generated config validated successfully") + return validated.to_dict() + except Exception as e: + raise ValueError(f"Generated config failed validation: {e}") from e + + return config + + +def cli_generate_config(sys_argv): + """CLI entry point for config generation.""" + import argparse + + import yaml + + parser = argparse.ArgumentParser( + description="Generate a Ludwig config from a natural language task description", + prog="ludwig generate_config", + ) + parser.add_argument( + "description", + nargs="?", + help="Natural language description of the ML task", + ) + parser.add_argument( + "--model", + default="claude-sonnet-4-20250514", + help="LLM model to use (default: claude-sonnet-4-20250514)", + ) + parser.add_argument( + "--api_key", + default=None, + help="API key for the LLM provider (default: reads from ANTHROPIC_API_KEY or OPENAI_API_KEY env)", + ) + parser.add_argument( + "--output", + "-o", + default=None, + help="Output file path. Prints to stdout if not specified.", + ) + parser.add_argument( + "--no-validate", + action="store_true", + help="Skip config validation", + ) + + args = parser.parse_args(sys_argv) + + if not args.description: + # Read from stdin if no description provided + import sys + + print("Enter your ML task description (Ctrl+D when done):", file=sys.stderr) + args.description = sys.stdin.read().strip() + + if not args.description: + parser.error("A task description is required") + + config = generate_config( + task_description=args.description, + model=args.model, + api_key=args.api_key, + validate=not args.no_validate, + ) + + output = yaml.dump(config, default_flow_style=False, sort_keys=False) + + if args.output: + with open(args.output, "w") as f: + f.write(output) + print(f"Config saved to {args.output}") + else: + print(output) diff --git a/ludwig/config_sampling/__init__.py b/ludwig/config_sampling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/config_sampling/explore_schema.py b/ludwig/config_sampling/explore_schema.py new file mode 100644 index 0000000..4f7a0ae --- /dev/null +++ b/ludwig/config_sampling/explore_schema.py @@ -0,0 +1,315 @@ +import copy +import random +from collections import deque, namedtuple +from typing import Any + +import pandas as pd +import pydantic + +from ludwig.config_sampling.parameter_sampling import handle_property_type, ParameterBaseTypes +from ludwig.constants import SEQUENCE, TEXT, TIMESERIES +from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df +from ludwig.schema.model_types.base import ModelConfig +from ludwig.types import ModelConfigDict +from ludwig.utils.misc_utils import merge_dict + +# number of examples to generate for synthetic dataset +NUM_SYNTHETIC_EXAMPLES = 10 + +ConfigOption = namedtuple("ConfigOption", ["config_option", "fully_explored"]) + + +def explore_properties( + jsonschema_properties: dict[str, Any], + parent_parameter_path: str, + dq: deque[ConfigOption], + allow_list: list[str] | None = None, +) -> deque[tuple[dict, bool]]: + """Recursively explores the `properties` part of any subsection of the schema. + + Args: + jsonschema_properties: any properties section of the schema. + parent_parameter_path: period-delimited list of parent dictionary keys up to the given jsonschema_properties + (e.g. defaults.number.preprocessing) + dq: dequeue data structure that stores tuples of (config_options, fully_explored). + config_options: Dict[str, List], fully_explored: bool is a dictionary is a dictionary of parameter name to + list of values to explore. + fully_explored is a boolean value indicating whether all subsections of the properties dictionary have been + explored. + allow_list: list of top level keys of the properties dictionary to skip. + + Returns: + A deque of (dict, bool) tuples. + - The first element of the tuple contains a dictionary of config options, which maps from a ludwig + config parameter to a list of the values to be explored for that parameter. Here's an example: + trainer.batch_size: ["auto", 2, 43] + trainer.learning_rate: ["auto", 0.1, 0.00002, 0.32424] + ... + - The second element of the tuple is whether we've explored this "config path" + fully. This is important to track when recursing into nested structures. + """ + # processed_dq will contain complete config options with all the parameters in the properties dictionary + # dq will contain configs options that are still being completed. + processed_dq = deque() + while dq and not dq[0].fully_explored: + for parameter_name_or_section, jsonschema_property in jsonschema_properties.items(): + if allow_list and parameter_name_or_section not in allow_list: + continue + + parameter_path = ( + f"{parent_parameter_path}.{parameter_name_or_section}" + if parent_parameter_path + else parameter_name_or_section + ) + config_options, _ = dq.popleft() + + if "properties" in jsonschema_property and "allOf" in jsonschema_property: + for child_item in jsonschema_property["allOf"]: + expanded_config_options_dq = explore_from_all_of( + config_options=copy.deepcopy(config_options), item=child_item, key_so_far=parameter_path + ) + # add returned child config options to the deque to be processed. + dq.extend(expanded_config_options_dq) + + elif "properties" in jsonschema_property and "allOf" not in jsonschema_property: + # This is the case where we don't have a list of properties, just a properties + # dictionary nested inside another. + child_properties = jsonschema_property["properties"] + # a new dequeue to be passed to explore parameters from + raw_entry = deque([ConfigOption(copy.deepcopy(config_options), False)]) + child_config_options_dq = explore_properties(child_properties, parameter_path, raw_entry) + merged_config_options_dq = merge_dq(config_options, child_config_options_dq) + # add returned config options to the deque to be processed. + dq.extend(merged_config_options_dq) + + else: + # this is the base case. + parameter_samples = get_samples(jsonschema_property) + if parameter_samples: + config_options[parameter_path] = parameter_samples + + # add config_options back to queue. fully_explored = False because we still didn't finish + # exploring all the keys in the properties dictionary. + dq.appendleft(ConfigOption(config_options, False)) + + # at this point, we finished exploring all keys of the properties dictionary. Add all config options + # to the processed queue. + while dq: + config_options, _ = dq.popleft() + processed_dq.append(ConfigOption(config_options, True)) + + return processed_dq + + +def get_samples(jsonschema_property: dict[str, Any]) -> list[ParameterBaseTypes]: + """Get possible values for a leaf property (no sub-properties). + + Args: + jsonschema_property: leaf property in the schema. Has no sub-properties. + """ + if "oneOf" in jsonschema_property: + temp = [] + for elem in jsonschema_property["oneOf"]: + temp += get_potential_values(elem) + return temp + else: + return get_potential_values(jsonschema_property) + + +def merge_dq(config_options: dict[str, Any], child_config_options_dq: deque[ConfigOption]) -> deque[ConfigOption]: + """Merge config_options with the child_config_options in the dq.""" + dq = deque() + while child_config_options_dq: + child_config_options, visited = child_config_options_dq.popleft() + cfg = merge_dict(child_config_options, config_options) + dq.append(ConfigOption(cfg, visited)) + return dq + + +def explore_from_all_of(config_options: dict[str, Any], item: dict[str, Any], key_so_far: str) -> deque[ConfigOption]: + """Takes a child of `allOf` and calls `explore_properties` on it.""" + for parameter_name_or_section in item["if"]["properties"]: + config_options[key_so_far + "." + parameter_name_or_section] = item["if"]["properties"][ + parameter_name_or_section + ]["const"] + jsonschema_properties = item["then"]["properties"] + raw_entry = deque([ConfigOption(copy.deepcopy(config_options), False)]) + return explore_properties(jsonschema_properties, parent_parameter_path=key_so_far, dq=raw_entry) + + +def get_potential_values(item: dict[str, Any]) -> list[ParameterBaseTypes | list[ParameterBaseTypes]]: + """Returns a list of values to explore for a config parameter. + + Param: + item: config parameter-specific dictionary. Considered as a leaf in the schema. Contains type, default, and + parameter metadata, etc. + """ + temp = [] + item_type = item.get("type") + if item_type is None: + # No explicit type — try to infer from enum/const/default + if "enum" in item: + return [v for v in item["enum"] if v is not None] + if "const" in item: + return [item["const"]] + if "default" in item: + return [item["default"]] + return [] + # Case where we're using OneOf (e.g. to allow batch size 'auto' and integers) + if isinstance(item_type, list): + for property_type in item_type: + temp += handle_property_type(property_type, item) + else: + temp += handle_property_type(item_type, item) + + # Make sure values are unique. Not using set because some values are unhashable. + unique_temp = [] + for temp_item in temp: + if temp_item not in unique_temp: + unique_temp.append(temp_item) + return unique_temp + + +def generate_possible_configs(config_options: dict[str, Any]): + """Generate exhaustive configs from config_options. + + This function does not take a cross product of all the options for all the config parameters. It selects parameter + values independently from each other. + + Args: + config_options: dictionary mapping from ludwig config parameter to all values to be explored. + Here's an example of what it could look like: + + trainer.batch_size: ["auto", 2, 43] + trainer.learning_rate: ["auto", 0.1, 0.00002, 0.32424] + ... + """ + # The number of configs to generate is the max length of the lists of samples over all parameters. + num_configs = 1 + for parameter_name in config_options: + if isinstance(config_options[parameter_name], list): + num_configs = max(num_configs, len(config_options[parameter_name])) + config_options[parameter_name] = deque(config_options[parameter_name]) + + for _ in range(num_configs): + config = {} + for parameter_name in config_options: + # if parameter is regular parameter with explored values. + if config_options[parameter_name] and not isinstance(config_options[parameter_name], str): + config[parameter_name] = config_options[parameter_name].popleft() + # case for parameters where we don't have choices such as `encoder.type: parallel_cnn` that + # cause the downstream parameters to change. + elif isinstance(config_options[parameter_name], str): + config[parameter_name] = config_options[parameter_name] + yield create_nested_dict(config) + + +def create_nested_dict(flat_dict: dict[str, float | str]) -> ModelConfigDict: + """Generate a nested dict out of a flat dict whose keys are delimited by a delimiter character. + + Args: + flat_dict: potential generated baseline config. Here's an example of what it could look like: + + trainer.batch_size: 324 + trainer.learning_rate: 0.0635 + + The expected output would be + + trainer: + batch_size: 324 + learning_rate: 0.0635 + """ + + def to_nested_format(parameter_name: str, value: str | int | float, delimiter: str = ".") -> dict[str, Any]: + # https://stackoverflow.com/a/40401961 + split_parameter_name = parameter_name.split(delimiter) + for parameter_name_or_section in reversed(split_parameter_name): + value = {parameter_name_or_section: value} + return value + + config = {} + for parameter_name_or_section in flat_dict: + config = merge_dict( + config, to_nested_format(parameter_name_or_section, copy.deepcopy(flat_dict[parameter_name_or_section])) + ) + return config + + +def combine_configs( + explored: deque[tuple[dict, bool]], config: ModelConfigDict +) -> list[tuple[ModelConfigDict, pd.DataFrame]]: + """Merge base config with explored sections. + + Args: + explored: deque containing all the config options. + config: base Ludwig config to merge the explored configs with. + """ + dataset = build_synthetic_dataset_df(NUM_SYNTHETIC_EXAMPLES, config) + ret = [] + for config_options, _ in explored: + for default_config in generate_possible_configs(config_options=config_options): + merged_config = merge_dict(copy.deepcopy(config), default_config) + try: + ModelConfig.from_dict(merged_config) + ret.append((merged_config, dataset)) + except pydantic.ValidationError: + pass + return ret + + +def combine_configs_for_comparator_combiner( + explored: deque[tuple], config: ModelConfigDict +) -> list[tuple[ModelConfigDict, pd.DataFrame]]: + """Merge base config with explored sections. + + Completes the entity_1 and entity_2 paramters of the comparator combiner. + + Args: + explored: deque containing all the config options. + config: base Ludwig config to merge the explored configs with. + """ + dataset = build_synthetic_dataset_df(NUM_SYNTHETIC_EXAMPLES, config) + ret = [] + for item in explored: + for default_config in generate_possible_configs(config_options=item[0]): + merged_config = merge_dict(copy.deepcopy(config), default_config) + + # create two random lists for entity1 and entity2 + entity_names = [feature["name"] for feature in config["input_features"]] + random.shuffle(entity_names) + entity_1_size = random.randint(1, len(entity_names) - 1) + merged_config["combiner"]["entity_1"] = entity_names[:entity_1_size] + merged_config["combiner"]["entity_2"] = entity_names[entity_1_size:] + try: + ModelConfig.from_dict(merged_config) + ret.append((merged_config, dataset)) + except pydantic.ValidationError: + pass + return ret + + +def combine_configs_for_sequence_combiner( + explored: deque[tuple], config: ModelConfigDict +) -> list[tuple[ModelConfigDict, pd.DataFrame]]: + """Merge base config with explored sections. + + Uses the right reduce_output strategy for the sequence and sequence_concat combiners. + + Args: + explored: deque containing all the config options. + config: base Ludwig config to merge the explored configs with. + """ + dataset = build_synthetic_dataset_df(NUM_SYNTHETIC_EXAMPLES, config) + ret = [] + for item in explored: + for default_config in generate_possible_configs(config_options=item[0]): + merged_config = merge_dict(copy.deepcopy(config), default_config) + for i in range(len(merged_config["input_features"])): + if merged_config["input_features"][i]["type"] in {SEQUENCE, TEXT, TIMESERIES}: + merged_config["input_features"][0]["encoder"] = {"type": "embed", "reduce_output": None} + try: + ModelConfig.from_dict(merged_config) + ret.append((merged_config, dataset)) + except pydantic.ValidationError: + pass + return ret diff --git a/ludwig/config_sampling/parameter_sampling.py b/ludwig/config_sampling/parameter_sampling.py new file mode 100644 index 0000000..2673644 --- /dev/null +++ b/ludwig/config_sampling/parameter_sampling.py @@ -0,0 +1,145 @@ +import random +from typing import Any + +from ludwig.schema.metadata.parameter_metadata import ExpectedImpact + +# base types for ludwig config parameters. +ParameterBaseTypes = str | float | int | bool | None + + +def handle_property_type( + property_type: str, item: dict[str, Any], expected_impact: ExpectedImpact = ExpectedImpact.HIGH +) -> list[ParameterBaseTypes | list[ParameterBaseTypes]]: + """Return possible parameter values for a parameter type. + + Args: + property_type: type of the parameter (e.g. array, number, etc.) + item: dictionary containing details on the parameter such as default, min and max values. + expected_impact: threshold expected impact that we'd like to include. + """ + parameter_metadata = item.get("parameter_metadata") + if not parameter_metadata: + return [] + + # don't explore internal only parameters. + if parameter_metadata.get("internal_only", True): + return [] + + # don't explore parameters that have expected impact less than HIGH. + if parameter_metadata.get("expected_impact", ExpectedImpact.LOW) < expected_impact: + return [] + + if property_type == "number": + return explore_number(item) + elif property_type == "integer": + return explore_integer(item) + elif property_type == "string": + return explore_string(item) + elif property_type == "boolean": + return explore_boolean() + elif property_type == "null": + return explore_null() + elif property_type == "array": + return explore_array(item) + else: + return [] + + +def explore_array(item: dict[str, Any]) -> list[list[ParameterBaseTypes]]: + """Return possible parameter values for the `array` parameter type. + + Args: + item: dictionary containing details on the parameter such as default, min and max values. + """ + + candidates = [] + if item.get("default"): + candidates.append(item["default"]) + + item_choices = [] + maxlen = 0 + + # In the case where the length of the array isn't defined. + if not isinstance(item["items"], list): + return [] + + for item_of in item["items"]: + choices = handle_property_type(item_of["type"], item_of) + maxlen = max(maxlen, len(choices)) + item_choices.append(choices) + + # pad to same length + for i in range(len(item_choices)): + item_choices[i] = maxlen * item_choices[i] + item_choices[i] = item_choices[i][:maxlen] + + merged = list(zip(*item_choices)) + candidates + return [list(tup) for tup in merged] + + +def explore_number(item: dict[str, Any]) -> list[ParameterBaseTypes]: + """Return possible parameter values for the `number` parameter type. + + Args: + item: dictionary containing details on the parameter such as default, min and max values. + TODO(Wael): Improve logic. + """ + minimum, maximum = 0, 1 + if "default" not in item or item["default"] is None: + candidates = [] + else: + candidates = [1, 2, item["default"], 2 * (item["default"] + 1), item["default"] // 2, -1 * item["default"]] + + if "minimum" in item: + minimum = item["minimum"] + candidates = [num for num in candidates if num > minimum] + if "maximum" in item: + maximum = item["maximum"] + candidates = [num for num in candidates if num < maximum] + return candidates + [random.random() * 0.99 * maximum] + + +def explore_integer(item: dict[str, Any]) -> list[ParameterBaseTypes]: + """Return possible parameter values for the `integer` parameter type. + + Args: + item: dictionary containing details on the parameter such as default, min and max values. + TODO(Wael): Improve logic. + """ + minimum, maximum = 0, 10 + + if "default" not in item or item["default"] is None: + candidates = [] + else: + candidates = [item["default"], 2 * (item["default"] + 1), item["default"] // 2, -1 * item["default"]] + + if "minimum" in item: + minimum = item["minimum"] + candidates = [num for num in candidates if num >= item["minimum"]] + if "maximum" in item: + maximum = item["maximum"] + candidates = [num for num in candidates if num <= item["maximum"]] + + return candidates + [random.randint(minimum, maximum)] + + +def explore_string(item: dict[str, Any]) -> list[ParameterBaseTypes]: + """Return possible parameter values for the `string` parameter type. + + Args: + item: dictionary containing details on the parameter such as default, min and max values. + """ + + if "enum" in item: + return item["enum"] + return [item["default"]] + + +def explore_boolean() -> list[bool]: + """Return possible parameter values for the `boolean` parameter type (i.e. [True, False])""" + return [True, False] + + +def explore_null() -> list[None]: + """Return possible parameter values for the `null` parameter type (i.e. [None])""" + return [None] diff --git a/ludwig/config_validation/__init__.py b/ludwig/config_validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/config_validation/checks.py b/ludwig/config_validation/checks.py new file mode 100644 index 0000000..576363f --- /dev/null +++ b/ludwig/config_validation/checks.py @@ -0,0 +1,738 @@ +"""Checks that are not easily covered by marshmallow JSON schema validation like parameter interdependencies.""" + +from abc import ABC, abstractmethod +from collections.abc import Callable +from re import findall +from typing import TYPE_CHECKING + +from transformers import AutoConfig + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + AUDIO, + BINARY, + IMAGE, + IN_MEMORY, + MIN_QUANTIZATION_BITS_FOR_MERGE_AND_UNLOAD, + MODEL_ECD, + MODEL_LLM, + SEQUENCE, + SET, + TEXT, + TIMESERIES, + VECTOR, +) +from ludwig.error import ConfigValidationError +from ludwig.utils.metric_utils import get_feature_to_metric_names_map_from_feature_collection +from ludwig.utils.misc_utils import merge_dict + +if TYPE_CHECKING: + from ludwig.schema.model_config import ModelConfig + +# Set of all sequence feature types. +SEQUENCE_OUTPUT_FEATURE_TYPES = {SEQUENCE, TEXT, SET, VECTOR} + + +class ConfigCheckRegistry: + """A registry of configuration checks.""" + + def __init__(self): + self._registry = [] + + def register(self, check_fn): + self._registry.append(check_fn) + + def check_config(self, config: "ModelConfig") -> None: + for check_fn in self._registry: + check_fn(config) + + +_CONFIG_CHECK_REGISTRY = ConfigCheckRegistry() + + +def get_config_check_registry(): + """Returns the config check registry.""" + return _CONFIG_CHECK_REGISTRY + + +@DeveloperAPI +def register_config_check(fn) -> Callable: + """Registers a config check function.""" + _CONFIG_CHECK_REGISTRY.register(fn) + + +class ConfigCheck(ABC): + """Checks instances of comprehensive (all parameters and defaults filled in) schema-validated config.""" + + @staticmethod + @abstractmethod + def check(config: "ModelConfig") -> None: + """Checks config for validity.""" + raise NotImplementedError + + +@register_config_check +def check_feature_names_unique(config: "ModelConfig") -> None: + """Checks that all feature names are unique.""" + input_features = config.input_features + input_feature_names = {input_feature.name for input_feature in input_features} + + output_features = config.output_features + output_feature_names = {output_feature.name for output_feature in output_features} + + if len(input_feature_names) + len(output_feature_names) != len(input_features) + len(output_features): + raise ConfigValidationError("Feature names must be unique.") + + +@register_config_check +def check_tied_features_valid(config: "ModelConfig") -> None: + """Checks that all tied features are valid.""" + input_features = config.input_features + input_feature_names = {input_feature.name for input_feature in input_features} + + for input_feature in input_features: + if input_feature.tied and input_feature.tied not in input_feature_names: + raise ConfigValidationError( + f"Feature {input_feature.name} is tied to feature {input_feature.tied}, but the " + f"'{input_feature.tied}' feature does not exist." + ) + + +@register_config_check +def check_training_runway(config: "ModelConfig") -> None: + """Checks that checkpoints_per_epoch and steps_per_checkpoint aren't simultaneously defined.""" + if config.model_type == MODEL_ECD: + if config.trainer.checkpoints_per_epoch != 0 and config.trainer.steps_per_checkpoint != 0: + raise ConfigValidationError( + "It is invalid to specify both trainer.checkpoints_per_epoch AND " + "trainer.steps_per_checkpoint. Please specify one or the other, or specify neither to " + "checkpoint/eval the model every epoch." + ) + + +@register_config_check +def check_ray_backend_in_memory_preprocessing(config: "ModelConfig") -> None: + """Checks that in memory preprocessing is used with Ray backend.""" + if config.backend is None: + return + if not hasattr(config.trainer, "preprocessing") or not hasattr(config.trainer.preprocessing, IN_MEMORY): + return + + if config.backend.type == "ray" and not config.trainer.preprocessing.in_memory: + raise ConfigValidationError( + "RayBackend does not support lazy loading of data files at train time. " + "Set preprocessing config `in_memory: True`" + ) + + for input_feature in config.input_features: + if input_feature.type == AUDIO or input_feature.type == IMAGE: + if not input_feature.preprocessing.in_memory and config.backend.type != "ray": + raise ConfigValidationError( + "RayBackend does not support lazy loading of data files at train time. " + f"Set preprocessing config `in_memory: True` for input feature {input_feature.name}" + ) + + +def check_sequence_concat_combiner_requirements(config: "ModelConfig") -> None: + """Checks that sequence concat combiner has at least one input feature that's sequential.""" + if config.model_type != MODEL_ECD: + return + if config.combiner != "sequence_concat": + return + has_sequence_input = False + for input_feature in config.input_features: + if input_feature.type in SEQUENCE_OUTPUT_FEATURE_TYPES: + has_sequence_input = True + break + if not has_sequence_input: + raise ConfigValidationError( + "Sequence concat combiner should only be used for at least one sequential input feature." + ) + + +@register_config_check +def check_comparator_combiner_requirements(config: "ModelConfig") -> None: + """Checks that all of the feature names for entity_1 and entity_2 are valid features.""" + if config.model_type != MODEL_ECD: + return + if config.combiner.type != "comparator": + return + + input_feature_names = [input_feature.name for input_feature in config.input_features] + for feature_name in config.combiner.entity_1: + if feature_name not in input_feature_names: + raise ConfigValidationError( + f"Feature {feature_name} in entity_1 for the comparator combiner is not a valid input feature name." + ) + for feature_name in config.combiner.entity_2: + if feature_name not in input_feature_names: + raise ConfigValidationError( + f"Feature {feature_name} in entity_2 for the comparator combiner is not a valid input feature name." + ) + + if sorted(config.combiner.entity_1 + config.combiner.entity_2) != sorted(input_feature_names): + raise ConfigValidationError("Not all input features are present as entities in the comparator combiner.") + + +@register_config_check +def check_class_balance_preprocessing(config: "ModelConfig") -> None: + """Class balancing is only available for datasets with a single output feature.""" + if config.preprocessing.oversample_minority or config.preprocessing.undersample_majority: + if len(config.output_features) != 1: + raise ConfigValidationError("Class balancing is only available for datasets with a single output feature.") + if config.output_features[0].type != BINARY: + raise ConfigValidationError("Class balancing is only supported for binary output features.") + + +@register_config_check +def check_sampling_exclusivity(config: "ModelConfig") -> None: + """Oversample minority and undersample majority are mutually exclusive.""" + if config.preprocessing.oversample_minority and config.preprocessing.undersample_majority: + raise ConfigValidationError( + "Oversample minority and undersample majority are mutually exclusive. Specify only one method." + ) + + +@register_config_check +def check_validation_metric_exists(config: "ModelConfig") -> None: + """Checks that the specified validation metric exists.""" + validation_metric_name = config.trainer.validation_metric + + # Get all valid metrics. + feature_to_metric_names_map = get_feature_to_metric_names_map_from_feature_collection(config.output_features) + all_valid_metrics = set() + for metric_names in feature_to_metric_names_map.values(): + all_valid_metrics.update(metric_names) + + if validation_metric_name not in all_valid_metrics: + raise ConfigValidationError( + f"User-specified trainer.validation_metric '{validation_metric_name}' is not valid. " + f"Available metrics are: {all_valid_metrics}" + ) + + +@register_config_check +def check_splitter(config: "ModelConfig") -> None: + """Checks the validity of the splitter configuration.""" + from ludwig.data.split import get_splitter + + splitter = get_splitter(**config.preprocessing.split.to_dict()) + splitter.validate(config) + + +@register_config_check +def check_hf_tokenizer_requirements(config: "ModelConfig") -> None: + """Checks that the HuggingFace tokenizer has a pretrained_model_name_or_path specified.""" + + for input_feature in config.input_features: + if input_feature.type == TEXT: + if input_feature.preprocessing.tokenizer == "hf_tokenizer": + if input_feature.preprocessing.pretrained_model_name_or_path is None: + raise ConfigValidationError( + "Pretrained model name or path must be specified for HuggingFace tokenizer." + ) + + +@register_config_check +def check_hf_encoder_requirements(config: "ModelConfig") -> None: + """Checks that a HuggingFace encoder has a pretrained_model_name_or_path specified.""" + + for input_feature in config.input_features: + if input_feature.type == TEXT: + if hasattr(input_feature.encoder, "use_pretrained"): + if input_feature.preprocessing.pretrained_model_name_or_path is None: + raise ConfigValidationError( + "Pretrained model name or path must be specified for HuggingFace encoder." + ) + + +@register_config_check +def check_stacked_transformer_requirements(config: "ModelConfig") -> None: + """Checks that the transformer encoder type correctly configures `num_heads` and `hidden_size`""" + + def is_divisible(hidden_size: int, num_heads: int) -> bool: + """Checks that hidden_size is divisible by num_heads.""" + return hidden_size % num_heads == 0 + + sequence_types = [SEQUENCE, TEXT, TIMESERIES] + + for input_feature in config.input_features: + if_type = input_feature.type + encoder = input_feature.encoder + if ( + if_type in sequence_types + and encoder.type == "transformer" + and not is_divisible(encoder.hidden_size, encoder.num_heads) + ): + raise ConfigValidationError( + f"Input feature {input_feature.name} transformer encoder requires encoder.hidden_size to be divisible " + f"by encoder.num_heads. Found hidden_size {encoder.hidden_size} and num_heads {encoder.num_heads}." + ) + + +@register_config_check +def check_hyperopt_search_algorithm_dependencies_installed(config: "ModelConfig") -> None: + """Check that the hyperopt search algorithm dependencies are installed.""" + if config.hyperopt is None: + return + + try: + config.hyperopt.search_alg.dependencies_installed() + except ImportError as e: + raise ConfigValidationError(e.msg) + + +@register_config_check +def check_hyperopt_scheduler_dependencies_installed(config: "ModelConfig") -> None: + """Check that the hyperopt scheduler dependencies are installed.""" + if config.hyperopt is None: + return + + try: + config.hyperopt.executor.scheduler.dependencies_installed() + except ImportError as e: + raise ConfigValidationError(e.msg) + + +@register_config_check +def check_tagger_decoder_requirements(config: "ModelConfig") -> None: + """Checks that the tagger decoder has at least one sequence, text or timeseries input feature where the + encoder's reduce_output will produce a 3D shaped output from the combiner.""" + # Check if there is a text or sequence output feature using a tagger decoder + output_feature_with_tagger_decoder = False + for output_feature in config.output_features: + if output_feature.type in {TEXT, SEQUENCE} and output_feature.decoder.type == "tagger": + output_feature_with_tagger_decoder = True + + if not output_feature_with_tagger_decoder: + return + + # Check that there is at least one sequence, text or timeseries input feature that doesn't reduce the + # output of the encoder. + has_sequence_feature = False + for input_feature in config.input_features: + if input_feature.type in {SEQUENCE, TEXT, TIMESERIES}: + has_sequence_feature = True + if input_feature.encoder.reduce_output is None: + return + + if not has_sequence_feature: + raise ConfigValidationError("Tagger decoder requires at least one text, sequence or timeseries input feature.") + else: + raise ConfigValidationError( + "Tagger decoder requires at least one of the text, sequence or timeseries input feature encoders to have " + "`reduce_output` set to `None`." + ) + + +@register_config_check +def check_hyperopt_parameter_dicts(config: "ModelConfig") -> None: + """Checks for hyperopt parameter dicts against their config objects.""" + if config.hyperopt is None: + return + + from ludwig.schema.hyperopt.utils import get_parameter_cls, parameter_config_registry + + for parameter, space in config.hyperopt.parameters.items(): + # skip nested hyperopt parameters + if parameter != ".": + parameter_attribute_path = parameter.split(".") + passed = False + + for root in [config, config.input_features, config.output_features]: + current = root + for p in parameter_attribute_path: + try: + current = current.__getattribute__(p) + if p == parameter_attribute_path[-1]: + passed = True + except AttributeError: + break + if passed: + break + + if not passed: + raise ConfigValidationError( + f"The supplied hyperopt parameter {parameter} is not a valid config field. Check the Ludwig " + "docs for the list of valid parameters." + ) + + try: + space_cls = get_parameter_cls(space["space"]) + space_cls.from_dict(space) + except KeyError: + space_types = ", ".join(parameter_config_registry.keys()) + raise ConfigValidationError( + f"Invalid hyperopt parameter space requested for `hyperopt.parameters.{parameter}`. Valid spaces " + f"are {space_types}." + ) + + +@register_config_check +def check_concat_combiner_requirements(config: "ModelConfig") -> None: + """Checks that if the concat combiner receives a mixture of sequence and non-sequence features, that all + sequence features are configured with reduce_output to be 2D tensors.""" + if config.model_type != MODEL_ECD: + return + if config.combiner.type != "concat": + return + + has_unreduced_sequence_feature = False + has_non_sequence_feature = False + for input_feature in config.input_features: + if ( + input_feature.type in {SEQUENCE, TEXT, TIMESERIES} + and hasattr(input_feature.encoder, "reduce_output") + and input_feature.encoder.reduce_output is None + ): + has_unreduced_sequence_feature = True + else: + has_non_sequence_feature = True + + if has_unreduced_sequence_feature and has_non_sequence_feature: + raise ConfigValidationError( + "The concat combiner cannot receive a mix of unreduced sequence features (3D) and non-sequence features " + "(2D). Options: 1) Set reduce_output in sequence feature encoders to a value other than None to ensure 2D " + "encoder outputs, 2) Choose a different combiner like `sequence_concat` which can handle a mix of 2D and " + "3D encoder output shapes, or 3) Remove features to ensure that output shapes from all encoders are the " + "same dimension (all 2D or all 3D)." + ) + + +@register_config_check +def check_hyperopt_nested_parameter_dicts(config: "ModelConfig") -> None: + """Checks that all nested parameters in a hyperopt config exist.""" + if config.hyperopt is None or "." not in config.hyperopt.parameters: + return + + from ludwig.schema.hyperopt.utils import get_parameter_cls + from ludwig.schema.model_types.base import ModelConfig + + space = config.hyperopt.parameters["."] + + # Build the config that would be produced by each parameter dict to validate subsections that may be in + config_dict = config.to_dict() + del config_dict["hyperopt"] + for category in space["categories"]: + for i, k in enumerate(category.keys()): + try: + config.__getattribute__(k) + except AttributeError: + raise ConfigValidationError(f"Invalid config block {k} in nested hyperopt parameter dict {i}: {space}.") + + category_dict = merge_dict(config_dict, category) + try: + ModelConfig.from_dict(category_dict) + except ConfigValidationError as e: + raise ConfigValidationError(f"Invalid config in hyperopt nested parameter config: {category}. {e.message}") + + try: + space_cls = get_parameter_cls("choice") + space_cls.from_dict(space) + except KeyError: + raise ConfigValidationError( + f"Nested hyperparameter search spaces must be of type 'choice'. Requested space type: {space['space']}" + ) + + +@register_config_check +def check_llm_exactly_one_input_text_feature(config: "ModelConfig"): + if config.model_type != MODEL_LLM: + return + + if len(config.input_features) == 1 and config.input_features[0].type == TEXT: + return + else: + raise ConfigValidationError("LLM requires exactly one text input feature.") + + +@register_config_check +def check_llm_finetuning_output_feature_config(config: "ModelConfig"): + """Checks that the output feature config for LLM finetuning is valid.""" + if config.model_type != MODEL_LLM: + return + + if config.trainer.type != "finetune": + return + + if config.output_features[0].type != TEXT: + raise ConfigValidationError( + "LLM finetuning requires the output feature to be a text feature. If you are trying to use a different " + "output feature type such as category or binary, please change the output feature type to text." + ) + + +@register_config_check +def check_llm_finetuning_trainer_config(config: "ModelConfig"): + """Ensures that trainer type is finetune if adapter is not None.""" + if config.model_type != MODEL_LLM: + return + + if ( + config.trainer.type == "none" + and config.adapter is not None + and config.adapter.pretrained_adapter_weights is not None + ): + # If performing zero-shot, we must specify pretrained adapter weights + return + + if config.adapter is not None and config.trainer.type != "finetune": + raise ConfigValidationError("LLM finetuning requires trainer type to be finetune.") + + +@register_config_check +def check_llm_finetuning_backend_config(config: "ModelConfig"): + """Checks that the LLM finetuning using Ray is configured correctly.""" + if config.model_type != MODEL_LLM: + return + + # LLM finetuning is only supported by the finetune trainer type + if ( + config.trainer.type != "finetune" + and config.adapter is not None + and config.adapter.pretrained_adapter_weights is not None + ): + return + + # Using local backend, so skip the checks below + if not hasattr(config.backend, "type"): + return + + backend = config.backend + if not hasattr(backend.trainer, "strategy") or backend.trainer.strategy != "accelerate": + raise ConfigValidationError("LLM finetuning with Ray requires the Accelerate strategy.") + + # Distributed LLM finetuning requires GPU + if not backend.trainer.use_gpu or backend.trainer.resources_per_worker.GPU < 1: + raise ConfigValidationError("LLM finetuning with distributed training requires GPU.") + + +@register_config_check +def check_llm_finetuning_adalora_config(config: "ModelConfig"): + """Checks that the adalora adapter is configured correctly. + + We check against PEFT's predefined target module list for ADALORA to see if this target_modules is present there. If + not, AdaloraModel will run into issues downstream. + """ + if config.model_type != MODEL_LLM: + return + + if not config.adapter: + return + + if config.adapter.type != "adalora": + return + + from peft.utils import TRANSFORMERS_MODELS_TO_ADALORA_TARGET_MODULES_MAPPING + + model_config = _get_llm_model_config(config.base_model) + if model_config.model_type not in TRANSFORMERS_MODELS_TO_ADALORA_TARGET_MODULES_MAPPING: + raise ConfigValidationError( + f"Adalora adapter is not supported for {model_config.model_type} model. " + f"Supported model types are: {list(TRANSFORMERS_MODELS_TO_ADALORA_TARGET_MODULES_MAPPING.keys())}. " + "If you know the target modules for your model, please specify them in the config through the " + "`target_modules` key." + ) + + +@register_config_check +def check_llm_finetuning_adaption_prompt_parameters(config: "ModelConfig"): + """Checks that the adaption_prompt adapter is configured correctly. + + Adaption prompt is only supported for Llama models. + """ + if config.model_type != MODEL_LLM: + return + + if not config.adapter: + return + + if config.adapter.type != "adaption_prompt": + return + + from peft.tuners.adaption_prompt.config import TRANSFORMERS_MODEL_CONFIG + + # Adaption Config is currently only supported for Llama model types + model_config = _get_llm_model_config(config.base_model) + if model_config.model_type not in TRANSFORMERS_MODEL_CONFIG: + raise ConfigValidationError( + f"Adaption prompt adapter is not supported for {model_config.model_type} model. " + f"Supported model types are: {list(TRANSFORMERS_MODEL_CONFIG.keys())}." + ) + + +def _get_llm_model_config(model_name: str) -> AutoConfig: + """Returns the LLM model config.""" + return AutoConfig.from_pretrained(model_name) + + +# TODO(geoffrey, arnav): uncomment this when we have reconciled the config with the backend kwarg in api.py +# @register_config_check +def check_llm_quantization_backend_incompatibility(config: "ModelConfig") -> None: + """Checks that LLM model type with quantization uses the local backend.""" + if config.model_type != MODEL_LLM: + return + + if config.quantization is None: + return + + backend_type = None + if config.backend: + backend_type = config.backend.get("type", None) + + # If backend was explicitly set to Ray, then we need to raise an error + if backend_type == "ray": + raise ConfigValidationError(f"LLM with quantization requires the 'local' backend, found: '{backend_type}'") + + # If the backend is not explicitly set, then we need to check if a Ray process is running + # If a Ray process is running, then we need to raise an error because the backend will be set to Ray + if config.backend is None: + try: + # May not be installed, so we need to catch the ImportError + import ray + + if ray.is_initialized(): + raise ConfigValidationError( + "LLM with quantization requires the 'local' backend, but backend will be set " + "to Ray since Ray is already running locally." + ) + except ImportError: + pass + + +@register_config_check +def check_llm_text_encoder_is_not_used_with_ecd(config: "ModelConfig") -> None: + """Checks that a pretrained text encoder is not used for ECD models with a text output feature.""" + if config.model_type != MODEL_ECD: + return + + if config.input_features[0].type != TEXT: + return + + if config.output_features[0].type != TEXT: + return + + if ( + hasattr(config.input_features[0].encoder, "pretrained_model_name_or_path") + and config.input_features[0].encoder.pretrained_model_name_or_path + ): + raise ConfigValidationError("Please use the `model_type: llm` for text-to-text models.") + + +@register_config_check +def check_qlora_requirements(config: "ModelConfig") -> None: + """Checks that all the necessary settings are in place for QLoRA.""" + if config.model_type != MODEL_LLM or config.trainer.type == "none": + return + + if config.quantization and (not config.adapter or config.adapter.type != "lora"): + raise ConfigValidationError("Fine-tuning and LLM with quantization requires using the 'lora' adapter") + + +@register_config_check +def check_qlora_merge_and_unload_compatibility(config: "ModelConfig") -> None: + """Checks that model.merge_and_unload() is supported by underlying model.save_pretrained() when merging QLoRA + layers.""" + if config.model_type != MODEL_LLM or config.trainer.type == "none": + return + + if not ( + config.adapter + and config.adapter.type in ["lora", "adalora"] + and config.adapter.postprocessor + and config.adapter.postprocessor.merge_adapter_into_base_model + and config.quantization + ): + return + + if config.quantization.bits < MIN_QUANTIZATION_BITS_FOR_MERGE_AND_UNLOAD: + raise ConfigValidationError( + f"""This operation will entail merging LoRA layers on a {config.quantization.bits}-bit \ +quantized model. Calling "save_pretrained()" on that model is currently unsupported. If you want to merge the LoRA \ +adapter weights into the base model, you need to use 8-bit quantization or do non-quantized based training by removing \ +the quantization section from your Ludwig configuration.""" + ) + + +@register_config_check +def check_prompt_requirements(config: "ModelConfig") -> None: + """Checks that prompt's template and task properties are valid, according to the description on the schema.""" + if config.model_type != MODEL_LLM: + return + + # TODO: `prompt` by default should be set to null, not a default dict: + # # If no prompt is provided, no validation necessary: + # if not config.prompt: + # return + from ludwig.schema.llms.prompt import PromptConfig, RetrievalConfig + + if config.prompt == PromptConfig(): + return + + template = config.prompt.template + task = config.prompt.task + retrieval = config.prompt.retrieval + + # If template is NOT provided, then task is required for zero/few shot learning: + if not template and not task: + raise ConfigValidationError("A prompt task is required if no template is provided!") + + template_refs = set(findall(r"\{(.*?)\}", template)) if isinstance(template, str) else set() + + # If a template IS provided (i.e. we are not doing a built-in zero/few-shot learning), then... + if template: + # If task is also provided, the template must contain it: + if task and "__task__" not in template_refs: + raise ConfigValidationError( + "When providing a task, you must make sure that the task keyword `{__task__} is " + "present somewhere in the template string!" + ) + + # If retrieval is also provided, the template must reference it: + # TODO: retrieval by default should be set to null, not a default dict: + if retrieval and retrieval != RetrievalConfig() and "__context__" not in template_refs: + raise ConfigValidationError( + "When providing a retrieval config, you must make sure that the task keyword `{__context__}` is " + "present somewhere in the template string!" + ) + + # Otherwise, the template should at least contain the sample keyword or some input column: + # TODO: len(template_refs) is a hacky attempt to check that there are references to *something* in the + # string. The proper validation is to check the references against the features in the user's dataset - but we + # do not have access to the dataset in this code path right now. + if not task: + if len(template_refs) == 0 and "__sample__" not in template_refs: + raise ConfigValidationError( + "A template must contain at least one reference to a column or the sample keyword {__sample__} for " + "a JSON-serialized representation of non-output feature columns." + ) + + # Raise an error if template has a placeholder for the output feature name (column). + output_feature_col = config.output_features[0].column + if output_feature_col in template_refs: + raise ConfigValidationError( + "Prompt template should not have a reference to the output feature. The output feature is " + "automatically added to the end of the prompt template merged with the input at training time." + ) + + +@register_config_check +def check_sample_ratio_and_size_compatible(config: "ModelConfig") -> None: + sample_ratio = config.preprocessing.sample_ratio + sample_size = config.preprocessing.sample_size + if sample_size is not None and sample_ratio < 1.0: + raise ConfigValidationError("sample_size cannot be used when sample_ratio < 1.0") + + +@register_config_check +def check_grpo_requires_text_output(config: "ModelConfig") -> None: + """GRPO trainer requires a text output feature.""" + if config.model_type != MODEL_LLM: + return + if config.trainer.type != "grpo": + return + if not config.output_features or config.output_features[0].type != TEXT: + raise ConfigValidationError( + "The GRPO trainer requires a text output feature. " + "Set your output feature type to 'text' or use a different trainer type." + ) diff --git a/ludwig/config_validation/preprocessing.py b/ludwig/config_validation/preprocessing.py new file mode 100644 index 0000000..c8d76f5 --- /dev/null +++ b/ludwig/config_validation/preprocessing.py @@ -0,0 +1,20 @@ +def check_global_max_sequence_length_fits_prompt_template(metadata, global_preprocessing_parameters): + """Checks that the prompt template fits within the global max sequence length.""" + + if ( + "global_max_sequence_length" in global_preprocessing_parameters + and global_preprocessing_parameters["global_max_sequence_length"] is not None + ): + for _feature_name, feature_metadata in metadata.items(): + if ( + "prompt_template_num_tokens" in feature_metadata + and feature_metadata["prompt_template_num_tokens"] + > global_preprocessing_parameters["global_max_sequence_length"] + ): + raise ValueError( + f"The prompt contains ({feature_metadata['prompt_template_num_tokens']}) tokens, which is more " + f"than the the global_max_sequence_length " + f"({global_preprocessing_parameters['global_max_sequence_length']}), which will remove all unique " + "information. Shorten the prompt, or increase the global max sequence length to > " + f"({feature_metadata['prompt_template_num_tokens']}) to include the full prompt." + ) diff --git a/ludwig/config_validation/validation.py b/ludwig/config_validation/validation.py new file mode 100644 index 0000000..2872cd6 --- /dev/null +++ b/ludwig/config_validation/validation.py @@ -0,0 +1,75 @@ +from functools import lru_cache +from threading import Lock + +import jsonschema.exceptions +from jsonschema import Draft7Validator, validate +from jsonschema.validators import extend + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BASE_MODEL, MODEL_ECD, MODEL_LLM, MODEL_TYPE +from ludwig.error import ConfigValidationError + +# TODO(travis): figure out why we need these imports to avoid circular import error +from ludwig.schema.combiners.utils import get_combiner_jsonschema # noqa +from ludwig.schema.features.utils import get_input_feature_jsonschema, get_output_feature_jsonschema # noqa +from ludwig.schema.hyperopt import get_hyperopt_jsonschema # noqa +from ludwig.schema.trainer import get_model_type_jsonschema, get_trainer_jsonschema # noqa +from ludwig.schema.utils import unload_jsonschema_from_config_class + +VALIDATION_LOCK = Lock() + + +@DeveloperAPI +@lru_cache(maxsize=3) +def get_schema(model_type: str = MODEL_ECD): + # Force populate combiner registry: + import ludwig.combiners.combiners # noqa: F401 + from ludwig.schema.model_types.base import model_type_schema_registry + + cls = model_type_schema_registry[model_type] + props = unload_jsonschema_from_config_class(cls)["properties"] + + required = ["input_features", "output_features"] + if model_type == MODEL_LLM: + required += [BASE_MODEL] + + return { + "type": "object", + "properties": props, + "title": "model_options", + "description": "Settings for Ludwig configuration", + "required": required, + "additionalProperties": True, + } + + +@lru_cache(maxsize=1) +def get_validator(): + # Manually add support for tuples (pending upstream changes: https://github.com/Julian/jsonschema/issues/148): + def custom_is_array(checker, instance): + return isinstance(instance, list) or isinstance(instance, tuple) + + # This creates a new class, so cache to prevent a memory leak: + # https://github.com/python-jsonschema/jsonschema/issues/868 + type_checker = Draft7Validator.TYPE_CHECKER.redefine("array", custom_is_array) + return extend(Draft7Validator, type_checker=type_checker) + + +@DeveloperAPI +def check_schema(updated_config): + """Emulates the pure JSONSchema validation that could be used in an environment without marshmallow. + + The incoming config may not be comprehensive, but is assumed to be up to date with the latest ludwig schema. + """ + model_type = updated_config.get(MODEL_TYPE, MODEL_ECD) + error = None + with VALIDATION_LOCK: + try: + validate(instance=updated_config, schema=get_schema(model_type=model_type), cls=get_validator()) + except jsonschema.exceptions.ValidationError as e: + # Capture error but don't raise here, otherwise we get the full output from `e`, which contains a dump + # of the entire schema + error = e + + if error is not None: + raise ConfigValidationError(f"Failed to validate JSON schema for config. Error: {error.message}") from error diff --git a/ludwig/constants.py b/ludwig/constants.py new file mode 100644 index 0000000..afcdf9c --- /dev/null +++ b/ludwig/constants.py @@ -0,0 +1,351 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +INPUT_FEATURES = "input_features" +OUTPUT_FEATURES = "output_features" + +INPUT = "input" +OUTPUT = "output" +BINARY = "binary" +CATEGORY = "category" +CATEGORY_DISTRIBUTION = "category_distribution" +INT = "int" +FLOAT = "float" +SPACE = "space" +NUMBER = "number" +SET = "set" +BAG = "bag" +TEXT = "text" +SEQUENCE = "sequence" +TIMESERIES = "timeseries" +IMAGE = "image" +AUDIO = "audio" +DATE = "date" +H3 = "h3" + +# Date feature vector: [year, month, day, weekday, yearday, hour, minute, second, second_of_day]. +DATE_VECTOR_LENGTH = 9 + +# H3 geospatial index vector: [mode, edge, resolution, base_cell, cell_0, ..., cell_14]. +MAX_H3_RESOLUTION = 15 +H3_VECTOR_LENGTH = MAX_H3_RESOLUTION + 4 +VECTOR = "vector" +HEIGHT = "height" +WIDTH = "width" +INFER_IMAGE_DIMENSIONS = "infer_image_dimensions" +INFER_IMAGE_MAX_HEIGHT = "infer_image_max_height" +INFER_IMAGE_MAX_WIDTH = "infer_image_max_width" +INFER_IMAGE_SAMPLE_SIZE = "infer_image_sample_size" +INFER_IMAGE_NUM_CLASSES = "infer_image_num_classes" +IMAGE_MAX_CLASSES = 128 +NUM_CLASSES = "num_classes" +NUM_CHANNELS = "num_channels" +REQUIRES_EQUAL_DIMENSIONS = "requires_equal_dimensions" +USE_PRETRAINED = "use_pretrained" +TRAINABLE = "trainable" +CLASS_WEIGHTS = "class_weights" +USED_TOKENS = "used_tokens" +LOSS = "loss" +ROC_AUC = "roc_auc" +EVAL_LOSS = "eval_loss" +TRAIN_MEAN_LOSS = "train_mean_loss" +SEQUENCE_SOFTMAX_CROSS_ENTROPY = "sequence_softmax_cross_entropy" +NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY = "next_token_softmax_cross_entropy" +SOFTMAX_CROSS_ENTROPY = "softmax_cross_entropy" +SIGMOID_CROSS_ENTROPY = "sigmoid_cross_entropy" +BINARY_WEIGHTED_CROSS_ENTROPY = "binary_weighted_cross_entropy" +THRESHOLD = "threshold" +VALIDATION_METRIC = "validation_metric" +ACCURACY = "accuracy" +ACCURACY_MICRO = "accuracy_micro" +HITS_AT_K = "hits_at_k" +MEAN_HITS_AT_K = "mean_hits_at_k" +ERROR = "error" +ABSOLUTE_ERROR = "absolute_error" +SQUARED_ERROR = "squared_error" +MEAN_SQUARED_ERROR = "mean_squared_error" +ROOT_MEAN_SQUARED_ERROR = "root_mean_squared_error" +ROOT_MEAN_SQUARED_PERCENTAGE_ERROR = "root_mean_squared_percentage_error" +MEAN_ABSOLUTE_ERROR = "mean_absolute_error" +MEAN_ABSOLUTE_PERCENTAGE_ERROR = "mean_absolute_percentage_error" +MEAN_ABSOLUTE_SCALED_ERROR = "mean_absolute_scaled_error" +SYMMETRIC_MEAN_ABSOLUTE_PERCENTAGE_ERROR = "symmetric_mean_absolute_percentage_error" +HUBER = "huber" +CORN = "corn" +R2 = "r2" +EDIT_DISTANCE = "edit_distance" +PERPLEXITY = "perplexity" +NEXT_TOKEN_PERPLEXITY = "next_token_perplexity" +JACCARD = "jaccard" +PRECISION = "precision" +RECALL = "recall" +SPECIFICITY = "specificity" +PREDICTIONS = "predictions" +RESPONSE = "RESPONSE" +TOP_K = "top_k" +TOP_K_PREDICTIONS = "top_k_predictions" +PROBABILITY = "probability" +PROBABILITIES = "probabilities" +SPLIT_PROBABILITIES = "split_probabilities" +TOKEN_ACCURACY = "token_accuracy" +LAST_ACCURACY = "last_accuracy" +SEQUENCE_ACCURACY = "sequence_accuracy" +LAST_PROBABILITIES = "last_probabilities" +LAST_PREDICTIONS = "last_predictions" +LENGTHS = "lengths" +TIED = "tied" +COMBINED = "combined" + +PREPROCESSING = "preprocessing" +FILL_WITH_CONST = "fill_with_const" +FILL_WITH_MODE = "fill_with_mode" +FILL_WITH_MEAN = "fill_with_mean" +FILL_WITH_FALSE = "fill_with_false" +FILL_WITH_TRUE = "fill_with_true" +BFILL = "bfill" +FFILL = "ffill" +DROP_ROW = "drop_row" +MISSING_VALUE_STRATEGY = "missing_value_strategy" +MISSING_VALUE_STRATEGY_OPTIONS = [ + FILL_WITH_CONST, + FILL_WITH_MODE, + BFILL, + FFILL, + DROP_ROW, +] + +CROP_OR_PAD = "crop_or_pad" +INTERPOLATE = "interpolate" +RESIZE_METHODS = [CROP_OR_PAD, INTERPOLATE] + +# Special symbols for text. +STOP_SYMBOL = "" +START_SYMBOL = "" +PADDING_SYMBOL = "" +UNKNOWN_SYMBOL = "" + +TRAINER = "trainer" +OPTIMIZER = "optimizer" +METRIC = "metric" +PREDICTION = "prediction" +LOGITS = "logits" +HIDDEN = "hidden" +LAST_HIDDEN = "last_hidden" +ENCODER_OUTPUT = "encoder_output" +ENCODER_OUTPUT_STATE = "encoder_output_state" +PROJECTION_INPUT = "projection_input" +LEARNING_RATE_SCHEDULER = "learning_rate_scheduler" + +SEMANTIC = "semantic" + +RANDOM = "random" +SUM = "sum" +APPEND = "append" +SEQ_SUM = "seq_sum" +AVG_EXP = "avg_exp" + +TRAIN = "train" +TRAINING = "training" +VALIDATION = "validation" +TEST = "test" +EVALUATION = "evaluation" +SPLIT = "split" +FORCE_SPLIT = "force_split" +STRATIFY = "stratify" +FULL = "full" +TRAIN_SPLIT = 0 +VALIDATION_SPLIT = 1 +TEST_SPLIT = 2 +MIN_DATASET_SPLIT_ROWS = 3 # The minimum number of rows in a split. Splits smaller than this size are treated as empty. + +META = "meta" + +HYPEROPT = "hyperopt" +STRATEGY = "strategy" +EXECUTOR = "executor" +MINIMIZE = "minimize" +MAXIMIZE = "maximize" +SAMPLER = "sampler" +NUM_SAMPLES = "num_samples" +SEARCH_ALG = "search_alg" +SCHEDULER = "scheduler" +PARAMETERS = "parameters" +MAX_CONCURRENT_TRIALS = "max_concurrent_trials" +CPU_RESOURCES_PER_TRIAL = "cpu_resources_per_trial" +GPU_RESOURCES_PER_TRIAL = "gpu_resources_per_trial" +GOAL = "goal" +GRID_SEARCH = "grid_search" + +NAME = "name" +COLUMN = "column" +TYPE = "type" +ACTIVE = "active" + +RAY = "ray" +IN_MEMORY = "in_memory" + +PROC_COLUMN = "proc_column" + +CHECKSUM = "checksum" + +HDF5 = "hdf5" +PARQUET = "parquet" + +SRC = "dataset_src" + +EARLY_STOP = "early_stop" +EPOCHS = "epochs" +BATCH_SIZE = "batch_size" +EVAL_BATCH_SIZE = "eval_batch_size" +EFFECTIVE_BATCH_SIZE = "effective_batch_size" +MAX_BATCH_SIZE = "max_batch_size" +DEFAULT_BATCH_SIZE = "auto" +FALLBACK_BATCH_SIZE = 128 +# The smallest batch size that is supported on Ludwig. +MINIMUM_BATCH_SIZE = 1 +# 2^40. Used for `max_batch_size` config param. Not a hard constraint for `batch_size` config param. +MAX_POSSIBLE_BATCH_SIZE = 1099511627776 +# min batch size. Used as a floor for batch size tuning. +MIN_POSSIBLE_BATCH_SIZE = 1 +# max batch size for dataset is 20% of dataset size +MAX_BATCH_SIZE_DATASET_FRACTION = 0.2 +MAX_CPU_BATCH_SIZE = 128 +LEARNING_RATE = "learning_rate" +INPUT_SIZE = "input_size" +USE_BIAS = "use_bias" +BIAS = "bias" +DEFAULT_USE_BIAS = "default_use_bias" +DEFAULT_BIAS = "default_bias" +CONV_USE_BIAS = "conv_use_bias" +CONV_BIAS = "conv_bias" +AUTO = "auto" +CONFIG = "config" + +CLIP = "clip" +DEPENDENCIES = "dependencies" +REDUCE_INPUT = "reduce_input" +REDUCE_DEPENDENCIES = "reduce_dependencies" + +BACKEND = "backend" +COMBINER = "combiner" + +ENCODER = "encoder" +DECODER = "decoder" + +TRAINABLE = "trainable" + +DEFAULTS = "defaults" +DEFAULT = "default" +DEFAULT_VALIDATION_METRIC = "default_validation_metric" + +BALANCE_PERCENTAGE_TOLERANCE = 0.03 +IMBALANCE_DETECTION_RATIO = 0.05 + +TABULAR = "tabular" +AUTOML_DEFAULT_TABULAR_MODEL = "ft_transformer" +AUTOML_DEFAULT_TEXT_ENCODER = "bert" +AUTOML_SMALLER_TEXT_ENCODER = "distilbert" +AUTOML_TEXT_ENCODER_MAX_TOKEN_LEN = 512 +AUTOML_SMALLER_TEXT_LENGTH = 128 +AUTOML_LARGE_TEXT_DATASET = 100000 +AUTOML_MAX_ROWS_PER_CHECKPOINT = 350000 +AUTOML_DEFAULT_IMAGE_ENCODER = "stacked_cnn" + +HYPEROPT_WARNING = ( + "You are running the ludwig train command but there’s a hyperopt section present in your config. " + "It will be ignored. If you want to run hyperopt you should use the following command: ludwig " + "hyperopt\n\n" +) + +CONTINUE_PROMPT = "Do you want to continue? " + +DEFAULT_AUDIO_TENSOR_LENGTH = 70000 +AUDIO_FEATURE_KEYS = [ + "type", + "window_length_in_s", + "window_shift_in_s", + "num_fft_points", + "window_type", + "num_filter_bands", +] + +BASE_MODEL = "base_model" +MODEL_TYPE = "model_type" +MODEL_ECD = "ecd" +MODEL_LLM = "llm" +DASK_MODULE_NAME = "dask.dataframe" +LUDWIG_VERSION = "ludwig_version" + +PREPROCESSOR = "preprocessor" +PREDICTOR = "predictor" +POSTPROCESSOR = "postprocessor" +TARGET_MODULES = "target_modules" + +GENERATION = "generation" +PROMPT = "prompt" +ADAPTER = "adapter" +QUANTIZATION = "quantization" +MIN_QUANTIZATION_BITS_FOR_MERGE_AND_UNLOAD = 8 +PRETRAINED_ADAPTER_WEIGHTS = "pretrained_adapter_weights" +MERGE_ADAPTER_INTO_BASE_MODEL = "merge_adapter_into_base_model" +PROGRESSBAR = "progressbar" + +# CrossEntropyLoss for LLMs +IGNORE_INDEX_TOKEN_ID = -100 + +S3 = "s3" +CACHE = "cache" + +# If `use_torch_profiler=True` in LudwigProfiler, LUDWIG_TAG is prepended to the specified experiment tag +# (LudwigProfiler(tag="...", ..)). This edited tag is passed in to `torch.profiler.record_function` so we can +# retrieve torch ops for the tagged code blocks/functions. +LUDWIG_TAG = "[ludwig]" + +# Retry constants +TRIES = 5 +DELAY = 1 +BACKOFF = 2 +JITTER = (0, 1) + +# image support constants +IMAGENET1K = "imagenet1k" + +AUGMENTATION = "augmentation" + +LUDWIG_SCHEMA_VALIDATION_POLICY = "LUDWIG_SCHEMA_VALIDATION_POLICY" + +# New loss function constants +FOCAL_LOSS = "focal" +DICE_LOSS = "dice" +LOVASZ_SOFTMAX_LOSS = "lovasz_softmax" +NT_XENT_LOSS = "nt_xent" +POLY_LOSS = "poly" +SPARSEMAX_LOSS = "sparsemax" +ENTMAX15_LOSS = "entmax15" + + +# Open-set recognition constants +ENTROPIC_OPEN_SET = "entropic_open_set" +OBJECTOSPHERE = "objectosphere" + +# Anomaly detection constants +ANOMALY = "anomaly" +ANOMALY_SCORE = "anomaly_score" +DEEP_SVDD = "deep_svdd" +DEEP_SAD = "deep_sad" +DROCC = "drocc" +F1_MAX = "f1_max" +ANOMALY_AUROC = "anomaly_auroc" +UNCERTAINTY = "uncertainty" # MC Dropout uncertainty output key diff --git a/ludwig/contrib.py b/ludwig/contrib.py new file mode 100644 index 0000000..3c30bf6 --- /dev/null +++ b/ludwig/contrib.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Module for handling contributed support.""" + +import argparse + +from ludwig.contribs import contrib_registry, ContribLoader + + +def create_load_action(contrib_loader: ContribLoader) -> argparse.Action: + class LoadContribAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string): + items = getattr(namespace, self.dest) or [] + items.append(contrib_loader.load()) + setattr(namespace, self.dest, items) + + return LoadContribAction + + +def add_contrib_callback_args(parser: argparse.ArgumentParser): + for contrib_name, contrib_loader in contrib_registry.items(): + parser.add_argument( + f"--{contrib_name}", + dest="callbacks", + nargs=0, + action=create_load_action(contrib_loader), + ) + + +def preload(argv): + for arg in argv: + if arg.startswith("--"): + arg = arg[2:] + + if arg in contrib_registry: + contrib_registry[arg].preload() diff --git a/ludwig/contribs/__init__.py b/ludwig/contribs/__init__.py new file mode 100644 index 0000000..fe733f6 --- /dev/null +++ b/ludwig/contribs/__init__.py @@ -0,0 +1,85 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""All contrib classes must implement the `ludwig.callbacks.Callback` interface. + +If you don't want to handle the call, either provide an empty method with `pass`, or just don't implement the method. +""" + +from abc import ABC, abstractmethod + +from ludwig.callbacks import Callback + + +class ContribLoader(ABC): + @abstractmethod + def load(self) -> Callback: + """Returns an instantiation of the callback instance, whose callback hooks will be invoked at runtime.""" + + def preload(self): + """Will always be called when Ludwig CLI is invoked, preload gives the callback an opportunity to import or + create any shared resources. + + Importing required 3rd-party libraries should be done here i.e. import wandb. preload is guaranteed to be called + before any other callback method, and will only be called once per process. + """ + + +# Contributors, load your class here: + + +class AimLoader(ContribLoader): + def load(self) -> Callback: + from ludwig.contribs.aim import AimCallback + + return AimCallback() + + def preload(self): + import aim # noqa + + +class CometLoader(ContribLoader): + def load(self) -> Callback: + from ludwig.contribs.comet import CometCallback + + return CometCallback() + + def preload(self): + import comet_ml # noqa + + +class WandbLoader(ContribLoader): + def load(self) -> Callback: + from ludwig.contribs.wandb import WandbCallback + + return WandbCallback() + + def preload(self): + import wandb # noqa + + +class MlflowLoader(ContribLoader): + def load(self) -> Callback: + from ludwig.contribs.mlflow import MlflowCallback + + return MlflowCallback() + + +contrib_registry = { + # Contributors, add your class here: + "comet": CometLoader(), + "wandb": WandbLoader(), + "mlflow": MlflowLoader(), + "aim": AimLoader(), +} diff --git a/ludwig/contribs/aim.py b/ludwig/contribs/aim.py new file mode 100644 index 0000000..3fa2f6e --- /dev/null +++ b/ludwig/contribs/aim.py @@ -0,0 +1,105 @@ +import json +import logging + +from ludwig.api_annotations import PublicAPI +from ludwig.callbacks import Callback +from ludwig.utils.data_utils import NumpyEncoder +from ludwig.utils.package_utils import LazyLoader + +aim = LazyLoader("aim", globals(), "aim") + +logger = logging.getLogger(__name__) + + +@PublicAPI +class AimCallback(Callback): + """Class that defines the methods necessary to hook into process.""" + + def __init__(self, repo=None): + self.repo = repo + + def on_train_init( + self, + base_config, + experiment_directory, + experiment_name, + model_name, + output_directory, + resume_directory, + ): + logger.info("aim.on_train_init() called...") + + try: + query = f'run.name == "{model_name}"' + if self.repo is None: + aim_repo = aim.Repo.default_repo() + else: + aim_repo = aim.Repo.from_path(self.repo) + runs_generator = aim_repo.query_runs(query) + run = next(runs_generator.iter_runs()) + run_hash = run.run.hash + self.aim_run = aim.Run(run_hash=run_hash, repo=self.repo, experiment=experiment_name) + except Exception: + self.aim_run = aim.Run(repo=self.repo, experiment=experiment_name) + self.aim_run.name = model_name + + self.aim_run["base_config"] = self.normalize_config(base_config) + + params = {"name": model_name, "dir": experiment_directory} + self.aim_run["params"] = params + + def aim_track(self, progress_tracker): + logger.info(f"aim.aim_track() called for epoch {progress_tracker.epoch}, step: {progress_tracker.steps}") + + if self.aim_run: + for key, value in progress_tracker.log_metrics().items(): + if "metrics" in key and "best" not in key: + metrics_dict_name, feature_name, metric_name = key.split(".") + + self.aim_run.track( + value, + name=metric_name, + context={metrics_dict_name: feature_name}, + epoch=progress_tracker.epoch, + step=progress_tracker.steps, + ) + + def on_trainer_train_teardown(self, trainer, progress_tracker, save_path, is_coordinator: bool): + pass + + def on_train_start(self, model, config, *args, **kwargs): + logger.info("aim.on_train_start() called...") + + config = config.copy() + del config["input_features"] + del config["output_features"] + + self.aim_run["train_config"] = self.normalize_config(config) + + def on_train_end(self, output_directory, *args, **kwargs): + pass + + def on_eval_end(self, trainer, progress_tracker, save_path): + optimizer_config = {} + for index, group in enumerate(trainer.optimizer.param_groups): + for key in group: + if "param" not in key: + optimizer_config[f"param_group_{index}_{key}"] = group[key] + + self.aim_run["optimizer_config"] = self.normalize_config(optimizer_config) + + self.aim_track(progress_tracker) + + def on_ludwig_end(self): + self.aim_run.close() + self.aim_run = None + + def on_visualize_figure(self, fig): + logger.info("aim.on_visualize_figure() called...") + if self.aim_run: + self.aim_run.track(aim.Figure(fig), name="Figure", context={"type": "Training Figure"}) + + @staticmethod + def normalize_config(config): + """Convert to json string and back again to remove numpy types.""" + return json.loads(json.dumps(config, cls=NumpyEncoder)) diff --git a/ludwig/contribs/comet.py b/ludwig/contribs/comet.py new file mode 100644 index 0000000..915e40e --- /dev/null +++ b/ludwig/contribs/comet.py @@ -0,0 +1,140 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os +from datetime import datetime + +from ludwig.api_annotations import PublicAPI +from ludwig.callbacks import Callback +from ludwig.utils.package_utils import LazyLoader + +comet_ml = LazyLoader("comet_ml", globals(), "comet_ml") + +logger = logging.getLogger(__name__) + + +@PublicAPI +class CometCallback(Callback): + """Class that defines the methods necessary to hook into process.""" + + def __init__(self): + self.cometml_experiment = None + + def on_train_init( + self, + base_config, + experiment_directory, + experiment_name, + model_name, + output_directory, + resume_directory, + ): + if self.cometml_experiment: + # Comet ML already initialized + return + + try: + self.cometml_experiment = comet_ml.Experiment(log_code=False, project_name=experiment_name) + except Exception: + self.cometml_experiment = None + logger.exception("comet_ml.Experiment() had errors. Perhaps you need to define COMET_API_KEY") + raise + + self.cometml_experiment.set_name(model_name) + self.cometml_experiment.set_filename("Ludwig API") + config = comet_ml.get_config() + self._save_config(config, directory=experiment_directory) + + def on_train_start(self, model, config, config_fp, *args, **kwargs): + if self.cometml_experiment: + # todo v0.4: currently not clear way to set model graph + # see: https://github.com/comet-ml/issue-tracking/issues/296 + # if model: + # self.cometml_experiment.set_model_graph( + # str(model._graph.as_graph_def())) + + if config: + if config_fp: + base_name = os.path.basename(config_fp) + else: + base_name = "config.yaml" + if "." in base_name: + base_name = base_name.rsplit(".", 1)[0] + ".json" + else: + base_name = base_name + ".json" + self.cometml_experiment.log_asset_data(config, base_name) + + def on_train_end(self, output_directory, *args, **kwargs): + if self.cometml_experiment: + self.cometml_experiment.log_asset_folder(output_directory) + + def on_eval_end(self, trainer, progress_tracker, save_path): + """Called from ludwig/models/model.py.""" + if self.cometml_experiment: + for key, value in progress_tracker.log_metrics().items(): + self.cometml_experiment.log_metric(key, value) + + def on_epoch_end(self, trainer, progress_tracker, save_path): + """Called from ludwig/models/model.py.""" + if self.cometml_experiment: + for key, value in progress_tracker.log_metrics().items(): + self.cometml_experiment.log_metric(key, value) + + def on_visualize_figure(self, fig): + if self.cometml_experiment: + self.cometml_experiment.log_figure(fig) + + def on_cmdline(self, cmd, *args): + self.cometml_experiment = None + if cmd in {"train", "experiment"}: + # create a new experiment + try: + self.cometml_experiment = comet_ml.Experiment(log_code=False) + except Exception: + logger.exception("comet_ml.Experiment() had errors. Perhaps you need to define COMET_API_KEY") + return + elif cmd in {"visualize", "predict", "evaluate"}: + # restore from an existing experiment + try: + self.cometml_experiment = comet_ml.ExistingExperiment() + except Exception: + logger.exception("Ignored --comet. No '.comet.config' file") + return + else: + # unhandled command + return + + cli = self._make_command_line(cmd, args) + self.cometml_experiment.set_code(cli) + self.cometml_experiment.set_filename("Ludwig CLI") + self._log_html(cli) + config = comet_ml.get_config() + self._save_config(config) + + def _save_config(self, config, directory="."): + # save the .comet.config here: + config["comet.experiment_key"] = self.cometml_experiment.id + config.save(directory=directory) + + def _log_html(self, text): + # log the text to the html tab: + now = datetime.now() + timestamp = now.strftime("%m/%d/%Y %H:%M:%S") + self.cometml_experiment.log_html(f"

{timestamp}: {text}

") + + def _make_command_line(self, cmd, args): + # put the commet flag back in: + arg_str = " ".join(list(args[:2]) + ["--comet"] + list(args[2:])) + return f"ludwig {cmd} {arg_str}" diff --git a/ludwig/contribs/mlflow/__init__.py b/ludwig/contribs/mlflow/__init__.py new file mode 100644 index 0000000..6e67bd6 --- /dev/null +++ b/ludwig/contribs/mlflow/__init__.py @@ -0,0 +1,279 @@ +import logging +import os +import queue +import threading + +from ludwig.api_annotations import DeveloperAPI, PublicAPI +from ludwig.callbacks import Callback +from ludwig.constants import TRAINER +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, TRAIN_SET_METADATA_FILE_NAME +from ludwig.types import TrainingSetMetadataDict +from ludwig.utils.data_utils import chunk_dict, flatten_dict, save_json, to_json_dict +from ludwig.utils.package_utils import LazyLoader + +mlflow = LazyLoader("mlflow", globals(), "mlflow") + +logger = logging.getLogger(__name__) + + +def _get_runs(experiment_id: str): + return mlflow.tracking.client.MlflowClient().search_runs([experiment_id]) + + +@DeveloperAPI +def get_or_create_experiment_id(experiment_name, artifact_uri: str | None = None): + """Gets experiment id from mlflow.""" + experiment = mlflow.get_experiment_by_name(experiment_name) + if experiment is not None: + return experiment.experiment_id + return mlflow.create_experiment(name=experiment_name, artifact_location=artifact_uri) + + +@PublicAPI +class MlflowCallback(Callback): + def __init__(self, tracking_uri=None, log_artifacts: bool = True): + self.logged_steps = set() + + if tracking_uri: + mlflow.set_tracking_uri(tracking_uri) + self.tracking_uri = mlflow.get_tracking_uri() + + active_run = mlflow.active_run() + if active_run is not None: + # Use experiment already set in the current environment + self.run = active_run + self.experiment_id = self.run.info.experiment_id + self.experiment_name = mlflow.get_experiment(self.experiment_id).name + self.external_run = True + else: + # Will create an experiment at training time + self.run = None + self.experiment_id = None + self.experiment_name = None + self.external_run = False + + self.run_ended = False + self.training_set_metadata = None + self.config = None + self.save_in_background = True + self.save_fn = None + self.save_thread = None + self.log_artifacts = log_artifacts + + def get_experiment_id(self, experiment_name): + return get_or_create_experiment_id(experiment_name) + + def on_preprocess_end( + self, + training_set: "Dataset", # noqa + validation_set: "Dataset", # noqa + test_set: "Dataset", # noqa + training_set_metadata: TrainingSetMetadataDict, + ): + self.training_set_metadata = training_set_metadata + + def on_hyperopt_init(self, experiment_name): + self.experiment_id = self.get_experiment_id(experiment_name) + self.experiment_name = experiment_name + + def on_hyperopt_trial_start(self, parameters): + # Filter out mlflow params like tracking URI, experiment ID, etc. + params = {k: v for k, v in parameters.items() if k != "mlflow"} + self._log_params({"hparam": params}) + + # TODO(travis): figure out a good way to support this. The problem with + # saving artifacts in the background with hyperopt is early stopping. If + # the scheduler decides to terminate a process, then currently there's no + # mechanism to detect this a "flush" the queue of pending writes before + # stopping. Should work with Ray Tune team to come up with a solution. + self.save_in_background = False + + def on_train_init(self, base_config, experiment_name, output_directory, resume_directory, **kwargs): + # Experiment may already have been set during hyperopt init, in + # which case we don't want to create a new experiment / run, as + # this should be handled by the executor. + if self.experiment_id is None: + mlflow.end_run() + self.experiment_id = self.get_experiment_id(experiment_name) + self.experiment_name = experiment_name + + active_run = mlflow.active_run() + if active_run is not None: + # Currently active run started by Ray Tune MLflow mixin or external run + self.run = active_run + else: + run_id = None + if resume_directory is not None: + previous_runs = _get_runs(self.experiment_id) + if len(previous_runs) > 0: + run_id = previous_runs[0].info.run_id + if run_id is not None: + self.run = mlflow.start_run(run_id=run_id) + else: + run_name = os.path.basename(output_directory) + self.run = mlflow.start_run(experiment_id=self.experiment_id, run_name=run_name) + + self.log_config(base_config) + + def log_config(self, config): + if self.log_artifacts: + mlflow.log_dict(to_json_dict(config), "config.yaml") + + def on_train_start(self, config, **kwargs): + self.config = config + self._log_params({TRAINER: config[TRAINER]}) + + def on_train_end(self, output_directory): + if self.log_artifacts: + _log_artifacts(output_directory) + if self.run is not None and not self.external_run: + # Only end runs managed internally to this callback + mlflow.end_run() + self.run_ended = True + + def on_trainer_train_setup(self, trainer, save_path, is_coordinator): + if not is_coordinator: + return + + # When running on a remote worker, the model metadata files will only have been + # saved to the driver process, so re-save it here before uploading. + training_set_metadata_path = os.path.join(save_path, TRAIN_SET_METADATA_FILE_NAME) + if not os.path.exists(training_set_metadata_path): + save_json(training_set_metadata_path, self.training_set_metadata) + + model_hyperparameters_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME) + if not os.path.exists(model_hyperparameters_path): + save_json(model_hyperparameters_path, self.config) + + if self.save_in_background: + save_queue = queue.Queue() + self.save_fn = lambda args: save_queue.put(args) + self.save_thread = threading.Thread(target=_log_mlflow_loop, args=(save_queue, self.log_artifacts)) + self.save_thread.start() + else: + self.save_fn = lambda args: _log_mlflow(*args, self.log_artifacts) + + def on_eval_end(self, trainer, progress_tracker, save_path): + if progress_tracker.steps not in self.logged_steps: + self.logged_steps.add(progress_tracker.steps) + # Adds a tuple to the logging queue. + # True is passed to indicate that the background saving loop should continue. + self.save_fn((progress_tracker.log_metrics(), progress_tracker.steps, save_path, True)) + + def on_trainer_train_teardown(self, trainer, progress_tracker, save_path, is_coordinator): + if is_coordinator: + if progress_tracker.steps not in self.logged_steps: + self.logged_steps.add(progress_tracker.steps) + # Adds a tuple to the logging queue. + # False is passed to indicate that the background saving loop should break. + self.save_fn((progress_tracker.log_metrics(), progress_tracker.steps, save_path, False)) + # False ensures that the background saving loop breaks. + # TODO(Justin): This should probably live in on_ludwig_end, once that's implemented. + self.save_fn((None, None, None, False)) + + # Close the save_thread. + if self.save_thread is not None: + self.save_thread.join() + # if self.save_thread.is_alive(): + # logger.warning("MLFlow save thread timed out and did not close properly.") + + def on_visualize_figure(self, fig): + # TODO: need to also include a filename for this figure + # mlflow.log_figure(fig) + pass + + def prepare_ray_tune(self, train_fn, tune_config, tune_callbacks): + from functools import wraps + + from ray.air.integrations.mlflow import setup_mlflow + + mlflow_config = { + "experiment_id": self.experiment_id, + "experiment_name": self.experiment_name, + "tracking_uri": mlflow.get_tracking_uri(), + } + + @wraps(train_fn) + def wrapper(config, **kwargs): + setup_mlflow(config, **mlflow_config) + return train_fn(config, **kwargs) + + return wrapper, { + **tune_config, + } + + def _log_params(self, params): + flat_params = flatten_dict(params) + for chunk in chunk_dict(flat_params, chunk_size=100): + mlflow.log_params(chunk) + + def __setstate__(self, d): + self.__dict__ = d + if self.tracking_uri: + mlflow.set_tracking_uri(self.tracking_uri) + if self.run and not self.run_ended: + # Run has already been set, but may not be active due to training workers running in a separate + # process, so resume the run + mlflow.end_run() + self.run = mlflow.start_run(run_id=self.run.info.run_id, experiment_id=self.run.info.experiment_id) + + +def _log_mlflow_loop(q: queue.Queue, log_artifacts: bool = True): + """The save_fn for the background thread that logs to MLFlow when save_in_background is True.""" + should_continue = True + while should_continue: + elem = q.get() + log_metrics, steps, save_path, should_continue = elem + if log_metrics is None: + # Break out of the loop if we're not going to log anything. + break + + if "llm_eval_examples" in log_metrics and log_metrics["llm_eval_examples"] is not None: + # mlflow.log_dict(log_metrics["llm_eval_examples"], artifact_file="llm_eval_examples.json") + # Delete the table from the metrics dict so we don't try to log it with the other metrics + del log_metrics["llm_eval_examples"] + mlflow.log_metrics(log_metrics, step=steps) + + if not q.empty(): + # in other words, don't bother saving the model artifacts + # if we're about to do it again + continue + + if log_artifacts: + _log_model(save_path) + + +def _log_mlflow(log_metrics, steps, save_path, should_continue, log_artifacts: bool = True): + """The save_fn for the MlflowCallback. + + This is used when save_in_background is False. + """ + if log_metrics is not None: + if "llm_eval_examples" in log_metrics and log_metrics["llm_eval_examples"] is not None: + # mlflow.log_dict(log_metrics["llm_eval_examples"], artifact_file="llm_eval_examples.json") + # Delete the table from the metrics dict so we don't try to log it with the other metrics + del log_metrics["llm_eval_examples"] + mlflow.log_metrics(log_metrics, step=steps) + if log_artifacts: + _log_model(save_path) + + +def _log_artifacts(output_directory): + try: + contents = os.listdir(output_directory) + except FileNotFoundError: + logger.warning(f"_log_artifacts: output_directory does not exist: {output_directory}") + return + for fname in contents: + lpath = os.path.join(output_directory, fname) + if fname == MODEL_FILE_NAME: + _log_model(lpath) + else: + mlflow.log_artifact(lpath) + + +def _log_model(lpath): + # Lazy import to avoid requiring this package + from ludwig.contribs.mlflow.model import log_saved_model + + log_saved_model(lpath) diff --git a/ludwig/contribs/mlflow/mlflow3.py b/ludwig/contribs/mlflow/mlflow3.py new file mode 100644 index 0000000..43698ac --- /dev/null +++ b/ludwig/contribs/mlflow/mlflow3.py @@ -0,0 +1,206 @@ +"""MLflow 3.x integration enhancements. + +Adds support for MLflow 3.x features: +- LoggedModel: model-centric tracking (not run-centric) +- GenAI tracing: structured logging of LLM prompts, responses, and evaluation +- Cost tracking: automatic model info extraction for cost estimation + +These features supplement the existing MLflow integration in model.py. +They are opt-in and gracefully degrade if MLflow 3.x is not available. + +Usage: + from ludwig.contribs.mlflow.mlflow3 import log_training_run + + # After training: + log_training_run( + model=ludwig_model, + train_stats=train_stats, + config=config, + ) +""" + +import logging + +logger = logging.getLogger(__name__) + + +def log_training_run(model, train_stats=None, config=None, tags=None): + """Log a Ludwig training run to MLflow with 3.x features. + + Creates a LoggedModel entity (if MLflow 3.x is available) that is + model-centric rather than run-centric. This allows tracking the + model across multiple runs (retraining, fine-tuning, evaluation). + + Args: + model: Trained LudwigModel instance. + train_stats: Training statistics from model.train(). + config: Ludwig config dict. + tags: Additional MLflow tags. + """ + try: + import mlflow + + mlflow_version = tuple(int(x) for x in mlflow.__version__.split(".")[:2]) + except ImportError: + logger.warning("MLflow not installed. Skipping training run logging.") + return + + with mlflow.start_run(nested=True) as run: + # Log config as params + if config: + _log_config_params(config) + + # Log training metrics + if train_stats: + _log_training_metrics(train_stats) + + # Log model info and cost tracking + import ludwig + + mlflow.set_tag("ludwig.version", ludwig.__version__) + mlflow.set_tag("model.type", config.get("model_type", "ecd") if config else "unknown") + + # Cost tracking: log model size and parameter counts for cost estimation + if model and hasattr(model, "model") and model.model is not None: + try: + total_params = sum(p.numel() for p in model.model.parameters()) + trainable_params = sum(p.numel() for p in model.model.parameters() if p.requires_grad) + model_size_mb = sum(p.numel() * p.element_size() for p in model.model.parameters()) / (1024 * 1024) + mlflow.log_metric("model.total_params", total_params) + mlflow.log_metric("model.trainable_params", trainable_params) + mlflow.log_metric("model.size_mb", round(model_size_mb, 2)) + mlflow.set_tag("model.param_efficiency", f"{trainable_params / max(total_params, 1) * 100:.1f}%") + except Exception: + logger.debug("Failed to log model parameter counts to MLflow.", exc_info=True) + + # Log base model name for LLMs (useful for cost estimation) + if config and config.get("model_type") == "llm": + base_model = config.get("base_model", "unknown") + mlflow.set_tag("model.base_model", base_model) + + if tags: + for k, v in tags.items(): + mlflow.set_tag(k, v) + + # Log model artifact + try: + from ludwig.contribs.mlflow.model import log_model + + log_model(model, artifact_path="ludwig-model") + except Exception as e: + logger.warning(f"Failed to log model artifact: {e}") + + # MLflow 3.x: Create LoggedModel if available + if mlflow_version >= (3, 0): + try: + _create_logged_model(run, model, config) + except Exception as e: + logger.debug(f"MLflow 3.x LoggedModel not available: {e}") + + logger.info(f"Training run logged to MLflow: {run.info.run_id}") + return run.info.run_id + + +def log_llm_trace(prompt, response, model_name=None, latency_ms=None, tokens_used=None): + """Log an LLM inference trace to MLflow GenAI tracking. + + MLflow 3.x provides structured tracing for LLM interactions including + prompts, responses, latency, and token usage. + + Args: + prompt: The input prompt text. + response: The model's response text. + model_name: Name/ID of the model used. + latency_ms: Inference latency in milliseconds. + tokens_used: Number of tokens consumed. + """ + try: + import mlflow + + mlflow_version = tuple(int(x) for x in mlflow.__version__.split(".")[:2]) + if mlflow_version < (3, 0): + logger.debug("MLflow GenAI tracing requires MLflow 3.x") + return + except ImportError: + return + + try: + # MLflow 3.x GenAI tracing + mlflow.log_table( + data={ + "prompt": [prompt], + "response": [response], + "model": [model_name or "ludwig-llm"], + "latency_ms": [latency_ms], + "tokens": [tokens_used], + }, + artifact_file="genai_traces.json", + ) + except Exception as e: + logger.debug(f"GenAI tracing failed: {e}") + + +def _log_config_params(config): + """Log Ludwig config as MLflow params (flattened).""" + import mlflow + + def flatten(d, prefix=""): + params = {} + for k, v in d.items(): + key = f"{prefix}.{k}" if prefix else k + if isinstance(v, dict): + params.update(flatten(v, key)) + elif isinstance(v, (list, tuple)): + params[key] = str(v)[:250] # MLflow param value limit + else: + params[key] = str(v)[:250] + return params + + flat = flatten(config) + # MLflow has a 100-param batch limit + items = list(flat.items()) + for i in range(0, len(items), 100): + batch = dict(items[i : i + 100]) + try: + mlflow.log_params(batch) + except Exception: + logger.debug("Failed to log config param batch to MLflow (duplicates or value limits).", exc_info=True) + + +def _log_training_metrics(train_stats): + """Log best training metrics to MLflow.""" + import mlflow + + for split_name in ["validation", "test"]: + split_data = getattr(train_stats, split_name, None) + if not split_data: + continue + for feat_name, feat_metrics in split_data.items(): + if not isinstance(feat_metrics, dict): + continue + for metric_name, values in feat_metrics.items(): + if isinstance(values, list) and values: + if "loss" in metric_name or "error" in metric_name: + best = min(values) + else: + best = max(values) + try: + mlflow.log_metric(f"{split_name}.{feat_name}.{metric_name}", best) + except Exception: + logger.debug( + f"Failed to log metric {split_name}.{feat_name}.{metric_name} to MLflow.", exc_info=True + ) + + +def _create_logged_model(run, model, config): + """Create an MLflow 3.x LoggedModel entity.""" + import mlflow + + try: + # MLflow 3.x API + mlflow.register_model( + model_uri=f"runs:/{run.info.run_id}/ludwig-model", + name=config.get("model_name", "ludwig-model") if config else "ludwig-model", + ) + except Exception as e: + logger.debug(f"Model registration failed: {e}") diff --git a/ludwig/contribs/mlflow/model.py b/ludwig/contribs/mlflow/model.py new file mode 100644 index 0000000..ce2656b --- /dev/null +++ b/ludwig/contribs/mlflow/model.py @@ -0,0 +1,248 @@ +import logging +import os +import shutil +import tempfile + +import mlflow +import yaml +from mlflow import pyfunc +from mlflow.exceptions import MlflowException +from mlflow.models import Model +from mlflow.models.model import MLMODEL_FILE_NAME +from mlflow.models.signature import ModelSignature +from mlflow.models.utils import _save_example, ModelInputExample +from mlflow.tracking._model_registry import DEFAULT_AWAIT_MAX_SLEEP_SECONDS +from mlflow.tracking.artifact_utils import _download_artifact_from_uri +from mlflow.utils.environment import _mlflow_conda_env +from mlflow.utils.model_utils import _get_flavor_configuration + +from ludwig.api_annotations import DeveloperAPI +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME +from ludwig.utils.data_utils import load_json + +FLAVOR_NAME = "ludwig" + +_logger = logging.getLogger(__name__) + + +def get_default_conda_env(): + """Returns the default Conda environment for MLflow Models produced by calls to save_model() and log_model().""" + import ludwig + + # Ludwig is not yet available via the default conda channels, so we install it via pip + return _mlflow_conda_env( + additional_conda_deps=None, + additional_pip_deps=[f"ludwig=={ludwig.__version__}"], + additional_conda_channels=None, + ) + + +def save_model( + ludwig_model, + path, + conda_env=None, + mlflow_model=None, + signature: ModelSignature = None, + input_example: ModelInputExample = None, + **kwargs, +): + """Save a Ludwig model to a path on the local file system. + + Args: + ludwig_model: Ludwig model (an instance of `ludwig.api.LudwigModel`) to be saved. + path: Local path where the model is to be saved. + conda_env: Either a dictionary representation of a Conda environment or the path to a Conda environment + yaml file. If provided, this describes the environment this model should be run in. At minimum, it + should specify the dependencies contained in `get_default_conda_env()`. If ``None``, the default + `get_default_conda_env()` environment is added to the model. + mlflow_model: `mlflow.models.Model` this flavor is being added to. + signature: Describes model input and output schema. The model signature can be inferred from datasets + with valid model input (e.g. the training dataset with target column omitted) and valid model output + (e.g. model predictions generated on the training dataset). + input_example: Input example provides one or several instances of valid model input. The example can be + used as a hint of what data to feed the model. The given example will be converted to a Pandas + DataFrame and then serialized to json using the Pandas split-oriented format. Bytes are + base64-encoded. + """ + import ludwig + + path = os.path.abspath(path) + if os.path.exists(path): + raise MlflowException(f"Path '{path}' already exists") + model_data_subpath = MODEL_FILE_NAME + model_data_path = os.path.join(path, model_data_subpath) + os.makedirs(path) + if mlflow_model is None: + mlflow_model = Model() + if signature is not None: + mlflow_model.signature = signature + if input_example is not None: + _save_example(mlflow_model, input_example, path) + + # Save the Ludwig model + ludwig_model.save(model_data_path) + + conda_env_subpath = "conda.yaml" + if conda_env is None: + conda_env = get_default_conda_env() + elif not isinstance(conda_env, dict): + with open(conda_env) as f: + conda_env = yaml.safe_load(f) + with open(os.path.join(path, conda_env_subpath), "w") as f: + yaml.safe_dump(conda_env, stream=f, default_flow_style=False) + + pyfunc.add_to_model( + mlflow_model, + loader_module="ludwig.contribs.mlflow.model", + data=model_data_subpath, + env=conda_env_subpath, + ) + + schema_keys = {"name", "column", "type"} + config = ludwig_model.config + + mlflow_model.add_flavor( + FLAVOR_NAME, + ludwig_version=ludwig.__version__, + ludwig_schema={ + "input_features": [ + {k: v for k, v in feature.items() if k in schema_keys} for feature in config["input_features"] + ], + "output_features": [ + {k: v for k, v in feature.items() if k in schema_keys} for feature in config["output_features"] + ], + }, + data=model_data_subpath, + ) + mlflow_model.save(os.path.join(path, MLMODEL_FILE_NAME)) + + +def log_model( + ludwig_model, + artifact_path, + conda_env=None, + registered_model_name=None, + signature: ModelSignature = None, + input_example: ModelInputExample = None, + await_registration_for=DEFAULT_AWAIT_MAX_SLEEP_SECONDS, +): + """Log a Ludwig model as an MLflow artifact for the current run. + + Saves the model locally in MLflow format, then logs it as a run artifact using mlflow.log_artifacts(). This ensures + the model appears as a run artifact (compatible with MLflow 3.x where Model.log() uses the model registry instead). + """ + with tempfile.TemporaryDirectory() as tmpdir: + local_path = os.path.join(tmpdir, "model") + save_model( + ludwig_model, + path=local_path, + conda_env=conda_env, + signature=signature, + input_example=input_example, + ) + mlflow.log_artifacts(local_path, artifact_path) + + if registered_model_name is not None: + run_id = mlflow.active_run().info.run_id + mlflow.register_model( + f"runs:/{run_id}/{artifact_path}", + registered_model_name, + await_registration_for=await_registration_for, + ) + + +def _load_model(path): + from ludwig.api import LudwigModel + + return LudwigModel.load(path, backend="local") + + +def _load_pyfunc(path): + """Load PyFunc implementation. Called by ``pyfunc.load_pyfunc``. + + Args: + path: Local filesystem path to the MLflow Model with the ``ludwig`` flavor. + """ + return _LudwigModelWrapper(_load_model(path)) + + +def load_model(model_uri): + """Load a Ludwig model from a local file or a run. + + Args: + model_uri: The location, in URI format, of the MLflow model. For example: + - ``/Users/me/path/to/local/model`` + - ``relative/path/to/local/model`` + - ``s3://my_bucket/path/to/model`` + - ``runs://run-relative/path/to/model`` + + For more information about supported URI schemes, see + `Referencing Artifacts `_. + + Returns: + A Ludwig model (an instance of `ludwig.api.LudwigModel`). + """ + local_model_path = _download_artifact_from_uri(artifact_uri=model_uri) + flavor_conf = _get_flavor_configuration(model_path=local_model_path, flavor_name=FLAVOR_NAME) + model_data_path = os.path.join(local_model_path, flavor_conf.get("data", "model")) + return _load_model(path=model_data_path) + + +class _LudwigModelWrapper: + def __init__(self, ludwig_model): + self.ludwig_model = ludwig_model + + def predict(self, dataframe): + pred_df, _ = self.ludwig_model.predict(dataframe) + return pred_df + + +def export_model(model_path, output_path, registered_model_name=None): + if registered_model_name: + if not model_path.startswith("runs:/") or output_path is not None: + # No run specified, so in order to register the model in mlflow, we need + # to create a new run and upload the model as an artifact first + output_path = output_path or MODEL_FILE_NAME + log_model( + _CopyModel(model_path), + artifact_path=output_path, + registered_model_name=registered_model_name, + ) + else: + # Registering a model from an artifact of an existing run + mlflow.register_model( + model_path, + registered_model_name, + ) + else: + # No model name means we only want to save the model locally + save_model( + _CopyModel(model_path), + path=output_path, + ) + + +@DeveloperAPI +def log_saved_model(lpath): + """Log a saved Ludwig model directory as a proper MLflow model artifact.""" + if os.path.isdir(lpath): + log_model( + _CopyModel(lpath), + artifact_path="model", + ) + elif os.path.isfile(lpath): + mlflow.log_artifact(lpath, "model") + + +class _CopyModel: + """Get model data without requiring us to read the model weights into memory.""" + + def __init__(self, lpath): + self.lpath = lpath + + def save(self, path): + shutil.copytree(self.lpath, path) + + @property + def config(self): + return load_json(os.path.join(self.lpath, MODEL_HYPERPARAMETERS_FILE_NAME)) diff --git a/ludwig/contribs/wandb.py b/ludwig/contribs/wandb.py new file mode 100644 index 0000000..a940fae --- /dev/null +++ b/ludwig/contribs/wandb.py @@ -0,0 +1,72 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os + +from ludwig.api_annotations import PublicAPI +from ludwig.callbacks import Callback +from ludwig.utils.package_utils import LazyLoader + +wandb = LazyLoader("wandb", globals(), "wandb") + +logger = logging.getLogger(__name__) + + +@PublicAPI +class WandbCallback(Callback): + """Class that defines the methods necessary to hook into process.""" + + def on_train_init( + self, + base_config, + experiment_directory, + experiment_name, + model_name, + output_directory, + resume_directory, + ): + logger.info("wandb.on_train_init() called...") + wandb.init( + project=os.getenv("WANDB_PROJECT", experiment_name), + name=model_name, + sync_tensorboard=True, + dir=output_directory, + ) + wandb.save(os.path.join(experiment_directory, "*")) + + def on_train_start(self, model, config, *args, **kwargs): + logger.info("wandb.on_train_start() called...") + config = config.copy() + del config["input_features"] + del config["output_features"] + wandb.config.update(config) + + def on_eval_end(self, trainer, progress_tracker, save_path): + """Called from ludwig/models/model.py.""" + for key, value in progress_tracker.log_metrics().items(): + wandb.log({key: value}) + + def on_epoch_end(self, trainer, progress_tracker, save_path): + """Called from ludwig/models/model.py.""" + for key, value in progress_tracker.log_metrics().items(): + wandb.log({key: value}) + + def on_visualize_figure(self, fig): + logger.info("wandb.on_visualize_figure() called...") + if wandb.run: + wandb.log({"figure": fig}) + + def on_train_end(self, output_directory): + wandb.finish() diff --git a/ludwig/data/__init__.py b/ludwig/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/data/batcher/__init__.py b/ludwig/data/batcher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/data/batcher/base.py b/ludwig/data/batcher/base.py new file mode 100644 index 0000000..a686e3e --- /dev/null +++ b/ludwig/data/batcher/base.py @@ -0,0 +1,33 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from abc import ABC, abstractmethod + +import numpy as np + + +class Batcher(ABC): + @abstractmethod + def next_batch(self) -> dict[str, np.ndarray]: + raise NotImplementedError() + + @abstractmethod + def last_batch(self) -> bool: + raise NotImplementedError() + + @abstractmethod + def set_epoch(self, epoch: int, batch_size: int): + raise NotImplementedError() diff --git a/ludwig/data/batcher/bucketed.py b/ludwig/data/batcher/bucketed.py new file mode 100644 index 0000000..3ac0207 --- /dev/null +++ b/ludwig/data/batcher/bucketed.py @@ -0,0 +1,114 @@ +#! /usr/bin/env python +# Copyright (c) 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import numpy as np + +from ludwig.data.batcher.base import Batcher + + +class BucketedBatcher(Batcher): + def __init__( + self, + dataset, + bucketing_field, + batch_size=128, + buckets=10, + should_shuffle=True, + ignore_last=False, + should_trim=False, + trim_side="right", + ): + self.should_shuffle = should_shuffle + self.bucketing_field = bucketing_field + self.should_trim = should_trim + self.trim_side = trim_side + + # store our dataset as well + self.dataset = dataset + + field = dataset.get_dataset()[bucketing_field] + field_lengths = np.apply_along_axis(lambda x: np.sign(x).sum(), 1, field) + sorted_idcs = np.argsort(field_lengths) + self.buckets_idcs = [] + datapoints_per_bucket = len(field) // buckets + for b in range(buckets): + start = datapoints_per_bucket * b + end = datapoints_per_bucket * (b + 1) if b < buckets - 1 else len(sorted_idcs) + self.buckets_idcs.append(sorted_idcs[start:end]) + + if should_shuffle: + self.shuffle(self.buckets_idcs) + + self.ignore_last = ignore_last + self.batch_size = batch_size + self.total_size = min(map(len, dataset.get_dataset().values())) + self.bucket_sizes = np.array(list(map(len, self.buckets_idcs))) + self.steps_per_epoch = self._compute_steps_per_epoch() + self.indices = np.array([0] * buckets) + self.step = 0 + self.epoch = 0 + + def shuffle(self, buckets_idcs): + for i in range(len(buckets_idcs)): + np.random.shuffle(buckets_idcs[i]) + + def next_batch(self): + if self.last_batch(): + if self.should_shuffle: + self.shuffle(self.buckets_idcs) + self.set_epoch(self.epoch + 1) + + if self.ignore_last: + idcs_below_size = self.indices + self.batch_size < self.bucket_sizes + else: + idcs_below_size = self.indices < self.bucket_sizes + i = np.random.choice(np.arange(0, len(self.buckets_idcs))[idcs_below_size]) + + selected_bucket = self.buckets_idcs[i] + selected_idcs = selected_bucket[self.indices[i] : self.indices[i] + self.batch_size] + + sub_batch = {} + for key in self.dataset.get_dataset(): + if key == self.bucketing_field and self.should_trim: + selected_samples = self.dataset.get(key, selected_idcs) + max_length = np.sign(selected_samples).sum(axis=1).max() + if self.trim_side == "right": + sub_batch[key] = selected_samples[:, :max_length] + elif self.trim_side == "left": + sub_batch[key] = selected_samples[:, -max_length:] + else: + raise ValueError(f"Invalid trim_side '{self.trim_side}'. Expected 'left' or 'right'.") + + else: + sub_batch[key] = self.dataset.get(key, selected_idcs) + + self.indices[i] += self.batch_size + self.step += 1 + return sub_batch + + def last_batch(self): + return not np.any(self.indices < self.bucket_sizes) or ( + self.ignore_last and not np.any(self.indices + self.batch_size < self.bucket_sizes) + ) + + def set_epoch(self, epoch, batch_size): + self.indices = np.array([0] * len(self.buckets_idcs)) + self.step = 0 + self.epoch = epoch + self.batch_size = batch_size + self.steps_per_epoch = self._compute_steps_per_epoch() + + def _compute_steps_per_epoch(self) -> int: + return int(np.sum(np.ceil(self.bucket_sizes / self.batch_size)).item()) diff --git a/ludwig/data/batcher/iterable.py b/ludwig/data/batcher/iterable.py new file mode 100644 index 0000000..be0fa26 --- /dev/null +++ b/ludwig/data/batcher/iterable.py @@ -0,0 +1,46 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from ludwig.data.batcher.base import Batcher + + +class IterableBatcher(Batcher): + def __init__(self, dataset, data, steps_per_epoch, ignore_last=False): + self.dataset = dataset + self.data = data + self.data_it = iter(data) + + self.ignore_last = ignore_last + self.steps_per_epoch = steps_per_epoch + self.step = 0 + + def next_batch(self): + if self.last_batch(): + raise StopIteration() + + sub_batch = {} + batch = next(self.data_it) + for features_name in self.dataset.features: + sub_batch[features_name] = self.dataset.get(features_name, batch) + + self.step += 1 + return sub_batch + + def last_batch(self): + return self.step >= self.steps_per_epoch or (self.ignore_last and self.step + 1 >= self.steps_per_epoch) + + def set_epoch(self, epoch, batch_size): + # TODO ray: implement dynamic batch size + self.step = 0 diff --git a/ludwig/data/batcher/random_access.py b/ludwig/data/batcher/random_access.py new file mode 100644 index 0000000..ad602b2 --- /dev/null +++ b/ludwig/data/batcher/random_access.py @@ -0,0 +1,271 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import math +import queue +import threading + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.data.batcher.base import Batcher + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +class RandomAccessBatcher(Batcher): + """Batcher for a PandasDataset (local, non-distributed training). + + When ``prefetch_size > 0``, a background producer thread decodes the next + ``prefetch_size`` batches concurrently with GPU work, so the GPU is never + stalled waiting for disk I/O or compute decoding. This is equivalent to + the ``RayDatasetBatcher._create_async_reader`` pattern used in the Ray + backend. + + ``prefetch_size=0`` (the default) uses the original synchronous path, + which is appropriate for eager (pre-decoded) datasets where ``dataset.get`` + is a cheap numpy slice. + + ``PandasDataset.initialize_batcher`` automatically sets ``prefetch_size`` + from each feature's preprocessing config (``None`` → 4 for ``lazy`` / + ``lazy_cached`` features, 0 for ``eager``). Users can override this via + the ``prefetch_size`` field in the feature's preprocessing config, or by + passing ``prefetch_size`` directly to ``initialize_batcher``. + + After the first epoch completes in ``lazy_cached`` mode, ``set_epoch`` + checks ``dataset.is_fully_cached()`` and resets ``prefetch_size`` to 0 — + memmap reads (~0.1 ms/batch) are fast enough that background pipelining + adds no measurable benefit and avoids unnecessary thread overhead. + """ + + def __init__( + self, + dataset, + sampler, + batch_size: int = 128, + ignore_last: bool = False, + augmentation_pipeline=None, + prefetch_size: int = 0, + ): + self.dataset = dataset + self.sampler = sampler + self.ignore_last = ignore_last + self.batch_size = batch_size + self.total_size = len(sampler) + self.augmentation_pipeline = augmentation_pipeline + self.steps_per_epoch = self._compute_steps_per_epoch() + self._prefetch_size = prefetch_size + + # Mutable state consumed by both the sync and async paths. + # When prefetch is active, ONLY the producer thread modifies these + # (index / step / sample_it); the main thread only dequeues. + self.index = 0 + self.step = 0 + self.sample_it = iter(self.sampler) + + # Async-path state — unused when prefetch_size == 0. + self._prefetch_queue: queue.Queue | None = None + self._prefetch_stop: threading.Event | None = None + self._prefetch_thread: threading.Thread | None = None + self._async_last = False + self._async_next = None + + if prefetch_size > 0: + self._start_async_epoch() + + # ------------------------------------------------------------------ + # Sync helpers + # ------------------------------------------------------------------ + + def _sync_exhausted(self) -> bool: + """Return True if there are no more batches for this epoch.""" + if self.index >= self.total_size: + return True + if self.ignore_last and self.step and self.batch_size > 1 and self.index - self.total_size == -1: + logger.info("Last batch in epoch only has 1 sample and will be dropped.") + return True + return False + + def _fetch_sync(self) -> dict: + """Fetch and decode one batch. Only called from the producer thread when prefetch is on.""" + indices = [] + for _ in range(self.batch_size): + try: + indices.append(next(self.sample_it)) + self.index += 1 + except StopIteration: + break + + sub_batch = {feature_name: self.dataset.get(feature_name, indices) for feature_name in self.dataset.features} + + if self.augmentation_pipeline: + for feature_name, augmentations in self.augmentation_pipeline.items(): + logger.debug(f"RandomAccessBatcher applying augmentation pipeline to batch for feature {feature_name}") + sub_batch[feature_name] = augmentations(torch.tensor(sub_batch[feature_name])) + + self.step += 1 + return sub_batch + + # ------------------------------------------------------------------ + # Async (prefetch) epoch management + # ------------------------------------------------------------------ + + def _start_async_epoch(self) -> None: + """Spin up a fresh producer thread for one epoch.""" + stop = threading.Event() + q = queue.Queue(maxsize=self._prefetch_size) + self._prefetch_stop = stop + self._prefetch_queue = q + self._async_last = False + self._async_next = None + + def producer(): + try: + while not stop.is_set() and not self._sync_exhausted(): + batch = self._fetch_sync() + # Blocking put with periodic stop-event checks so we don't + # hang forever if the consumer stops early (e.g. set_epoch). + while not stop.is_set(): + try: + q.put(batch, timeout=0.2) + break + except queue.Full: + continue + finally: + # Sentinel is only useful for the normal-exhaustion case. + # When stopped externally, _stop_async drains the queue before + # this put, so there is always a free slot. + if not stop.is_set(): + q.put(None) + else: + # Best-effort: there is always at least one free slot after + # _stop_async drains, so this won't block. + try: + q.put(None, block=False) + except queue.Full: + pass + + self._prefetch_thread = threading.Thread(target=producer, daemon=True) + self._prefetch_thread.start() + # Pre-load first value so last_batch() is accurate before the first + # call to next_batch(). + self._async_advance() + + def _async_advance(self) -> None: + """Dequeue the next pre-fetched batch; update _async_last on sentinel.""" + item = self._prefetch_queue.get() + if item is None: + self._async_last = True + self._async_next = None + else: + self._async_last = False + self._async_next = item + + def _stop_async(self) -> None: + """Signal the producer to stop and wait for it to exit cleanly.""" + if self._prefetch_stop is not None: + self._prefetch_stop.set() + # Drain so the producer unblocks from q.put() on its next timeout, + # and so the sentinel put in finally always has a free slot. + if self._prefetch_queue is not None: + while True: + try: + self._prefetch_queue.get_nowait() + except queue.Empty: + break + if self._prefetch_thread is not None: + self._prefetch_thread.join(timeout=5.0) + self._prefetch_thread = None + self._prefetch_stop = None + self._prefetch_queue = None + + # ------------------------------------------------------------------ + # Public Batcher interface + # ------------------------------------------------------------------ + + def next_batch(self) -> dict: + if self.last_batch(): + raise StopIteration() + + if self._prefetch_size > 0: + # Fast path: return the already-decoded batch and kick off advance + # of the next one (which overlaps with GPU work on the returned batch). + batch = self._async_next + self._async_advance() + return batch + + # Sync path — unchanged from original; used for eager (pre-decoded) datasets. + indices = [] + for _ in range(self.batch_size): + try: + indices.append(next(self.sample_it)) + self.index += 1 + except StopIteration: + break + + sub_batch = {feature_name: self.dataset.get(feature_name, indices) for feature_name in self.dataset.features} + + if self.augmentation_pipeline: + for feature_name, augmentations in self.augmentation_pipeline.items(): + logger.debug(f"RandomAccessBatcher applying augmentation pipeline to batch for feature {feature_name}") + sub_batch[feature_name] = augmentations(torch.tensor(sub_batch[feature_name])) + + self.step += 1 + return sub_batch + + def last_batch(self) -> bool: + """Returns whether we've exhausted all batches for this epoch.""" + if self._prefetch_size > 0: + return self._async_last + + if self.index >= self.total_size: + return True + elif self.ignore_last and self.step: + if self.batch_size > 1 and self.index - self.total_size == -1: + logger.info("Last batch in epoch only has 1 sample and will be dropped.") + return True + return False + + def set_epoch(self, epoch: int, batch_size: int) -> None: + """Reset state for a new epoch; disables prefetch when the dataset is fully cached. + + If the dataset exposes ``is_fully_cached()`` and it returns ``True`` + (i.e. all ``lazy_cached`` columns have finished their first-pass decode), + ``prefetch_size`` is set to 0 so subsequent epochs read directly from + the memmap without spinning up a producer thread. + """ + if self._prefetch_size > 0: + self._stop_async() + + self.batch_size = batch_size + self.steps_per_epoch = self._compute_steps_per_epoch() + self.index = 0 + self.step = 0 + self.sampler.set_epoch(epoch) + self.sample_it = iter(self.sampler) + + # After epoch 1, if all lazy columns are decoded and cached in memmaps, + # disable prefetch — memmap reads are fast enough that pipelining adds + # no measurable benefit and avoids unnecessary thread overhead. + if self._prefetch_size > 0 and hasattr(self.dataset, "is_fully_cached") and self.dataset.is_fully_cached(): + self._prefetch_size = 0 + + if self._prefetch_size > 0: + self._start_async_epoch() + + def _compute_steps_per_epoch(self) -> int: + return int(math.ceil(self.total_size / self.batch_size)) diff --git a/ludwig/data/batcher/test_batcher.py b/ludwig/data/batcher/test_batcher.py new file mode 100644 index 0000000..c51698b --- /dev/null +++ b/ludwig/data/batcher/test_batcher.py @@ -0,0 +1,109 @@ +import logging + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel +from ludwig.data.dataset.pandas import PandasDataset + + +def test_pandas_size(): + df = pd.DataFrame( + {"name": ["joe", "janice", "sara"], "mask": ["green", "black", "pink"], "weapon": ["stick", "gun", "gun"]} + ) + config = yaml.safe_load(""" + model_type: llm + base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM + input_features: + - name: name + type: text + preprocessing: + max_sequence_length: 256 + column: name + output_features: + - name: weapon + type: text + preprocessing: + max_sequence_length: 256 + column: weapon + preprocessing: + split: + type: random + probabilities: + - 1 + - 0 + - 0 + """) + model = LudwigModel(config=config, logging_level=logging.INFO) + data = model.preprocess(df, skip_save_processed_input=False) + training_set = data[0] + assert training_set.size == len(df) + + # Check if string loading works as well + # data[0].data_cache_fp is the string filepath to the cached data from preprocessing + data_from_str = PandasDataset(data[0].data_cache_fp, data[0].features, None) + assert data_from_str.size == len(df) + + +def test_pandas_batcher_use_all_samples(): + df = pd.DataFrame( + {"name": ["joe", "janice", "sara"], "mask": ["green", "black", "pink"], "weapon": ["stick", "gun", "gun"]} + ) + config = yaml.safe_load(""" + model_type: llm + base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM + input_features: + - name: name + type: text + preprocessing: + max_sequence_length: 256 + column: name + output_features: + - name: weapon + type: text + preprocessing: + max_sequence_length: 256 + column: weapon + preprocessing: + split: + type: random + probabilities: + - 1 + - 0 + - 0 + """) + model = LudwigModel(config=config, logging_level=logging.INFO) + data = model.preprocess(df, skip_save_processed_input=False) + training_set = data[0] + features = training_set.dataset.keys() + + batches = [] + with training_set.initialize_batcher(batch_size=1) as batcher: + while not batcher.last_batch(): + batch = batcher.next_batch() + batches.append(batch) + assert (len(batches)) == training_set.size + + # Check to see if all items are used exactly once + for feature in features: + for i in range(len(training_set.dataset[feature])): + # Each of the arrays in the line below should contain the vector representation of a feature of sample i + assert (batches[i][feature].squeeze() == training_set.dataset[feature][i].squeeze()).all() + + # Check if string loading works as well + batches = [] + # data[0].data_cache_fp is the string filepath to the cached data from preprocessing + data_from_str = PandasDataset(data[0].data_cache_fp, data[0].features, None) + features = data_from_str.dataset.keys() + + with data_from_str.initialize_batcher(batch_size=1) as batcher: + while not batcher.last_batch(): + batch = batcher.next_batch() + batches.append(batch) + assert (len(batches)) == data_from_str.size + + # Check to see if all items are used exactly once + for feature in features: + for i in range(len(data_from_str.dataset[feature])): + # Each of the arrays in the line below should contain the vector representation of a feature of sample i + assert (batches[i][feature].squeeze() == data_from_str.dataset[feature][i].squeeze()).all() diff --git a/ludwig/data/cache/__init__.py b/ludwig/data/cache/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/data/cache/manager.py b/ludwig/data/cache/manager.py new file mode 100644 index 0000000..dd6f847 --- /dev/null +++ b/ludwig/data/cache/manager.py @@ -0,0 +1,165 @@ +import logging +import os +import tempfile + +from ludwig.constants import CHECKSUM, META, TEST, TRAINING, VALIDATION +from ludwig.data.cache.types import alphanum, CacheableDataset +from ludwig.data.cache.util import calculate_checksum +from ludwig.data.dataset.base import DatasetManager +from ludwig.utils import data_utils +from ludwig.utils.fs_utils import delete, path_exists + +logger = logging.getLogger(__name__) + + +class DatasetCache: + def __init__(self, config, checksum, cache_map, dataset_manager): + self.config = config + self.checksum = checksum + self.cache_map = cache_map + self.dataset_manager = dataset_manager + + def get(self): + training_set_metadata_fp = self.cache_map[META] + if not path_exists(training_set_metadata_fp): + return None + + try: + cached_training_set_metadata = data_utils.load_json(training_set_metadata_fp) + except Exception: + logger.exception(f"Failed to load cached training set metadata at {training_set_metadata_fp}") + return None + + cached_training_set = self.cache_map[TRAINING] if path_exists(self.cache_map[TRAINING]) else None + if not cached_training_set: + logger.warning(f"Failed to load cached training set at {self.cache_map[TRAINING]}") + + cached_validation_set = self.cache_map[VALIDATION] if path_exists(self.cache_map[VALIDATION]) else None + if not cached_validation_set: + logger.warning(f"Failed to load cached validation set at {self.cache_map[VALIDATION]}") + + cached_test_set = self.cache_map[TEST] if path_exists(self.cache_map[TEST]) else None + if not cached_test_set: + logger.warning(f"Failed to load cached test set at {self.cache_map[TEST]}") + + valid = self.checksum == cached_training_set_metadata.get(CHECKSUM) and cached_training_set is not None + + return valid, cached_training_set_metadata, cached_training_set, cached_test_set, cached_validation_set + + def put(self, training_set, test_set, validation_set, training_set_metadata): + logger.info(f"Writing preprocessed training set cache to {self.cache_map[TRAINING]}") + training_set = self.dataset_manager.save( + self.cache_map[TRAINING], + training_set, + self.config, + training_set_metadata, + TRAINING, + ) + + if validation_set is not None: + logger.info(f"Writing preprocessed validation set cache to {self.cache_map[VALIDATION]}") + validation_set = self.dataset_manager.save( + self.cache_map[VALIDATION], + validation_set, + self.config, + training_set_metadata, + VALIDATION, + ) + + if test_set is not None: + logger.info(f"Writing preprocessed test set cache to {self.cache_map[TEST]}") + test_set = self.dataset_manager.save( + self.cache_map[TEST], + test_set, + self.config, + training_set_metadata, + TEST, + ) + + logger.info(f"Writing train set metadata to {self.cache_map[META]}") + data_utils.save_json(self.cache_map[META], training_set_metadata) + + return training_set, test_set, validation_set, training_set_metadata + + def delete(self): + for fname in self.cache_map.values(): + if path_exists(fname): + # Parquet entries in the cache_map can be pointers to directories. + delete(fname, recursive=True) + # Also clean up shapes sidecar files (*.shapes.json) written alongside Parquet caches + if fname.endswith(".parquet"): + import os + + shapes_fp = os.path.splitext(fname)[0] + ".shapes.json" + if path_exists(shapes_fp): + delete(shapes_fp) + + def get_cached_obj_path(self, cached_obj_name: str) -> str: + return self.cache_map.get(cached_obj_name) + + +class PreprocessedDataCache: + def __init__( + self, + dataset_manager: DatasetManager, + cache_dir: str | None = None, + ): + self._dataset_manager = dataset_manager + self._cache_dir = cache_dir + + def get_dataset_cache( + self, + config: dict, + dataset: CacheableDataset | None = None, + training_set: CacheableDataset | None = None, + test_set: CacheableDataset | None = None, + validation_set: CacheableDataset | None = None, + ) -> DatasetCache: + if dataset is not None: + key = self.get_cache_key(dataset, config) + cache_map = { + META: self.get_cache_path(dataset, key, META, "json"), + TRAINING: self.get_cache_path(dataset, key, TRAINING), + TEST: self.get_cache_path(dataset, key, TEST), + VALIDATION: self.get_cache_path(dataset, key, VALIDATION), + } + return DatasetCache(config, key, cache_map, self._dataset_manager) + else: + key = self.get_cache_key(training_set, config) + cache_map = { + META: self.get_cache_path(training_set, key, META, "json"), + TRAINING: self.get_cache_path(training_set, key, TRAINING), + TEST: self.get_cache_path(test_set, key, TEST), + VALIDATION: self.get_cache_path(validation_set, key, VALIDATION), + } + return DatasetCache(config, key, cache_map, self._dataset_manager) + + def get_cache_key(self, dataset: CacheableDataset, config: dict) -> str: + return calculate_checksum(dataset, config) + + def get_cache_path(self, dataset: CacheableDataset | None, key: str, tag: str, ext: str | None = None) -> str: + if self._cache_dir is None and dataset is not None: + # Use the input dataset filename (minus the extension) as the cache path + stem = dataset.get_cache_path() + else: + # To avoid collisions across different directories, we use the unique checksum + # as the cache path + stem = alphanum(key) + + ext = ext or self.data_format + cache_fname = f"{stem}.{tag}.{ext}" + return os.path.join(self.get_cache_directory(dataset), cache_fname) + + def get_cache_directory(self, dataset: CacheableDataset | None) -> str: + if self._cache_dir is None: + if dataset is None: + return tempfile.gettempdir() + return dataset.get_cache_directory() + return self._cache_dir + + def can_cache(self, skip_save_processed_input: bool) -> bool: + return self._dataset_manager.can_cache(skip_save_processed_input) + + @property + def data_format(self) -> str: + return self._dataset_manager.data_format diff --git a/ludwig/data/cache/types.py b/ludwig/data/cache/types.py new file mode 100644 index 0000000..f296f7e --- /dev/null +++ b/ludwig/data/cache/types.py @@ -0,0 +1,108 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import os +import re +import tempfile +import uuid +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.fs_utils import checksum +from ludwig.utils.types import DataFrame + + +def alphanum(v): + """Filters a string to only its alphanumeric characters.""" + return re.sub(r"\W+", "", v) + + +@DeveloperAPI +class CacheableDataset(ABC): + name: str + checksum: str + + @abstractmethod + def get_cache_path(self) -> str: + raise NotImplementedError() + + @abstractmethod + def get_cache_directory(self) -> str: + raise NotImplementedError() + + @abstractmethod + def unwrap(self) -> str | DataFrame: + raise NotImplementedError() + + +@DeveloperAPI +@dataclass +class CacheableDataframe(CacheableDataset): + df: DataFrame + name: str + checksum: str + + def get_cache_path(self) -> str: + return alphanum(self.name) + + def get_cache_directory(self) -> str: + return tempfile.gettempdir() + + def unwrap(self) -> str | DataFrame: + return self.df + + +@DeveloperAPI +@dataclass +class CacheablePath(CacheableDataset): + path: str + + @property + def name(self) -> str: + return Path(self.path).stem + + @property + def checksum(self) -> str: + return checksum(self.path) + + def get_cache_path(self) -> str: + return self.name + + def get_cache_directory(self) -> str: + return os.path.dirname(self.path) + + def unwrap(self) -> str | DataFrame: + return self.path + + +CacheInput = str | DataFrame | CacheableDataset + + +def wrap(dataset: CacheInput | None) -> CacheableDataset: + if dataset is None: + return None + + if isinstance(dataset, CacheableDataset): + return dataset + if isinstance(dataset, str): + return CacheablePath(path=dataset) + + # TODO(travis): could try hashing the in-memory dataset, but this is tricky for Dask + checksum = str(uuid.uuid1()) + name = checksum + return CacheableDataframe(df=dataset, name=name, checksum=checksum) diff --git a/ludwig/data/cache/util.py b/ludwig/data/cache/util.py new file mode 100644 index 0000000..14b53c7 --- /dev/null +++ b/ludwig/data/cache/util.py @@ -0,0 +1,34 @@ +import ludwig +from ludwig.constants import DEFAULTS, INPUT_FEATURES, OUTPUT_FEATURES, PREPROCESSING, PROC_COLUMN, TYPE +from ludwig.data.cache.types import CacheableDataset +from ludwig.types import ModelConfigDict +from ludwig.utils.data_utils import hash_dict + + +def calculate_checksum(original_dataset: CacheableDataset, config: ModelConfigDict): + """Calculates a checksum for a dataset and model config. + + The checksum is used to determine if the dataset and model config have changed since the last time the model was + trained. If either has changed, a different checksum will be produced which will lead to a cache miss and force + preprocessing to be performed again. + """ + features = config.get(INPUT_FEATURES, []) + config.get(OUTPUT_FEATURES, []) + config.get("features", []) + info = { + "ludwig_version": ludwig.globals.LUDWIG_VERSION, + "dataset_checksum": original_dataset.checksum, + "global_preprocessing": config.get(PREPROCESSING, {}), + "global_defaults": config.get(DEFAULTS, {}), + # PROC_COLUMN contains both the feature name and the feature hash that is computed + # based on each feature's preprocessing parameters and the feature's type. + # creating a sorted list out of the dict because hash_dict requires all values + # of the dict to be ordered object to ensure the creation fo the same hash + "feature_proc_columns": sorted({feature[PROC_COLUMN] for feature in features}), + "feature_types": [feature[TYPE] for feature in features], + "feature_preprocessing": [feature.get(PREPROCESSING, {}) for feature in features], + } + + # LLM-specific params + if "prompt" in config: + info["prompt"] = config["prompt"] + + return hash_dict(info, max_length=None).decode("ascii") diff --git a/ludwig/data/concatenate_datasets.py b/ludwig/data/concatenate_datasets.py new file mode 100644 index 0000000..5f94846 --- /dev/null +++ b/ludwig/data/concatenate_datasets.py @@ -0,0 +1,101 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging + +import numpy as np + +from ludwig.backend import LOCAL_BACKEND +from ludwig.constants import SPLIT +from ludwig.utils.data_utils import read_csv + +logger = logging.getLogger(__name__) + + +def concatenate_csv(train_csv, vali_csv, test_csv, output_csv): + concatenated_df = concatenate_files(train_csv, vali_csv, test_csv, read_csv, LOCAL_BACKEND) + + logger.info("Saving concatenated dataset as csv..") + concatenated_df.to_csv(output_csv, encoding="utf-8", index=False) + logger.info("done") + + +def concatenate_files(train_fname, vali_fname, test_fname, read_fn, backend): + df_lib = backend.df_engine.df_lib + + logger.info("Loading training file...") + train_df = read_fn(train_fname, df_lib) + logger.info("done") + + logger.info("Loading validation file..") + vali_df = read_fn(vali_fname, df_lib) if vali_fname is not None else None + logger.info("done") + + logger.info("Loading test file..") + test_df = read_fn(test_fname, df_lib) if test_fname is not None else None + logger.info("done") + + logger.info("Concatenating files..") + concatenated_df = concatenate_df(train_df, vali_df, test_df, backend) + logger.info("done") + + return concatenated_df + + +def concatenate_df(train_df, vali_df, test_df, backend): + train_size = len(train_df) + vali_size = len(vali_df) if vali_df is not None else 0 + + concatenated_df = backend.df_engine.df_lib.concat( + [df for df in [train_df, vali_df, test_df] if df is not None], ignore_index=True + ) + + def get_split(idx): + if idx < train_size: + return 0 + if idx < train_size + vali_size: + return 1 + return 2 + + concatenated_df[SPLIT] = concatenated_df.index.to_series().map(get_split).astype(np.int8) + return concatenated_df + + +def concatenate_splits(train_df, vali_df, test_df, backend): + def to_frame(df, split): + if df is None: + return None + + df = df.index.to_frame(name=SPLIT) + df[SPLIT] = split + return df + + dfs = [train_df, vali_df, test_df] + dfs = [to_frame(df, split) for split, df in enumerate(dfs)] + return backend.df_engine.df_lib.concat([df for df in dfs if df is not None]) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Concatenate train validation and test set") + + parser.add_argument("-train", "--train_csv", help="CSV containing the training set") + parser.add_argument("-vali", "--vali_csv", help="CSV containing the validation set") + parser.add_argument("-test", "--test_csv", help="CSV containing the test set") + + parser.add_argument("-o", "--output_csv", help="output csv") + args = parser.parse_args() + + concatenate_csv(args.train_csv, args.vali_csv, args.test_csv, args.output_csv) diff --git a/ludwig/data/dataframe/__init__.py b/ludwig/data/dataframe/__init__.py new file mode 100644 index 0000000..92b9464 --- /dev/null +++ b/ludwig/data/dataframe/__init__.py @@ -0,0 +1,15 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== diff --git a/ludwig/data/dataframe/base.py b/ludwig/data/dataframe/base.py new file mode 100644 index 0000000..be3a29b --- /dev/null +++ b/ludwig/data/dataframe/base.py @@ -0,0 +1,105 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from abc import ABC, abstractmethod + +from ludwig.utils.types import DataFrame + + +class DataFrameEngine(ABC): + @abstractmethod + def df_like(self, df, proc_cols): + raise NotImplementedError() + + @abstractmethod + def parallelize(self, data): + raise NotImplementedError() + + @abstractmethod + def persist(self, data): + raise NotImplementedError() + + @abstractmethod + def compute(self, data): + raise NotImplementedError() + + @abstractmethod + def from_pandas(self, df): + raise NotImplementedError() + + @abstractmethod + def map_objects(self, series, map_fn, meta=None): + raise NotImplementedError() + + @abstractmethod + def map_partitions(self, series, map_fn, meta=None, progress_tracker=None): + raise NotImplementedError() + + @abstractmethod + def map_batches(self, df, map_fn, enable_tensor_extension_casting=True): + raise NotImplementedError() + + @abstractmethod + def apply_objects(self, series, map_fn, meta=None): + raise NotImplementedError() + + @abstractmethod + def reduce_objects(self, series, reduce_fn): + raise NotImplementedError() + + @abstractmethod + def split(self, df, probabilities): + """Splits the input DataFrame into sections with the given proportions.""" + raise NotImplementedError() + + @abstractmethod + def to_parquet(self, df, path, index=False): + """Write the input DataFrame to the path in the Parquet format. + + Optionally includes the DataFrame index in the Parquet file. + """ + raise NotImplementedError() + + @abstractmethod + def write_predictions(self, df: DataFrame, path: str): + raise NotImplementedError() + + @abstractmethod + def read_predictions(self, path: str) -> DataFrame: + raise NotImplementedError() + + @abstractmethod + def to_ray_dataset(self, df): + raise NotImplementedError() + + @property + @abstractmethod + def array_lib(self): + raise NotImplementedError() + + @property + @abstractmethod + def df_lib(self): + raise NotImplementedError() + + @property + @abstractmethod + def partitioned(self): + raise NotImplementedError() + + @abstractmethod + def set_parallelism(self, parallelism): + raise NotImplementedError() diff --git a/ludwig/data/dataframe/dask.py b/ludwig/data/dataframe/dask.py new file mode 100644 index 0000000..6b4b4f6 --- /dev/null +++ b/ludwig/data/dataframe/dask.py @@ -0,0 +1,317 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import collections +import logging +from contextlib import contextmanager + +import dask +import dask.array as da +import dask.dataframe as dd +import ray +from dask.diagnostics import ProgressBar +from packaging import version +from pyarrow.fs import FSSpecHandler, PyFileSystem +from ray.data import Dataset, read_parquet + +from ludwig.api_annotations import DeveloperAPI +from ludwig.data.dataframe.base import DataFrameEngine +from ludwig.utils.data_utils import get_pa_schema, get_parquet_filename, split_by_slices +from ludwig.utils.dataframe_utils import set_index_name +from ludwig.utils.fs_utils import get_fs_and_path + +TMP_COLUMN = "__TMP_COLUMN__" + +# This is to be compatible with pyarrow.lib.schema +PandasBlockSchema = collections.namedtuple("PandasBlockSchema", ["names", "types"]) + +logger = logging.getLogger(__name__) + + +_ray_230 = version.parse(ray.__version__) >= version.parse("2.3.0") + + +@DeveloperAPI +def set_scheduler(scheduler): + dask.config.set(scheduler=scheduler) + + +@DeveloperAPI +def reset_index_across_all_partitions(df): + """Compute a monotonically increasing index across all partitions. + + This differs from dd.reset_index, which computes an independent index for each partition. + Source: https://stackoverflow.com/questions/61395351/how-to-reset-index-on-concatenated-dataframe-in-dask + """ + # Create temporary column of ones + df = df.assign(**{TMP_COLUMN: 1}) + + # Set the index to the cumulative sum of TMP_COLUMN, which we know to be sorted; this improves efficiency. + df = df.set_index(df[TMP_COLUMN].cumsum() - 1, sorted=True) + + # Drop temporary column and ensure the index is not named TMP_COLUMN + df = df.drop(columns=TMP_COLUMN) + df = df.map_partitions(lambda pd_df: set_index_name(pd_df, None)) + return df + + +@DeveloperAPI +class DaskEngine(DataFrameEngine): + def __init__(self, parallelism=None, persist=True, _use_ray=True, **kwargs): + from ray.util.dask import ray_dask_get + + self._parallelism = parallelism + self._persist = persist + if _use_ray: + set_scheduler(ray_dask_get) + + def set_parallelism(self, parallelism): + self._parallelism = parallelism + + def df_like(self, df: dd.DataFrame, proc_cols: dict[str, dd.Series]): + """Outer joins the given DataFrame with the given processed columns. + + NOTE: If any of the processed columns have been repartitioned, the original index is replaced with a + monotonically increasing index, which is used to define the new divisions and align the various partitions. + """ + # Our goal is to preserve the index of the input dataframe but to drop + # all its columns. Because to_frame() creates a column from the index, + # we need to drop it immediately following creation. + dataset = df.index.to_frame(name=TMP_COLUMN).drop(columns=TMP_COLUMN) + + repartitioned_cols = {} + for k, v in proc_cols.items(): + if v.npartitions == dataset.npartitions: + # Outer join cols with equal partitions. + # Dask aligns by index automatically, so no need to force divisions. + dataset[k] = v + else: + # If partitions have changed (e.g. due to conversion from Ray dataset), we handle separately + repartitioned_cols[k] = v + + # Assumes that there is a globally unique index (see preprocessing.build_dataset) + if repartitioned_cols: + if not dataset.known_divisions: + # Sometimes divisions are unknown despite having a usable index– set_index to know divisions + dataset = dataset.assign(**{TMP_COLUMN: dataset.index}) + dataset = dataset.set_index(TMP_COLUMN, drop=True) + dataset = dataset.map_partitions(lambda pd_df: set_index_name(pd_df, dataset.index.name)) + + # Find the divisions of the column with the largest number of partitions + proc_col_with_max_npartitions = max(repartitioned_cols.values(), key=lambda x: x.npartitions) + new_divisions = proc_col_with_max_npartitions.divisions + + # Repartition all columns to have the same divisions + dataset = dataset.repartition(divisions=new_divisions) + repartitioned_cols = {k: v.repartition(divisions=new_divisions) for k, v in repartitioned_cols.items()} + + # Outer join the remaining columns + for k, v in repartitioned_cols.items(): + dataset[k] = v + + return dataset + + def parallelize(self, data): + if self.parallelism: + return data.repartition(npartitions=self.parallelism) + return data + + def persist(self, data): + # No graph optimizations to prevent dropping custom annotations + # https://github.com/dask/dask/issues/7036 + return data.persist(optimize_graph=False) if self._persist else data + + def concat(self, dfs): + return self.df_lib.concat(dfs) + + def compute(self, data): + return data.compute() + + def from_pandas(self, df): + parallelism = self._parallelism or 1 + return dd.from_pandas(df, npartitions=parallelism) + + def map_objects(self, series, map_fn, meta=None): + meta = meta if meta is not None else (series.name, "object") + return series.map(map_fn, meta=meta) + + def map_partitions(self, series, map_fn, meta=None, progress_tracker=None): + meta = meta if meta is not None else (series.name, "object") + if progress_tracker is not None: + actor = progress_tracker.get_actor() + if actor is not None: + + def map_fn_with_progress(partition): + result = map_fn(partition) + actor.increment.remote() + return result + + return series.map_partitions(map_fn_with_progress, meta=meta) + return series.map_partitions(map_fn, meta=meta) + + def map_batches(self, series, map_fn, enable_tensor_extension_casting=True): + """Map a function over batches of a Dask Series. + + Args: + series: Dask Series + map_fn: Function to apply to each batch + enable_tensor_extension_casting: Whether to enable tensor extension casting at the end of the Ray Datasets + map_batches call. This is useful in cases where the output is not supported by the ray Tensor dtype + extension, such as when the output consists of ragged tensors. + """ + import ray.data + + with tensor_extension_casting(enable_tensor_extension_casting): + ds = ray.data.from_dask(series) + ds = ds.map_batches(map_fn, batch_format="pandas") + return ds.to_dask() + + def apply_objects(self, df, apply_fn, meta=None): + meta = meta if meta is not None else ("result", "object") + return df.apply(apply_fn, axis=1, meta=meta) + + def reduce_objects(self, series, reduce_fn): + result = series.reduction(reduce_fn, aggregate=reduce_fn, meta=(series.name, "object")).compute() + # The result type depends on the Dask version and what reduce_fn returns. + # Access the scalar value safely regardless of return type. + if hasattr(result, "iloc"): + return result.iloc[0] + return result + + def split(self, df, probabilities): + # Split the DataFrame proprotionately along partitions. This is an inexact solution designed + # to speed up the split process, as splitting within partitions would be significantly + # more expensive. + # TODO(travis): revisit in the future to make this more precise + + # First ensure that every split receives at least one partition. + # If not, we need to increase the number of partitions to satisfy this constraint. + min_prob = min(probabilities) + min_partitions = int(1 / min_prob) + if df.npartitions < min_partitions: + df = df.repartition(npartitions=min_partitions) + + n = df.npartitions + slices = df.partitions + return split_by_slices(slices, n, probabilities) + + def remove_empty_partitions(self, df): + # Reference: https://stackoverflow.com/questions/47812785/remove-empty-partitions-in-dask + ll = list(df.map_partitions(len).compute()) + if all([ll_i > 0 for ll_i in ll]): + return df + + df_delayed = df.to_delayed() + df_delayed_new = [] + empty_partition = None + for ix, n in enumerate(ll): + if n == 0: + empty_partition = df.get_partition(ix) + else: + df_delayed_new.append(df_delayed[ix]) + if not df_delayed_new: + # All partitions are empty, return a single empty partition + return empty_partition + df = dd.from_delayed(df_delayed_new, meta=empty_partition) + return df + + def to_parquet(self, df, path, index=False): + schema = get_pa_schema(df) + with ProgressBar(): + df.to_parquet( + path, + engine="pyarrow", + write_index=index, + schema=schema, + name_function=get_parquet_filename, + ) + + def write_predictions(self, df: dd.DataFrame, path: str): + ds = self.to_ray_dataset(df) + # We disable tensor extension casting here because we are writing out to Parquet and there is no need + # to cast to the ray Tensor dtype extension before doing so (they will be written out as object dtype as if + # we were writing to parquet using dask). + with tensor_extension_casting(False): + fs, path = get_fs_and_path(path) + ds.write_parquet(path, filesystem=PyFileSystem(FSSpecHandler(fs))) + + def read_predictions(self, path: str) -> dd.DataFrame: + fs, path = get_fs_and_path(path) + ds = read_parquet(path, filesystem=PyFileSystem(FSSpecHandler(fs))) + return self.from_ray_dataset(ds) + + def to_ray_dataset(self, df) -> Dataset: + from ray.data import from_dask + + return from_dask(df) + + def from_ray_dataset(self, dataset) -> dd.DataFrame: + # NOTE: When the dataset is an empty MapBatches(BatchInferModel), Ray's native to_dask() raises an IndexError. + try: + # verify_meta=False: Ray's to_dask() intentionally sets TensorDtype columns to `object` in the Dask + # metadata (since Dask does not natively support TensorDtype), but the actual Arrow blocks returned by + # block_to_df may have TensorDtype columns when tensor extension casting is enabled. With verify_meta=True + # (the default) Dask eagerly computes the first partition to check types, finds TensorDtype != object, and + # raises a ValueError. Passing verify_meta=False skips that check and lets downstream code handle any + # type coercions (e.g. write_predictions already disables tensor casting before writing to Parquet). + return dataset.to_dask(verify_meta=False) + except IndexError as e: + logging.warning( + f"Encountered an empty Dataset, {dataset.show()} with error {e}. Manually returning an empty dask " + "DataFrame." + ) + return dd.DataFrame.from_dict({}, npartitions=1) + + def reset_index(self, df): + return reset_index_across_all_partitions(df) + + @property + def array_lib(self): + return da + + @property + def df_lib(self): + return dd + + @property + def parallelism(self): + return self._parallelism + + @property + def partitioned(self): + return True + + +@contextmanager +def tensor_extension_casting(enforced: bool): + """This context manager is used to enforce or disable tensor extension casting. + + Ray Datasets will automatically cast tensor columns to the ray Tensor dtype extension at the end of + map_batches calls and before writing to Parquet. This context manager can be used to disable this behavior + and keep the tensor columns as object dtype. This is useful for writing to Parquet using dask. + + Args: + enforced (bool): Whether to enforce tensor extension casting. + """ + from ray.data.context import DatasetContext + + ctx = DatasetContext.get_current() + prev_enable_tensor_extension_casting = ctx.enable_tensor_extension_casting + try: + ctx.enable_tensor_extension_casting = enforced + yield + finally: + ctx.enable_tensor_extension_casting = prev_enable_tensor_extension_casting diff --git a/ludwig/data/dataframe/modin.py b/ludwig/data/dataframe/modin.py new file mode 100644 index 0000000..a064cce --- /dev/null +++ b/ludwig/data/dataframe/modin.py @@ -0,0 +1,115 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import os + +import modin.pandas as pd +import numpy as np + +from ludwig.data.dataframe.base import DataFrameEngine +from ludwig.globals import PREDICTIONS_SHAPES_FILE_NAME +from ludwig.utils.data_utils import get_pa_schema, load_json, save_json, split_by_slices +from ludwig.utils.dataframe_utils import flatten_df, unflatten_df + + +class ModinEngine(DataFrameEngine): + def __init__(self, **kwargs): + super().__init__() + + def df_like(self, df, proc_cols): + # df argument unused for pandas, which can instantiate df directly + return pd.DataFrame(proc_cols) + + def parallelize(self, data): + return data + + def persist(self, data): + return data + + def compute(self, data): + return data + + def from_pandas(self, df): + return pd.DataFrame(df) + + def map_objects(self, series, map_fn, meta=None): + return series.map(map_fn) + + def map_batches(self, df, map_fn, enable_tensor_extension_casting=True): + return map_fn(df) + + def map_partitions(self, series, map_fn, meta=None, progress_tracker=None): + result = map_fn(series) + if progress_tracker is not None: + progress_tracker.increment() + return result + + def apply_objects(self, df, apply_fn, meta=None): + return df.apply(apply_fn, axis=1) + + def reduce_objects(self, series, reduce_fn): + return reduce_fn(series) + + def split(self, df, probabilities): + return split_by_slices(df.iloc, len(df), probabilities) + + def remove_empty_partitions(self, df): + return df + + def to_parquet(self, df, path, index=False): + schema = get_pa_schema(df) + df.to_parquet( + path, + engine="pyarrow", + index=index, + schema=schema, + ) + + def write_predictions(self, df: pd.DataFrame, path: str): + df, column_shapes = flatten_df(df, self) + self.to_parquet(df, path) + save_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME), column_shapes) + + def read_predictions(self, path: str) -> pd.DataFrame: + pred_df = pd.read_parquet(path) + column_shapes = load_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME)) + return unflatten_df(pred_df, column_shapes, self) + + def to_ray_dataset(self, df): + from ray.data import from_modin + + return from_modin(df) + + def from_ray_dataset(self, dataset) -> pd.DataFrame: + return dataset.to_modin() + + def reset_index(self, df): + return df.reset_index(drop=True) + + @property + def array_lib(self): + return np + + @property + def df_lib(self): + return pd + + @property + def partitioned(self): + return False + + def set_parallelism(self, parallelism): + pass diff --git a/ludwig/data/dataframe/pandas.py b/ludwig/data/dataframe/pandas.py new file mode 100644 index 0000000..340c1c2 --- /dev/null +++ b/ludwig/data/dataframe/pandas.py @@ -0,0 +1,118 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import numpy as np +import pandas as pd + +from ludwig.data.dataframe.base import DataFrameEngine +from ludwig.globals import PREDICTIONS_SHAPES_FILE_NAME +from ludwig.utils.data_utils import load_json, save_json, split_by_slices +from ludwig.utils.dataframe_utils import flatten_df, unflatten_df + + +class PandasEngine(DataFrameEngine): + def __init__(self, **kwargs): + super().__init__() + + def df_like(self, df, proc_cols): + # df argument unused for pandas, which can instantiate df directly + return pd.DataFrame(proc_cols) + + def parallelize(self, data): + return data + + def persist(self, data): + return data + + def compute(self, data): + return data + + @staticmethod + def concat(dfs) -> pd.DataFrame: + return pd.concat(dfs) + + def from_pandas(self, df): + return df + + def map_objects(self, series, map_fn, meta=None): + return series.map(map_fn) + + def map_batches(self, df, map_fn, enable_tensor_extension_casting=True): + return map_fn(df) + + def map_partitions(self, series, map_fn, meta=None, progress_tracker=None): + result = map_fn(series) + if progress_tracker is not None: + progress_tracker.increment() + return result + + def apply_objects(self, df, apply_fn, meta=None): + return df.apply(apply_fn, axis=1) + + def reduce_objects(self, series, reduce_fn): + return reduce_fn(series) + + def split(self, df, probabilities): + return split_by_slices(df.iloc, len(df), probabilities) + + @staticmethod + def remove_empty_partitions(df: pd.DataFrame) -> pd.DataFrame: + return df + + def to_parquet(self, df, path, index=False): + df.to_parquet(path, engine="pyarrow", index=index) + + def write_predictions(self, df: pd.DataFrame, path: str): + df, column_shapes = flatten_df(df, self) + self.to_parquet(df, path) + save_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME), column_shapes) + + def read_predictions(self, path: str) -> pd.DataFrame: + pred_df = pd.read_parquet(path) + column_shapes = load_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME)) + return unflatten_df(pred_df, column_shapes, self) + + def to_ray_dataset(self, df): + from ray.data import from_pandas + + return from_pandas(df) + + @staticmethod + def from_ray_dataset(dataset) -> pd.DataFrame: + return dataset.to_pandas() + + @staticmethod + def reset_index(df) -> pd.DataFrame: + return df.reset_index(drop=True) + + @property + def array_lib(self): + return np + + @property + def df_lib(self): + return pd + + @property + def partitioned(self): + return False + + def set_parallelism(self, parallelism): + pass + + +PANDAS = PandasEngine() diff --git a/ludwig/data/dataset/__init__.py b/ludwig/data/dataset/__init__.py new file mode 100644 index 0000000..68c773b --- /dev/null +++ b/ludwig/data/dataset/__init__.py @@ -0,0 +1,38 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + + +def get_pandas_dataset_manager(**kwargs): + from ludwig.data.dataset.pandas import PandasDatasetManager + + return PandasDatasetManager(**kwargs) + + +def get_ray_dataset_manager(**kwargs): + from ludwig.data.dataset.ray import RayDatasetManager + + return RayDatasetManager(**kwargs) + + +dataset_registry = { + "hdf5": get_pandas_dataset_manager, + "ray": get_ray_dataset_manager, + None: get_pandas_dataset_manager, +} + + +def create_dataset_manager(backend, cache_format, **kwargs): + return dataset_registry[cache_format](backend=backend, **kwargs) diff --git a/ludwig/data/dataset/base.py b/ludwig/data/dataset/base.py new file mode 100644 index 0000000..fb5b3cf --- /dev/null +++ b/ludwig/data/dataset/base.py @@ -0,0 +1,76 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from __future__ import annotations + +import contextlib +from abc import ABC, abstractmethod +from collections.abc import Iterable + +from ludwig.data.batcher.base import Batcher +from ludwig.distributed import DistributedStrategy +from ludwig.features.base_feature import BaseFeature +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.types import DataFrame + + +class Dataset(ABC): + @abstractmethod + def __len__(self) -> int: + raise NotImplementedError() + + @contextlib.contextmanager + @abstractmethod + def initialize_batcher( + self, + batch_size: int = 128, + should_shuffle: bool = True, + random_seed: int = default_random_seed, + ignore_last: bool = False, + distributed: DistributedStrategy = None, + ) -> Batcher: + raise NotImplementedError() + + @abstractmethod + def to_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame: + raise NotImplementedError() + + @abstractmethod + def to_scalar_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame: + raise NotImplementedError() + + @property + def in_memory_size_bytes(self) -> int: + raise NotImplementedError() + + +class DatasetManager(ABC): + @abstractmethod + def create(self, dataset, config, training_set_metadata) -> Dataset: + raise NotImplementedError() + + @abstractmethod + def save(self, cache_path, dataset, config, training_set_metadata, tag) -> Dataset: + raise NotImplementedError() + + @abstractmethod + def can_cache(self, skip_save_processed_input) -> bool: + raise NotImplementedError() + + @property + @abstractmethod + def data_format(self) -> str: + raise NotImplementedError() diff --git a/ludwig/data/dataset/pandas.py b/ludwig/data/dataset/pandas.py new file mode 100644 index 0000000..49171d2 --- /dev/null +++ b/ludwig/data/dataset/pandas.py @@ -0,0 +1,397 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from __future__ import annotations + +import contextlib +import logging +import os +from collections.abc import Iterable +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from pandas import DataFrame + +from ludwig.constants import TRAINING +from ludwig.data.batcher.base import Batcher +from ludwig.data.batcher.random_access import RandomAccessBatcher +from ludwig.data.dataset.base import Dataset, DatasetManager +from ludwig.data.sampler import DistributedSampler +from ludwig.distributed import DistributedStrategy +from ludwig.features.base_feature import BaseFeature +from ludwig.utils.dataframe_utils import from_numpy_dataset, to_numpy_dataset, to_scalar_df +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.misc_utils import get_proc_features + +if TYPE_CHECKING: + from ludwig.backend.base import Backend + +logger = logging.getLogger(__name__) + +# Key for storing the path to the training Parquet cache in metadata +DATA_TRAIN_PARQUET_FP = "data_train_parquet_fp" + +# Legacy key -- kept for backward-compat loading of old caches +DATA_TRAIN_HDF5_FP = "data_train_hdf5_fp" + + +def _shapes_path(data_fp): + """Return the path to the column-shapes sidecar JSON file for a given Parquet cache file.""" + return os.path.splitext(data_fp)[0] + ".shapes.json" + + +def _save_parquet(data_fp, data): + """Save a preprocessed dataset (dict of numpy arrays) to Parquet. + + Multi-dimensional columns (e.g. images with shape [H, W, C]) are flattened to 1-D + before writing because Parquet cannot natively represent N-D arrays inside cells. + The original shapes are persisted in a sidecar JSON file so that ``_load_parquet`` + can restore them. + """ + from ludwig.utils.data_utils import save_json + + dataset = data if isinstance(data, dict) else to_numpy_dataset(data) + + column_shapes: dict[str, list[int]] = {} + flat_dataset: dict[str, np.ndarray] = {} + for col, arr in dataset.items(): + arr = np.asarray(arr) + if arr.ndim > 2: + # Record the per-sample shape (everything after the batch dimension) + column_shapes[col] = list(arr.shape[1:]) + # Flatten each sample to 1-D so Parquet can store it + flat_dataset[col] = arr.reshape(arr.shape[0], -1) + else: + flat_dataset[col] = arr + + df = from_numpy_dataset(flat_dataset) + df.to_parquet(data_fp, engine="pyarrow", index=False) + + # Persist shapes sidecar (even if empty, so _load_parquet can always read it) + save_json(_shapes_path(data_fp), column_shapes) + + +def _load_parquet(data_fp): + """Load a preprocessed dataset from Parquet, returning a dict of numpy arrays. + + If a sidecar ``*.shapes.json`` file exists alongside the Parquet file the + recorded shapes are used to restore multi-dimensional columns. + """ + from ludwig.utils.data_utils import load_json + + df = pd.read_parquet(data_fp, engine="pyarrow") + dataset = to_numpy_dataset(df) + + # Restore N-D shapes if available + shapes_fp = _shapes_path(data_fp) + if os.path.exists(shapes_fp): + column_shapes = load_json(shapes_fp) + for col, shape in column_shapes.items(): + if col in dataset: + arr = dataset[col] + dataset[col] = arr.reshape(arr.shape[0], *shape) + + return dataset + + +def _load_dataset(dataset): + """Load a dataset from a file path (Parquet or legacy HDF5) or return as-is if already in-memory.""" + if isinstance(dataset, str): + if dataset.endswith(".parquet"): + return _load_parquet(dataset) + elif dataset.endswith(".hdf5") or dataset.endswith(".h5"): + # Legacy HDF5 loading for backward compatibility + from ludwig.utils.data_utils import load_hdf5 + + logger.info(f"Loading legacy HDF5 cache: {dataset}. Consider re-preprocessing to use Parquet.") + return to_numpy_dataset(load_hdf5(dataset)) + else: + raise ValueError(f"Unsupported cache format: {dataset}. Expected .parquet or .hdf5") + return dataset + + +class PandasDataset(Dataset): + def __init__(self, dataset, features, data_cache_fp, training_set_metadata=None): + self.features = features + self.data_cache_fp = data_cache_fp + + dataset = _load_dataset(dataset) + self.dataset = to_numpy_dataset(dataset) + + # Restore N-D shapes for columns that were flattened for Parquet compatibility + # (e.g. images [H,W,C] stored as flat 1-D arrays). + # Lazy columns (audio/image stored as path strings) are skipped here because + # their reshape is set to None in the metadata. + if training_set_metadata is not None: + for feature_name, feature_meta in training_set_metadata.items(): + if not isinstance(feature_meta, dict): + continue + reshape = feature_meta.get("reshape") + if reshape is None: + continue + # Find the proc_column for this feature + for proc_col, feat_cfg in features.items(): + if feat_cfg.get("name") == feature_name or feat_cfg.get("column") == feature_name: + if proc_col in self.dataset: + arr = self.dataset[proc_col] + # Skip object-dtype columns (path strings for lazy features) + if arr.dtype == object: + break + expected_shape = (arr.shape[0], *reshape) + if arr.shape != expected_shape and np.prod(arr.shape[1:]) == np.prod(reshape): + self.dataset[proc_col] = arr.reshape(expected_shape) + break + + # Reconstruct LazyColumn objects for lazy media features (audio / image). + # During preprocessing, lazy features stored file paths as strings. Here + # we wrap those path arrays with the appropriate per-sample decode function + # so that batches are decoded on demand rather than all at once. + self._auto_prefetch_size = 0 + if training_set_metadata is not None: + self._init_lazy_columns(features, training_set_metadata) + + self.size = len(list(self.dataset.values())[0]) + + def _decoded_cache_path(self, proc_col: str, n: int, sample_shape: tuple) -> str: + """Return the path for the decoded numpy memmap file for a given column. + + Placed next to the Parquet cache when ``data_cache_fp`` is set, otherwise + falls back to ``~/.cache/ludwig/lazy_media/``. + """ + flat_shape = "_".join(str(d) for d in sample_shape) + fname = f"{proc_col}_decoded_n{n}_{flat_shape}_f32.npy" + if self.data_cache_fp: + return os.path.join(os.path.dirname(self.data_cache_fp), fname) + from ludwig.data.lazy_utils import get_default_lazy_cache_dir + + return str(get_default_lazy_cache_dir() / fname) + + def _init_lazy_columns(self, features: dict, training_set_metadata: dict) -> None: + """Replace string-path arrays for lazy features with LazyColumn or CachedLazyColumn objects.""" + from ludwig.data.lazy_utils import CachedLazyColumn, LazyColumn + + max_prefetch = 0 + + for proc_col, feat_cfg in features.items(): + if proc_col not in self.dataset: + continue + feature_name = feat_cfg.get("name") or feat_cfg.get("column") or proc_col + feat_meta = training_set_metadata.get(feature_name, {}) + if not isinstance(feat_meta, dict) or not feat_meta.get("lazy"): + continue + + paths = self.dataset[proc_col] + feat_type = feat_cfg.get("type", "") + mode = feat_meta.get("mode", "lazy") + # Per-feature prefetch override (None means auto → 4 for lazy modes) + feat_prefetch = feat_meta.get("prefetch_size") + effective_prefetch = feat_prefetch if feat_prefetch is not None else 4 + max_prefetch = max(max_prefetch, effective_prefetch) + + if feat_type == "audio" and "lazy_audio_params" in feat_meta: + from ludwig.features.audio_feature import AudioFeatureMixin + + p = feat_meta["lazy_audio_params"] + decode_fn = AudioFeatureMixin._make_lazy_decode_fn( + audio_feature_dict=p["audio_feature_dict"], + feature_dim=p["feature_dim"], + max_length=p["max_length"], + padding_value=p["padding_value"], + normalization_type=p["normalization_type"], + ) + # Audio FBANK is CPU-bound: each decode() already uses PyTorch's + # internal thread pool. Over-subscribing CPUs by running many + # workers in parallel causes severe slowdowns (up to 5×). Cap at + # cpu_count // torch_threads so total threads ≈ cpu_count. + import torch as _torch + + _audio_workers = max(1, (os.cpu_count() or 4) // max(1, _torch.get_num_threads())) + + if mode == "lazy_cached": + # decode_fn returns (max_length, feature_dim) — match that order. + sample_shape = (p["max_length"], p["feature_dim"]) + cache_path = self._decoded_cache_path(proc_col, len(paths), sample_shape) + self.dataset[proc_col] = CachedLazyColumn( + paths, decode_fn, cache_path, sample_shape, max_workers=_audio_workers + ) + else: + self.dataset[proc_col] = LazyColumn(paths, decode_fn, max_workers=_audio_workers) + + elif feat_type == "image" and "lazy_image_params" in feat_meta: + import torch + + from ludwig.features.image_feature import ImageFeatureMixin + + p = feat_meta["lazy_image_params"] + channel_class_map = torch.tensor(p["channel_class_map"]) + shape = p["default_image_shape"] + default_image = np.zeros(shape, dtype=np.float32) + decode_fn = ImageFeatureMixin._make_lazy_decode_fn( + img_width=p["img_width"], + img_height=p["img_height"], + should_resize=p["should_resize"], + num_channels=p["num_channels"], + resize_method=p["resize_method"], + user_specified_num_channels=p["user_specified_num_channels"], + standardize_image=p["standardize_image"], + channel_class_map=channel_class_map, + default_image=default_image, + ) + + if mode == "lazy_cached": + sample_shape = tuple(p["default_image_shape"]) + cache_path = self._decoded_cache_path(proc_col, len(paths), sample_shape) + self.dataset[proc_col] = CachedLazyColumn(paths, decode_fn, cache_path, sample_shape) + else: + self.dataset[proc_col] = LazyColumn(paths, decode_fn) + + self._auto_prefetch_size = max_prefetch + + def to_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame: + """Convert the dataset to a Pandas DataFrame. + + Lazy columns (audio / image path arrays) are excluded because they + contain file-path strings, not decoded tensors. These features are + always input features and are never needed for postprocessing. + """ + from ludwig.data.lazy_utils import is_lazy_column + + if features: + subset = {} + for feature in features: + col = self.dataset.get(feature.proc_column) + if col is not None and not is_lazy_column(col): + subset[feature.feature_name] = col + return from_numpy_dataset(subset) + non_lazy = {k: v for k, v in self.dataset.items() if not is_lazy_column(v)} + return from_numpy_dataset(non_lazy) + + def to_scalar_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame: + return to_scalar_df(self.to_df(features)) + + def get(self, proc_column, idx=None): + if idx is None: + idx = range(self.size) + return self.dataset[proc_column][idx] + + def get_dataset(self) -> dict[str, np.ndarray]: + return self.dataset + + def __len__(self): + return self.size + + @property + def processed_data_fp(self) -> str | None: + return self.data_cache_fp + + @property + def in_memory_size_bytes(self) -> int: + # to_df() already excludes lazy columns (path strings, not arrays). + df = self.to_df() + return df.memory_usage(deep=True).sum() if df is not None else 0 + + def _has_lazy_columns(self) -> bool: + """Return True if any column in this dataset is a LazyColumn (path-array needing decode).""" + from ludwig.data.lazy_utils import is_lazy_column + + return any(is_lazy_column(v) for v in self.dataset.values()) + + def is_fully_cached(self) -> bool: + """Return ``True`` when every ``CachedLazyColumn`` in this dataset has finished its first-pass decode. + + Returns ``False`` if there are no ``CachedLazyColumn`` instances (i.e. the dataset + uses plain ``LazyColumn`` or eager mode). + """ + from ludwig.data.lazy_utils import is_cached_lazy_column + + cached_cols = [v for v in self.dataset.values() if is_cached_lazy_column(v)] + if not cached_cols: + return False + return all(col.is_fully_cached() for col in cached_cols) + + @contextlib.contextmanager + def initialize_batcher( + self, + batch_size: int = 128, + should_shuffle: bool = True, + random_seed: int = default_random_seed, + ignore_last: bool = False, + distributed: DistributedStrategy = None, + augmentation_pipeline=None, + prefetch_size: int | None = None, + ) -> Batcher: + """Yield a :class:`RandomAccessBatcher` configured for this dataset. + + Parameters + ---------- + prefetch_size: + Number of batches to pipeline in a background thread while the GPU + processes the current batch. ``None`` (default) uses + ``self._auto_prefetch_size``, which is derived from the + ``prefetch_size`` field in each feature's preprocessing config + (``None`` → 4 for ``lazy``/``lazy_cached`` features, 0 for + ``eager``). Pass ``0`` to disable prefetch entirely. + + For ``lazy_cached`` mode, ``RandomAccessBatcher.set_epoch`` will + automatically reset ``prefetch_size`` to 0 after epoch 1 once + ``dataset.is_fully_cached()`` returns ``True`` — memmap reads are + fast enough that background pipelining adds no measurable benefit. + """ + # When the dataset contains lazy columns (audio / image file paths that + # are decoded per-batch), automatically enable prefetch so the GPU is + # not idle during decode. Callers can override by passing prefetch_size + # explicitly (0 to disable, N to set a specific depth). + if prefetch_size is None: + prefetch_size = self._auto_prefetch_size if self._has_lazy_columns() else 0 + + sampler = DistributedSampler( + len(self), shuffle=should_shuffle, random_seed=random_seed, distributed=distributed + ) + batcher = RandomAccessBatcher( + self, + sampler, + batch_size=batch_size, + ignore_last=ignore_last, + augmentation_pipeline=augmentation_pipeline, + prefetch_size=prefetch_size, + ) + yield batcher + + +class PandasDatasetManager(DatasetManager): + def __init__(self, backend: Backend): + self.backend: Backend = backend + + def create(self, dataset, config, training_set_metadata) -> Dataset: + cache_fp = training_set_metadata.get(DATA_TRAIN_PARQUET_FP) or training_set_metadata.get(DATA_TRAIN_HDF5_FP) + return PandasDataset(dataset, get_proc_features(config), cache_fp, training_set_metadata) + + def save(self, cache_path, dataset, config, training_set_metadata, tag) -> Dataset: + # Ensure path ends with .parquet + if not cache_path.endswith(".parquet"): + cache_path = os.path.splitext(cache_path)[0] + ".parquet" + _save_parquet(cache_path, dataset) + if tag == TRAINING: + training_set_metadata[DATA_TRAIN_PARQUET_FP] = cache_path + return dataset + + def can_cache(self, skip_save_processed_input) -> bool: + return self.backend.is_coordinator() and not skip_save_processed_input + + @property + def data_format(self) -> str: + return "parquet" diff --git a/ludwig/data/dataset/ray.py b/ludwig/data/dataset/ray.py new file mode 100644 index 0000000..19480b9 --- /dev/null +++ b/ludwig/data/dataset/ray.py @@ -0,0 +1,491 @@ +#! /usr/bin/env python +# Copyright (c) 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import contextlib +import logging +import math +import os +import queue +import threading +from concurrent.futures import ThreadPoolExecutor +from functools import lru_cache +from typing import Any + +import numpy as np +import pandas as pd +from pyarrow.fs import FSSpecHandler, PyFileSystem +from ray.data import Dataset as RayNativeDataset +from ray.data import read_parquet +from ray.data.extensions import TensorArray + +from ludwig.backend.base import Backend +from ludwig.constants import BINARY, CATEGORY, NAME, NUMBER, TYPE +from ludwig.data.batcher.base import Batcher +from ludwig.data.dataset.base import Dataset, DatasetManager +from ludwig.utils.data_utils import DATA_TRAIN_HDF5_FP, DATA_TRAIN_PARQUET_FP +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import get_fs_and_path +from ludwig.utils.misc_utils import get_proc_features +from ludwig.utils.types import DataFrame, Series + +logger = logging.getLogger(__name__) + +_SCALAR_TYPES = {BINARY, CATEGORY, NUMBER} + +_LAZY_DECODE_MAX_WORKERS = min(16, (os.cpu_count() or 4) + 4) + + +def cast_as_tensor_dtype(series: Series) -> Series: + return TensorArray(series) + + +def _make_lazy_decode_batch_fn(col: str, decode_fn): + """Return a map_batches-compatible function that decodes a path-string column to numpy arrays. + + Using a factory avoids the late-binding closure problem (col and decode_fn are captured + correctly per call). The returned function must be picklable so Ray can serialize it + for distribution to remote workers. + + decode_fn must be thread-safe and stateless (see AudioFeatureMixin._make_lazy_decode_fn + and ImageFeatureMixin._make_lazy_decode_fn). + """ + + def decode_batch(batch: pd.DataFrame) -> pd.DataFrame: + paths = batch[col].tolist() + n = len(paths) + max_workers = min(_LAZY_DECODE_MAX_WORKERS, n) if n > 0 else 1 + with ThreadPoolExecutor(max_workers=max_workers) as executor: + decoded = list(executor.map(decode_fn, paths)) + batch[col] = decoded + return batch + + decode_batch.__name__ = f"lazy_decode_{col}" + return decode_batch + + +def read_remote_parquet(path: str): + fs, path = get_fs_and_path(path) + return read_parquet(path, filesystem=PyFileSystem(FSSpecHandler(fs))) + + +class RayDataset(Dataset): + """Wrapper around ray.data.Dataset.""" + + def __init__( + self, + df: str | DataFrame, + features: dict[str, dict], + training_set_metadata: dict[str, Any], + backend: Backend, + ): + self.df_engine = backend.df_engine + self.ds = self.df_engine.to_ray_dataset(df) if not isinstance(df, str) else read_remote_parquet(df) + self.features = features + self.training_set_metadata = training_set_metadata + self.data_cache_fp = training_set_metadata.get(DATA_TRAIN_PARQUET_FP) or training_set_metadata.get( + DATA_TRAIN_HDF5_FP + ) + self.data_parquet_fp = training_set_metadata.get(DATA_TRAIN_PARQUET_FP) + + def to_ray_dataset( + self, + shuffle: bool = True, + shuffle_seed: int = default_random_seed, + ) -> RayNativeDataset: + """Returns a ray.data.Dataset, optionally shuffled, with lazy decode transforms attached. + + Lazy media features (audio/image) stored as file paths are decoded per-batch inside + each Ray worker via map_batches transforms. This means workers receive decoded tensors + while only decoding batch_size samples at a time — no full materialization in memory. + + In modern Ray (2.5+), datasets use lazy execution by default, so there's no need for + explicit windowing or pipelining. + """ + ds = self.ds + if shuffle: + ds = ds.random_shuffle(seed=shuffle_seed) + ds = self._with_lazy_decode(ds) + return ds + + def _with_lazy_decode(self, ds: RayNativeDataset) -> RayNativeDataset: + """Attach map_batches decode stages for lazy media features (audio/image). + + For each feature with ``lazy=True`` in training_set_metadata, the file-path column + is wrapped with a map_batches transform that decodes paths to numpy arrays. + The transform runs inside the worker (or task) that calls iter_batches, so decoding + is distributed and memory-bounded to batch_size × media_size per worker. + + This is the Ray equivalent of PandasDataset._init_lazy_columns(), which wraps path + arrays with LazyColumn objects for local (non-distributed) training. + """ + for proc_col, feat_cfg in self.features.items(): + feature_name = feat_cfg.get("name") or feat_cfg.get("column") or proc_col + feat_meta = self.training_set_metadata.get(feature_name, {}) + if not isinstance(feat_meta, dict) or ( + not feat_meta.get("lazy") and feat_meta.get("mode", "eager") == "eager" + ): + continue + + feat_type = feat_cfg.get("type", "") + + if feat_type == "audio": + if "lazy_audio_params" not in feat_meta: + logger.warning( + f"Feature '{feature_name}' has lazy=True but 'lazy_audio_params' is missing from " + "training_set_metadata. Decode transform will not be added — workers will receive " + "raw file paths instead of tensors. Re-run preprocessing to fix this." + ) + continue + from ludwig.features.audio_feature import AudioFeatureMixin + + p = feat_meta["lazy_audio_params"] + decode_fn = AudioFeatureMixin._make_lazy_decode_fn( + audio_feature_dict=p["audio_feature_dict"], + feature_dim=p["feature_dim"], + max_length=p["max_length"], + padding_value=p["padding_value"], + normalization_type=p["normalization_type"], + ) + ds = ds.map_batches(_make_lazy_decode_batch_fn(proc_col, decode_fn), batch_format="pandas") + + elif feat_type == "image": + if "lazy_image_params" not in feat_meta: + logger.warning( + f"Feature '{feature_name}' has lazy=True but 'lazy_image_params' is missing from " + "training_set_metadata. Decode transform will not be added — workers will receive " + "raw file paths instead of tensors. Re-run preprocessing to fix this." + ) + continue + import torch as _torch + + from ludwig.features.image_feature import ImageFeatureMixin + + p = feat_meta["lazy_image_params"] + channel_class_map = _torch.tensor(p["channel_class_map"]) + shape = p["default_image_shape"] + default_image = np.zeros(shape, dtype=np.float32) + decode_fn = ImageFeatureMixin._make_lazy_decode_fn( + img_width=p["img_width"], + img_height=p["img_height"], + should_resize=p["should_resize"], + num_channels=p["num_channels"], + resize_method=p["resize_method"], + user_specified_num_channels=p["user_specified_num_channels"], + standardize_image=p["standardize_image"], + channel_class_map=channel_class_map, + default_image=default_image, + ) + ds = ds.map_batches(_make_lazy_decode_batch_fn(proc_col, decode_fn), batch_format="pandas") + + return ds + + @contextlib.contextmanager + def initialize_batcher(self, batch_size=128, should_shuffle=True, random_seed=0, ignore_last=False, **kwargs): + ds = self.ds + if should_shuffle: + ds = ds.random_shuffle(seed=random_seed) + # Materialize path strings (small) to avoid re-reading Parquet on every epoch. + # Lazy decode transforms are added AFTER materialize so each epoch decodes from + # cached path strings rather than re-running Parquet I/O. + ds = ds.materialize() + ds = self._with_lazy_decode(ds) + yield RayDatasetBatcher( + ds, + self.features, + self.training_set_metadata, + batch_size, + self.size, + ) + + def __len__(self): + return self.ds.count() + + @property + def size(self): + return len(self) + + @property + def in_memory_size_bytes(self): + return self.ds.size_bytes() if self.ds is not None else 0 + + def to_df(self, features=None): + return self.df_engine.from_ray_dataset(self.ds) + + def to_scalar_df(self, features=None): + from ludwig.utils.dataframe_utils import to_scalar_df + + return to_scalar_df(self.to_df(features)) + + +class RayDatasetManager(DatasetManager): + def __init__(self, backend): + self.backend = backend + + def create(self, dataset: str | DataFrame, config: dict[str, Any], training_set_metadata: dict[str, Any]): + return RayDataset(dataset, get_proc_features(config), training_set_metadata, self.backend) + + def save( + self, + cache_path: str, + dataset: DataFrame, + config: dict[str, Any], + training_set_metadata: dict[str, Any], + tag: str, + ): + self.backend.df_engine.to_parquet(dataset, cache_path) + return cache_path + + def can_cache(self, skip_save_processed_input): + return not skip_save_processed_input + + @property + def data_format(self): + return "parquet" + + +class RayDatasetShard(Dataset): + """Wraps a Ray DataIterator (from ray.train.get_dataset_shard) for distributed training.""" + + def __init__( + self, + dataset_shard, + features: dict[str, dict], + training_set_metadata: dict[str, Any], + ): + self.dataset_shard = dataset_shard + self.features = features + self.training_set_metadata = training_set_metadata + + @contextlib.contextmanager + def initialize_batcher(self, batch_size=128, should_shuffle=True, random_seed=0, ignore_last=False, **kwargs): + yield RayDatasetShardBatcher( + self.dataset_shard, + self.features, + self.training_set_metadata, + batch_size, + self.size, + ) + + @lru_cache(1) + def __len__(self): + # TODO(travis): find way to avoid calling this, as it's expensive + # DataIterator doesn't have a direct count method; use iter to count + count = 0 + for batch in self.dataset_shard.iter_batches(batch_size=4096, batch_format="pandas"): + count += len(batch) + return count + + @property + def size(self): + return len(self) + + def to_df(self, features=None): + raise NotImplementedError("RayDatasetShard does not support to_df; use full RayDataset instead.") + + def to_scalar_df(self, features=None): + raise NotImplementedError("RayDatasetShard does not support to_scalar_df; use full RayDataset instead.") + + +class _BaseBatcher(Batcher): + """Shared batching logic for preparing batches from pandas DataFrames.""" + + def __init__( + self, + features: dict[str, dict], + training_set_metadata: dict[str, Any], + batch_size: int, + samples_per_epoch: int, + ): + self.batch_size = batch_size + self.samples_per_epoch = samples_per_epoch + self.training_set_metadata = training_set_metadata + + self.features = features + self.columns = list(features.keys()) + self.reshape_map = { + proc_column: training_set_metadata[feature[NAME]].get("reshape") + for proc_column, feature in features.items() + } + + self.dataset_batch_iter = None + self._epoch = 0 + self._next_batch = None + self._last_batch = False + self._step = 0 + + def next_batch(self): + if self.last_batch(): + raise StopIteration() + + batch = self._next_batch + self._fetch_next_batch() + self._step += 1 + return batch + + def last_batch(self): + return self._last_batch + + def set_epoch(self, epoch, batch_size): + self.batch_size = batch_size + if epoch != self._epoch: + self._fetch_next_epoch() + self._epoch = epoch + + @property + def step(self): + return self._step + + @property + def steps_per_epoch(self): + return math.ceil(self.samples_per_epoch / self.batch_size) + + def _fetch_next_batch(self): + if self.dataset_batch_iter is None: + self._last_batch = True + return + + self._last_batch = False + try: + self._next_batch = next(self.dataset_batch_iter) + except StopIteration: + self._last_batch = True + + def _fetch_next_epoch(self): + raise NotImplementedError + + def _to_tensors_fn(self): + columns = self.columns + features = self.features + + def to_tensors(df: pd.DataFrame) -> pd.DataFrame: + for c in columns: + # do not convert scalar columns: https://github.com/ray-project/ray/issues/20825 + if features[c][TYPE] not in _SCALAR_TYPES: + df[c] = cast_as_tensor_dtype(df[c]) + elif features[c][TYPE] == BINARY: + df[c] = df[c].astype(np.bool_) + return df + + return to_tensors + + def _prepare_batch(self, batch: pd.DataFrame) -> dict[str, np.ndarray]: + res = {} + for c in self.columns: + if self.features[c][TYPE] not in _SCALAR_TYPES: + res[c] = np.stack(batch[c].values) + else: + res[c] = batch[c].to_numpy() + + for c in self.columns: + reshape = self.reshape_map.get(c) + if reshape is not None: + res[c] = res[c].reshape((-1, *reshape)) + return res + + +class RayDatasetBatcher(_BaseBatcher): + """Batcher for a full ray.data.Dataset (used by non-distributed/local Ray training).""" + + def __init__( + self, + dataset: RayNativeDataset, + features: dict[str, dict], + training_set_metadata: dict[str, Any], + batch_size: int, + samples_per_epoch: int, + ): + self.dataset = dataset + super().__init__(features, training_set_metadata, batch_size, samples_per_epoch) + self._fetch_next_epoch() + + def _fetch_next_epoch(self): + """Create an async reader over the dataset for one epoch.""" + self.dataset_batch_iter = self._create_async_reader(self.dataset) + self._step = 0 + self._fetch_next_batch() + + def _create_async_reader(self, dataset: RayNativeDataset): + q = queue.Queue(maxsize=100) + batch_size = self.batch_size + to_tensors = self._to_tensors_fn() + + def producer(): + for batch in dataset.iter_batches(prefetch_batches=4, batch_size=batch_size, batch_format="pandas"): + batch = to_tensors(batch) + res = self._prepare_batch(batch) + q.put(res) + q.put(None) + + def async_read(): + t = threading.Thread(target=producer) + t.start() + while True: + batch = q.get(block=True) + if batch is None: + break + yield batch + t.join() + + return async_read() + + +class RayDatasetShardBatcher(_BaseBatcher): + """Batcher for a Ray DataIterator shard (used in distributed training workers).""" + + def __init__( + self, + data_iterator, + features: dict[str, dict], + training_set_metadata: dict[str, Any], + batch_size: int, + samples_per_epoch: int, + ): + self.data_iterator = data_iterator + super().__init__(features, training_set_metadata, batch_size, samples_per_epoch) + self._fetch_next_epoch() + + def _fetch_next_epoch(self): + """Create an async reader from the DataIterator for one epoch.""" + self.dataset_batch_iter = self._create_async_reader() + self._step = 0 + self._fetch_next_batch() + + def _create_async_reader(self): + q = queue.Queue(maxsize=100) + batch_size = self.batch_size + to_tensors = self._to_tensors_fn() + + def producer(): + for batch in self.data_iterator.iter_batches( + batch_size=batch_size, + batch_format="pandas", + prefetch_batches=4, + ): + batch = to_tensors(batch) + res = self._prepare_batch(batch) + q.put(res) + q.put(None) + + def async_read(): + t = threading.Thread(target=producer) + t.start() + while True: + batch = q.get(block=True) + if batch is None: + break + yield batch + t.join() + + return async_read() diff --git a/ludwig/data/dataset_synthesizer.py b/ludwig/data/dataset_synthesizer.py new file mode 100644 index 0000000..25f26ce --- /dev/null +++ b/ludwig/data/dataset_synthesizer.py @@ -0,0 +1,641 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import os +import random +import string +import sys +import tempfile +import uuid + +import numpy as np +import pandas as pd +import torch +import torchaudio +import yaml + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + AUDIO, + BAG, + BINARY, + CATEGORY, + CATEGORY_DISTRIBUTION, + DATE, + DECODER, + ENCODER, + H3, + IMAGE, + INPUT_FEATURES, + NAME, + NUMBER, + OUTPUT_FEATURES, + PREPROCESSING, + SEQUENCE, + SET, + TEXT, + TIMESERIES, + TYPE, + VECTOR, +) +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.types import ModelConfigDict +from ludwig.utils.data_utils import save_csv +from ludwig.utils.h3_util import components_to_h3 +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.print_utils import print_ludwig + +logger = logging.getLogger(__name__) + + +letters = string.ascii_letters + +DATETIME_FORMATS = { + "%m-%d-%Y": "{m:02d}-{d:02d}-{Y:04d}", + "%m-%d-%Y %H:%M:%S": "{m:02d}-{d:02d}-{Y:04d} {H:02d}:{M:02d}:{S:02d}", + "%m/%d/%Y": "{m:02d}/{d:02d}/{Y:04d}", + "%m/%d/%Y %H:%M:%S": "{m:02d}/{d:02d}/{Y:04d} {H:02d}:{M:02d}:{S:02d}", + "%m-%d-%y": "{m:02d}-{d:02d}-{y:02d}", + "%m-%d-%y %H:%M:%S": "{m:02d}-{d:02d}-{y:02d} {H:02d}:{M:02d}:{S:02d}", + "%m/%d/%y": "{m:02d}/{d:02d}/{y:02d}", + "%m/%d/%y %H:%M:%S": "{m:02d}/{d:02d}/{y:02d} {H:02d}:{M:02d}:{S:02d}", + "%d-%m-%Y": "{d:02d}-{m:02d}-{Y:04d}", + "%d-%m-%Y %H:%M:%S": "{d:02d}-{m:02d}-{Y:04d} {H:02d}:{M:02d}:{S:02d}", + "%d/%m/%Y": "{d:02d}/{m:02d}/{Y:04d}", + "%d/%m/%Y %H:%M:%S": "{d:02d}/{m:02d}/{Y:04d} {H:02d}:{M:02d}:{S:02d}", + "%d-%m-%y": "{d:02d}-{m:02d}-{y:02d}", + "%d-%m-%y %H:%M:%S": "{d:02d}-{m:02d}-{y:02d} {H:02d}:{M:02d}:{S:02d}", + "%d/%m/%y": "{d:02d}/{m:02d}/{y:02d}", + "%d/%m/%y %H:%M:%S": "{d:02d}/{m:02d}/{y:02d} {H:02d}:{M:02d}:{S:02d}", + "%y-%m-%d": "{y:02d}-{m:02d}-{d:02d}", + "%y-%m-%d %H:%M:%S": "{y:02d}-{m:02d}-{d:02d} {H:02d}:{M:02d}:{S:02d}", + "%y/%m/%d": "{y:02d}/{m:02d}/{d:02d}", + "%y/%m/%d %H:%M:%S": "{y:02d}/{m:02d}/{d:02d} {H:02d}:{M:02d}:{S:02d}", + "%Y-%m-%d": "{Y:04d}-{m:02d}-{d:02d}", + "%Y-%m-%d %H:%M:%S": "{Y:04d}-{m:02d}-{d:02d} {H:02d}:{M:02d}:{S:02d}", + "%Y/%m/%d": "{Y:04d}/{m:02d}/{d:02d}", + "%Y/%m/%d %H:%M:%S": "{Y:04d}/{m:02d}/{d:02d} {H:02d}:{M:02d}:{S:02d}", + "%y-%d-%m": "{y:02d}-{d:02d}-{m:02d}", + "%y-%d-%m %H:%M:%S": "{y:02d}-{d:02d}-{m:02d} {H:02d}:{M:02d}:{S:02d}", + "%y/%d/%m": "{y:02d}/{d:02d}/{m:02d}", + "%y/%d/%m %H:%M:%S": "{y:02d}/{d:02d}/{m:02d} {H:02d}:{M:02d}:{S:02d}", + "%Y-%d-%m": "{Y:04d}-{d:02d}-{m:02d}", + "%Y-%d-%m %H:%M:%S": "{Y:04d}-{d:02d}-{m:02d} {H:02d}:{M:02d}:{S:02d}", + "%Y/%d/%m": "{Y:04d}/{d:02d}/{m:02d}", + "%Y/%d/%m %H:%M:%S": "{Y:04d}/{d:02d}/{m:02d} {H:02d}:{M:02d}:{S:02d}", +} + + +def _get_feature_encoder_or_decoder(feature): + """Returns the nested decoder or encoder dictionary for a feature. + + If neither encoder nor decoder is present, creates an empty encoder dict and returns it. + """ + if DECODER in feature: + return feature[DECODER] + elif ENCODER in feature: + return feature[ENCODER] + else: + feature[ENCODER] = {} + return feature[ENCODER] + + +def generate_string(length): + sequence = [] + for _ in range(length): + sequence.append(random.choice(letters)) + return "".join(sequence) + + +def build_vocab(size): + vocab = [] + for _ in range(size): + vocab.append(generate_string(random.randint(2, 10))) + return vocab + + +def return_none(feature): + return None + + +def assign_vocab(feature): + encoder_or_decoder = _get_feature_encoder_or_decoder(feature) + encoder_or_decoder["idx2str"] = build_vocab(encoder_or_decoder.get("vocab_size", 10)) + encoder_or_decoder["vocab_size"] = len(encoder_or_decoder["idx2str"]) + + +def build_feature_parameters(features): + feature_parameters = {} + for feature in features: + feature_builder_function = get_from_registry(feature[TYPE], parameters_builders_registry) + feature_parameters[feature[NAME]] = feature_builder_function(feature) + return feature_parameters + + +parameters_builders_registry = { + "category": assign_vocab, + "text": assign_vocab, + "number": return_none, + "binary": return_none, + "set": assign_vocab, + "bag": assign_vocab, + "sequence": assign_vocab, + "timeseries": return_none, + "image": return_none, + "audio": return_none, + "date": return_none, + "h3": return_none, + VECTOR: return_none, + CATEGORY_DISTRIBUTION: return_none, +} + + +@DeveloperAPI +def build_synthetic_dataset_df(dataset_size: int, config: ModelConfigDict) -> pd.DataFrame: + for feature in config[OUTPUT_FEATURES]: + if DECODER not in feature: + feature[DECODER] = {} + features = config[INPUT_FEATURES] + config[OUTPUT_FEATURES] + df = build_synthetic_dataset(dataset_size, features) + data = [next(df) for _ in range(dataset_size + 1)] + return pd.DataFrame(data[1:], columns=data[0]) + + +@DeveloperAPI +def build_synthetic_dataset(dataset_size: int, features: list[dict], outdir: str | None = None): + """Synthesizes a dataset for testing purposes. + + Args: + dataset_size: size of the dataset. + features: list of features to generate in YAML format. Provide a list containing one dictionary for + each feature, each dictionary must include a name, a type and can include some generation parameters + depending on the type. + outdir: Path to an output directory. Used for saving synthetic image and audio files. + + Example content for features: + + [ + {name: text_1, type: text, vocab_size: 20, max_len: 20}, + {name: text_2, type: text, vocab_size: 20, max_len: 20}, + {name: category_1, type: category, vocab_size: 10}, + {name: category_2, type: category, vocab_size: 15}, + {name: number_1, type: number}, + {name: number_2, type: number}, + {name: binary_1, type: binary}, + {name: binary_2, type: binary}, + {name: set_1, type: set, vocab_size: 20, max_len: 20}, + {name: set_2, type: set, vocab_size: 20, max_len: 20}, + {name: bag_1, type: bag, vocab_size: 20, max_len: 10}, + {name: bag_2, type: bag, vocab_size: 20, max_len: 10}, + {name: sequence_1, type: sequence, vocab_size: 20, max_len: 20}, + {name: sequence_2, type: sequence, vocab_size: 20, max_len: 20}, + {name: timeseries_1, type: timeseries, max_len: 20}, + {name: timeseries_2, type: timeseries, max_len: 20}, + {name: date_1, type: date}, + {name: date_2, type: date}, + {name: h3_1, type: h3}, + {name: h3_2, type: h3}, + {name: vector_1, type: vector}, + {name: vector_2, type: vector}, + ] + """ + if outdir is None: + outdir = tempfile.gettempdir() + build_feature_parameters(features) + header = [] + for feature in features: + header.append(feature[NAME]) + + yield header + for _ in range(dataset_size): + yield generate_datapoint(features=features, outdir=outdir) + + +def generate_datapoint(features: list[dict], outdir: str) -> str | int | bool: + """Returns a synthetic example containing features specified by the features spec. + + `outdir` is only used for generating synthetic image and synthetic audio features. Otherwise, it is unused. + """ + datapoint = [] + for feature in features: + if "cycle" in feature and feature["cycle"] is True and feature[TYPE] in cyclers_registry: + cycler_function = cyclers_registry[feature[TYPE]] + feature_value = cycler_function(feature) + else: + generator_function = get_from_registry(feature[TYPE], generators_registry) + feature_value = generator_function(feature=feature, outdir=outdir) + datapoint.append(feature_value) + return datapoint + + +def generate_category(feature, outdir: str | None = None) -> str: + """Returns a random category. + + `outdir` is unused. + """ + encoder_or_decoder = _get_feature_encoder_or_decoder(feature) + return random.choice(encoder_or_decoder["idx2str"]) + + +def generate_number(feature, outdir: str | None = None) -> int: + """Returns a random number. + + `outdir` is unused. + """ + return random.uniform(feature["min"] if "min" in feature else 0, feature["max"] if "max" in feature else 1) + + +def generate_binary(feature, outdir: str | None = None) -> bool: + """Returns a random boolean. + + `outdir` is unused. + """ + choices = feature.get("bool2str", [False, True]) + p = feature["prob"] if "prob" in feature else 0.5 + return np.random.choice(choices, p=[1 - p, p]) + + +def generate_sequence(feature, outdir: str | None = None) -> str: + """Returns a random sequence. + + `outdir` is unused. + """ + encoder_or_decoder = _get_feature_encoder_or_decoder(feature) + length = encoder_or_decoder.get("max_len", 10) + if "min_len" in encoder_or_decoder: + length = random.randint(encoder_or_decoder["min_len"], length) + sequence = [random.choice(encoder_or_decoder["idx2str"]) for _ in range(length)] + encoder_or_decoder["vocab_size"] = ( + encoder_or_decoder["vocab_size"] + 4 + ) # For special symbols: START, STOP, PAD, UNK. + return " ".join(sequence) + + +def generate_set(feature, outdir: str | None = None) -> str: + """Returns a random set. + + `outdir` is unused. + """ + encoder_or_decoder = _get_feature_encoder_or_decoder(feature) + elems = [] + for _ in range(random.randint(0, encoder_or_decoder.get("max_len", 3))): + elems.append(random.choice(encoder_or_decoder["idx2str"])) + return " ".join(list(set(elems))) + + +def generate_bag(feature, outdir: str | None = None) -> str: + """Returns a random bag. + + `outdir` is unused. + """ + encoder_or_decoder = _get_feature_encoder_or_decoder(feature) + elems = [] + for _ in range(random.randint(0, encoder_or_decoder.get("max_len", 3))): + elems.append(random.choice(encoder_or_decoder["idx2str"])) + return " ".join(elems) + + +def generate_text(feature, outdir: str | None = None) -> str: + """Returns random text. + + `outdir` is unused. + """ + encoder_or_decoder = _get_feature_encoder_or_decoder(feature) + length = encoder_or_decoder.get("max_len", 10) + text = [] + for _ in range(random.randint(length - int(length * 0.2), length)): + text.append(random.choice(encoder_or_decoder["idx2str"])) + return " ".join(text) + + +def generate_timeseries(feature, max_len=10, outdir: str | None = None) -> str: + """Returns a random timeseries. + + `outdir` is unused. + """ + encoder = _get_feature_encoder_or_decoder(feature) + series = [] + max_len = encoder.get("max_len", max_len) + series_len = random.randint(max_len - 2, max_len) # simulates variable length + for _ in range(series_len): + series.append(str(random.uniform(encoder.get("min", 0), encoder.get("max", 1)))) + return " ".join(series) + + +def generate_audio(feature, outdir: str) -> str: + """Generates random audio and saves it to the outdir. + + Returns the path to the directory of saved files. + """ + destination_folder = feature.get("destination_folder", outdir) + if PREPROCESSING in feature: + audio_length = feature[PREPROCESSING].get("audio_file_length_limit_in_s", 2) + else: + audio_length = feature.get("audio_file_length_limit_in_s", 1) + sampling_rate = 16000 + num_samples = int(audio_length * sampling_rate) + audio = np.sin(np.arange(num_samples) / 100 * 2 * np.pi) * 2 * (np.random.random(num_samples) - 0.5) + audio_tensor = torch.tensor(np.array([audio])).type(torch.float32) + audio_filename = uuid.uuid4().hex[:10].upper() + ".wav" + + if not os.path.exists(destination_folder): + os.makedirs(destination_folder) + audio_dest_path = os.path.join(destination_folder, audio_filename) + + try: + torchaudio.save(audio_dest_path, audio_tensor, sample_rate=sampling_rate) + except OSError as e: + raise OSError(f"Unable to save audio to disk: {e}") + + return audio_dest_path + + +def generate_image(feature, outdir: str, save_as_numpy: bool = False) -> str: + """Generates random images and saves it to the outdir. + + Returns the path to the directory of saved files. + """ + save_as_numpy = feature.get("save_as_numpy", save_as_numpy) + + try: + from torchvision.io import write_png + except ImportError: + logger.error( + " torchvision is not installed. " + "In order to install all image feature dependencies run " + "pip install ludwig[image]" + ) + sys.exit(-1) + + # Read num_channels, width, height + destination_folder = feature.get("destination_folder", outdir) + if PREPROCESSING in feature: + height = feature[PREPROCESSING].get("height", 28) + width = feature[PREPROCESSING].get("width", 28) + num_channels = feature[PREPROCESSING].get("num_channels", 1) + else: + encoder = _get_feature_encoder_or_decoder(feature) + height = encoder.get("height", 28) + width = encoder.get("width", 28) + num_channels = encoder.get("num_channels", 1) + + if width <= 0 or height <= 0 or num_channels < 1: + raise ValueError("Invalid arguments for generating images") + + # Create a Random Image + img = torch.randint(0, 255, (num_channels, width, height), dtype=torch.uint8) + + # Generate a unique random filename + image_filename = uuid.uuid4().hex[:10].upper() + ".png" + + # Save the image to disk either in a specified location/new folder + if not os.path.exists(destination_folder): + os.makedirs(destination_folder) + image_dest_path = os.path.join(destination_folder, image_filename) + try: + # save_image(torch.from_numpy(img.astype("uint8")), image_dest_path) + if save_as_numpy: + with open(image_dest_path, "wb") as f: + np.save(f, img.detach().cpu().numpy()) + else: + write_png(img, image_dest_path) + except OSError as e: + raise OSError(f"Unable to save images to disk: {e}") + + return image_dest_path + + +def generate_datetime(feature, outdir: str | None = None) -> str: + """Generates a random date time, picking a format among different types. + + If no format is specified, the first one is used. + """ + if "datetime_format" in feature: + datetime_generation_format = DATETIME_FORMATS[feature["datetime_format"]] + elif "preprocessing" in feature and "datetime_format" in feature["preprocessing"]: + datetime_generation_format = DATETIME_FORMATS[feature["preprocessing"]["datetime_format"]] + else: + datetime_generation_format = DATETIME_FORMATS[next(iter(DATETIME_FORMATS))] + + y = random.randint(1, 99) + Y = random.randint(1, 9999) + m = random.randint(1, 12) + d = random.randint(1, 28) + H = random.randint(1, 12) + M = random.randint(1, 59) + S = random.randint(1, 59) + + return datetime_generation_format.format(y=y, Y=Y, m=m, d=d, H=H, M=M, S=S) + + +def generate_h3(feature, outdir: str | None = None) -> str: + """Returns a random h3. + + `outdir` is unused. + """ + resolution = random.randint(0, 15) # valid values [0, 15] + h3_components = { + "mode": 1, # we can avoid testing other modes + "edge": 0, # only used in other modes + "resolution": resolution, + "base_cell": random.randint(0, 121), # valid values [0, 121] + # valid values [0, 7] + "cells": [random.randint(0, 7) for _ in range(resolution)], + } + + return components_to_h3(h3_components) + + +def generate_vector(feature, outdir: str | None = None) -> str: + """Returns a random vector. + + `outdir` is unused. + """ + # Space delimited string with floating point numbers + if PREPROCESSING in feature: + vector_size = feature[PREPROCESSING].get("vector_size", 10) + else: + vector_size = feature.get("vector_size", 10) + return " ".join([str(100 * random.random()) for _ in range(vector_size)]) + + +def generate_category_distribution(feature, outdir: str | None = None) -> str: + """Returns a random category distribution. + + `outdir` is unused. + """ + # Space delimited string with floating point numbers that sum to 1 + preprocessing = feature.get(PREPROCESSING, {}) + vector_size = len(preprocessing.get("vocab", ["a", "b", "c"])) + v = np.random.rand(vector_size) + v = v / v.sum() + return " ".join([str(x) for x in v]) + + +generators_registry = { + BINARY: generate_binary, + NUMBER: generate_number, + CATEGORY: generate_category, + SET: generate_set, + BAG: generate_bag, + SEQUENCE: generate_sequence, + TEXT: generate_text, + TIMESERIES: generate_timeseries, + IMAGE: generate_image, + AUDIO: generate_audio, + H3: generate_h3, + DATE: generate_datetime, + VECTOR: generate_vector, + CATEGORY_DISTRIBUTION: generate_category_distribution, +} + +category_cycle = 0 + + +def cycle_category(feature): + global category_cycle + idx2str = feature[DECODER]["idx2str"] if DECODER in feature else feature[ENCODER]["idx2str"] + if category_cycle >= len(idx2str): + category_cycle = 0 + category = idx2str[category_cycle] + category_cycle += 1 + return category + + +binary_cycle = False + + +def cycle_binary(feature): + global binary_cycle + if binary_cycle: + binary_cycle = False + return True + else: + binary_cycle = True + return False + + +cyclers_registry = {"category": cycle_category, "binary": cycle_binary} + + +def cli_synthesize_dataset(dataset_size: int, features: list[dict], output_path: str, **kwargs) -> None: + """Synthesizes a dataset for testing purposes. + + Args: + dataset_size: size of the dataset. + features: list of features to generate in YAML format. Provide a list containing one dictionary for + each feature, each dictionary must include a name, a type and can include some generation parameters + depending on the type. + output_path: path where to save the output CSV file. + + Example content for features: + + [ + {name: text_1, type: text, vocab_size: 20, max_len: 20}, + {name: text_2, type: text, vocab_size: 20, max_len: 20}, + {name: category_1, type: category, vocab_size: 10}, + {name: category_2, type: category, vocab_size: 15}, + {name: number_1, type: number}, + {name: number_2, type: number}, + {name: binary_1, type: binary}, + {name: binary_2, type: binary}, + {name: set_1, type: set, vocab_size: 20, max_len: 20}, + {name: set_2, type: set, vocab_size: 20, max_len: 20}, + {name: bag_1, type: bag, vocab_size: 20, max_len: 10}, + {name: bag_2, type: bag, vocab_size: 20, max_len: 10}, + {name: sequence_1, type: sequence, vocab_size: 20, max_len: 20}, + {name: sequence_2, type: sequence, vocab_size: 20, max_len: 20}, + {name: timeseries_1, type: timeseries, max_len: 20}, + {name: timeseries_2, type: timeseries, max_len: 20}, + {name: date_1, type: date}, + {name: date_2, type: date}, + {name: h3_1, type: h3}, + {name: h3_2, type: h3}, + {name: vector_1, type: vector}, + {name: vector_2, type: vector}, + ] + """ + if dataset_size is None or features is None or output_path is None: + raise ValueError("Missing one or more required parameters: '--dataset_size', '--features' or '--output_path'") + dataset = build_synthetic_dataset(dataset_size, features) + save_csv(output_path, dataset) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script generates a synthetic dataset.", + prog="ludwig synthesize_dataset", + usage="%(prog)s [options]", + ) + parser.add_argument("-od", "--output_path", type=str, help="output CSV file path") + parser.add_argument("-d", "--dataset_size", help="size of the dataset", type=int, default=100) + parser.add_argument( + "-f", + "--features", + default="[\ + {name: text_1, type: text, vocab_size: 20, max_len: 20}, \ + {name: text_2, type: text, vocab_size: 20, max_len: 20}, \ + {name: category_1, type: category, vocab_size: 10}, \ + {name: category_2, type: category, vocab_size: 15}, \ + {name: number_1, type: number}, \ + {name: number_2, type: number}, \ + {name: binary_1, type: binary}, \ + {name: binary_2, type: binary}, \ + {name: set_1, type: set, vocab_size: 20, max_len: 20}, \ + {name: set_2, type: set, vocab_size: 20, max_len: 20}, \ + {name: bag_1, type: bag, vocab_size: 20, max_len: 10}, \ + {name: bag_2, type: bag, vocab_size: 20, max_len: 10}, \ + {name: sequence_1, type: sequence, vocab_size: 20, max_len: 20}, \ + {name: sequence_2, type: sequence, vocab_size: 20, max_len: 20}, \ + {name: timeseries_1, type: timeseries, max_len: 20}, \ + {name: timeseries_2, type: timeseries, max_len: 20}, \ + {name: date_1, type: date}, \ + {name: date_2, type: date}, \ + {name: h3_1, type: h3}, \ + {name: h3_2, type: h3}, \ + {name: vector_1, type: vector}, \ + {name: vector_2, type: vector}, \ + ]", + type=yaml.safe_load, + help="list of features to generate in YAML format. " + "Provide a list containing one dictionary for each feature, " + "each dictionary must include a name, a type " + "and can include some generation parameters depending on the type", + ) + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("synthesize_dataset", *sys_argv) + + # No log level parameter this is placeholder if we add at later date + # args.logging_level = get_logging_level_registry[args.logging_level] + # logging.getLogger('ludwig').setLevel( + # args.logging_level + # ) + # global logger + # logger = logging.getLogger('ludwig.data.dataset_synthesizer') + + print_ludwig("Synthesize Dataset", LUDWIG_VERSION) + + cli_synthesize_dataset(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/data/format_registry.py b/ludwig/data/format_registry.py new file mode 100644 index 0000000..35851b5 --- /dev/null +++ b/ludwig/data/format_registry.py @@ -0,0 +1,77 @@ +"""Data format detection and registry for Ludwig. + +Provides automatic format detection from file extensions and a registry of format-specific preprocessor classes. + +Extracted from the monolithic preprocessing.py for better modularity. +""" + +import logging +import os + +logger = logging.getLogger(__name__) + +# Maps file extensions to Ludwig format names +EXTENSION_TO_FORMAT = { + ".csv": "csv", + ".tsv": "tsv", + ".json": "json", + ".jsonl": "jsonl", + ".xlsx": "excel", + ".xls": "excel", + ".parquet": "parquet", + ".feather": "feather", + ".fwf": "fwf", + ".html": "html", + ".orc": "orc", + ".sas7bdat": "sas", + ".sav": "spss", + ".dta": "stata", + # .pickle / .pkl intentionally omitted: pd.read_pickle() deserializes arbitrary + # Python objects via pickle, enabling RCE from attacker-controlled files. + # Users who need pickle must pass data_format="pickle" explicitly. + ".hdf5": "hdf5", + ".h5": "hdf5", +} + + +def detect_format(path: str) -> str | None: + """Detect data format from file extension. + + Args: + path: Path to the data file. + + Returns: + Format string (e.g., "csv", "parquet") or None if unrecognized. + """ + if not isinstance(path, str): + return None + + _, ext = os.path.splitext(path.lower()) + return EXTENSION_TO_FORMAT.get(ext) + + +def detect_format_from_dataset(dataset) -> str: + """Detect format from a dataset argument (path, dict, or DataFrame). + + Args: + dataset: Input dataset (str path, dict, pd.DataFrame, etc.) + + Returns: + Format string. + """ + import pandas as pd + + if isinstance(dataset, pd.DataFrame): + return "df" + elif isinstance(dataset, dict): + return "dict" + elif isinstance(dataset, str): + detected = detect_format(dataset) + if detected: + return detected + # Could be a directory or unknown format + if os.path.isdir(dataset): + return "auto" + return "auto" + else: + return "auto" diff --git a/ludwig/data/lazy_utils.py b/ludwig/data/lazy_utils.py new file mode 100644 index 0000000..fc52aec --- /dev/null +++ b/ludwig/data/lazy_utils.py @@ -0,0 +1,310 @@ +"""Lazy media column utilities for Ludwig's preprocessing pipeline. + +``LazyColumn`` wraps an array of file paths together with a per-sample decode +function. It exposes the same ``__getitem__`` / ``__len__`` interface as a +numpy array, so ``PandasDataset.get()`` works transparently — callers never +need to know whether data was decoded eagerly or lazily. + +Decoding happens inside a ``ThreadPoolExecutor`` at batch-slice time, which +means: + +* Only ``batch_size`` samples are ever in memory simultaneously. +* CPU decode runs in parallel with GPU forward pass (pipeline overlap). +* Throughput matches the existing eager ``read_binary_files`` path because + both use the same thread-pool approach. +""" + +from __future__ import annotations + +import os +import threading +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Callable + +import numpy as np + +# Cap at 16 workers; beyond this point thread-switching overhead dominates +# for typical audio/image decode workloads. +_DEFAULT_MAX_WORKERS = min(16, (os.cpu_count() or 4) + 4) + + +class LazyColumn: + """Array-like wrapper that decodes file paths on demand per batch. + + Decoding runs in a ``ThreadPoolExecutor`` at batch-slice time, keeping peak + memory bounded to ``batch_size`` samples at any one time. The thread pool + overlaps with the GPU forward pass when ``RandomAccessBatcher`` prefetch is + enabled. + + .. warning:: + For audio FBANK features, each ``decode_fn`` call already spawns PyTorch's + internal thread pool. Creating many ``LazyColumn`` workers in parallel will + over-subscribe CPUs (up to 5× slowdown). ``PandasDataset._init_lazy_columns`` + caps ``max_workers`` at ``cpu_count // torch_num_threads`` for this case. + + Parameters + ---------- + paths: + 1-D array (or list) of file paths (strings) or raw bytes objects. + decode_fn: + Callable that takes a single path/bytes and returns a numpy array. + Must be thread-safe (stateless or using only thread-local state). + max_workers: + Number of threads to use for parallel decode. Defaults to + ``min(16, cpu_count + 4)`` to match Python's ThreadPoolExecutor + default policy. + """ + + def __init__( + self, + paths: np.ndarray | list, + decode_fn: Callable[[str], np.ndarray], + max_workers: int = _DEFAULT_MAX_WORKERS, + ) -> None: + self._paths = np.asarray(paths, dtype=object) + self._decode_fn = decode_fn + self._max_workers = max_workers + + # ------------------------------------------------------------------ + # numpy-compatible interface + # ------------------------------------------------------------------ + + def __len__(self) -> int: + return len(self._paths) + + def __getitem__(self, indices) -> np.ndarray: + """Decode the requested samples in parallel and return a numpy array. + + ``indices`` may be an integer, a list of ints, a numpy integer array, + a boolean mask, or a Python ``slice`` — matching numpy semantics. + """ + selected = self._paths[indices] + + # Scalar index (int or 0-d array) → wrap so map always sees an iterable + scalar = isinstance(indices, (int, np.integer)) or (isinstance(selected, np.ndarray) and selected.ndim == 0) + if scalar: + selected = np.array([selected], dtype=object) + + paths_list = selected.tolist() + + with ThreadPoolExecutor(max_workers=min(self._max_workers, len(paths_list))) as executor: + decoded = list(executor.map(self._decode_fn, paths_list)) + + result = np.stack(decoded) + if scalar: + return result[0] + return result + + @property + def dtype(self): + return object # paths are strings; callers that check dtype see 'object' + + @property + def shape(self): + # Only the batch dimension is known; sample shape requires a decode. + return (len(self._paths),) + + def __repr__(self) -> str: + return f"LazyColumn(n={len(self._paths)}, decode_fn={self._decode_fn.__name__!r})" + + +def _select(paths: np.ndarray, indices) -> tuple[list, list, bool]: + """Normalise any index type to ``(paths_list, int_indices_list, is_scalar)``.""" + scalar = isinstance(indices, (int, np.integer)) + if not scalar and isinstance(indices, np.ndarray) and indices.ndim == 0: + scalar = True + + if scalar: + idx = int(indices) + return [paths[idx]], [idx], True + + if isinstance(indices, slice): + int_indices = list(range(*indices.indices(len(paths)))) + elif isinstance(indices, np.ndarray) and indices.dtype == bool: + int_indices = list(np.where(indices)[0]) + elif isinstance(indices, np.ndarray): + int_indices = indices.tolist() + else: + int_indices = list(indices) + + return [paths[i] for i in int_indices], int_indices, False + + +class CachedLazyColumn: + """Like :class:`LazyColumn`, but writes decoded arrays to a numpy memmap for reuse. + + **First pass (epoch 1):** behaves identically to :class:`LazyColumn` — decodes via + ``ThreadPoolExecutor`` — but also writes each decoded sample to a ``np.memmap`` file. + A per-sample boolean array tracks which indices have been written. When every sample + has been written, the memmap is flushed and an empty ``.done`` sentinel file is created + next to the memmap. + + **Subsequent passes (epoch 2+):** ``is_fully_cached()`` returns ``True`` and + ``__getitem__`` reads directly from the memmap (~0.1 ms/batch), bypassing the thread + pool entirely. ``RandomAccessBatcher.set_epoch`` detects this via + ``dataset.is_fully_cached()`` and automatically disables prefetch. + + If the ``.done`` file already exists when the object is constructed (e.g. a resumed + run), the memmap is opened in read-only mode immediately — no decode occurs at all. + + Parameters + ---------- + paths: + 1-D array or list of file paths. + decode_fn: + Callable ``path -> np.ndarray`` (same contract as :class:`LazyColumn`). + cache_path: + Full path for the memmap file (e.g. ``/data/audio_proc_decoded_n1000_8_23_f32.npy``). + sample_shape: + Shape of a single decoded sample, e.g. ``(max_length, feature_dim)``. + dtype: + Element dtype for the memmap. Default ``np.float32``. + max_workers: + Thread-pool size for parallel decode on cache-miss. Default ``_DEFAULT_MAX_WORKERS``. + """ + + def __init__( + self, + paths: np.ndarray | list, + decode_fn: Callable[[str], np.ndarray], + cache_path: str, + sample_shape: tuple, + dtype=np.float32, + max_workers: int = _DEFAULT_MAX_WORKERS, + ) -> None: + self._paths = np.asarray(paths, dtype=object) + self._decode_fn = decode_fn + self._cache_path = cache_path + self._done_path = cache_path + ".done" + self._sample_shape = sample_shape + self._dtype = dtype + self._max_workers = max_workers + self._n = len(self._paths) + self._written = np.zeros(self._n, dtype=bool) + self._lock = threading.Lock() + self._memmap = None + self._fully_cached = os.path.exists(self._done_path) + if self._fully_cached: + self._written[:] = True + self._memmap = np.memmap(cache_path, dtype=dtype, mode="r", shape=(self._n, *sample_shape)) + + # ------------------------------------------------------------------ + # numpy-compatible interface + # ------------------------------------------------------------------ + + def __len__(self) -> int: + return self._n + + def __getitem__(self, indices) -> np.ndarray: + _, int_indices, scalar = _select(self._paths, indices) + + if self._fully_cached: + result = np.array(self._memmap[int_indices]) + return result[0] if scalar else result + + # Decode any samples not yet in the cache. + need_decode = [i for i in int_indices if not self._written[i]] + + if need_decode: + paths_list = [self._paths[i] for i in need_decode] + with ThreadPoolExecutor(max_workers=min(self._max_workers, len(paths_list))) as ex: + decoded = list(ex.map(self._decode_fn, paths_list)) + + with self._lock: + if self._memmap is None: + os.makedirs(os.path.dirname(self._cache_path) or ".", exist_ok=True) + self._memmap = np.memmap( + self._cache_path, dtype=self._dtype, mode="w+", shape=(self._n, *self._sample_shape) + ) + for i, arr in zip(need_decode, decoded): + if not self._written[i]: + self._memmap[i] = arr + self._written[i] = True + + if self._written.all() and not self._fully_cached: + self._fully_cached = True + self._memmap.flush() + Path(self._done_path).touch() + + result = np.array(self._memmap[int_indices]) + return result[0] if scalar else result + + @property + def dtype(self): + return object + + @property + def shape(self): + return (self._n,) + + def is_fully_cached(self) -> bool: + """Return True once every sample has been decoded and written to the memmap.""" + return self._fully_cached + + def __repr__(self) -> str: + return f"CachedLazyColumn(n={self._n}, cached={self._fully_cached})" + + +def is_lazy_column(col) -> bool: + """Return True if *col* is a ``LazyColumn`` or ``CachedLazyColumn`` instance.""" + return isinstance(col, (LazyColumn, CachedLazyColumn)) + + +def is_cached_lazy_column(col) -> bool: + """Return True if *col* is a ``CachedLazyColumn`` instance.""" + return isinstance(col, CachedLazyColumn) + + +def get_default_lazy_cache_dir() -> Path: + """Return the root directory used for lazy media caches. + + Creates ``~/.cache/ludwig/lazy_media/`` on first call if it does not + already exist. All per-feature subdirectories are nested inside this root. + + Returns + ------- + Path + Absolute path to the root cache directory. + """ + cache_root = Path.home() / ".cache" / "ludwig" / "lazy_media" + cache_root.mkdir(parents=True, exist_ok=True) + return cache_root + + +def resolve_lazy_cache_dir(cache_dir_param: str | None, feature_name: str) -> Path: + """Resolve and create the per-feature lazy cache directory. + + If *cache_dir_param* is given, it is used as the parent directory and + *feature_name* is appended as a subdirectory. When *cache_dir_param* is + ``None``, the default root returned by :func:`get_default_lazy_cache_dir` + is used instead. + + Parameters + ---------- + cache_dir_param: + Explicit cache directory string from the preprocessing config, or + ``None`` to use the default location. + feature_name: + Name of the Ludwig feature (e.g. ``"audio"`` or ``"image"``). Used as + the leaf directory name so that multiple features do not share a cache. + + Returns + ------- + Path + Absolute path to the per-feature cache directory. The directory is + guaranteed to exist after this call. + + Examples + -------- + >>> resolve_lazy_cache_dir(None, "my_audio") + PosixPath('/home/user/.cache/ludwig/lazy_media/my_audio') + >>> resolve_lazy_cache_dir("/tmp/my_cache", "my_image") + PosixPath('/tmp/my_cache/my_image') + """ + if cache_dir_param is not None: + feature_cache_dir = Path(cache_dir_param) / feature_name + else: + feature_cache_dir = get_default_lazy_cache_dir() / feature_name + feature_cache_dir.mkdir(parents=True, exist_ok=True) + return feature_cache_dir diff --git a/ludwig/data/multimodal_collator.py b/ludwig/data/multimodal_collator.py new file mode 100644 index 0000000..ec38e27 --- /dev/null +++ b/ludwig/data/multimodal_collator.py @@ -0,0 +1,93 @@ +"""Multimodal data collator for vision-language fine-tuning. + +Bridges Ludwig's tabular-style dataset (one row = one example) with HuggingFace's multimodal +``AutoProcessor`` interface used by Qwen2-VL / LLaVA / InternVL. Each processor accepts +text + images and emits a single ``BatchFeature`` suitable for ``AutoModelForVision2Seq``. + +The collator is intentionally thin — Ludwig already does tokenization and image preprocessing +through feature-level encoders, so at collate time we only need to stack tensors into the +shape the VLM expects. For end-to-end VLM training that keeps the ``AutoProcessor`` as the +single source of truth for tokenization, pass ``use_processor=True`` and the raw columns +(``images`` as a list of PIL Images / paths, ``text`` as strings) — the collator then calls +``processor(text=..., images=..., return_tensors="pt")``. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +import torch + + +class MultimodalCollator: + """Collate image+text batches for a HuggingFace VLM. + + Args: + processor: an ``AutoProcessor`` instance (e.g. ``Qwen2VLProcessor``). The collator + calls the processor with the raw ``text`` and ``images`` columns pulled from the + incoming dict of per-example records. + image_key: column name in each example dict that holds the image (PIL / path / bytes). + text_key: column name that holds the text / prompt. + label_key: column name holding the target text (for fine-tuning). When present, it + is tokenised by the processor's tokenizer and placed under ``labels`` in the + returned batch with proper -100 masking on prompt tokens. + max_length: optional max token length for truncation of text / labels. + """ + + def __init__( + self, + processor: Any, + *, + image_key: str = "image", + text_key: str = "text", + label_key: str = "labels", + max_length: int | None = None, + ) -> None: + self.processor = processor + self.image_key = image_key + self.text_key = text_key + self.label_key = label_key + self.max_length = max_length + + def __call__(self, examples: Sequence[dict[str, Any]]) -> dict[str, Any]: + images = [ex[self.image_key] for ex in examples] + texts = [ex[self.text_key] for ex in examples] + labels = [ex.get(self.label_key) for ex in examples] + + kwargs = {"text": texts, "images": images, "return_tensors": "pt", "padding": True} + if self.max_length is not None: + kwargs["truncation"] = True + kwargs["max_length"] = self.max_length + batch = self.processor(**kwargs) + + # Fine-tuning path: turn the label strings into token ids with -100 masking on prompt tokens. + n_labels = sum(1 for label in labels if label is not None) + if n_labels > 0: + if n_labels != len(labels): + missing = [i for i, label in enumerate(labels) if label is None] + raise ValueError( + f"MultimodalCollator: {len(missing)} of {len(labels)} examples are missing " + f"'{self.label_key}' (indices {missing}). Provide labels for all examples " + "in the batch or none at all." + ) + tokenizer = getattr(self.processor, "tokenizer", None) + if tokenizer is None: + raise ValueError("MultimodalCollator: processor has no .tokenizer; cannot produce labels") + label_ids = tokenizer( + labels, + return_tensors="pt", + padding=True, + truncation=self.max_length is not None, + max_length=self.max_length, + )["input_ids"] + # Replace pad tokens with -100 so the loss skips them. + pad_id = tokenizer.pad_token_id + if pad_id is not None: + label_ids = label_ids.masked_fill(label_ids == pad_id, -100) + # Resolve device from whatever tensor the processor emitted first. + first_tensor = next((v for v in batch.values() if isinstance(v, torch.Tensor)), None) + target_device = first_tensor.device if first_tensor is not None else torch.device("cpu") + batch["labels"] = label_ids.to(target_device) + + return batch diff --git a/ludwig/data/negative_sampling.py b/ludwig/data/negative_sampling.py new file mode 100644 index 0000000..958c84a --- /dev/null +++ b/ludwig/data/negative_sampling.py @@ -0,0 +1,106 @@ +import logging +import time +from typing import Any + +import numpy as np +import pandas as pd +import scipy + +from ludwig.utils.types import DataFrame + + +def _negative_sample_user(interaction_row: np.array, neg_pos_ratio: int, extra_samples: int) -> tuple[list[int], int]: + """Returns a list of negative item indices for given user-item interactions. + + If there are not enough negative items, takes all of them and adds the difference to the extra_samples + otherwise, samples with replacement. + + Params: + interaction_row: user-item interaction row + neg_pos_ratio: number of negative samples per positive sample + extra_samples: number of additional samples to add to the negative sample list + Returns: + Tuple of list of negative item indices and number of extra samples + """ + # Find all items that are not interacted with by the user + neg_items = np.where(interaction_row == 0)[1] + available_samples = len(neg_items) + + # Randomly sample negative items + npos = interaction_row.shape[1] - len(neg_items) + samples_required = npos * neg_pos_ratio + extra_samples + should_sample = samples_required <= available_samples + + neg_items = np.random.choice(neg_items, samples_required, replace=False) if should_sample else neg_items + + return neg_items.tolist(), max(0, samples_required - available_samples) + + +def negative_sample( + df: DataFrame, + user_id_col: str = "customer_id", + item_id_col: str = "article_id", + label_col: str = "label", + neg_pos_ratio: int = 1, + neg_val: Any = 0, + log_pct: int = 0, +): + """Negative sampling for implicit feedback datasets. + + Params: + df: DataFrame containing user-item interactions + user_id_col: column name for user ids + item_id_col: column name for item ids + label_col: column name for interaction labels (e.g. 1 for positive interaction) + n_neg: number of negative samples per positive sample + neg_val: label value for the negative samples + percent_print: print progress every percent_print percent. 0 to disable + Returns: + Input DataFrame with negative samples appended + + Source: https://petamind.com/fast-uniform-negative-sampling-for-rating-matrix/ + """ + # TODO(joppe): support out of memory negative sampling using Dask + if not isinstance(df, pd.DataFrame): + df = df.compute() + + # Initialize sparse COOrdinate matrix from users and items in existing interactions + user_id_cat = df[user_id_col].astype("category").cat + user_id_codes = user_id_cat.codes.values + + item_id_cat = df[item_id_col].astype("category").cat + item_id_codes = item_id_cat.codes.values + + interactions_sparse = scipy.sparse.coo_matrix((df[label_col], (user_id_codes, item_id_codes))) + + # Convert to dense user-item matrix so we can iterate + interactions_dense = interactions_sparse.todense() + + nrows = interactions_dense.shape[0] + niter_log = int(nrows * log_pct / 100) + start_time = time.time() + + user_indices, item_indices = [], [] + extra_samples = 0 + for user_idx, interaction_row in enumerate(interactions_dense): + if log_pct > 0 and user_idx % niter_log == 0: + logging.info( + f"Negative sampling progress: {float(user_idx) * 100 / nrows:0.0f}% in {time.time() - start_time:0.2f}s" + ) + + neg_items_for_user, extra_samples = _negative_sample_user(interaction_row, neg_pos_ratio, extra_samples) + + # Add to negative user-item pairs + item_indices += neg_items_for_user + user_indices += [user_idx] * len(neg_items_for_user) + + negative_samples = pd.DataFrame( + { + # Map back to original user and item ids + user_id_col: user_id_cat.categories[user_indices], + item_id_col: item_id_cat.categories[item_indices], + label_col: [neg_val] * len(item_indices), + } + ) + + return pd.concat([df[[user_id_col, item_id_col, label_col]], negative_samples]) diff --git a/ludwig/data/postprocessing.py b/ludwig/data/postprocessing.py new file mode 100644 index 0000000..81f0578 --- /dev/null +++ b/ludwig/data/postprocessing.py @@ -0,0 +1,139 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os +from typing import Any, Optional + +import numpy as np +import pandas as pd +import torch + +from ludwig.backend import LOCAL_BACKEND +from ludwig.data.utils import convert_to_dict +from ludwig.utils.data_utils import DATAFRAME_FORMATS, DICT_FORMATS +from ludwig.utils.dataframe_utils import to_numpy_dataset +from ludwig.utils.fs_utils import has_remote_protocol, open_file +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.strings_utils import make_safe_filename +from ludwig.utils.types import DataFrame + + +def postprocess( + predictions, + output_features, + training_set_metadata, + output_directory="", + backend=LOCAL_BACKEND, + skip_save_unprocessed_output=False, +) -> DataFrame: + if not backend.is_coordinator(): + # Only save unprocessed output on the coordinator + skip_save_unprocessed_output = True + + saved_keys = set() + if not skip_save_unprocessed_output: + _save_as_numpy(predictions, output_directory, saved_keys, backend) + + def postprocess_batch(df): + for of_name, output_feature in output_features.items(): + df = output_feature.postprocess_predictions( + df, + training_set_metadata[of_name], + ) + return df + + # We disable tensor extension casting here because this step is the final data processing step and + # we do not expect return to Ray Datasets after this point. The dtype of the predictions will be + # whatever they would be if we did all postprocessing in Dask. + predictions = backend.df_engine.map_batches(predictions, postprocess_batch, enable_tensor_extension_casting=False) + + # Save any new columns but do not save the original columns again + if not skip_save_unprocessed_output: + _save_as_numpy(predictions, output_directory, saved_keys, backend) + + return predictions + + +def _save_as_numpy(predictions, output_directory, saved_keys, backend): + predictions = predictions[[c for c in predictions.columns if c not in saved_keys]] + npy_filename = os.path.join(output_directory, "{}.npy") + numpy_predictions = to_numpy_dataset(predictions, backend) + for k, v in numpy_predictions.items(): + k = k.replace("<", "[").replace(">", "]") # Replace and with [UNK], [PAD] + if k not in saved_keys: + if has_remote_protocol(output_directory): + with open_file(npy_filename.format(make_safe_filename(k)), mode="wb") as f: + np.save(f, v) + else: + np.save(npy_filename.format(make_safe_filename(k)), v) + saved_keys.add(k) + + +def convert_dict_to_df(predictions: dict[str, dict[str, list[Any] | torch.Tensor | np.ndarray]]) -> pd.DataFrame: + """Converts a dictionary of predictions into a pandas DataFrame. + + Example format of predictions dictionary: + + { + "binary_C82EB": { + "predictions": torch.tensor([True, True, True, False]), + "probabilities": torch.tensor([[0.4777, 0.5223], [0.4482, 0.5518], [0.4380, 0.5620], [0.5059, 0.4941]]), + }, + "category_1491D": { + "predictions": ["NkNUG", "NkNUG", "NkNUG", "NkNUG"], + "probabilities": torch.tensor( + [ + [0.1058, 0.4366, 0.1939, 0.2637], + [0.0816, 0.4807, 0.1978, 0.2399], + [0.0907, 0.4957, 0.1829, 0.2308], + [0.0728, 0.5015, 0.1900, 0.2357], + ] + ), + }, + "num_7B25F": {"predictions": torch.tensor([2.0436, 2.1158, 2.1222, 2.1964])}, + } + """ + output = {} + for of_name, preds_dict in predictions.items(): + for key, value in preds_dict.items(): + output_key = f"{of_name}_{key}" + if not isinstance(value, list): + value = value.tolist() + output[output_key] = value + return pd.DataFrame.from_dict(output) + + +def convert_predictions( + predictions, + output_features, + return_type="dict", + backend: Optional["Backend"] = None, # noqa: F821 +): + convert_fn = get_from_registry(return_type, conversion_registry) + return convert_fn(predictions, output_features, backend) + + +def convert_to_df( + predictions, + output_features, + backend: Optional["Backend"] = None, # noqa: F821 +): + return predictions + + +conversion_registry = { + **dict.fromkeys(DICT_FORMATS, convert_to_dict), + **dict.fromkeys(DATAFRAME_FORMATS, convert_to_df), +} diff --git a/ludwig/data/preprocessing.py b/ludwig/data/preprocessing.py new file mode 100644 index 0000000..fef2cdc --- /dev/null +++ b/ludwig/data/preprocessing.py @@ -0,0 +1,1821 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import contextlib +import logging +import warnings +from collections.abc import Callable + +import numpy as np +import pandas as pd +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.backend import Backend, LOCAL_BACKEND +from ludwig.config_validation.preprocessing import check_global_max_sequence_length_fits_prompt_template +from ludwig.constants import ( + BFILL, + CHECKSUM, + COLUMN, + DEFAULTS, + DROP_ROW, + ENCODER, + FFILL, + FILL_WITH_CONST, + FILL_WITH_FALSE, + FILL_WITH_MEAN, + FILL_WITH_MODE, + FILL_WITH_TRUE, + FULL, + META, + MIN_DATASET_SPLIT_ROWS, + MODEL_ECD, + NAME, + NUMBER, + PREPROCESSING, + PROC_COLUMN, + SPLIT, + SRC, + TEST, + TEXT, + TRAINING, + TYPE, + VALIDATION, +) +from ludwig.data.cache.manager import DatasetCache +from ludwig.data.cache.types import wrap +from ludwig.data.concatenate_datasets import concatenate_df, concatenate_files, concatenate_splits +from ludwig.data.dataset.base import Dataset +from ludwig.data.prompt import format_input_with_prompt, index_column +from ludwig.data.split import get_splitter, split_dataset +from ludwig.data.utils import get_input_and_output_features, set_fixed_split +from ludwig.datasets import load_dataset_uris +from ludwig.features.feature_registries import get_base_type_registry +from ludwig.models.embedder import create_embed_batch_size_evaluator, create_embed_transform_fn +from ludwig.schema.encoders.utils import get_encoder_cls +from ludwig.schema.model_types.base import ModelConfig +from ludwig.types import FeatureConfigDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict +from ludwig.utils import data_utils, strings_utils +from ludwig.utils.backward_compatibility import upgrade_metadata +from ludwig.utils.data_utils import ( + CACHEABLE_FORMATS, + CSV_FORMATS, + DATA_TEST_PARQUET_FP, + DATA_TRAIN_HDF5_FP, # legacy, kept for backward compat + DATA_TRAIN_PARQUET_FP, + DATA_VALIDATION_PARQUET_FP, + DATAFRAME_FORMATS, + DICT_FORMATS, + EXCEL_FORMATS, + FEATHER_FORMATS, + figure_data_format, + FWF_FORMATS, + get_split_path, + HDF5_FORMATS, + HTML_FORMATS, + JSON_FORMATS, + JSONL_FORMATS, + ORC_FORMATS, + override_in_memory_flag, + PARQUET_FORMATS, + PICKLE_FORMATS, + read_csv, + read_excel, + read_feather, + read_fwf, + read_html, + read_json, + read_jsonl, + read_orc, + read_parquet, + read_pickle, + read_sas, + read_spss, + read_stata, + read_tsv, + sanitize_column_names, + SAS_FORMATS, + SPSS_FORMATS, + STATA_FORMATS, + TSV_FORMATS, +) +from ludwig.utils.dataframe_utils import is_dask_series_or_df +from ludwig.utils.defaults import ( + default_prediction_preprocessing_parameters, + default_random_seed, + default_training_preprocessing_parameters, +) +from ludwig.utils.fs_utils import file_lock, path_exists +from ludwig.utils.misc_utils import get_from_registry, merge_dict +from ludwig.utils.types import DataFrame, Series + +# Opt-in to future pandas behavior: fillna/ffill/bfill will no longer silently downcast dtypes +pd.set_option("future.no_silent_downcasting", True) + +REPARTITIONING_FEATURE_TYPES = {"image", "audio"} + +logger = logging.getLogger(__name__) + + +def _get_config_dict(config: ModelConfig | ModelConfigDict) -> ModelConfigDict: + """Return a plain config dict from either a ModelConfig or an existing dict. + + When a ModelConfig is provided, serializes it once via to_dict(). When a plain dict is provided, returns it as-is — + avoids round-tripping through from_dict(), which would re-run JSON schema validation and reject numpy scalar types. + """ + if isinstance(config, ModelConfig): + return config.to_dict() + return config + + +class DataFormatPreprocessor: + """Dispatches preprocessing for a single data format. + + Subclasses implement `preprocess_for_training` and `preprocess_for_prediction`. + `prepare_processed_data` is only needed for cacheable formats (Parquet, HDF5). + """ + + def preprocess_for_training( + self, + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, + ): + raise NotImplementedError + + def preprocess_for_prediction( + self, config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks + ): + raise NotImplementedError + + def prepare_processed_data( + self, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + ): + raise NotImplementedError + + +class FileBasedPreprocessor(DataFormatPreprocessor): + """Preprocessor for tabular file formats, parameterized by a reader function. + + All tabular file formats (CSV, TSV, JSON, Parquet, etc.) share identical + training and prediction logic; only the reader function differs. Use this + class via the `data_format_preprocessor_registry` rather than directly. + """ + + def __init__(self, read_fn: Callable): + self._read_fn = read_fn + + def preprocess_for_training( + self, + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, + ): + return _preprocess_file_for_training( + config, + features, + dataset, + training_set, + validation_set, + test_set, + read_fn=self._read_fn, + training_set_metadata=training_set_metadata, + skip_save_processed_input=skip_save_processed_input, + preprocessing_params=preprocessing_params, + backend=backend, + random_seed=random_seed, + callbacks=callbacks, + ) + + def preprocess_for_prediction( + self, config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks + ): + dataset_df = self._read_fn(dataset, backend.df_engine.df_lib) + training_set_metadata[SRC] = dataset + dataset, training_set_metadata = build_dataset( + config, + dataset_df, + features, + preprocessing_params, + mode="prediction", + metadata=training_set_metadata, + backend=backend, + callbacks=callbacks, + ) + return dataset, training_set_metadata, None + + +class ParquetPreprocessor(FileBasedPreprocessor): + """Parquet-specific preprocessor with cache-path bookkeeping in prepare_processed_data.""" + + def __init__(self): + super().__init__(read_parquet) + + def prepare_processed_data( + self, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + ): + test_set = test_set if test_set and path_exists(test_set) else None + if test_set and isinstance(test_set, str) and DATA_TEST_PARQUET_FP not in training_set_metadata: + training_set_metadata[DATA_TEST_PARQUET_FP] = test_set + + validation_set = validation_set if validation_set and path_exists(validation_set) else None + if ( + validation_set + and isinstance(validation_set, str) + and DATA_VALIDATION_PARQUET_FP not in training_set_metadata + ): + training_set_metadata[DATA_VALIDATION_PARQUET_FP] = validation_set + + if training_set and isinstance(training_set, str) and DATA_TRAIN_PARQUET_FP not in training_set_metadata: + training_set_metadata[DATA_TRAIN_PARQUET_FP] = training_set + return training_set, test_set, validation_set, training_set_metadata + + +class DictPreprocessor(DataFormatPreprocessor): + def preprocess_for_training( + self, + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, + ): + num_overrides = override_in_memory_flag(features, True) + if num_overrides > 0: + logger.warning("Using in_memory = False is not supported with {} data format.".format("dict")) + + df_engine = backend.df_engine + if dataset is not None: + dataset = df_engine.from_pandas(pd.DataFrame(dataset)) + if training_set is not None: + training_set = df_engine.from_pandas(pd.DataFrame(training_set)) + if validation_set is not None: + validation_set = df_engine.from_pandas(pd.DataFrame(validation_set)) + if test_set is not None: + test_set = df_engine.from_pandas(pd.DataFrame(test_set)) + + return _preprocess_df_for_training( + config, + features, + dataset, + training_set, + validation_set, + test_set, + training_set_metadata=training_set_metadata, + preprocessing_params=preprocessing_params, + backend=backend, + random_seed=random_seed, + ) + + def preprocess_for_prediction( + self, config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks + ): + dataset, training_set_metadata = build_dataset( + config, + pd.DataFrame(dataset), + features, + preprocessing_params, + mode="prediction", + metadata=training_set_metadata, + backend=backend, + callbacks=callbacks, + ) + return dataset, training_set_metadata, None + + +class DataFramePreprocessor(DataFormatPreprocessor): + def preprocess_for_training( + self, + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, + ): + num_overrides = override_in_memory_flag(features, True) + if num_overrides > 0: + logger.warning("Using in_memory = False is not supported with {} data format.".format("dataframe")) + + if isinstance(dataset, pd.DataFrame): + dataset = backend.df_engine.from_pandas(dataset) + + return _preprocess_df_for_training( + config, + features, + dataset, + training_set, + validation_set, + test_set, + training_set_metadata=training_set_metadata, + preprocessing_params=preprocessing_params, + backend=backend, + random_seed=random_seed, + callbacks=callbacks, + ) + + def preprocess_for_prediction( + self, config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks + ): + if isinstance(dataset, pd.DataFrame): + dataset = backend.df_engine.from_pandas(dataset) + + dataset, training_set_metadata = build_dataset( + config, + dataset, + features, + preprocessing_params, + mode="prediction", + metadata=training_set_metadata, + backend=backend, + callbacks=callbacks, + ) + return dataset, training_set_metadata, None + + +class HDF5Preprocessor(DataFormatPreprocessor): + def preprocess_for_training( + self, + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, + ): + return self.prepare_processed_data( + features, + dataset, + training_set, + validation_set, + test_set, + training_set_metadata, + skip_save_processed_input, + preprocessing_params, + backend, + random_seed, + ) + + def preprocess_for_prediction( + self, config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks + ): + hdf5_fp = dataset + dataset = load_hdf5(dataset, preprocessing_params, backend, split_data=False, shuffle_training=False) + return dataset, training_set_metadata, hdf5_fp + + def prepare_processed_data( + self, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + ): + if dataset is None and training_set is None: + raise ValueError("One of `dataset` or `training_set` must be not None") + not_none_set = dataset if dataset is not None else training_set + + if not training_set_metadata: + raise ValueError("When providing HDF5 data, training_set_metadata must not be None.") + + logger.info("Using full hdf5 and json") + + if DATA_TRAIN_HDF5_FP not in training_set_metadata: + logger.warning( + "data_train_hdf5_fp not present in training_set_metadata. " + f"Adding it with the current HDF5 file path {not_none_set}" + ) + training_set_metadata[DATA_TRAIN_HDF5_FP] = not_none_set + + elif training_set_metadata[DATA_TRAIN_HDF5_FP] != not_none_set: + logger.warning( + f"data_train_hdf5_fp in training_set_metadata is {training_set_metadata[DATA_TRAIN_HDF5_FP]}, " + f"different from the current HDF5 file path {not_none_set}. " + "Replacing it" + ) + training_set_metadata[DATA_TRAIN_HDF5_FP] = not_none_set + + if dataset is not None: + training_set, test_set, validation_set = load_hdf5( + dataset, preprocessing_params, backend, shuffle_training=True + ) + + elif training_set is not None: + kwargs = {"preprocessing_params": preprocessing_params, "backend": backend, "split_data": False} + training_set = load_hdf5(training_set, shuffle_training=True, **kwargs) + + if validation_set is not None: + validation_set = load_hdf5(validation_set, shuffle_training=False, **kwargs) + + if test_set is not None: + test_set = load_hdf5(test_set, shuffle_training=False, **kwargs) + + return training_set, test_set, validation_set, training_set_metadata + + +data_format_preprocessor_registry = { + **dict.fromkeys(DICT_FORMATS, DictPreprocessor()), + **dict.fromkeys(DATAFRAME_FORMATS, DataFramePreprocessor()), + **dict.fromkeys(CSV_FORMATS, FileBasedPreprocessor(read_csv)), + **dict.fromkeys(TSV_FORMATS, FileBasedPreprocessor(read_tsv)), + **dict.fromkeys(JSON_FORMATS, FileBasedPreprocessor(read_json)), + **dict.fromkeys(JSONL_FORMATS, FileBasedPreprocessor(read_jsonl)), + **dict.fromkeys(EXCEL_FORMATS, FileBasedPreprocessor(read_excel)), + **dict.fromkeys(PARQUET_FORMATS, ParquetPreprocessor()), + **dict.fromkeys(PICKLE_FORMATS, FileBasedPreprocessor(read_pickle)), + **dict.fromkeys(FWF_FORMATS, FileBasedPreprocessor(read_fwf)), + **dict.fromkeys(FEATHER_FORMATS, FileBasedPreprocessor(read_feather)), + **dict.fromkeys(HTML_FORMATS, FileBasedPreprocessor(read_html)), + **dict.fromkeys(ORC_FORMATS, FileBasedPreprocessor(read_orc)), + **dict.fromkeys(SAS_FORMATS, FileBasedPreprocessor(read_sas)), + **dict.fromkeys(SPSS_FORMATS, FileBasedPreprocessor(read_spss)), + **dict.fromkeys(STATA_FORMATS, FileBasedPreprocessor(read_stata)), + **dict.fromkeys(HDF5_FORMATS, HDF5Preprocessor()), +} + + +def build_dataset( + config, + dataset_df, + features, + global_preprocessing_parameters, + mode, + metadata=None, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + skip_save_processed_input=False, + callbacks=None, +): + """Builds a dataset from a dataframe and a list of features. + + Args: + config: A dictionary containing the Ludwig model configuration + dataset_df: Pandas or Dask dataframe + features: List of features + global_preprocessing_parameters: Global preprocessing parameters + mode: One of ['training', 'prediction'] + metadata: Training set metadata if available + backend: Backend + random_seed: Random seed + skip_save_processed_input: Whether to skip saving the processed input + callbacks: List of callbacks + + Returns: + A tuple of (dataset, metadata) + """ + + df_engine = backend.df_engine + + if df_engine.partitioned: + if any(f["type"] in REPARTITIONING_FEATURE_TYPES for f in features) and dataset_df.npartitions > 1: + # A globally unique index only matters if you know that there will be a repartition downstream for some + # particular feature, i.e. for Image and Audio features on a Ray backend. + # - There is a join operation in `df_like`, and the only way to do the operation is if the partitions across + # all feature columns are aligned. + # - In order to align the partitions, we require a way of matching samples to one another across all + # partitions. Therefore, we must reset_index to create a globally unique index. + # - If the number of partitions is 1, it is *highly likely* the index is globally unique. Auto-assigned + # Dask indices in this case are unique, and we pd.concat train, val, and test sets with ignore_index=True + # If there will NOT be a repartition downstream, then we can skip this step. + # - In this case, the partitions should remain aligned throughout. + # - Further, while the indices might not be globally unique, they should be unique within each partition. + # - These two properties make it possible to do the join op within each partition without a global index. + logger.warning( + f"Dataset has {dataset_df.npartitions} partitions and feature types that cause repartitioning. " + f"Resetting index to ensure globally unique indices." + ) + dataset_df = df_engine.reset_index(dataset_df) + + dataset_df = df_engine.parallelize(dataset_df) + + # Ensure that column names with non-word characters won't cause problems for downstream operations. + # NOTE: Must be kept consistent with config sanitization in schema/model_types/base.py. + dataset_df = sanitize_column_names(dataset_df) + + if mode == "training": + sample_ratio = global_preprocessing_parameters["sample_ratio"] + sample_size = global_preprocessing_parameters["sample_size"] + dataset_df = _get_sampled_dataset_df(dataset_df, df_engine, sample_ratio, sample_size, random_seed) + + # If persisting DataFrames in memory is enabled, we want to do this after + # each batch of parallel ops in order to avoid redundant computation + dataset_df = df_engine.persist(dataset_df) + + if mode == "training": + default_preprocessing_parameters = default_training_preprocessing_parameters + elif mode == "prediction": + default_preprocessing_parameters = default_prediction_preprocessing_parameters + else: + raise ValueError(f"Invalid mode {mode}") + global_preprocessing_parameters = merge_dict(default_preprocessing_parameters, global_preprocessing_parameters) + + split_col = None + if global_preprocessing_parameters["split"]["type"] == "fixed": + if global_preprocessing_parameters["split"]["column"] in dataset_df.columns: + split_col = dataset_df[global_preprocessing_parameters["split"]["column"]] + else: + logger.warning( + f"Specified split column {global_preprocessing_parameters['split']['column']} for fixed " + f"split strategy was not found in dataset." + ) + + # update input features with prompt configs during preprocessing (as opposed to during the model forward pass) + # so that we can compute metadata and build the dataset correctly. + logger.debug("handle text features with prompt parameters") + synthesized_dataset_cols = handle_features_with_prompt_config( + config, dataset_df, features, split_col=split_col, backend=backend + ) + + # Get all the unique preprocessing features to compute + feature_configs = [] + feature_hashes = set() + for feature in features: + if feature[PROC_COLUMN] not in feature_hashes: + feature_configs.append(feature) + feature_hashes.add(feature[PROC_COLUMN]) + + dataset_cols = {} + for feature_config in feature_configs: + col_name = feature_config[COLUMN] + dataset_cols[col_name] = ( + synthesized_dataset_cols[col_name] if col_name in synthesized_dataset_cols else dataset_df[col_name] + ) + + logger.debug("build preprocessing parameters") + feature_name_to_preprocessing_parameters = build_preprocessing_parameters( + dataset_cols, feature_configs, global_preprocessing_parameters, backend, metadata=metadata + ) + + # Happens after preprocessing parameters are built, so we can use precomputed fill values. + logger.debug("handle missing values") + + # In some cases, there can be a (temporary) mismatch between the dtype of the column and the type expected by the + # preprocessing config (e.g., a categorical feature represented as an int-like column). In particular, Dask + # may raise an error even when there are no missing values in the column itself. + # + # Since we immediately cast all columns in accordance with their expected feature types after filling missing + # values, we work around the above issue by temporarily treating all columns as object dtype. + for col_key in dataset_cols: + dataset_cols[col_key] = dataset_cols[col_key].astype(object) + + for feature_config in feature_configs: + if feature_config[NAME] not in feature_name_to_preprocessing_parameters: + # Some output feature types (e.g. anomaly) have no preprocessing config; skip them. + continue + preprocessing_parameters = feature_name_to_preprocessing_parameters[feature_config[NAME]] + handle_missing_values(dataset_cols, feature_config, preprocessing_parameters, backend) + + # Happens after missing values are handled to avoid NaN casting issues. + logger.debug("cast columns") + cast_columns(dataset_cols, feature_configs, backend) + + for callback in callbacks or []: + callback.on_build_metadata_start(dataset_df, mode) + + logger.debug("build metadata") + metadata: TrainingSetMetadataDict = build_metadata( + config, metadata, feature_name_to_preprocessing_parameters, dataset_cols, feature_configs, backend + ) + + check_global_max_sequence_length_fits_prompt_template(metadata, global_preprocessing_parameters) + + for callback in callbacks or []: + callback.on_build_metadata_end(dataset_df, mode) + + for callback in callbacks or []: + callback.on_build_data_start(dataset_df, mode) + + logger.debug("build data") + from ludwig.data.preprocessing_progress import get_total_partitions, PreprocessingProgressTracker + + use_ray = backend.df_engine.partitioned + n_active_features = sum(1 for f in feature_configs if PREPROCESSING in metadata.get(f[NAME], {})) + total_partitions = get_total_partitions(dataset_cols, use_ray) * max(n_active_features, 1) + progress_tracker = PreprocessingProgressTracker(total_partitions, callbacks or [], use_ray=use_ray) + progress_tracker.start() + try: + proc_cols = build_data( + dataset_cols, feature_configs, metadata, backend, skip_save_processed_input, progress_tracker + ) + + for callback in callbacks or []: + callback.on_build_data_end(dataset_df, mode) + + # Get any additional columns needed for splitting downstream, otherwise they will not be + # included in the preprocessed output. + split_params = global_preprocessing_parameters.get(SPLIT, {}) + if "type" not in split_params and SPLIT in dataset_df: + warnings.warn( + 'Detected "split" column in the data, but using default split type ' + '"random". Did you mean to set split type to "fixed"?' + ) + + splitter = get_splitter(**split_params) + for column in splitter.required_columns: + if column not in dataset_df: + warnings.warn( + f"column: '{column}' is required by the dataset splitter with params: {split_params}, but '{column}' " + f"is not present in the `dataset_df` with columns: {dataset_df.columns}. This is acceptable during " + "serving setting where dataset splitting is irrelevant. You may see this warning if, for example, the " + "model was trained with a configuration that used a stratified split on the target column, but for " + "live predictions, a value for the target column is not to be provided." + ) + continue + proc_cols[column] = dataset_df[column] + + # TODO pyarrow: this is needed for caching to work with pyarrow. if removed, the following error is raised: + # "pyarrow.lib.ArrowInvalid: Can only convert 1-dimensional array values". The data is reshaped when loaded + # by the batcher in the RayDataset class (see _prepare_batch). + if not skip_save_processed_input and backend.cache.data_format == "parquet": + for feature in features: + name = feature[NAME] + proc_column = feature[PROC_COLUMN] + reshape = metadata[name].get("reshape") + if reshape is not None: + proc_cols[proc_column] = backend.df_engine.map_objects( + proc_cols[proc_column], lambda x: x.reshape(-1) + ) + + # Implements an outer join of proc_cols + dataset = backend.df_engine.df_like(dataset_df, proc_cols) + + # At this point, there should be no missing values left in the dataframe, unless + # the DROP_ROW preprocessing option was selected, in which case we need to drop those + # rows. + len_dataset_before_drop_rows = len(dataset) + dataset = dataset.dropna() + len_dataset_after_drop_rows = len(dataset) + + if len_dataset_before_drop_rows != len_dataset_after_drop_rows: + logger.warning( + f"Dropped a total of {len_dataset_before_drop_rows - len_dataset_after_drop_rows} rows out of " + f"{len_dataset_before_drop_rows} due to missing values" + ) + + # NaNs introduced by outer join change dtype of dataset cols (upcast to float64), so we need to cast them back. + col_name_to_dtype = {} + for col_name, col in proc_cols.items(): + # if col is a list of list-like objects, we assume the internal dtype of each col[i] remains unchanged. + if isinstance(col, list) and isinstance(col[0], (list, np.ndarray, torch.Tensor)): + continue + dtype = col.dtype + # Skip non-numpy extension dtypes (e.g. TensorDtype from Ray, ArrowDtype from PyArrow) + # as they cannot be used with DataFrame.astype() reliably. + if not isinstance(dtype, np.dtype): + continue + col_name_to_dtype[col_name] = dtype + dataset = dataset.astype(col_name_to_dtype) + + # Persist the completed dataset with no NaNs. + # For Dask/Ray: actual partition computation happens here (and during the + # len() call above). Keeping the tracker alive through this call ensures + # all actor.increment.remote() calls baked into the lazy graph are visible + # before stop() drains the actor queue and fires the final 1.0. + dataset = backend.df_engine.persist(dataset) + finally: + progress_tracker.stop() + + # Remove partitions that are empty after removing NaNs + dataset = backend.df_engine.remove_empty_partitions(dataset) + + # Embed features with fixed encoders + dataset = embed_fixed_features(dataset, feature_configs, metadata, backend) + + return dataset, metadata + + +def embed_fixed_features( + dataset: DataFrame, feature_configs: list[FeatureConfigDict], metadata: TrainingSetMetadataDict, backend: Backend +) -> DataFrame: + """Transforms every input feature with cacheable encoder embeddings into its encoded form and updates + metadata.""" + # Encode features in bulk at the end + features_to_encode = get_features_with_cacheable_fixed_embeddings(feature_configs, metadata) + if not features_to_encode: + return dataset + + logger.info(f"Cache encoder embeddings for features: {[f[NAME] for f in features_to_encode]}") + for feature in features_to_encode: + # Temporarily set to False to ensure proper encoding + metadata[feature[NAME]][PREPROCESSING]["cache_encoder_embeddings"] = False + + batch_size = backend.tune_batch_size(create_embed_batch_size_evaluator(features_to_encode, metadata), len(dataset)) + transform_fn = create_embed_transform_fn(features_to_encode, metadata) + results = backend.batch_transform(dataset, batch_size, transform_fn, name="Caching encoder embeddings") + + for feature in features_to_encode: + # Set metadata so we know to skip encoding the feature + metadata[feature[NAME]][PREPROCESSING]["cache_encoder_embeddings"] = True + + return results + + +def _get_sampled_dataset_df(dataset_df, df_engine, sample_ratio, sample_size, random_seed): + df_len = len(dataset_df) + if sample_ratio < 1.0: + if not df_engine.partitioned and df_len * sample_ratio < 1: + raise ValueError( + f"sample_ratio {sample_ratio} is too small for dataset of length {df_len}. " + f"Please increase sample_ratio or use a larger dataset." + ) + + logger.debug(f"sample {sample_ratio} of data") + dataset_df = dataset_df.sample(frac=sample_ratio, random_state=random_seed) + + if sample_size: + if sample_size < df_len: + # Cannot use 'n' parameter when using dask DataFrames -- only 'frac' is supported + sample_ratio = sample_size / df_len + dataset_df = dataset_df.sample(frac=sample_ratio, random_state=random_seed) + else: + logger.warning("sample_size is larger than dataset size, ignoring sample_size") + + return dataset_df + + +def get_features_with_cacheable_fixed_embeddings( + feature_configs: list[FeatureConfigDict], metadata: TrainingSetMetadataDict +) -> list[FeatureConfigDict]: + """Returns list of features with `cache_encoder_embeddings=True` set in the preprocessing config.""" + features_to_encode = [] + for feature_config in feature_configs: + # deal with encoders that have fixed preprocessing + if ENCODER in feature_config: + encoder_params = feature_config[ENCODER] + if TYPE in encoder_params: + preprocessing = metadata[feature_config[NAME]][PREPROCESSING] + if preprocessing.get("cache_encoder_embeddings"): + # TODO(travis): passing in MODEL_ECD is a hack here that can be removed once we move to using + # the config object everywhere in preprocessing. Then we won't need to do the lookup on the + # encoder schema at all. + encoder_class = get_encoder_cls(MODEL_ECD, feature_config[TYPE], encoder_params[TYPE]) + encoder = encoder_class.from_dict(encoder_params) + if not encoder.can_cache_embeddings(): + raise ValueError( + f"Set `cache_encoder_embeddings=True` for feature {feature_config[NAME]} with " + f"encoder {encoder_params[TYPE]}, but encoder embeddings are not static." + ) + + # Convert to Ray Datasets, map batches to encode, then convert back to Dask + features_to_encode.append(feature_config) + + return features_to_encode + + +def cast_columns(dataset_cols, features, backend) -> None: + """Casts columns based on their feature type.""" + for feature in features: + # todo figure out if additional parameters are needed + # for the cast_column function + try: + dataset_cols[feature[COLUMN]] = get_from_registry(feature[TYPE], get_base_type_registry()).cast_column( + dataset_cols[feature[COLUMN]], backend + ) + except KeyError as e: + raise KeyError( + f"Feature name {e} specified in the config was not found in dataset with columns: " + + f"{list(dataset_cols.keys())}" + ) + + +def merge_preprocessing( + feature_config: FeatureConfigDict, global_preprocessing_parameters: PreprocessingConfigDict +) -> FeatureConfigDict: + if PREPROCESSING not in feature_config: + return global_preprocessing_parameters[feature_config[TYPE]] + + return merge_dict(global_preprocessing_parameters[feature_config[TYPE]], feature_config[PREPROCESSING]) + + +def build_preprocessing_parameters( + dataset_cols: dict[str, Series], + feature_configs: list[FeatureConfigDict], + global_preprocessing_parameters: PreprocessingConfigDict, + backend: Backend, + metadata: TrainingSetMetadataDict | None = None, +) -> PreprocessingConfigDict: + if metadata is None: + metadata = {} + + feature_name_to_preprocessing_parameters = {} + for feature_config in feature_configs: + feature_name = feature_config[NAME] + + # if metadata already exists, we can use it to get preprocessing parameters + if feature_name in metadata: + feature_name_to_preprocessing_parameters[feature_name] = metadata[feature_name][PREPROCESSING] + continue + + # Some output feature types (e.g. anomaly) have no preprocessing config; skip them. + if PREPROCESSING not in feature_config: + continue + + preprocessing_parameters = feature_config[PREPROCESSING] + missing_value_strategy = preprocessing_parameters["missing_value_strategy"] + fill_value = precompute_fill_value( + dataset_cols, feature_config, missing_value_strategy, preprocessing_parameters, backend + ) + if fill_value is not None: + preprocessing_parameters.update({"computed_fill_value": fill_value}) + + # Handle outlier replacement + outlier_strategy = preprocessing_parameters.get("outlier_strategy") + if outlier_strategy is not None: + if outlier_strategy != missing_value_strategy: + outlier_fill_value = precompute_fill_value( + dataset_cols, feature_config, outlier_strategy, preprocessing_parameters, backend + ) + else: + # Use fill value from missing_value_strategy to avoid redundant computation + outlier_fill_value = fill_value + + if outlier_fill_value is not None: + preprocessing_parameters.update({"computed_outlier_fill_value": outlier_fill_value}) + + feature_name_to_preprocessing_parameters[feature_name] = preprocessing_parameters + + return feature_name_to_preprocessing_parameters + + +def is_input_feature(feature_config: FeatureConfigDict) -> bool: + """Utility function to check for the presence of encoder in the feature config to determine if the feature is + an input feature or output feature.""" + return ENCODER in feature_config + + +def build_metadata( + config: ModelConfigDict, + metadata: TrainingSetMetadataDict, + feature_name_to_preprocessing_parameters: dict[str, PreprocessingConfigDict], + dataset_cols: dict[str, Series], + feature_configs: list[FeatureConfigDict], + backend: Backend, +) -> TrainingSetMetadataDict: + for feature_config in feature_configs: + feature_name = feature_config[NAME] + if feature_name in metadata: + continue + + # Some output feature types (e.g. anomaly) have no preprocessing config. + # Add a minimal empty metadata entry so downstream lookups don't KeyError. + if feature_name not in feature_name_to_preprocessing_parameters: + metadata[feature_name] = {} + continue + + preprocessing_parameters = feature_name_to_preprocessing_parameters[feature_name] + + column = dataset_cols[feature_config[COLUMN]] + metadata[feature_name] = get_from_registry(feature_config[TYPE], get_base_type_registry()).get_feature_meta( + config, column, preprocessing_parameters, backend, is_input_feature(feature_config) + ) + + metadata[feature_name][PREPROCESSING] = preprocessing_parameters + + return metadata + + +def build_data( + input_cols: DataFrame, + feature_configs: list[dict], + training_set_metadata: dict, + backend: Backend, + skip_save_processed_input: bool, + progress_tracker=None, +) -> dict[str, DataFrame]: + """Preprocesses the input dataframe columns, handles missing values, and potentially adds metadata to + training_set_metadata. + + Args: + input_cols: Input dataframe to be processed. + feature_configs: List of feature configs. + training_set_metadata: Training set metadata. Additional fields may be added. + backend: Backend for data processing. + skip_save_processed_input: (bool) Whether to skip saving the processed input. + + Returns: + Dictionary of (feature name) -> (processed data). + """ + # Dask/Ray only: bake a progress increment into every map_partitions call so + # that remote workers fire actor.increment.remote() when their partition + # actually executes (during persist(), not during lazy graph construction). + # We do NOT apply this to pandas/modin: most feature types use map_objects + # or fillna/bfill rather than map_partitions, so only a small fraction of + # features would be counted. pandas increments happen at the feature level + # below (one per feature, synchronous). + _orig_map_partitions = backend.df_engine.map_partitions + if progress_tracker is not None and backend.df_engine.partitioned: + backend.df_engine.map_partitions = lambda series, map_fn, meta=None: _orig_map_partitions( + series, map_fn, meta=meta, progress_tracker=progress_tracker + ) + + proc_cols = {} + try: + for feature_config in feature_configs: + # TODO(travis): instead of using raw dictionary, this should be loaded into a proper PreprocessingConfig + # object, so we don't need to hackily check for the presence of added keys. + # Some output feature types (e.g. anomaly) have empty metadata (no preprocessing config). + feature_meta = training_set_metadata.get(feature_config[NAME], {}) + if PREPROCESSING not in feature_meta: + continue + preprocessing_parameters = feature_meta[PREPROCESSING] + + # Need to run this again here as cast_columns may have introduced new missing values + handle_missing_values(input_cols, feature_config, preprocessing_parameters, backend) + + # For features that support it, we perform outlier removal here using metadata computed on the full dataset + handle_outliers( + input_cols, + feature_config, + preprocessing_parameters, + training_set_metadata[feature_config[NAME]], + backend, + ) + + get_from_registry(feature_config[TYPE], get_base_type_registry()).add_feature_data( + feature_config, + input_cols, + proc_cols, + training_set_metadata, + preprocessing_parameters, + backend, + skip_save_processed_input, + ) + + # Pandas/modin: fire progress synchronously after each feature + # completes. One increment per feature gives exactly n_active_features + # ticks with a perfectly accurate denominator. + if progress_tracker is not None and not backend.df_engine.partitioned: + progress_tracker.increment() + finally: + backend.df_engine.map_partitions = _orig_map_partitions + + return proc_cols + + +def balance_data( + dataset_df: DataFrame, + output_features: list[dict], + preprocessing_parameters: dict, + backend: Backend, + random_seed: int, +): + """The purpose of this function is to balance the training dataset using either over-sampling or under- + sampling. + + Args: + dataset_df: Input dataframe to be over-sampled or under-sampled. + output_features: List of feature configs. + preprocessing_parameters: Dictionary of the global preprocessing parameters. + backend: Backend for data processing. + random_seed: Integer to seed the random sampling to ensure determinism. + + Returns: An over-sampled or under-sampled training dataset. + """ + target = output_features[0][PROC_COLUMN] + + if backend.df_engine.partitioned: + majority_class = backend.df_engine.compute(dataset_df[target].value_counts()).idxmax() + minority_class = backend.df_engine.compute(dataset_df[target].value_counts()).idxmin() + else: + majority_class = dataset_df[target].value_counts().idxmax() + minority_class = dataset_df[target].value_counts().idxmin() + majority_df = dataset_df[dataset_df[target] == majority_class] + minority_df = dataset_df[dataset_df[target] == minority_class] + + if preprocessing_parameters["oversample_minority"]: + sample_fraction = (len(majority_df) * preprocessing_parameters["oversample_minority"]) / len(minority_df) + minority_df = minority_df.sample(frac=sample_fraction, replace=True, random_state=random_seed) + elif preprocessing_parameters["undersample_majority"]: + sample_fraction = int(len(minority_df) / preprocessing_parameters["undersample_majority"]) / len(majority_df) + majority_df = majority_df.sample(frac=sample_fraction, replace=False, random_state=random_seed) + + balanced_df = backend.df_engine.concat([minority_df, majority_df]) + + return balanced_df + + +def precompute_fill_value( + dataset_cols, feature, missing_value_strategy: str, preprocessing_parameters: PreprocessingConfigDict, backend +): + """Precomputes the fill value for a feature. + + NOTE: this is called before NaNs are removed from the dataset. Modifications here must handle NaNs gracefully. + NOTE: this is called before columns are cast. Modifications here must handle dtype conversion gracefully. + """ + if missing_value_strategy == FILL_WITH_CONST: + return preprocessing_parameters["fill_value"] + elif missing_value_strategy == FILL_WITH_MODE: + # Requires separate handling if Dask since Dask has lazy evaluation + # Otherwise, dask returns a Dask index structure instead of a value to use as a fill value + return ( + dataset_cols[feature[COLUMN]].value_counts().index.compute()[0] + if is_dask_series_or_df(dataset_cols[feature[COLUMN]], backend) + else dataset_cols[feature[COLUMN]].value_counts().index[0] + ) + elif missing_value_strategy == FILL_WITH_MEAN: + if feature[TYPE] != NUMBER: + raise ValueError( + f"Filling missing values with mean is supported only for number types, not for type {feature[TYPE]}.", + ) + return backend.df_engine.compute(dataset_cols[feature[COLUMN]].astype(float).mean()) + elif missing_value_strategy in {FILL_WITH_FALSE, FILL_WITH_TRUE}: + distinct_values = backend.df_engine.compute( + dataset_cols[feature[COLUMN]].drop_duplicates().dropna() + ).values.tolist() + if len(distinct_values) > 2: + raise ValueError( + f"Missing value strategy `{missing_value_strategy}` " + f"for column {feature[COLUMN]} expects 2 distinct values, " + f"found: {len(distinct_values)} (ex: {distinct_values[:10]})" + ) + + fill_to_bool_value = {FILL_WITH_FALSE: False, FILL_WITH_TRUE: True} + bool_needed = fill_to_bool_value[missing_value_strategy] + + # Determine the False label. + # Distinct values are sorted in reverse to mirror the selection of the default fallback_true_label (in + # binary_feature.get_feature_meta) for binary columns with unconventional boolean values, "human"/"bot". + for v in sorted(distinct_values, reverse=True): + fallback_true_label = ( + preprocessing_parameters["fallback_true_label"] + # By default, preprocessing_parameters.fallback_true_label is None. + if preprocessing_parameters["fallback_true_label"] + else "true" + ) + if strings_utils.str2bool(v, fallback_true_label) is bool_needed: + return v + raise ValueError( + f"Unable to determine {bool_needed} value for column {feature[COLUMN]} " + f"with distinct values: {distinct_values}." + ) + # Otherwise, we cannot precompute the fill value for this dataset + return None + + +@DeveloperAPI +def handle_missing_values(dataset_cols, feature, preprocessing_parameters: PreprocessingConfigDict, backend): + missing_value_strategy = preprocessing_parameters["missing_value_strategy"] + computed_fill_value = preprocessing_parameters.get("computed_fill_value") + _handle_missing_values(dataset_cols, feature, missing_value_strategy, computed_fill_value, backend) + + +@DeveloperAPI +def handle_outliers(dataset_cols, feature, preprocessing_parameters: PreprocessingConfigDict, metadata, backend): + outlier_strategy = preprocessing_parameters.get("outlier_strategy") + if outlier_strategy is None: + return + + outlier_threshold = preprocessing_parameters["outlier_threshold"] + computed_fill_value = preprocessing_parameters.get("computed_outlier_fill_value") + + # Identify all outliers and set them to NA so they can be removed + series = dataset_cols[feature[COLUMN]] + dataset_cols[feature[COLUMN]] = series.mask( + series.sub(metadata["mean"]).div(metadata["std"]).abs().gt(outlier_threshold) + ) + + _handle_missing_values(dataset_cols, feature, outlier_strategy, computed_fill_value, backend) + + +def _handle_missing_values( + dataset_cols, feature, missing_value_strategy: str, computed_fill_value: float | None, backend +): + if ( + missing_value_strategy in {FILL_WITH_CONST, FILL_WITH_MODE, FILL_WITH_MEAN, FILL_WITH_FALSE, FILL_WITH_TRUE} + and computed_fill_value is not None + ): + dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].fillna( + computed_fill_value, + ) + elif missing_value_strategy in {BFILL, FFILL}: + if missing_value_strategy == BFILL: + dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].bfill() + else: + dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].ffill() + + # If the first few rows or last few rows of a dataset is a NaN, it will still be a NaN after ffill or bfill are + # applied. This causes downstream errors with Dask (https://github.com/ludwig-ai/ludwig/issues/2452) + # To get around this issue, apply the primary missing value strategy (say bfill) first, and then follow it + # up with the other missing value strategy (ffill) to ensure all NaNs are filled + if backend.df_engine.compute(dataset_cols[feature[COLUMN]].isna().sum()) > 0: + if missing_value_strategy == FFILL: + dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].bfill() + else: + dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].ffill() + elif missing_value_strategy == DROP_ROW: + # Here we only drop from this series, but after preprocessing we'll do a second + # round of dropping NA values from the entire output dataframe, which will + # result in the removal of the rows. + len_before_dropped_rows = len(dataset_cols[feature[COLUMN]]) + dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].dropna() + len_after_dropped_rows = len(dataset_cols[feature[COLUMN]]) + + if len_before_dropped_rows != len_after_dropped_rows: + logger.warning( + f"DROP_ROW missing value strategy applied. Dropped {len_before_dropped_rows - len_after_dropped_rows} " + f"samples out of {len_before_dropped_rows} from column {feature[COLUMN]}. The rows containing these " + f"samples will ultimately be dropped from the dataset." + ) + else: + raise ValueError(f"Invalid missing value strategy {missing_value_strategy}") + + +def handle_features_with_prompt_config( + config: ModelConfigDict, + dataset_df: DataFrame, + features: list[FeatureConfigDict], + backend: Backend, + split_col: Series | None = None, +) -> dict[str, Series]: + """Updates (in-place) dataset columns with prompt configurations containing a non-None task parameter. + + Dataset columns that are updated here are enriched to have prompts as specified by the prompt configuration. + + Args: + config: Model configuration. + dataset_df (DataFrame): Input dataset. + features (List[FeatureConfigDict]): List of feature configurations. + df_engine (DataFrameEngine): Dataframe engine. + split_col (Optional[Series], optional): Split column. Defaults to None. + + Returns: + Dict[str, Series]: Modified dataset columns. + """ + dataset_cols = {} + input_features, output_features = get_input_and_output_features(features) + for input_feature_config in input_features: + prompt_config = _get_prompt_config(config, input_feature_config) + if prompt_config is None: + continue + + input_col_name = input_feature_config[COLUMN] + if prompt_config["retrieval"]["type"] is not None: + # Ensure that the output features are in the dataset columns saved as part of the index + # so that they can be retrieved later at lookup time. + output_feature_col_names = [output_feature_config[COLUMN] for output_feature_config in output_features] + input_and_output_col_names = set([input_col_name] + output_feature_col_names) + input_and_output_cols = { + feature[NAME]: dataset_df[feature[COLUMN]] + for feature in features + if feature[NAME] in input_and_output_col_names + } + retrieval_model, index_name = index_column( + prompt_config["retrieval"], + col_name=input_col_name, + dataset_cols=input_and_output_cols, + backend=backend, + split_col=split_col, + ) + k = prompt_config["retrieval"]["k"] + + # NOTE: after indexing the input column, we update the index_name in the prompt config IN PLACE. + # This ensures that the preprocessing parameters for this feature have an up-to-date index_name + # when the training set metadata is saved. + prompt_config["retrieval"]["index_name"] = index_name + else: + retrieval_model = None + k = -1 + + dataset_cols[input_col_name] = format_input_with_prompt( + input_col_name, + dataset_df, + backend, + prompt_config["task"], + retrieval_model=retrieval_model, + k=k, + template=prompt_config["template"], + ) + + return dataset_cols + + +def _get_prompt_config(config: ModelConfigDict, input_feature_config: dict) -> dict: + if input_feature_config[TYPE] != TEXT: + # Prompt config is only applied to text features + return None + + preprocessing = input_feature_config["preprocessing"] + if _has_prompt_section(preprocessing): + return preprocessing["prompt"] + + if _has_prompt_section(config): + return config["prompt"] + + return None + + +def _has_prompt_section(config: dict) -> bool: + return "prompt" in config and (config["prompt"]["template"] is not None or config["prompt"]["task"] is not None) + + +def load_hdf5(hdf5_file_path, preprocessing_params, backend, split_data=True, shuffle_training=False): + # TODO dask: this needs to work with DataFrames + logger.info(f"Loading data from: {hdf5_file_path}") + + def shuffle(df): + return df.sample(frac=1).reset_index(drop=True) + + dataset = data_utils.load_hdf5(hdf5_file_path) + if not split_data: + if shuffle_training: + dataset = shuffle(dataset) + return dataset + + training_set, validation_set, test_set = split_dataset(dataset, preprocessing_params, backend) + + if shuffle_training: + training_set = shuffle(training_set) + + return training_set, test_set, validation_set + + +def load_metadata(metadata_file_path: str) -> TrainingSetMetadataDict: + logger.info(f"Loading metadata from: {metadata_file_path}") + training_set_metadata = data_utils.load_json(metadata_file_path) + # TODO(travis): decouple config from training_set_metadata so we don't need to + # upgrade it over time. + training_set_metadata = upgrade_metadata(training_set_metadata) + return training_set_metadata + + +def drop_extra_cols(features, dfs): + retain_cols = list({feature[PROC_COLUMN]: True for feature in features}.keys()) + return tuple(df[retain_cols] if df is not None else df for df in dfs) + + +def preprocess_for_training( + config: ModelConfig | ModelConfigDict, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + data_format=None, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, +) -> tuple[Dataset, Dataset, Dataset, TrainingSetMetadataDict]: + """Returns training, val and test datasets with training set metadata.""" + config_dict = _get_config_dict(config) + + if dataset is None and training_set is None: + raise ValueError( + "No training data provided. Pass either 'dataset' (combined dataset with split column) " + "or 'training_set' (pre-split training data) to train()." + ) + + # preload ludwig and HF datasets + dataset, training_set, validation_set, test_set = load_dataset_uris( + dataset, training_set, validation_set, test_set, backend + ) + + # determine data format if not provided or auto + if not data_format or data_format == "auto": + data_format = figure_data_format(dataset, training_set, validation_set, test_set) + + # Wrap dataset into a form we can use to manage within the cache + dataset = wrap(dataset) + training_set = wrap(training_set) + validation_set = wrap(validation_set) + test_set = wrap(test_set) + + try: + lock_path = backend.cache.get_cache_directory(dataset) + except (TypeError, ValueError): + lock_path = None + with file_lock(lock_path, lock_file=".lock_preprocessing"): + # if training_set_metadata is a string, assume it's a path to load the json + training_set_metadata = training_set_metadata or {} + if training_set_metadata and isinstance(training_set_metadata, str): + training_set_metadata = load_metadata(training_set_metadata) + + # setup — feature lists extracted from the config dict + features = config_dict["input_features"] + config_dict["output_features"] + + # in case data_format is one of the cacheable formats, + # check if there's a cached hdf5 file with the same name, + # and in case move on with the hdf5 branch. + cached = False + cache = backend.cache.get_dataset_cache(config_dict, dataset, training_set, test_set, validation_set) + + # Unwrap dataset into the form used for preprocessing + dataset = dataset.unwrap() if dataset is not None else None + training_set = training_set.unwrap() if training_set is not None else None + validation_set = validation_set.unwrap() if validation_set is not None else None + test_set = test_set.unwrap() if test_set is not None else None + + if data_format in CACHEABLE_FORMATS: + with backend.storage.cache.use_credentials(): + # cache.get() returns valid indicating if the checksum for the current config + # is equal to that from the cached training set metadata, as well as the paths to the + # cached training set metadata, training set, validation_set, test set + cache_results = cache.get() + if cache_results is not None: + valid, *cache_values = cache_results + if valid: + logger.info(_get_cache_hit_message(cache)) + training_set_metadata, training_set, test_set, validation_set = cache_values + config_dict["data_cache_fp"] = training_set + data_format = backend.cache.data_format + cached = True + dataset = None + else: + logger.info( + "Found cached dataset and meta.json with the same filename " + "of the dataset, but checksums don't match, " + "if saving of processed input is not skipped " + "they will be overridden" + ) + cache.delete() + else: + logger.info( + f"No cached dataset found at {cache.get_cached_obj_path('training')}. " + "Preprocessing the dataset." + ) + + training_set_metadata[CHECKSUM] = cache.checksum + data_format_processor = get_from_registry(data_format, data_format_preprocessor_registry) + + if cached or data_format == "hdf5": + with backend.storage.cache.use_credentials(): + # Always interpret hdf5 files as preprocessed, even if missing from the cache + processed = data_format_processor.prepare_processed_data( + features, + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + skip_save_processed_input=skip_save_processed_input, + preprocessing_params=preprocessing_params, + backend=backend, + random_seed=random_seed, + ) + training_set, test_set, validation_set, training_set_metadata = processed + else: + processed = data_format_processor.preprocess_for_training( + config_dict, + features, + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + skip_save_processed_input=skip_save_processed_input, + preprocessing_params=preprocessing_params, + backend=backend, + random_seed=random_seed, + callbacks=callbacks, + ) + training_set, test_set, validation_set, training_set_metadata = processed + processed = (training_set, test_set, validation_set, training_set_metadata) + + # cache the dataset + if backend.cache.can_cache(skip_save_processed_input): + with backend.storage.cache.use_credentials(): + logger.debug("cache processed data") + processed = cache.put(*processed) + # set cached=True to ensure credentials are used correctly below + cached = True + training_set, test_set, validation_set, training_set_metadata = processed + + with backend.storage.cache.use_credentials() if cached else contextlib.nullcontext(): + logger.debug("create training dataset") + training_dataset = backend.dataset_manager.create(training_set, config_dict, training_set_metadata) + training_set_size = len(training_dataset) + if training_set_size == 0: + raise ValueError("Training data is empty following preprocessing.") + elif training_set_size < MIN_DATASET_SPLIT_ROWS: + raise ValueError( + f"Training dataset has only {training_set_size} rows following preprocessing, need" + f" at least {MIN_DATASET_SPLIT_ROWS} to compute metrics." + ) + + validation_dataset = None + if validation_set is not None: + logger.debug("create validation dataset") + validation_dataset = backend.dataset_manager.create(validation_set, config_dict, training_set_metadata) + validation_set_size = len(validation_dataset) + if validation_set_size == 0: + logger.warning( + "Validation set empty. If this is unintentional, please check the preprocessing configuration." + ) + validation_dataset = None + elif validation_set_size < MIN_DATASET_SPLIT_ROWS: + logger.warning( + f"Validation set too small to compute metrics. Need at least {MIN_DATASET_SPLIT_ROWS} rows, got" + f" {validation_set_size} after preprocessing." + ) + + test_dataset = None + if test_set is not None: + logger.debug("create test dataset") + test_dataset = backend.dataset_manager.create(test_set, config_dict, training_set_metadata) + test_set_size = len(test_dataset) + if test_set_size == 0: + logger.warning( + "Test set empty. If this is unintentional, please check the preprocessing configuration." + ) + test_dataset = None + elif test_set_size < MIN_DATASET_SPLIT_ROWS: + logger.warning( + f"Test set too small to compute metrics. Need at least {MIN_DATASET_SPLIT_ROWS} rows, got" + f" {test_set_size} after preprocessing." + ) + + return (training_dataset, validation_dataset, test_dataset, training_set_metadata) + + +def _preprocess_file_for_training( + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + read_fn=read_csv, + skip_save_processed_input=False, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, +): + """Method to pre-process csv data. + + Args: + features: list of all features (input + output). + dataset: path to the data. + training_set: training data. + validation_set: validation data. + test_set: test data. + training_set_metadata: train set metadata. + skip_save_processed_input: if False, the pre-processed data is saved as .hdf5 files in the same location + as the csv files with the same names. + preprocessing_params: preprocessing parameters. + random_seed: random seed. + + Returns: + training, test, validation datasets, training metadata. + """ + if dataset: + # Use data and ignore _train, _validation and _test. + # Also ignore data and train set metadata needs preprocessing + logger.info("Using full raw dataset, no hdf5 and json file with the same name have been found") + logger.info("Building dataset (it may take a while)") + + dataset_df = read_fn(dataset, backend.df_engine.df_lib) + training_set_metadata[SRC] = dataset + + data, training_set_metadata = build_dataset( + config, + dataset_df, + features, + preprocessing_params, + mode="training", + metadata=training_set_metadata, + backend=backend, + random_seed=random_seed, + skip_save_processed_input=skip_save_processed_input, + callbacks=callbacks, + ) + + elif training_set: + # use data_train (including _validation and _test if they are present) + # and ignore data and train set metadata + # needs preprocessing + logger.info("Using training raw csv, no hdf5 and json file with the same name have been found") + logger.info("Building dataset (it may take a while)") + + concatenated_df = concatenate_files(training_set, validation_set, test_set, read_fn, backend) + training_set_metadata[SRC] = training_set + + # Data is pre-split. + preprocessing_params = set_fixed_split(preprocessing_params) + + data, training_set_metadata = build_dataset( + config, + concatenated_df, + features, + preprocessing_params, + mode="training", + metadata=training_set_metadata, + backend=backend, + random_seed=random_seed, + callbacks=callbacks, + ) + + else: + raise ValueError("either data or data_train have to be not None") + + logger.debug("split train-val-test") + training_data, validation_data, test_data = drop_extra_cols( + features, split_dataset(data, preprocessing_params, backend, random_seed) + ) + + if dataset and backend.is_coordinator() and not skip_save_processed_input: + logger.debug("writing split file") + splits_df = concatenate_splits(training_data, validation_data, test_data, backend) + split_fp = get_split_path(dataset or training_set) + try: + backend.df_engine.to_parquet(splits_df, split_fp, index=True) + except Exception as e: + logger.warning( + f"Encountered error: '{e}' while writing data to parquet during saving preprocessed data. " + "Skipping saving processed data." + ) + + logger.info("Building dataset: DONE") + if preprocessing_params["oversample_minority"] or preprocessing_params["undersample_majority"]: + training_data = balance_data( + training_data, config["output_features"], preprocessing_params, backend, random_seed + ) + + return training_data, test_data, validation_data, training_set_metadata + + +def _preprocess_df_for_training( + config, + features, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + preprocessing_params=default_training_preprocessing_parameters, + backend=LOCAL_BACKEND, + random_seed=default_random_seed, + callbacks=None, +): + """Method to pre-process dataframes. + + This doesn't have the option to save the processed data as hdf5 as we don't expect users to do this as the data can + be processed in memory + """ + if dataset is not None: + # needs preprocessing + logger.info("Using full dataframe") + elif training_set is not None: + # needs preprocessing + logger.info("Using training dataframe") + dataset = concatenate_df(training_set, validation_set, test_set, backend) + + # Data is pre-split. + preprocessing_params = set_fixed_split(preprocessing_params) + + logger.info("Building dataset (it may take a while)") + + data, training_set_metadata = build_dataset( + config, + dataset, + features, + preprocessing_params, + mode="training", + metadata=training_set_metadata, + random_seed=random_seed, + backend=backend, + callbacks=callbacks, + ) + + logger.debug("split train-val-test") + training_set, validation_set, test_set = drop_extra_cols( + features, split_dataset(data, preprocessing_params, backend, random_seed) + ) + + logger.info("Building dataset: DONE") + if preprocessing_params["oversample_minority"] or preprocessing_params["undersample_majority"]: + training_set = balance_data(training_set, config["output_features"], preprocessing_params, backend, random_seed) + + return training_set, test_set, validation_set, training_set_metadata + + +def preprocess_for_prediction( + config: ModelConfig | ModelConfigDict, + dataset, + training_set_metadata=None, + data_format=None, + split=FULL, + include_outputs=True, + backend=LOCAL_BACKEND, + callbacks=None, +): + """Preprocesses the dataset to parse it into a format that is usable by the Ludwig core. + + Args: + config: Ludwig ModelConfig (or legacy dict) corresponding to Ludwig Model + dataset: Dataset to be processed + training_set_metadata: Train set metadata for the input features + data_format: Format of the data + split: The split of dataset to return + include_outputs: Whether to include outputs + backend: Type of backend to use for preprocessing + callbacks: Any callbacks passed in + + Returns: + Processed dataset along with updated training set metadata + """ + config_dict = _get_config_dict(config) + + if dataset is None: + raise ValueError( + "No dataset provided. Pass 'dataset' as a path, DataFrame, or HuggingFace Dataset to preprocess()." + ) + + if isinstance(dataset, Dataset): + return dataset, training_set_metadata + + # preload ludwig and HF datasets + dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, backend) + + # determine data format if not provided or auto + if not data_format or data_format == "auto": + data_format = figure_data_format(dataset) + + # manage the in_memory parameter + if data_format not in HDF5_FORMATS: + num_overrides = override_in_memory_flag(config_dict["input_features"], True) + if num_overrides > 0: + logger.warning(f"Using in_memory = False is not supported with {data_format} data format.") + + preprocessing_params = {} + config_defaults = config_dict.get(DEFAULTS, {}) + for feature_type in config_defaults: + preprocessing_params[feature_type] = config_defaults[feature_type].get(PREPROCESSING, {}) + preprocessing_params[SPLIT] = config_dict.get(PREPROCESSING, {}).get(SPLIT, {}) + + preprocessing_params = merge_dict(default_prediction_preprocessing_parameters, preprocessing_params) + + # if training_set_metadata is a string, assume it's a path to load the json + if training_set_metadata and isinstance(training_set_metadata, str): + training_set_metadata = load_metadata(training_set_metadata) + + # setup — feature lists extracted from the already-computed config_dict + output_features = [] + if include_outputs: + output_features += config_dict["output_features"] + features = config_dict["input_features"] + output_features + + # Check the cache for an already preprocessed dataset. This only + # applies to scenarios where the user wishes to predict on a split + # of the full dataset, where we preprocess the whole dataset together + # during training. If the user wishes to predict on the full dataset, + # it is assumed they are predicting on unseen data. This is done + # because the cached data is stored in its split form, and would be + # expensive to recombine, requiring further caching. + cached = False + + dataset = wrap(dataset) + cache = backend.cache.get_dataset_cache(config_dict, dataset) + dataset = dataset.unwrap() + + training_set = test_set = validation_set = None + if data_format in CACHEABLE_FORMATS and split != FULL: + with backend.storage.cache.use_credentials(): + cache_results = cache.get() + if cache_results is not None: + valid, *cache_values = cache_results + if valid: + logger.info(_get_cache_hit_message(cache)) + training_set_metadata, training_set, test_set, validation_set = cache_values + config_dict["data_cache_fp"] = training_set + data_format = backend.cache.data_format + cached = True + + data_format_processor = get_from_registry(data_format, data_format_preprocessor_registry) + if cached: + with backend.storage.cache.use_credentials(): + processed = data_format_processor.prepare_processed_data( + features, + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + preprocessing_params=preprocessing_params, + backend=backend, + ) + training_set, test_set, validation_set, training_set_metadata = processed + else: + processed = data_format_processor.preprocess_for_prediction( + config_dict, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks + ) + dataset, training_set_metadata, new_cache_fp = processed + training_set_metadata = training_set_metadata.copy() + + if new_cache_fp: + training_set_metadata[DATA_TRAIN_PARQUET_FP] = new_cache_fp + + if split != FULL: + logger.debug("split train-val-test") + training_set, validation_set, test_set = drop_extra_cols( + features, split_dataset(dataset, preprocessing_params, backend) + ) + + if split == TRAINING: + dataset = training_set + elif split == VALIDATION: + dataset = validation_set + elif split == TEST: + dataset = test_set + + # Build a config dict with only the output features included in this prediction + prediction_config_dict = { + **config_dict, + "output_features": output_features, + } + + with backend.storage.cache.use_credentials() if cached else contextlib.nullcontext(): + dataset = backend.dataset_manager.create( + dataset, + prediction_config_dict, + training_set_metadata, + ) + + return dataset, training_set_metadata + + +def _get_cache_hit_message(cache: DatasetCache) -> str: + return ( + "Found cached dataset and meta.json with the same filename of the dataset.\n" + "Using cached values instead of preprocessing the dataset again.\n" + f"- Cached training set metadata path: {cache.get_cached_obj_path(META)}\n" + f"- Cached training set path: {cache.get_cached_obj_path(TRAINING)}\n" + f"- Cached validation set path: {cache.get_cached_obj_path(VALIDATION)}\n" + f"- Cached test set path: {cache.get_cached_obj_path(TEST)}" + ) diff --git a/ludwig/data/preprocessing_progress.py b/ludwig/data/preprocessing_progress.py new file mode 100644 index 0000000..169b5be --- /dev/null +++ b/ludwig/data/preprocessing_progress.py @@ -0,0 +1,204 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Partition-level progress tracking for the preprocessing pipeline. + +Each df-engine wraps its map_partitions call to increment a shared counter after +every partition completes. + +Pandas/modin (non-Ray): the counter is an in-process integer; progress is fired +*synchronously* from increment() so every map_partitions call produces a callback +with no polling delay. + +Dask/Ray: the counter is a Ray named actor so increments from remote workers are +visible on the head node. A background thread polls the counter and fires +on_preprocess_progress(fraction) callbacks. stop() drains the actor's queue +before emitting the final 1.0 so that increments fired during persist() are +always reflected. +""" + +import threading +import time +from typing import Any + + +class _LocalProgressCounter: + """In-process counter used by the pandas/modin backends.""" + + def __init__(self, total: int): + self.total = total + self._completed = 0 + self._lock = threading.Lock() + + def increment(self): + with self._lock: + self._completed += 1 + + @property + def completed(self) -> int: + with self._lock: + return self._completed + + +class _RayProgressCounter: + """Head-node proxy around a Ray named actor.""" + + def __init__(self, actor): + self._actor = actor + + def increment(self): + self._actor.increment.remote() + + @property + def completed(self) -> int: + import ray + + return ray.get(self._actor.get_completed.remote()) + + @property + def total(self) -> int: + import ray + + return ray.get(self._actor.get_total.remote()) + + +def _make_ray_actor(total: int): + import ray + + @ray.remote + class _ProgressActor: + def __init__(self, t: int): + self._completed = 0 + self._total = t + + def increment(self): + self._completed += 1 + + def get_completed(self) -> int: + return self._completed + + def get_total(self) -> int: + return self._total + + return _ProgressActor.remote(total) + + +class PreprocessingProgressTracker: + """Fires ``on_preprocess_progress`` callbacks as partitions complete. + + Pandas/modin: callbacks fire synchronously on every ``increment()`` call -- + no background thread, no polling delay. + + Ray/Dask: a background thread polls the Ray actor at ``_POLL_INTERVAL_S`` + intervals and fires callbacks. ``stop()`` drains the actor queue so that + increments fired inside ``persist()`` are counted before the final 1.0 is + emitted. + """ + + _POLL_INTERVAL_S = 0.5 + + def __init__(self, total: int, callbacks: list, use_ray: bool = False): + self._total = total + self._callbacks = callbacks or [] + self._use_ray = use_ray + + if use_ray: + actor = _make_ray_actor(total) + self._counter = _RayProgressCounter(actor) + self._actor = actor + self._thread: threading.Thread | None = None + self._stop_event = threading.Event() + else: + self._counter = _LocalProgressCounter(total) + self._actor = None + self._thread = None + self._stop_event = threading.Event() + + def start(self): + if self._use_ray: + self._stop_event.clear() + self._thread = threading.Thread(target=self._poll_loop, daemon=True) + self._thread.start() + # pandas/modin: no thread needed; increment() fires synchronously. + + def stop(self): + if self._use_ray and self._actor is not None: + import ray + + # Drain the actor's call queue. Ray actors serialize calls in + # submission order, so issuing a get_completed() here guarantees + # all previously submitted increment() calls have been applied + # before we read the final count. Since persist() has already + # completed by the time stop() is called, this round-trip is + # effectively instantaneous. + try: + ray.get(self._actor.get_completed.remote(), timeout=30) + except Exception: + pass # best-effort; the final _fire(1.0) below covers it + + self._stop_event.set() + if self._thread is not None: + self._thread.join(timeout=5) + + self._fire(1.0) + + if self._actor is not None: + import ray + + ray.kill(self._actor) + + def increment(self): + """Called from inside each map_partitions wrapper (in-process or remote).""" + self._counter.increment() + if not self._use_ray: + # Synchronous fire for pandas/modin: every map_partitions call + # immediately produces a progress update. + completed = self._counter.completed + if self._total > 0: + self._fire(min(completed / self._total, 1.0)) + + def get_actor(self) -> Any: + """Returns the raw Ray actor so remote workers can call .increment.remote().""" + return self._actor + + def _poll_loop(self): + while not self._stop_event.is_set(): + completed = self._counter.completed + if self._total > 0: + self._fire(min(completed / self._total, 1.0)) + if completed >= self._total: + break + time.sleep(self._POLL_INTERVAL_S) + + def _fire(self, progress: float): + for cb in self._callbacks: + try: + cb.on_preprocess_progress(progress=progress) + except Exception: + pass + + +def get_total_partitions(input_cols: dict, use_ray: bool) -> int: + """Returns the number of partitions per feature column. + + For pandas/modin each column is a single partition (value=1). + For Dask/Ray, all columns share the same partition scheme after + repartitioning, so we read npartitions from the first column. + """ + if not input_cols: + return 1 + sample = next(iter(input_cols.values())) + if use_ray and hasattr(sample, "npartitions"): + return sample.npartitions + return 1 diff --git a/ludwig/data/prompt.py b/ludwig/data/prompt.py new file mode 100644 index 0000000..7f4210b --- /dev/null +++ b/ludwig/data/prompt.py @@ -0,0 +1,244 @@ +import json +import logging +import os +import string +from typing import Any, TYPE_CHECKING + +import pandas as pd + +if TYPE_CHECKING: + from ludwig.backend.base import Backend + +from ludwig.models.retrieval import df_checksum, get_retrieval_model, RetrievalModel +from ludwig.utils.fs_utils import get_default_cache_location, makedirs, path_exists +from ludwig.utils.types import DataFrame, Series + +logger = logging.getLogger(__name__) + +CONTEXT = "__context__" +SAMPLE = "__sample__" +TASK = "__task__" + +DEFAULT_ZERO_SHOT_PROMPT_TEMPLATE = """SAMPLE INPUT: {__sample__} + +USER: Complete the following task: {__task__} + +ASSISTANT: +""" + + +DEFAULT_FEW_SHOT_PROMPT_TEMPLATE = """Below is relevant context: + +CONTEXT: {__context__} + +CONTEXT is comprised of labeled samples whose embeddings were similar to that of the sample input. The labels in +these samples could aid you in your final prediction. Given this and no prior knowledge, follow the instructions +below. + +SAMPLE INPUT: {__sample__} + +USER: Complete the following task: {__task__} + +ASSISTANT: +""" + + +def index_column( + retrieval_config: dict[str, Any], + col_name: str, + dataset_cols: dict[str, Series], + backend: "Backend", + split_col: Series | None = None, +) -> tuple[RetrievalModel, str]: + """Indexes a column for sample retrieval via embedding index lookup. + + This function indexes a column and saves the index artifact to disk. If an index name is provided as part of the + `retrieval_config`, then the index in the ludwig cache with the corresponding name will be loaded instead of being + built from scratch. + + To prevent data leakage, a split column must be provided. This ensures that the retrieval model only ever fetches + samples from the training set. + + To ensure that the index is usable even if the original DataFrame is not available, the columns used to build the + index are stored as part of the index. + + All operations in this function are performed on pandas objects, which means that you may run out of memory if your + dataset is large. + + Args: + retrieval_config (Dict[str, Any]): The retrieval config from the config object. + col_name (str): The name of the column to index. + dataset_cols (Dict[str, Series]): A dictionary mapping column names to their corresponding Series. `col_name` + must be a key in this dictionary. These columns are stored as part of the index to ensure that the index + is usable even if the original DataFrame is not available. + df_engine (DataFrameEngine): The engine used to compute the columns into pandas objects. + split_col (Optional[Series]): A column that indicates whether a sample is part of the training set. A sample + is in the training set if the value in this column is 0. + Returns: + Tuple[RetrievalModel, str]: A tuple containing the retrieval model and the name of the index. + """ + retrieval_model = get_retrieval_model( + retrieval_config["type"], + model_name=retrieval_config["model_name"], + ) + + index_name = retrieval_config["index_name"] + index_cache_directory = os.path.join(get_default_cache_location(), "index") + if not path_exists(index_cache_directory): + makedirs(index_cache_directory, exist_ok=True) + + if index_name is None: + if split_col is None: + raise ValueError("split column must be provided if using retrieval") + split_col = backend.df_engine.compute(split_col).astype(int) + + # TODO(geoffrey): add support for Dask DataFrames + df = pd.DataFrame({name: backend.df_engine.compute(col) for name, col in dataset_cols.items()}) + df = df[split_col == 0] # Ensures that the index is only built on the training set + + # Even if index name is not provided, we still want to check if an index for this df already exists in cache + # If it does, load it and return immediately + index_hash = df_checksum(df) + index_name = f"embedding_index_{index_hash}" + if path_exists(os.path.join(index_cache_directory, index_name)): + logger.info( + f"Index for this DataFrame with name '{index_name}' already exists. " + f"Loading index from '{index_cache_directory}'" + ) + retrieval_model.load_index(index_name, cache_directory=index_cache_directory) + return retrieval_model, index_name + + # Build index if index name is not provided and index for this df does not already exist in cache + retrieval_model.create_dataset_index(df, backend, columns_to_index=[col_name]) + logger.info(f"Saving index to cache directory '{index_cache_directory}' with name '{index_name}'") + retrieval_model.save_index(index_name, cache_directory=index_cache_directory) + else: + logger.info(f"Loading index from cache directory '{index_cache_directory}' with name '{index_name}'") + retrieval_model.load_index(index_name, cache_directory=index_cache_directory) + return retrieval_model, index_name + + +def format_input_with_prompt( + input_col_name: str, + dataset_df: DataFrame, + backend: "Backend", + task_str: str, + retrieval_model: RetrievalModel | None = None, + k: int = -1, + template: str | None = None, +) -> Series: + """Returns a new Series with the input column data formatted with the prompt. + + A prompt can either be zero-shot or few-shot. A zero-shot prompt is comprised of some (unlabeled) input and a task + to be completed given the input. A few-shot prompt additionally includes some dynamically retrieved context, which + is retrieved using the `retrieval_model.search` function. + + A template can be provided to customize the prompt. The template must be a string with the following fields: + - __sample__ or at least one column from the input dataset: The input sample. + - __context__: The context retrieved by the `search_fn` function. Only required if `search_fn` is provided. + - __task__: The task to be completed given the input. Only required if `task` is set in the prompt config. + + Zero-shot example: + + Before formatting: + + input_col = ["I am happy"] + task_str = "sentiment analysis" + + After formatting: + + input_col = ["SAMPLE INPUT: I am happy\n\nUSER: Complete the following task: sentiment analysis\n\nASSISTANT:"] + + Args: + input_col_name (str): The name of the input column. + dataset_df (DataFrame): The input dataset. + backend (Backend): The backend used for map operations. + task_str (str): The task to be completed given the input. + retrieval_model (Optional[RetrievalModel]): The retrieval model used to retrieve context. If provided, the + prompt will be few-shot. If not provided, the prompt will be zero-shot. + k (int): The number of samples to retrieve. Only required if `retrieval_model` is provided. + template (Optional[str]): The template to use for the prompt. If not provided, the default will be used. + + Returns: + Series: A new Series with the input column data formatted with the prompt. + """ + # determine if this is a few-shot or zero-shot prompt + # few-shot prompts require a search function that returns samples from some dataset + is_few_shot = retrieval_model is not None + + # if no template is provided, use the default template + if template is None: + if is_few_shot: + template = DEFAULT_FEW_SHOT_PROMPT_TEMPLATE + else: + template = DEFAULT_ZERO_SHOT_PROMPT_TEMPLATE + + # ensure that the prompt template has all required fields + template_fields, field_to_dtype = _get_template_fields(template) + try: + _validate_prompt_template(template_fields, task_str, is_few_shot, dataset_df.columns, input_col_name) + except ValueError as e: + raise ValueError(f"template invalid for {'few-shot' if is_few_shot else 'zero-shot'} prompt: {e}") + + def generate_prompt(df: pd.DataFrame): + if CONTEXT in template_fields: + df[CONTEXT] = retrieval_model.search(df, backend, k=k, return_data=True) + if SAMPLE in template_fields: + # During preprocessing, we're inserting quotes that change the token IDs completely if we + # don't remove the " from the string. For parity with expected user output, we need to get rid of them. + # TODO(Arnav): see if there's a way to only remove them if the entry does't have quotes. This currently + # removes all " from the string (even those not added by json.dumps), which is not ideal. + df[SAMPLE] = df[input_col_name].map(lambda entry: json.dumps(entry, indent=2).strip('"')) + if TASK in template_fields: + df[TASK] = task_str + + def generate_prompt_for_row(row): + kwargs = {col: field_to_dtype[col](row[col]) for col in template_fields} + return template.format(**kwargs) + + return df.apply(generate_prompt_for_row, axis=1) + + result = backend.df_engine.map_partitions(dataset_df, generate_prompt, meta=(input_col_name, "object")) + result = backend.df_engine.persist(result) # persist to prevent re-computation + return result + + +def _validate_prompt_template( + template_fields: set[str], task: str | None, is_few_shot: bool, columns: list[str], input_col_name: str +): + """Validates that the template contains the necessary fields for the prompt.""" + if is_few_shot and CONTEXT not in template_fields: + raise ValueError(f"Prompt template must contain the '{CONTEXT}' field for few-shot learning") + + if task is not None and TASK not in template_fields: + raise ValueError(f"Prompt template must contain the '{TASK}' field if a task is provided") + + if SAMPLE in template_fields: + if input_col_name not in columns: + raise ValueError( + f"Prompt template contains the '{SAMPLE}' field, " + f"but the input column '{input_col_name}' is not in the dataset" + ) + elif not any(col in template_fields for col in columns): + raise ValueError( + f"Prompt template must contain either the '{SAMPLE}' field or one of the columns from the dataset" + ) + + +def _get_template_fields(template: str) -> tuple[set[str], dict[str, type]]: + """Returns the fields in the template.""" + parsed = [t for t in string.Formatter().parse(template) if t[1] is not None] + field_set = {field for _, field, _, _ in parsed} + dtype_map = {field: _get_dtype(format_spec) for _, field, format_spec, _ in parsed} + return field_set, dtype_map + + +def _get_dtype(format_spec: str) -> type: + # We need to prepare data in the row for different formatting options. + # If you have a number like 0.1234 in the DF and you want to format it like {number:.2f} it will fail if the + # number is represented as a string in the DF. So we need to cast it to a float before formatting. + if not format_spec: + return str + if "f" in format_spec: + return float + raise ValueError(f"Unsupported template format spec: {format_spec}") diff --git a/ludwig/data/sampler.py b/ludwig/data/sampler.py new file mode 100644 index 0000000..8a5db0c --- /dev/null +++ b/ludwig/data/sampler.py @@ -0,0 +1,81 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import math + +import numpy as np + +from ludwig.distributed import DistributedStrategy +from ludwig.utils.defaults import default_random_seed + + +class DistributedSampler: + """Adapted from `torch.utils.data.distributed.DistributedSampler`.""" + + def __init__( + self, + dataset_size: int, + shuffle: bool = True, + random_seed: int = default_random_seed, + distributed: DistributedStrategy = None, + ): + self.dataset_size = dataset_size + self.num_replicas = distributed.size() if distributed else 1 + self.rank = distributed.rank() if distributed else 0 + self.epoch = 0 + self.num_samples = int(math.ceil(self.dataset_size * 1.0 / self.num_replicas)) + self.total_size = self.num_samples * self.num_replicas + self.shuffle = shuffle + self.random_seed = random_seed + + def __iter__(self): + if self.shuffle: + # deterministically shuffle based on epoch and seed + indices = np.random.RandomState(seed=self.random_seed + self.epoch).permutation(self.dataset_size).tolist() + else: + indices = list(range(self.dataset_size)) + + # add extra samples to make it evenly divisible + indices += indices[: (self.total_size - len(indices))] + if len(indices) != self.total_size: + raise RuntimeError( + f"Sampler produced {len(indices)} indices but expected {self.total_size}. " + f"This is an internal error — please report it." + ) + + # subsample + indices = indices[self.rank : self.total_size : self.num_replicas] + if len(indices) != self.num_samples: + raise RuntimeError( + f"Sampler subsample produced {len(indices)} indices but expected {self.num_samples}. " + f"This is an internal error — please report it." + ) + + return iter(indices) + + def __len__(self): + return self.num_samples + + def set_epoch(self, epoch): + """Sets the epoch for this sampler. + + When `shuffle=True`, this ensures all replicas use a different random ordering for each epoch. Otherwise, the + next iteration of this sampler will yield the same ordering. + + Args: + epoch: Epoch number. + """ + self.epoch = epoch diff --git a/ludwig/data/split.py b/ludwig/data/split.py new file mode 100644 index 0000000..e0b30ef --- /dev/null +++ b/ludwig/data/split.py @@ -0,0 +1,391 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING +from zlib import crc32 + +import numpy as np +from sklearn.model_selection import train_test_split + +from ludwig.api_annotations import DeveloperAPI +from ludwig.backend.base import Backend +from ludwig.constants import BINARY, CATEGORY, DATE, MIN_DATASET_SPLIT_ROWS, SPLIT +from ludwig.error import ConfigValidationError +from ludwig.schema.split import ( + DateTimeSplitConfig, + FixedSplitConfig, + HashSplitConfig, + RandomSplitConfig, + StratifySplitConfig, +) +from ludwig.types import ModelConfigDict, PreprocessingConfigDict +from ludwig.utils.data_utils import hash_dict, split_dataset_ttv +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.registry import Registry +from ludwig.utils.types import DataFrame + +if TYPE_CHECKING: + from ludwig.schema.model_config import ModelConfig + +split_registry = Registry() +logger = logging.getLogger(__name__) + +TMP_SPLIT_COL = "__SPLIT__" +DEFAULT_PROBABILITIES = (0.7, 0.1, 0.2) + + +class Splitter(ABC): + @abstractmethod + def split( + self, df: DataFrame, backend: Backend, random_seed: int = default_random_seed + ) -> tuple[DataFrame, DataFrame, DataFrame]: + pass + + def validate(self, config: ModelConfigDict): + pass + + def has_split(self, split_index: int) -> bool: + return True + + @property + def required_columns(self) -> list[str]: + """Returns the list of columns that are required for splitting.""" + return [] + + +def _make_divisions_ensure_minimum_rows( + divisions: list[int], + n_examples: int, + min_val_rows: int = MIN_DATASET_SPLIT_ROWS, + min_test_rows: int = MIN_DATASET_SPLIT_ROWS, +) -> list[int]: + """Revises divisions to ensure no dataset split has too few examples.""" + result = list(divisions) + n = [dn - dm for dm, dn in zip((0,) + divisions, divisions + (n_examples,))] # Number of examples in each split. + if 0 < n[2] < min_test_rows and n[0] > 0: + # Test set is nonempty but too small, take examples from training set. + shift = min(min_test_rows - n[2], n[0]) + result = [d - shift for d in result] + if 0 < n[1] < min_val_rows and n[0] > 0: + # Validation set is nonempty but too small, take examples from training set. + result[0] -= min(min_val_rows - n[1], result[0]) + return result + + +def _split_divisions_with_min_rows(n_rows: int, probabilities: list[float]) -> list[int]: + """Generates splits for a dataset of n_rows into train, validation, and test sets according to split + probabilities, also ensuring that at least min_val_rows or min_test_rows are present in each nonempty split. + + Returns division indices to split on. + """ + d1 = int(np.ceil(probabilities[0] * n_rows)) + if probabilities[-1] > 0: + n2 = int(probabilities[1] * n_rows) + d2 = d1 + n2 + else: + # If the last probability is 0, then use the entire remaining dataset for validation. + d2 = n_rows + return _make_divisions_ensure_minimum_rows((d1, d2), n_rows) + + +@split_registry.register("random", default=True) +class RandomSplitter(Splitter): + def __init__(self, probabilities: list[float] = DEFAULT_PROBABILITIES, **kwargs): + self.probabilities = probabilities + + def split( + self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed + ) -> tuple[DataFrame, DataFrame, DataFrame]: + probabilities = self.probabilities + if not backend.df_engine.partitioned: + divisions = _split_divisions_with_min_rows(len(df), probabilities) + shuffled_df = df.sample(frac=1, random_state=random_seed) + return ( + shuffled_df.iloc[: divisions[0]], # Train + shuffled_df.iloc[divisions[0] : divisions[1]], # Validation + shuffled_df.iloc[divisions[1] :], # Test + ) + + # The above approach is very inefficient for partitioned backends, which can split by partition. + # This does not give exact guarantees on split size but is much more efficient for large datasets. + return df.random_split(self.probabilities, random_state=random_seed) + + def has_split(self, split_index: int) -> bool: + return self.probabilities[split_index] > 0 + + @staticmethod + def get_schema_cls(): + return RandomSplitConfig + + +@split_registry.register("fixed") +class FixedSplitter(Splitter): + def __init__(self, column: str = SPLIT, **kwargs): + self.column = column + + def split( + self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed + ) -> tuple[DataFrame, DataFrame, DataFrame]: + df[self.column] = df[self.column].astype(np.int8) + dfs = split_dataset_ttv(df, self.column) + train, test, val = tuple(df.drop(columns=self.column) if df is not None else None for df in dfs) + return train, val, test + + @property + def required_columns(self) -> list[str]: + return [self.column] + + @staticmethod + def get_schema_cls(): + return FixedSplitConfig + + +def stratify_split_dataframe( + df: DataFrame, column: str, probabilities: list[float], backend: Backend, random_seed: float +) -> tuple[DataFrame, DataFrame, DataFrame]: + """Splits a dataframe into train, validation, and test sets based on the values of a column. + + The column must be categorical (including binary). The split is stratified, meaning that the proportion of each + category in each split is the same as in the original dataset. + """ + + frac_train, frac_val, frac_test = probabilities + + def _safe_stratify(df, column, test_size): + # Get the examples with cardinality of 1 + df_cadinalities = df.groupby(column)[column].size() + low_cardinality_elems = df_cadinalities.loc[lambda x: x == 1] + df_low_card = df[df[column].isin(low_cardinality_elems.index)] + df = df[~df[column].isin(low_cardinality_elems.index)] + y = df[[column]] + + df_train, df_temp, _, _ = train_test_split(df, y, stratify=y, test_size=test_size, random_state=random_seed) + + # concat the examples with cardinality of 1 to the training DF. + if len(df_low_card.index) > 0: + df_train = backend.df_engine.concat([df_train, df_low_card]) + + return df_train, df_temp + + df_train, df_temp = _safe_stratify(df, column, 1.0 - frac_train) + + relative_frac_test = frac_test / (frac_val + frac_test) + df_val, df_test = _safe_stratify(df_temp, column, relative_frac_test) + + return df_train, df_val, df_test + + +@split_registry.register("stratify") +class StratifySplitter(Splitter): + def __init__(self, column: str, probabilities: list[float] = DEFAULT_PROBABILITIES, **kwargs): + self.column = column + self.probabilities = probabilities + + def split( + self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed + ) -> tuple[DataFrame, DataFrame, DataFrame]: + if not backend.df_engine.partitioned: + return stratify_split_dataframe(df, self.column, self.probabilities, backend, random_seed) + + # For a partitioned dataset, we can stratify split each partition individually + # to obtain a global stratified split. + + def split_partition(partition: DataFrame) -> DataFrame: + """Splits a single partition into train, val, test. + + Returns a single DataFrame with the split column populated. Assumes that the split column is already present + in the partition and has a default value of 0 (train). + """ + partition = partition.copy() + _, val, test = stratify_split_dataframe(partition, self.column, self.probabilities, backend, random_seed) + # Split column defaults to train, so only need to update val and test + partition.loc[val.index, TMP_SPLIT_COL] = 1 + partition.loc[test.index, TMP_SPLIT_COL] = 2 + return partition + + df[TMP_SPLIT_COL] = 0 + df = backend.df_engine.map_partitions(df, split_partition, meta=df) + + df_train = df[df[TMP_SPLIT_COL] == 0].drop(columns=TMP_SPLIT_COL) + df_val = df[df[TMP_SPLIT_COL] == 1].drop(columns=TMP_SPLIT_COL) + df_test = df[df[TMP_SPLIT_COL] == 2].drop(columns=TMP_SPLIT_COL) + + return df_train, df_val, df_test + + def validate(self, config: "ModelConfig"): + features = list(config.input_features) + list(config.output_features) + feature_cols = {f.column for f in features} + if self.column not in feature_cols: + logging.info( + f"Stratify column {self.column} is not among the features. " + f"Cannot establish if it is a binary or category feature." + ) + elif [f for f in features if f.column == self.column][0].type not in {BINARY, CATEGORY}: + raise ConfigValidationError(f"Feature for stratify column {self.column} must be binary or category") + + def has_split(self, split_index: int) -> bool: + return self.probabilities[split_index] > 0 + + @property + def required_columns(self) -> list[str]: + return [self.column] + + @staticmethod + def get_schema_cls(): + return StratifySplitConfig + + +@split_registry.register("datetime") +class DatetimeSplitter(Splitter): + def __init__( + self, + column: str, + probabilities: list[float] = DEFAULT_PROBABILITIES, + datetime_format: str | None = None, + fill_value: str = "", + **kwargs, + ): + self.column = column + self.probabilities = probabilities + self.datetime_format = datetime_format + self.fill_value = fill_value + + def split( + self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed + ) -> tuple[DataFrame, DataFrame, DataFrame]: + # In case the split column was preprocessed by Ludwig into a list, convert it back to a + # datetime string for the sort and split + def list_to_date_str(x): + if not isinstance(x, list): + if not isinstance(x, str): + # Convert timestamps, etc. to strings and return so it can direct cast to epoch time + return str(x) + + if len(x) != 9: + # Strings not in the expected format, so assume it's a formatted datetime and return + return x + + return f"{x[0]}-{x[1]}-{x[2]} {x[5]}:{x[6]}:{x[7]}" + + df[TMP_SPLIT_COL] = backend.df_engine.map_objects(df[self.column], list_to_date_str) + + # Convert datetime to int64 to workaround Dask limitation + # https://github.com/dask/dask/issues/9003 + df[TMP_SPLIT_COL] = backend.df_engine.df_lib.to_datetime(df[TMP_SPLIT_COL]).values.astype("int64") + + # Sort by ascending datetime and drop the temporary column + df = df.sort_values(TMP_SPLIT_COL).drop(columns=TMP_SPLIT_COL) + + # Split using different methods based on the underlying df engine. + # For Pandas, split by row index. + # For Dask, split by partition, as splitting by row is very inefficient. + return tuple(backend.df_engine.split(df, self.probabilities)) + + def validate(self, config: "ModelConfig"): + features = list(config.input_features) + list(config.output_features) + feature_cols = {f.column for f in features} + if self.column not in feature_cols: + logging.info( + f"Datetime split column {self.column} is not among the features. " + f"Cannot establish if it is a valid datetime." + ) + elif [f for f in features if f.column == self.column][0].type not in {DATE}: + raise ConfigValidationError(f"Feature for datetime split column {self.column} must be a datetime") + + def has_split(self, split_index: int) -> bool: + return self.probabilities[split_index] > 0 + + @property + def required_columns(self) -> list[str]: + return [self.column] + + @staticmethod + def get_schema_cls(): + return DateTimeSplitConfig + + +@split_registry.register("hash") +class HashSplitter(Splitter): + def __init__( + self, + column: str, + probabilities: list[float] = DEFAULT_PROBABILITIES, + **kwargs, + ): + self.column = column + self.probabilities = probabilities + + def split( + self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed + ) -> tuple[DataFrame, DataFrame, DataFrame]: + # Maximum value of the hash function crc32 + max_value = 2**32 + thresholds = [v * max_value for v in self.probabilities] + + def hash_column(x): + value = hash_dict({"value": x}, max_length=None) + hash_value = crc32(value) + if hash_value < thresholds[0]: + return 0 + elif hash_value < (thresholds[0] + thresholds[1]): + return 1 + else: + return 2 + + df[TMP_SPLIT_COL] = backend.df_engine.map_objects(df[self.column], hash_column).astype(np.int8) + dfs = split_dataset_ttv(df, TMP_SPLIT_COL) + train, test, val = tuple(df.drop(columns=TMP_SPLIT_COL) if df is not None else None for df in dfs) + return train, val, test + + def has_split(self, split_index: int) -> bool: + return self.probabilities[split_index] > 0 + + @property + def required_columns(self) -> list[str]: + return [self.column] + + @staticmethod + def get_schema_cls(): + return HashSplitConfig + + +@DeveloperAPI +def get_splitter(type: str | None = None, **kwargs) -> Splitter: + splitter_cls = split_registry.get(type) + if splitter_cls is None: + return ValueError(f"Invalid split type: {type}") + return splitter_cls(**kwargs) + + +@DeveloperAPI +def split_dataset( + df: DataFrame, + global_preprocessing_parameters: PreprocessingConfigDict, + backend: Backend, + random_seed: float = default_random_seed, +) -> tuple[DataFrame, DataFrame, DataFrame]: + splitter = get_splitter(**global_preprocessing_parameters.get(SPLIT, {})) + datasets: tuple[DataFrame, DataFrame, DataFrame] = splitter.split(df, backend, random_seed) + if len(datasets[0].columns) == 0: + raise ValueError( + "Encountered an empty training set while splitting data. Please double check the preprocessing split " + "configuration." + ) + + # Remove partitions that are empty after splitting + datasets = [None if dataset is None else backend.df_engine.remove_empty_partitions(dataset) for dataset in datasets] + return datasets diff --git a/ludwig/data/split_dataset.py b/ludwig/data/split_dataset.py new file mode 100644 index 0000000..adec98c --- /dev/null +++ b/ludwig/data/split_dataset.py @@ -0,0 +1,48 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import random + + +def split(input_path, output1, output2, split): + with open(input_path) as file: + lines = file.readlines() + + random.shuffle(lines) + split_idx = int(len(lines) * split) + + with open(output1, "w") as f: + for line in lines[:split_idx]: + line = line if line.endswith("\n") else line + "\n" + f.write(line) + + with open(output2, "w") as f: + for line in lines[split_idx:]: + line = line if line.endswith("\n") else line + "\n" + f.write(line) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Split a file based on its lines") + + parser.add_argument("-i", "--input", required=True, help="input file names") + parser.add_argument("-o1", "--output1", required=True, help="output 1 file name") + parser.add_argument("-o2", "--output2", required=True, help="output 2 file name") + parser.add_argument("-s", "--split", required=True, type=float, default=0.8, help="percentage of the split") + + args = parser.parse_args() + + split(args.input, args.output1, args.output2, args.split) diff --git a/ludwig/data/split_utils.py b/ludwig/data/split_utils.py new file mode 100644 index 0000000..780b23a --- /dev/null +++ b/ludwig/data/split_utils.py @@ -0,0 +1,81 @@ +"""Dataset splitting utilities. + +Provides functions for splitting datasets into train/validation/test sets +using various strategies: random, stratified, fixed column, datetime, hash. + +Extracted from preprocessing.py for modularity. The main split logic remains +in ludwig/data/split.py; this module provides additional utilities. +""" + +import logging + +import numpy as np + +logger = logging.getLogger(__name__) + + +def get_split_indices( + n_samples: int, + probabilities: tuple[float, float, float] = (0.7, 0.1, 0.2), + random_seed: int = 42, +) -> np.ndarray: + """Generate split indices (0=train, 1=validation, 2=test) for a dataset. + + Args: + n_samples: Number of samples in the dataset. + probabilities: (train, val, test) split ratios. Must sum to 1. + random_seed: Random seed for reproducibility. + + Returns: + Array of split indices (0, 1, or 2) for each sample. + """ + if abs(sum(probabilities) - 1.0) >= 1e-6: + raise ValueError( + f"Split probabilities must sum to 1, got {sum(probabilities)}.\n" + f"Fix: ensure your train/validation/test split fractions sum to 1.0 (e.g., [0.7, 0.1, 0.2])." + ) + + rng = np.random.RandomState(random_seed) + indices = rng.permutation(n_samples) + splits = np.zeros(n_samples, dtype=int) + + train_end = int(n_samples * probabilities[0]) + val_end = train_end + int(n_samples * probabilities[1]) + + splits[indices[train_end:val_end]] = 1 + splits[indices[val_end:]] = 2 + + return splits + + +def stratified_split_indices( + labels: np.ndarray, + probabilities: tuple[float, float, float] = (0.7, 0.1, 0.2), + random_seed: int = 42, +) -> np.ndarray: + """Generate stratified split indices that maintain label distribution. + + Args: + labels: Array of class labels for each sample. + probabilities: (train, val, test) split ratios. + random_seed: Random seed for reproducibility. + + Returns: + Array of split indices (0, 1, or 2) for each sample. + """ + rng = np.random.RandomState(random_seed) + splits = np.zeros(len(labels), dtype=int) + + unique_labels = np.unique(labels) + for label in unique_labels: + label_indices = np.where(labels == label)[0] + rng.shuffle(label_indices) + + n = len(label_indices) + train_end = int(n * probabilities[0]) + val_end = train_end + int(n * probabilities[1]) + + splits[label_indices[train_end:val_end]] = 1 + splits[label_indices[val_end:]] = 2 + + return splits diff --git a/ludwig/data/statistics.py b/ludwig/data/statistics.py new file mode 100644 index 0000000..87dfdfe --- /dev/null +++ b/ludwig/data/statistics.py @@ -0,0 +1,149 @@ +"""Online statistics accumulator using Welford's algorithm. + +Supports O(1) memory accumulation of mean, variance, min, max, and count +over arbitrarily large datasets without materialising the full data. + +Also supports merging accumulators from independent shards, making it +safe to use in distributed / multi-worker settings. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field + +import numpy as np + + +@dataclass +class WelfordAccumulator: + """Incremental mean/variance accumulator (Welford's online algorithm). + + Numerically stable for large N. All statistics are maintained per + scalar dimension: pass a 1-D numpy array to ``update`` and every + element is tracked independently (useful for per-channel image stats). + + Usage:: + + acc = WelfordAccumulator() + for batch in data_loader: + acc.update(batch) # batch: 1-D np.ndarray of floats + mean, std = acc.mean, acc.std # scalar or per-element arrays + + Merging shards:: + + acc = WelfordAccumulator.merge_all([acc_shard_0, acc_shard_1, ...]) + """ + + count: int = 0 + _mean: np.ndarray = field(default_factory=lambda: np.array(0.0)) + # Welford's M2 accumulator: sum of squared deviations from the running mean + _m2: np.ndarray = field(default_factory=lambda: np.array(0.0)) + _min: np.ndarray = field(default_factory=lambda: np.array(np.inf)) + _max: np.ndarray = field(default_factory=lambda: np.array(-np.inf)) + + def update(self, x: float | np.ndarray) -> None: + """Incorporate a new observation (scalar or 1-D array).""" + x = np.asarray(x, dtype=np.float64) + self.count += 1 + delta = x - self._mean + self._mean = self._mean + delta / self.count + delta2 = x - self._mean + self._m2 = self._m2 + delta * delta2 + self._min = np.minimum(self._min, x) + self._max = np.maximum(self._max, x) + + def update_batch(self, xs: np.ndarray) -> None: + """Incorporate a batch of scalar observations efficiently. + + ``xs`` shape: (N,) — one scalar per sample. + """ + xs = np.asarray(xs, dtype=np.float64).ravel() + for x in xs: + self.update(x) + + @property + def mean(self) -> np.ndarray: + return self._mean if self.count > 0 else np.array(0.0) + + @property + def variance(self) -> np.ndarray: + if self.count < 2: + return np.array(0.0) + return self._m2 / (self.count - 1) + + @property + def std(self) -> np.ndarray: + return np.sqrt(self.variance) + + @property + def min(self) -> np.ndarray: + return self._min if self.count > 0 else np.array(np.nan) + + @property + def max(self) -> np.ndarray: + return self._max if self.count > 0 else np.array(np.nan) + + def result(self) -> dict: + """Return all statistics as a plain dict.""" + return { + "count": self.count, + "mean": float(self.mean), + "std": float(self.std), + "variance": float(self.variance), + "min": float(self.min), + "max": float(self.max), + } + + @classmethod + def merge(cls, a: WelfordAccumulator, b: WelfordAccumulator) -> WelfordAccumulator: + """Parallel / Chan's algorithm merge of two independent accumulators. + + The result is mathematically equivalent to having accumulated all + samples from both accumulators in a single pass. + """ + if a.count == 0: + return b + if b.count == 0: + return a + + merged = cls() + merged.count = a.count + b.count + delta = b._mean - a._mean + merged._mean = (a._mean * a.count + b._mean * b.count) / merged.count + merged._m2 = a._m2 + b._m2 + delta**2 * a.count * b.count / merged.count + merged._min = np.minimum(a._min, b._min) + merged._max = np.maximum(a._max, b._max) + return merged + + @classmethod + def merge_all(cls, accumulators: list[WelfordAccumulator]) -> WelfordAccumulator: + """Reduce a list of accumulators into one.""" + result = cls() + for acc in accumulators: + result = cls.merge(result, acc) + return result + + +def welford_from_array(xs: np.ndarray) -> WelfordAccumulator: + """Convenience: build an accumulator from a 1-D array in one call.""" + acc = WelfordAccumulator() + for x in np.asarray(xs, dtype=np.float64).ravel(): + acc.update(x) + return acc + + +def welford_stats_match_numpy(acc: WelfordAccumulator, xs: np.ndarray, rtol: float = 1e-6) -> bool: + """Return True if Welford stats agree with numpy reference within rtol.""" + xs = np.asarray(xs, dtype=np.float64).ravel() + np_mean = float(np.mean(xs)) + np_std = float(np.std(xs, ddof=1)) if len(xs) > 1 else 0.0 + np_min = float(np.min(xs)) + np_max = float(np.max(xs)) + + return ( + math.isclose(float(acc.mean), np_mean, rel_tol=rtol) + and math.isclose(float(acc.std), np_std, rel_tol=rtol) + and math.isclose(float(acc.min), np_min, rel_tol=rtol) + and math.isclose(float(acc.max), np_max, rel_tol=rtol) + ) diff --git a/ludwig/data/types.py b/ludwig/data/types.py new file mode 100644 index 0000000..849de81 --- /dev/null +++ b/ludwig/data/types.py @@ -0,0 +1,239 @@ +"""Typed metadata classes for Ludwig features and training sets. + +Replaces the untyped TrainingSetMetadataDict = dict with structured dataclasses +that provide type safety, IDE autocomplete, and prevent key typo bugs. + +These classes are backward-compatible: they can be constructed from dicts (via +from_dict class methods) and serialized back to dicts (via to_dict methods). +Existing code that accesses metadata as dicts continues to work during migration. +""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class NumberMetadata: + """Metadata for number features computed during preprocessing.""" + + mean: float | None = None + std: float | None = None + min: float | None = None + max: float | None = None + q1: float | None = None + q2: float | None = None + q3: float | None = None + ple_bin_edges: list[float] | None = None + normalization: str | None = None + + def to_dict(self) -> dict: + return {k: v for k, v in self.__dict__.items() if v is not None} + + @classmethod + def from_dict(cls, d: dict) -> "NumberMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +@dataclass +class CategoryMetadata: + """Metadata for category features computed during preprocessing.""" + + idx2str: list[str] = field(default_factory=list) + str2idx: dict[str, int] = field(default_factory=dict) + str2freq: dict[str, int] = field(default_factory=dict) + vocab_size: int = 0 + most_common_value: str | None = None + + def to_dict(self) -> dict: + return {k: v for k, v in self.__dict__.items() if v is not None} + + @classmethod + def from_dict(cls, d: dict) -> "CategoryMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +@dataclass +class TextMetadata: + """Metadata for text features computed during preprocessing.""" + + idx2str: list[str] = field(default_factory=list) + str2idx: dict[str, int] = field(default_factory=dict) + str2freq: dict[str, int] = field(default_factory=dict) + vocab_size: int = 0 + max_sequence_length: int | None = None + pad_idx: int = 0 + padding: str = "right" + tokenizer_type: str | None = None + + def to_dict(self) -> dict: + return {k: v for k, v in self.__dict__.items() if v is not None} + + @classmethod + def from_dict(cls, d: dict) -> "TextMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +@dataclass +class BinaryMetadata: + """Metadata for binary features computed during preprocessing.""" + + str2bool: dict[str, bool] = field(default_factory=dict) + bool2str: list[str] = field(default_factory=list) + fallback_true_label: str | None = None + + def to_dict(self) -> dict: + return {k: v for k, v in self.__dict__.items() if v is not None} + + @classmethod + def from_dict(cls, d: dict) -> "BinaryMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +@dataclass +class ImageMetadata: + """Metadata for image features computed during preprocessing.""" + + num_channels: int = 3 + height: int = 0 + width: int = 0 + resize_method: str = "interpolate" + infer_image_dimensions: bool = True + infer_image_max_height: int = 256 + infer_image_max_width: int = 256 + infer_image_sample_size: int = 100 + scaling: str = "pixel_normalization" + + def to_dict(self) -> dict: + return self.__dict__.copy() + + @classmethod + def from_dict(cls, d: dict) -> "ImageMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +@dataclass +class SequenceMetadata: + """Metadata for sequence features computed during preprocessing.""" + + idx2str: list[str] = field(default_factory=list) + str2idx: dict[str, int] = field(default_factory=dict) + vocab_size: int = 0 + max_sequence_length: int | None = None + pad_idx: int = 0 + + def to_dict(self) -> dict: + return {k: v for k, v in self.__dict__.items() if v is not None} + + @classmethod + def from_dict(cls, d: dict) -> "SequenceMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +@dataclass +class AudioMetadata: + """Metadata for audio features computed during preprocessing.""" + + feature_dim: int = 0 + max_sequence_length: int | None = None + sampling_rate: int = 16000 + + def to_dict(self) -> dict: + return self.__dict__.copy() + + @classmethod + def from_dict(cls, d: dict) -> "AudioMetadata": + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return cls(**{k: v for k, v in d.items() if k in valid_fields}) + + +# Registry mapping feature type strings to metadata classes +FEATURE_METADATA_CLASSES: dict[str, type] = { + "number": NumberMetadata, + "category": CategoryMetadata, + "text": TextMetadata, + "binary": BinaryMetadata, + "image": ImageMetadata, + "sequence": SequenceMetadata, + "audio": AudioMetadata, +} + + +@dataclass +class TrainingSetMetadata: + """Typed container for training set metadata. + + Replaces the untyped TrainingSetMetadataDict = dict with a structured container. Provides both typed access and + dict-like backward compatibility. + """ + + features: dict[str, Any] = field(default_factory=dict) + data_train_parquet_fp: str | None = None + data_validation_parquet_fp: str | None = None + data_test_parquet_fp: str | None = None + + def __getitem__(self, key: str) -> Any: + """Dict-like access for backward compatibility.""" + return self.features.get(key, getattr(self, key, None)) + + def __setitem__(self, key: str, value: Any): + """Dict-like setting for backward compatibility.""" + if hasattr(self, key) and key != "features": + setattr(self, key, value) + else: + self.features[key] = value + + def __contains__(self, key: str) -> bool: + return key in self.features or hasattr(self, key) + + def get(self, key: str, default: Any = None) -> Any: + if key in self.features: + return self.features[key] + return getattr(self, key, default) + + def keys(self): + return list(self.features.keys()) + + def items(self): + return self.features.items() + + def to_dict(self) -> dict: + """Convert to plain dict for serialization.""" + result = {} + for key, value in self.features.items(): + if hasattr(value, "to_dict"): + result[key] = value.to_dict() + else: + result[key] = value + if self.data_train_parquet_fp: + result["data_train_parquet_fp"] = self.data_train_parquet_fp + if self.data_validation_parquet_fp: + result["data_validation_parquet_fp"] = self.data_validation_parquet_fp + if self.data_test_parquet_fp: + result["data_test_parquet_fp"] = self.data_test_parquet_fp + return result + + @classmethod + def from_dict(cls, d: dict) -> "TrainingSetMetadata": + """Construct from a plain dict (backward compatibility).""" + special_keys = { + "data_train_parquet_fp", + "data_validation_parquet_fp", + "data_test_parquet_fp", + "data_train_hdf5_fp", + "data_validation_hdf5_fp", + "data_test_hdf5_fp", + } + metadata = cls() + for key, value in d.items(): + if key in special_keys: + if "hdf5" not in key: # Skip HDF5 paths + setattr(metadata, key, value) + else: + metadata.features[key] = value + return metadata diff --git a/ludwig/data/utils.py b/ludwig/data/utils.py new file mode 100644 index 0000000..3073aaa --- /dev/null +++ b/ludwig/data/utils.py @@ -0,0 +1,64 @@ +from typing import Optional + +import numpy as np + +from ludwig.constants import DECODER, ENCODER, SPLIT +from ludwig.types import FeatureConfigDict, PreprocessingConfigDict +from ludwig.utils.dataframe_utils import is_dask_series_or_df +from ludwig.utils.types import DataFrame + + +def convert_to_dict( + predictions: DataFrame, + output_features: dict[str, FeatureConfigDict], + backend: Optional["Backend"] = None, # noqa: F821 +): + """Convert predictions from DataFrame format to a dictionary.""" + output = {} + for of_name, _output_feature in output_features.items(): + feature_keys = {k for k in predictions.columns if k.startswith(of_name)} + feature_dict = {} + for key in feature_keys: + subgroup = key[len(of_name) + 1 :] + + values = predictions[key] + if is_dask_series_or_df(values, backend): + values = values.compute() + try: + values = np.stack(values.to_numpy()) + except ValueError: + values = values.to_list() + + feature_dict[subgroup] = values + output[of_name] = feature_dict + return output + + +def set_fixed_split(preprocessing_params: PreprocessingConfigDict) -> PreprocessingConfigDict: + """Sets the split policy explicitly to a fixed split. + + This potentially overrides the split configuration that the user set or what came from schema defaults. + """ + + return { + **preprocessing_params, + "split": { + "type": "fixed", + "column": SPLIT, + }, + } + + +def get_input_and_output_features(feature_configs): + """Returns a tuple (input_features, output_features) where each element is a list of feature configs. + + Determines whether a feature is an input or output feature by checking the presence of the encoder or decoder keys. + """ + input_features = [] + output_features = [] + for feature in feature_configs: + if ENCODER in feature: + input_features.append(feature) + elif DECODER in feature: + output_features.append(feature) + return input_features, output_features diff --git a/ludwig/datasets/README.md b/ludwig/datasets/README.md new file mode 100644 index 0000000..74950c7 --- /dev/null +++ b/ludwig/datasets/README.md @@ -0,0 +1,140 @@ +## Ludwig Datasets API + +The Ludwig Dataset Zoo provides datasets that can be directly plugged into a Ludwig model. For each dataset, we've also +included an example Ludwig config which should train reasonably fast on a current-generation laptop. + +The simplest way to use a dataset is to import it: + +```python +from ludwig.datasets import titanic + +# Loads into single dataframe with a 'split' column: +dataset_df = titanic.load() + +# Loads into split dataframes: +train_df, test_df, _ = titanic.load(split=True) +``` + +The `ludwig.datasets` API provides functions to list, describe, and get datasets: + +______________________________________________________________________ + +### list_datasets + +Gets a list of the names of available datasets. + +**Example:** + +```python +dataset_names = ludwig.datasets.list_datasets() +``` + +______________________________________________________________________ + +### get_datasets_output_features + +If a specific dataset name is passed in, then returns the output features associated with that dataset. Otherwise, +returns an ordered dictionary with dataset names as keys and dictionaries containing the output features for each +dataset as values. + +**Example:** + +```python +output_features = ludwig.datasets.get_datasets_output_features(dataset="titanic") +``` + +______________________________________________________________________ + +### describe_dataset + +Gets a human-readable description string for a dataset + +**Example:** + +```python +print(ludwig.datasets.describe_dataset("titanic")) +``` + +______________________________________________________________________ + +### get_dataset + +Get a dataset module by name + +**Example:** + +```python +titanic_dataset = ludwig.datasets.get_dataset("titanic") +``` + +______________________________________________________________________ + +### model_configs_for_dataset + +Gets a dictionary of model configs for the specified dataset. Keys are the config names, and may +contain the special keys: + +- `default` - The default config for the dataset. Should train to decent performance under 10 minutes on a typical + laptop without GPU. +- `best` - The best known config for the dataset. Should be replaced when a better config is found. This is a good + opportunity for contributions, if you find a better one please check it in and open a PR! + +**Example:** + +```python +configs = ludwig.datasets.model_configs_for_dataset("higgs") +default_higgs_config = configs["default"] +best_higgs_config = configs["best"] +``` + +______________________________________________________________________ + +## Training a model using builtin dataset and config + +This example code trains a model on the Titanic dataset using the default config: + +```python +from ludwig.api import LudwigModel +import ludwig.datasets + +titanic = ludwig.datasets.get_dataset("titanic") + +dataset_df = titanic.load() + +titanic_config = titanic.default_model_config + +model = LudwigModel(titanic_config) +model.train(dataset_df) +``` + +Some datasets are hosted on [Kaggle](https://www.kaggle.com) and require a kaggle account. To use these, you'll need to +[set up Kaggle credentials](https://www.kaggle.com/docs/api) in your environment. If the dataset is part of a Kaggle +competition, you'll need to accept the terms on the competition page. + +To check programmatically, datasets have an `.is_kaggle_dataset` property. + +## Downloading, Processing, and Exporting + +Datasets are first downloaded into `LUDWIG_CACHE`, which may be set as an environment variable and defaults to +`$HOME/.ludwig_cache`. + +Datasets are automatically loaded, processed, and re-saved as parquet files. The processed dataset is saved in +LUDWIG_CACHE. + +If the dataset contains media files including images or audio, media files are saved in subdirectories and referenced by +relative paths from the dataset location. To ensure Ludwig can read these files during training, they should be +accessible from Ludwig's working directory. + +To export the processed dataset, including any media files it depends on, use the `.export` method: + +```python +from ludwig.datasets import twitter_bots + +# Exports twitter bots dataset and image files to the current working directory. +twitter_bots.export(".") + +# The working directory should now contain: +# ./twitter_bots.parquet - The twitter bots dataset +# ./profile_images - Account profile image files +# ./profile_background_images - Account profile background image files +``` diff --git a/ludwig/datasets/__init__.py b/ludwig/datasets/__init__.py new file mode 100644 index 0000000..b038021 --- /dev/null +++ b/ludwig/datasets/__init__.py @@ -0,0 +1,399 @@ +import argparse +import importlib +import logging +import os +from collections import OrderedDict +from functools import lru_cache +from io import BytesIO +from typing import Any, Literal + +import yaml + +from ludwig.api_annotations import DeveloperAPI, PublicAPI +from ludwig.backend.base import Backend +from ludwig.constants import AUDIO, BINARY, CATEGORY, IMAGE, NUMBER, TEST, TEXT, TRAIN, TYPE, VALIDATION +from ludwig.data.cache.types import CacheableDataframe +from ludwig.datasets import configs +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + +# PublicAPI +from ludwig.datasets.loaders.openml_loader import openml_suite_loaders # noqa: F401 +from ludwig.datasets.utils import model_configs_for_dataset # noqa +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import print_ludwig +from ludwig.utils.types import DataFrame + +URI_PREFIX = "ludwig://" +HF_PREFIX = "hf://" +SPLITS = [TRAIN, VALIDATION, TEST] + + +def _load_dataset_config(config_filename: str): + """Loads a dataset config.""" + config_path = os.path.join(os.path.dirname(configs.__file__), config_filename) + with open(config_path) as f: + return DatasetConfig.from_dict(yaml.safe_load(f)) + + +@lru_cache(maxsize=1) +def _get_dataset_configs() -> dict[str, DatasetConfig]: + """Returns all dataset configs indexed by name.""" + # Use os.listdir as the primary method - it's simple and works reliably + # with both editable and regular installs. + configs_dir = os.path.dirname(configs.__file__) + config_files = [f for f in os.listdir(configs_dir) if f.endswith(".yaml")] + config_objects = [_load_dataset_config(f) for f in config_files] + return {c.name: c for c in config_objects} + + +def _get_dataset_config(dataset_name) -> DatasetConfig: + """Get the config for a dataset.""" + configs = _get_dataset_configs() + if dataset_name not in configs: + raise AttributeError(f"No config found for dataset {dataset_name}") + return configs[dataset_name] + + +@PublicAPI +def get_dataset(dataset_name, cache_dir=None) -> DatasetLoader: + """Gets an instance of the dataset loader for a dataset.""" + config = _get_dataset_config(dataset_name) + class_name = config.loader.split(".")[-1] + module_name = "." + ".".join(config.loader.split(".")[:-1]) + loader_module = importlib.import_module(module_name, package="ludwig.datasets.loaders") + loader_cls = getattr(loader_module, class_name) + if cache_dir: + return loader_cls(config, cache_dir=cache_dir) + return loader_cls(config) + + +@DeveloperAPI +def load_dataset_uris( + dataset: str | DataFrame | None, + training_set: str | DataFrame | None, + validation_set: str | DataFrame | None, + test_set: str | DataFrame | None, + backend: Backend, +) -> tuple[ + CacheableDataframe | None, + CacheableDataframe | None, + CacheableDataframe | None, + CacheableDataframe | None, +]: + """Loads and returns any Ludwig dataset URIs as CacheableDataframes. + + Returns the input unmodified for any non-Ludwig datasets. + """ + + dataset_out, training_set_out, validation_set_out, test_set_out = dataset, training_set, validation_set, test_set + # Check that any of the datasets begin with the `hf://` prefix denoting a Hugging Face dataset URI + # Hugging Face datasets should follow the naming convention `hf://--` + if _is_hf(dataset, training_set): + return _load_hf_datasets(dataset, training_set, validation_set, test_set, backend) + + # Check that any of the datasets begin with the `ludwig://` prefix denoting a Ludwig dataset URI + if dataset is not None: + if isinstance(dataset, str) and dataset.startswith(URI_PREFIX): + dataset_out = _load_cacheable_dataset(dataset, backend) + return dataset_out, training_set_out, validation_set_out, test_set_out + if training_set is not None: + train_df = test_df = val_df = None + training_set_checksum = None + if isinstance(training_set, str) and training_set.startswith(URI_PREFIX): + # For the training set, we only want to use the TRAINING split of the dataset + dataset_name = training_set[len(URI_PREFIX) :] + loader = get_dataset(dataset_name) + train_df, test_df, val_df = loader.load(split=True) + training_set_checksum = str(loader.get_mtime()) + train_df = backend.df_engine.from_pandas(train_df) + training_set_out = CacheableDataframe(df=train_df, name=training_set, checksum=training_set_checksum) + + if isinstance(validation_set, str) and validation_set.startswith(URI_PREFIX): + if validation_set == training_set: + # Reuse the loaded DF from the training split + val_df = backend.df_engine.from_pandas(val_df) + validation_set_out = CacheableDataframe(df=val_df, name=validation_set, checksum=training_set_checksum) + else: + validation_set_out = _load_cacheable_dataset(validation_set, backend) + + if isinstance(test_set, str) and test_set.startswith(URI_PREFIX): + if test_set == training_set: + # Reuse the loaded DF from the training split + test_df = backend.df_engine.from_pandas(test_df) + test_set_out = CacheableDataframe(df=test_df, name=test_set, checksum=training_set_checksum) + else: + test_set_out = _load_cacheable_dataset(test_set, backend) + + return dataset_out, training_set_out, validation_set_out, test_set_out + + +def _is_hf(dataset, training_set): + dataset_is_hf = dataset is not None and isinstance(dataset, str) and dataset.startswith(HF_PREFIX) + training_set_is_hf = ( + training_set is not None and isinstance(training_set, str) and training_set.startswith(HF_PREFIX) + ) + return dataset_is_hf or training_set_is_hf + + +def _load_hf_datasets( + dataset: str | DataFrame | None, + training_set: str | DataFrame | None, + validation_set: str | DataFrame | None, + test_set: str | DataFrame | None, + backend: Backend, +) -> tuple[ + CacheableDataframe | None, + CacheableDataframe | None, + CacheableDataframe | None, + CacheableDataframe | None, +]: + """Loads and returns any Hugging Face datasets as CacheableDataframes. + + Returns the input unmodified for any non-HF datasets. + """ + dataset_out = dataset + training_set_out = training_set + validation_set_out = validation_set + test_set_out = test_set + + # Check that any of the datasets begin with the `hf://` prefix denoting a Hugging Face dataset URI + # Hugging Face datasets should follow the naming convention `hf://--` + if dataset is not None: + if isinstance(dataset, str) and dataset.startswith(HF_PREFIX): + dataset_out = _load_cacheable_hf_dataset(dataset, backend) + return dataset_out, training_set_out, validation_set_out, test_set_out + + # Because of the conditional logic (_is_hf) in load_dataset_uris, if the above block is not triggered, then + # training_set must be a string that starts with HF_PREFIX + train_df = test_df = val_df = None + loader = get_dataset("hugging_face") + hf_id, hf_subsample = _get_hf_dataset_and_subsample(training_set) + train_df, val_df, test_df = loader.load(hf_id, hf_subsample, split=True) # Call hugging_face loader + train_df = backend.df_engine.from_pandas(train_df) + training_set_out = CacheableDataframe(df=train_df, name=training_set, checksum=None) + + if isinstance(validation_set, str) and validation_set.startswith(HF_PREFIX): + if validation_set == training_set: + # Reuse the loaded DF from the training split + val_df = backend.df_engine.from_pandas(val_df) + validation_set_out = CacheableDataframe(df=val_df, name=validation_set, checksum=None) + else: # This handles an edge case -- NOT EXPECTED USER BEHAVIOR + logging.warning( + "A Hugging Face validation set has been passed in that is different from the test set. " + "This is not recommended." + ) + validation_set_out = _load_cacheable_hf_dataset(validation_set, backend, split_set=VALIDATION) + + if isinstance(test_set, str) and test_set.startswith(HF_PREFIX): + if test_set == training_set: + # Reuse the loaded DF from the training split + test_df = backend.df_engine.from_pandas(test_df) + test_set_out = CacheableDataframe(df=test_df, name=test_set, checksum=None) + else: # This handles an edge case -- NOT EXPECTED USER BEHAVIOR + logging.warning( + "A Hugging Face test set has been passed in that is different from the training set. " + "This is not recommended." + ) + test_set_out = _load_cacheable_hf_dataset(test_set, backend, split_set=TEST) + + return dataset_out, training_set_out, validation_set_out, test_set_out + + +def _load_cacheable_hf_dataset( + dataset: str, backend: Backend, split_set: Literal["train", "validation", "test"] | None = None +) -> CacheableDataframe: + loader = get_dataset("hugging_face") + hf_id, hf_subsample = _get_hf_dataset_and_subsample(dataset) + if split_set: + train_df, validation_df, test_df = loader.load(hf_id, hf_subsample, split=True) + df = [train_df, validation_df, test_df][ + SPLITS.index(split_set) + ] # split_set should be one of TRAIN, VALIDATION, or TEST + else: + df = loader.load(hf_id, hf_subsample, split=False) + df = backend.df_engine.from_pandas(df) + return CacheableDataframe(df=df, name=dataset, checksum=None) + + +def _load_cacheable_dataset(dataset: str, backend: Backend) -> CacheableDataframe: + dataset_name = dataset[len(URI_PREFIX) :] + loader = get_dataset(dataset_name) + df = loader.load(split=False) + df = backend.df_engine.from_pandas(df) + return CacheableDataframe(df=df, name=dataset, checksum=str(loader.get_mtime())) + + +@PublicAPI +def list_datasets() -> list[str]: + """Returns a list of the names of all available datasets.""" + return sorted(_get_dataset_configs().keys()) + + +@PublicAPI +def get_datasets_output_features( + dataset: str | None = None, include_competitions: bool = True, include_data_modalities: bool = False +) -> dict: + """Returns a dictionary with the output features for each dataset. Optionally, you can pass a dataset name + which will then cause the function to return a dictionary with the output features for that dataset. + + Because Hugging Face Datasets are loaded dynamically through a shared connector, they don't have fixed output + features. As such, we exclude Hugging Face datasets here. + + Args: + dataset: Name of the dataset. + include_competitions: Whether to include the output features from kaggle competition datasets. + include_data_modalities: Whether to include the data modalities associated with the prediction task. + + Returns: + Dictionary with the output features for each dataset, or a dictionary with the output features for + the specified dataset. + """ + ordered_configs = OrderedDict(sorted(_get_dataset_configs().items())) + competition_datasets = [] + hugging_face_datasets = [] + + for name, config in ordered_configs.items(): + if not include_competitions and config.kaggle_competition: + competition_datasets.append(name) + continue + + if config.name == "hugging_face": + # There is no output_features attribute for hugging_face datasets + hugging_face_datasets.append(name) + continue + + ordered_configs[name] = {"name": config.name, "output_features": config.output_features} + + if include_data_modalities: + column_types = {column[TYPE] for column in config.columns} + + data_modalities = set() + if NUMBER in column_types or CATEGORY in column_types or BINARY in column_types: + data_modalities.add("Tabular") + if TEXT in column_types: + data_modalities.add("Text") + if IMAGE in column_types: + data_modalities.add("Image") + if AUDIO in column_types: + data_modalities.add("Audio") + + ordered_configs[name]["data_modalities"] = data_modalities + + if dataset: + return ordered_configs[dataset] + + if not include_competitions: + for competition in competition_datasets: + del ordered_configs[competition] + + del ordered_configs["hugging_face"] + + return ordered_configs + + +@PublicAPI +def describe_dataset(dataset_name: str) -> str: + """Returns the description of the dataset.""" + return _get_dataset_configs()[dataset_name].description + + +@PublicAPI +def download_dataset(dataset_name: str, output_dir: str = "."): + """Downloads the dataset to the specified directory.""" + output_dir = os.path.expanduser(os.path.normpath(output_dir)) + dataset = get_dataset(dataset_name) + dataset.export(output_dir) + + +@DeveloperAPI +def get_buffer(dataset_name: str, kaggle_username: str | None = None, kaggle_key: str | None = None) -> BytesIO: + """Returns a byte buffer for the specified dataset.""" + try: + if dataset_name.startswith(HF_PREFIX): + hf_id, hf_subsample = _get_hf_dataset_and_subsample(dataset_name) + dataset = get_dataset("hugging_face").load(hf_id, hf_subsample) + else: + dataset = get_dataset(dataset_name).load(kaggle_username=kaggle_username, kaggle_key=kaggle_key) + buffer = BytesIO(dataset.to_parquet()) + return buffer + except Exception as e: + logging.error(f"Failed to upload dataset {dataset_name}: {e}") + + +def _get_hf_dataset_and_subsample(dataset_name: str) -> tuple[str, str | None]: + """Returns the Hugging Face ID and subsample name from the dataset name. + + The dataset name should follow the format "{HF_PREFIX}{hf_id}--{hf_subsample}" + + Examples (Dataset Name --> HF ID; HF subsample): "hf://wikisql" --> "wikisql"; None "hf://ColumbiaNLP/FLUTE" --> + "ColumbiaNLP/FLUTE"; None "hf://mstz/adult--income" --> "mstz/adult"; "income" + """ + dataset_name = dataset_name[len(HF_PREFIX) :] + dataset_name = dataset_name.split("--") + if len(dataset_name) == 1: + return dataset_name[0], None + return dataset_name[0], dataset_name[1] + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This command downloads and lists Ludwig-ready datasets.", + prog="ludwig datasets", + usage="%(prog)s [options]", + ) + sub_parsers = parser.add_subparsers(dest="command", help="download and list datasets") + + parser_download = sub_parsers.add_parser("download", help="download a dataset") + parser_download.add_argument("dataset", help="dataset to download") + parser_download.add_argument( + "-o", + "--output_dir", + type=str, + default=".", + help="output directory to download into", + required=False, + ) + + sub_parsers.add_parser("list", help="list datasets") + + parser_describe = sub_parsers.add_parser("describe", help="describe datasets") + parser_describe.add_argument("dataset", help="dataset to describe") + + args = parser.parse_args(sys_argv) + print_ludwig(f"Datasets {args.command}", LUDWIG_VERSION) + + if args.command == "list": + datasets = list_datasets() + for ds in datasets: + print(ds) + elif args.command == "describe": + print(describe_dataset(args.dataset)) + elif args.command == "download": + download_dataset(args.dataset, args.output_dir) + else: + raise ValueError(f"Unrecognized command: {args.command}") + + +def __getattr__(name: str) -> Any: + """Module-level __getattr__ allows us to return an instance of a class. For example: + + from ludwig.datasets import titanic + + returns an instance of DatasetLoader configured to load titanic. + + If you want to download a dataset in a non-default ludwig cache directory, there are two options: + 1. set the LUDWIG_CACHE environment variable to your desired path before importing the dataset + 2. Use ludwig.datasets.get_dataset(dataset_name, cache_dir=) + """ + public_methods = { + "list_datasets", + "describe_dataset", + "download_dataset", + "cli", + "get_dataset", + "model_configs_for_dataset", + } + if name in public_methods: + return globals()[name] + return get_dataset(name) diff --git a/ludwig/datasets/archives.py b/ludwig/datasets/archives.py new file mode 100644 index 0000000..fc9deb0 --- /dev/null +++ b/ludwig/datasets/archives.py @@ -0,0 +1,140 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import gzip +import logging +import os +import shutil +import tarfile +from enum import Enum +from zipfile import ZipFile + +from ludwig.utils.fs_utils import upload_output_directory + +logger = logging.getLogger(__name__) + + +class ArchiveType(str, Enum): + """The type of file archive.""" + + UNKNOWN = "unknown" + ZIP = "zip" + GZIP = "gz" + TAR = "tar" + TAR_ZIP = "tar.z" + TAR_BZ2 = "tar.bz2" + TAR_GZ = "tar.gz" + + +def infer_archive_type(archive_path): + """Try to infer archive type from file extension.""" + # Get the path extension including multiple extensions, ex. ".tar.gz" + extension = ".".join(["", *os.path.basename(archive_path).split(".")[1:]]) + extension = extension.lower() + if extension.endswith(".tar.z") or extension.endswith(".tar.zip"): + return ArchiveType.TAR_ZIP + elif extension.endswith(".tar.bz2") or extension.endswith(".tbz2"): + return ArchiveType.TAR_BZ2 + elif extension.endswith(".tar.gz") or extension.endswith(".tgz"): + return ArchiveType.TAR_GZ + elif extension.endswith(".tar"): + return ArchiveType.TAR + elif extension.endswith(".zip") or extension.endswith(".zipx"): + return ArchiveType.ZIP + elif extension.endswith(".gz") or extension.endswith(".gzip"): + return ArchiveType.GZIP + else: + return ArchiveType.UNKNOWN + + +def is_archive(path): + """Does this path a supported archive type.""" + return infer_archive_type(path) != ArchiveType.UNKNOWN + + +def list_archive(archive_path, archive_type: ArchiveType | None = None) -> list[str]: + """Return list of files extracted in an archive (without extracting them).""" + if archive_type is None: + archive_type = infer_archive_type(archive_path) + if archive_type == ArchiveType.UNKNOWN: + logger.error( + f"Could not infer type of archive {archive_path}. May be an unsupported archive type." + "Specify archive_type in the dataset config if this file has an unknown file extension." + ) + return [] + if archive_type == ArchiveType.ZIP: + with ZipFile(archive_path) as zfile: + return zfile.namelist() + elif archive_type == ArchiveType.GZIP: + return [".".join(archive_path.split(".")[:-1])] # Path minus the .gz extension + elif archive_type in {ArchiveType.TAR, ArchiveType.TAR_ZIP, ArchiveType.TAR_BZ2, ArchiveType.TAR_GZ}: + with tarfile.open(archive_path) as tar_file: + return tar_file.getnames() + else: + logger.error(f"Unsupported archive: {archive_path}") + return [] + + +def extract_archive(archive_path: str, archive_type: ArchiveType | None = None) -> list[str]: + """Extracts files from archive (into the same directory), returns a list of extracted files. + + Args: + archive_path - The full path to the archive. + + Returns A list of the files extracted. + """ + if archive_type is None: + archive_type = infer_archive_type(archive_path) + if archive_type == ArchiveType.UNKNOWN: + logger.error( + f"Could not infer type of archive {archive_path}. May be an unsupported archive type." + "Specify archive_type in the dataset config if this file has an unknown file extension." + ) + return [] + archive_directory = os.path.dirname(archive_path) + directory_contents_before = os.listdir(archive_directory) + with upload_output_directory(archive_directory) as (tmpdir, _): + if archive_type == ArchiveType.ZIP: + with ZipFile(archive_path) as zfile: + zfile.extractall(tmpdir) + elif archive_type == ArchiveType.GZIP: + gzip_content_file = ".".join(archive_path.split(".")[:-1]) # Path minus the .gz extension + with gzip.open(archive_path) as gzfile: + with open(os.path.join(tmpdir, gzip_content_file), "wb") as output: + shutil.copyfileobj(gzfile, output) + elif archive_type in {ArchiveType.TAR, ArchiveType.TAR_ZIP, ArchiveType.TAR_BZ2, ArchiveType.TAR_GZ}: + with tarfile.open(archive_path) as tar_file: + + def is_within_directory(directory, target): + abs_directory = os.path.abspath(directory) + abs_target = os.path.abspath(target) + + prefix = os.path.commonprefix([abs_directory, abs_target]) + + return prefix == abs_directory + + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): + for member in tar.getmembers(): + member_path = os.path.join(path, member.name) + if not is_within_directory(path, member_path): + raise Exception("Attempted Path Traversal in Tar File") + + tar.extractall(path, members, numeric_owner=numeric_owner) + + safe_extract(tar_file, path=tmpdir) + else: + logger.error(f"Unsupported archive: {archive_path}") + directory_contents_after = set(os.listdir(archive_directory)) + return directory_contents_after.difference(directory_contents_before) diff --git a/ludwig/datasets/configs/__init__.py b/ludwig/datasets/configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/datasets/configs/abjad_kids.yaml b/ludwig/datasets/configs/abjad_kids.yaml new file mode 100644 index 0000000..725cf8a --- /dev/null +++ b/ludwig/datasets/configs/abjad_kids.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: abjad_kids +huggingface_dataset_id: Aziz-snoubra/Abjad-Kids +loader: hugging_face.HFLoader +description: | + Abjad-Kids: Arabic letter audio classification (28 classes). 40K examples. +columns: + - name: audio + type: audio + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/acronym_identification.yaml b/ludwig/datasets/configs/acronym_identification.yaml new file mode 100644 index 0000000..e3abc32 --- /dev/null +++ b/ludwig/datasets/configs/acronym_identification.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: acronym_identification +huggingface_dataset_id: amirveyseh/acronym_identification +loader: ner_loader.AcronymIdentificationLoader +description: | + Acronym identification: tokens → B-long/I-long/B-short/I-short/O tags. +columns: + - name: sentence + type: text + - name: labels + type: sequence +output_features: + - name: labels + type: sequence diff --git a/ludwig/datasets/configs/adult_census_income.yaml b/ludwig/datasets/configs/adult_census_income.yaml new file mode 100644 index 0000000..b54ab91 --- /dev/null +++ b/ludwig/datasets/configs/adult_census_income.yaml @@ -0,0 +1,48 @@ +version: 1.0 +name: adult_census_income +download_urls: + - https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data + - https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test +train_filenames: adult.data +test_filenames: adult.test +sha256: + adult.data: 5b00264637dbfec36bdeaab5676b0b309ff9eb788d63554ca0a249491c86603d + adult.test: a2a9044bc167a35b2361efbabec64e89d69ce82d9790d2980119aac5fd7e9c05 +loader: adult_census_income.AdultCensusIncomeLoader +description: | + Predict whether income exceeds $50K/yr based on census data + https://archive.ics.uci.edu/ml/datasets/adult +columns: + - name: age + type: number + - name: workclass + type: category + - name: fnlwgt + type: category + - name: education + type: category + - name: education-num + type: category + - name: marital-status + type: category + - name: occupation + type: category + - name: relationship + type: category + - name: race + type: category + - name: sex + type: category + - name: capital-gain + type: number + - name: capital-loss + type: number + - name: hours-per-week + type: number + - name: native-country + type: category + - name: income + type: category +output_features: + - name: income + type: binary diff --git a/ludwig/datasets/configs/adult_income_hf.yaml b/ludwig/datasets/configs/adult_income_hf.yaml new file mode 100644 index 0000000..a685159 --- /dev/null +++ b/ludwig/datasets/configs/adult_income_hf.yaml @@ -0,0 +1,39 @@ +version: 1.0 +name: adult_income_hf +huggingface_dataset_id: mstz/adult +loader: hugging_face.HFLoader +description: 'Adult/Census Income dataset for income >50K classification + + ' +columns: +- name: age + type: category +- name: capital_gain + type: number +- name: capital_loss + type: number +- name: education + type: category +- name: final_weight + type: category +- name: hours_worked_per_week + type: category +- name: marital_status + type: text +- name: native_country + type: text +- name: occupation + type: text +- name: race + type: text +- name: relationship + type: text +- name: is_male + type: binary +- name: workclass + type: text +- name: over_threshold + type: category +output_features: +- name: over_threshold + type: binary diff --git a/ludwig/datasets/configs/ae_price_prediction.yaml b/ludwig/datasets/configs/ae_price_prediction.yaml new file mode 100644 index 0000000..9b97098 --- /dev/null +++ b/ludwig/datasets/configs/ae_price_prediction.yaml @@ -0,0 +1,46 @@ +version: 1.0 +name: ae_price_prediction +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/ae_price_prediction/train.pq + - https://automl-mm-bench.s3.amazonaws.com/ae_price_prediction/test.pq +sha256: + test.pq: d05242580e011f3ac5a1a8f0069fd7788ceeacd6b2fb00ca7f409991f998c95e + train.pq: 181cfebbedd5c6e2bdc6261706103edddfc6eeb4604b8c6ffdc3d084a6e09a4e +train_filenames: train.pq +test_filenames: test.pq +description: | + Innerwear Data from Victoria's Secret and Others + 600,000+ innerwear product data extracted from popular retail sites + https://www.kaggle.com/PromptCloudHQ/innerwear-data-from-victorias-secret-and-others +columns: + - name: product_name + type: category + - name: mrp + type: category + - name: price + type: number + - name: pdp_url + type: category + - name: brand_name + type: category + - name: product_category + type: category + - name: retailer + type: category + - name: description + type: text + - name: rating + type: number + - name: review_count + type: number + - name: style_attributes + type: set + - name: total_sizes + type: set + - name: available_size + type: set + - name: color + type: category +output_features: + - name: price + type: number diff --git a/ludwig/datasets/configs/aegis_safety.yaml b/ludwig/datasets/configs/aegis_safety.yaml new file mode 100644 index 0000000..8944b1f --- /dev/null +++ b/ludwig/datasets/configs/aegis_safety.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: aegis_safety +huggingface_dataset_id: nvidia/Aegis-AI-Content-Safety-Dataset-2.0 +loader: hugging_face.HFLoader +description: | + NVIDIA Aegis 2.0: AI content safety classification. 30K train examples. +columns: + - name: prompt + type: text + - name: response + type: text + - name: prompt_label + type: category +output_features: + - name: prompt_label + type: category diff --git a/ludwig/datasets/configs/aeslc.yaml b/ludwig/datasets/configs/aeslc.yaml new file mode 100644 index 0000000..21b0646 --- /dev/null +++ b/ludwig/datasets/configs/aeslc.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: aeslc +huggingface_dataset_id: aeslc +loader: hugging_face.HFLoader +description: | + AESLC: annotated email subject line corpus for email body → subject summarization. +columns: + - name: email_body + type: text + - name: subject_line + type: text +output_features: + - name: subject_line + type: text diff --git a/ludwig/datasets/configs/ag_news_hf.yaml b/ludwig/datasets/configs/ag_news_hf.yaml new file mode 100644 index 0000000..9be8016 --- /dev/null +++ b/ludwig/datasets/configs/ag_news_hf.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: ag_news_hf +huggingface_dataset_id: fancyzhx/ag_news +loader: hugging_face.HFLoader +description: | + AG News 4-class topic classification (HF version) +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/agnews.yaml b/ludwig/datasets/configs/agnews.yaml new file mode 100644 index 0000000..511a5c5 --- /dev/null +++ b/ludwig/datasets/configs/agnews.yaml @@ -0,0 +1,23 @@ +version: 1.0 +name: agnews +download_urls: + - https://raw.githubusercontent.com/mhjabreel/CharCnn_Keras/master/data/ag_news_csv/train.csv + - https://raw.githubusercontent.com/mhjabreel/CharCnn_Keras/master/data/ag_news_csv/test.csv +train_filenames: train.csv +test_filenames: test.csv +sha256: + test.csv: 521465c2428ed7f02f8d6db6ffdd4b5447c1c701962353eb2c40d548c3c85699 + train.csv: 76a0a2d2f92b286371fe4d4044640910a04a803fdd2538e0f3f29a5c6f6b672e +loader: agnews.AGNewsLoader +description: | + News articles categorized as "World", "Sports", "Business", and "Science". +columns: + - name: class_index + type: category + - name: title + type: text + - name: description + type: text +output_features: + - name: class_index + type: category diff --git a/ludwig/datasets/configs/ai2d_diagrams.yaml b/ludwig/datasets/configs/ai2d_diagrams.yaml new file mode 100644 index 0000000..d7062e6 --- /dev/null +++ b/ludwig/datasets/configs/ai2d_diagrams.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: ai2d_diagrams +huggingface_dataset_id: HuggingFaceM4/the_cauldron +huggingface_subsample: ai2d +loader: vqa_loader.AI2DiagramsLoader +description: | + AI2 Diagrams VQA: diagram image + question → answer. +columns: + - name: image_path + type: image + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/ai_generated_ecommerce.yaml b/ludwig/datasets/configs/ai_generated_ecommerce.yaml new file mode 100644 index 0000000..851ca6e --- /dev/null +++ b/ludwig/datasets/configs/ai_generated_ecommerce.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: ai_generated_ecommerce +huggingface_dataset_id: JoyCN/ai-generated-ecommerce-images +loader: hugging_face.HFLoader +description: | + AI-generated vs real e-commerce product images. 6K examples. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/allocine.yaml b/ludwig/datasets/configs/allocine.yaml new file mode 100644 index 0000000..cd6c632 --- /dev/null +++ b/ludwig/datasets/configs/allocine.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: allocine +huggingface_dataset_id: allocine +loader: hugging_face.HFLoader +description: | + AlloCine: French movie review sentiment dataset. Binary positive/negative labels. +columns: + - name: review + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/allstate_claims_severity.yaml b/ludwig/datasets/configs/allstate_claims_severity.yaml new file mode 100644 index 0000000..f525b4f --- /dev/null +++ b/ludwig/datasets/configs/allstate_claims_severity.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: allstate_claims_severity +kaggle_competition: allstate-claims-severity +archive_filenames: allstate-claims-severity.zip +sha256: + allstate-claims-severity.zip: 165f7b4bc5ed40f43656dc958da6572143a7e126e2d37bcd41f1299bfbaa68e2 +train_filenames: train.csv +test_filenames: test.csv +loader: allstate_claims_severity.AllstateClaimsSeverityLoader +description: | + Allstate Claims Severity. + https://www.kaggle.com/c/allstate-claims-severity/overview +output_features: + - name: loss + type: number diff --git a/ludwig/datasets/configs/alpaca.yaml b/ludwig/datasets/configs/alpaca.yaml new file mode 100644 index 0000000..e606612 --- /dev/null +++ b/ludwig/datasets/configs/alpaca.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: alpaca +download_urls: https://raw.githubusercontent.com/tatsu-lab/stanford_alpaca/main/alpaca_data.json +dataset_filenames: alpaca_data.json +description: | + Stanford Alpaca instruction-tuning dataset (https://github.com/tatsu-lab/stanford_alpaca) for LLM fine-tuning. +columns: + - name: instruction + type: text + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/alpaca_cleaned.yaml b/ludwig/datasets/configs/alpaca_cleaned.yaml new file mode 100644 index 0000000..43324c8 --- /dev/null +++ b/ludwig/datasets/configs/alpaca_cleaned.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: alpaca_cleaned +huggingface_dataset_id: yahma/alpaca-cleaned +loader: hugging_face.HFLoader +description: | + Alpaca Cleaned: 52K instruction-following samples, cleaned version of Stanford Alpaca. +columns: + - name: instruction + type: text + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/alpaca_gpt4.yaml b/ludwig/datasets/configs/alpaca_gpt4.yaml new file mode 100644 index 0000000..681018e --- /dev/null +++ b/ludwig/datasets/configs/alpaca_gpt4.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: alpaca_gpt4 +huggingface_dataset_id: vicgalle/alpaca-gpt4 +loader: hugging_face.HFLoader +description: | + Alpaca GPT-4: 52K instruction-following examples. +columns: + - name: instruction + type: text + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/alpaca_gpt4_zh.yaml b/ludwig/datasets/configs/alpaca_gpt4_zh.yaml new file mode 100644 index 0000000..a589f41 --- /dev/null +++ b/ludwig/datasets/configs/alpaca_gpt4_zh.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: alpaca_gpt4_zh +huggingface_dataset_id: llamafactory/alpaca_gpt4_zh +loader: hugging_face.HFLoader +description: | + Alpaca GPT-4 Chinese: 42K Chinese instruction-following examples. +columns: + - name: instruction + type: text + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/amazon_employee_access_challenge.yaml b/ludwig/datasets/configs/amazon_employee_access_challenge.yaml new file mode 100644 index 0000000..14a275c --- /dev/null +++ b/ludwig/datasets/configs/amazon_employee_access_challenge.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: amazon_employee_access_challenge +kaggle_competition: amazon-employee-access-challenge +archive_filenames: amazon-employee-access-challenge.zip +train_filenames: train.csv +test_filenames: test.csv +sha256: + amazon-employee-access-challenge.zip: bba1cf24bc01f390e7faf3f9cdbebd6267c875d51a36a2c625ce66e0c3e71db7 +description: | + There is a considerable amount of data regarding an employee’s role within an organization and the resources to which + they have access. Given the data related to current employees and their provisioned access, models can be built that + automatically determine access privileges as employees enter and leave roles within a company. + https://www.kaggle.com/c/amazon-employee-access-challenge +output_features: + - name: ACTION + type: binary diff --git a/ludwig/datasets/configs/amazon_massive_intent.yaml b/ludwig/datasets/configs/amazon_massive_intent.yaml new file mode 100644 index 0000000..d144f7d --- /dev/null +++ b/ludwig/datasets/configs/amazon_massive_intent.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: amazon_massive_intent +huggingface_dataset_id: mteb/amazon_massive_intent +loader: hugging_face.HFLoader +description: | + Amazon MASSIVE multilingual intent classification (60 intents, all 51 languages combined). 587K train examples. +columns: + - name: text + type: text + - name: label + type: category + - name: lang + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/amazon_massive_scenario.yaml b/ludwig/datasets/configs/amazon_massive_scenario.yaml new file mode 100644 index 0000000..99b46cf --- /dev/null +++ b/ludwig/datasets/configs/amazon_massive_scenario.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: amazon_massive_scenario +huggingface_dataset_id: mteb/amazon_massive_scenario +loader: hugging_face.HFLoader +description: | + Amazon MASSIVE multilingual scenario classification (18 scenarios, 60 languages). 587K train. +columns: + - name: text + type: text + - name: label + type: category + - name: lang + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/amazon_polarity.yaml b/ludwig/datasets/configs/amazon_polarity.yaml new file mode 100644 index 0000000..dde0b5c --- /dev/null +++ b/ludwig/datasets/configs/amazon_polarity.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: amazon_polarity +huggingface_dataset_id: fancyzhx/amazon_polarity +loader: hugging_face.HFLoader +description: | + Amazon product review polarity; positive/negative +columns: + - name: label + type: category + - name: title + type: text + - name: content + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/amazon_review_polarity.yaml b/ludwig/datasets/configs/amazon_review_polarity.yaml new file mode 100644 index 0000000..fb07766 --- /dev/null +++ b/ludwig/datasets/configs/amazon_review_polarity.yaml @@ -0,0 +1,27 @@ +version: 1.0 +name: amazon_review_polarity +download_urls: https://s3.amazonaws.com/fast-ai-nlp/amazon_review_polarity_csv.tgz +train_filenames: amazon_review_polarity_csv/train.csv +test_filenames: amazon_review_polarity_csv/test.csv +sha256: + amazon_review_polarity_csv.tgz: d2a3ee7a214497a5d1b8eaed7c8d7ba2737de00ada3b0ec46243983efa100361 +description: | + The Amazon Reviews Polarity dataset + Details: + 34,686,770 Amazon reviews from 6,643,669 users on 2,441,053 + products, from the Stanford Network Analysis Project (SNAP). + This dataset contains 600,000 training samples and 130,000 + testing samples in each class. + Dataset source: + Character-level Convolutional Networks for Text Classification + Xiang Zhang et al., 2015 +columns: + - name: label + type: binary + - name: review_title + type: text + - name: review_text + type: text +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/amazon_reviews.yaml b/ludwig/datasets/configs/amazon_reviews.yaml new file mode 100644 index 0000000..1795ff3 --- /dev/null +++ b/ludwig/datasets/configs/amazon_reviews.yaml @@ -0,0 +1,27 @@ +version: 1.0 +name: amazon_reviews +download_urls: https://s3.amazonaws.com/fast-ai-nlp/amazon_review_full_csv.tgz +train_filenames: amazon_review_full_csv/train.csv +test_filenames: amazon_review_full_csv/test.csv +sha256: + amazon_review_full_csv.tgz: 4af62eeee139d0142e0747340b68646d23483d9475c33ea0641ee9175b423443 +description: | + The Amazon Reviews dataset + Details: + 34,686,770 Amazon reviews from 6,643,669 users on 2,441,053 + products, from the Stanford Network Analysis Project (SNAP). + This dataset contains 600,000 training samples and 130,000 + testing samples in each class. + Dataset source: + Character-level Convolutional Networks for Text Classification + Xiang Zhang et al., 2015 +columns: + - name: label + type: category + - name: review_title + type: text + - name: review_text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/amazon_reviews_2023.yaml b/ludwig/datasets/configs/amazon_reviews_2023.yaml new file mode 100644 index 0000000..3261300 --- /dev/null +++ b/ludwig/datasets/configs/amazon_reviews_2023.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: amazon_reviews_2023 +huggingface_dataset_id: bagadbilla/amazon-reviews-2023-trimmed +loader: hugging_face.HFLoader +description: | + Amazon Reviews 2023: predict star rating from review title and text. 571M train examples. +columns: + - name: title + type: text + - name: text + type: text + - name: rating + type: number +output_features: + - name: rating + type: number diff --git a/ludwig/datasets/configs/amazon_science_massive.yaml b/ludwig/datasets/configs/amazon_science_massive.yaml new file mode 100644 index 0000000..aa944ee --- /dev/null +++ b/ludwig/datasets/configs/amazon_science_massive.yaml @@ -0,0 +1,22 @@ +version: 1.0 +name: amazon_science_massive +huggingface_dataset_id: AmazonScience/massive +huggingface_revision: refs/convert/parquet +huggingface_data_files: + train: all/train/0000.parquet +loader: hugging_face.HFLoader +description: | + Amazon MASSIVE multilingual intent classification dataset (51 languages, 60 intents). + Predict intent category from utterance text. +columns: + - name: utt + type: text + - name: locale + type: category + - name: intent + type: category + - name: scenario + type: category +output_features: + - name: intent + type: category diff --git a/ludwig/datasets/configs/ambig_qa.yaml b/ludwig/datasets/configs/ambig_qa.yaml new file mode 100644 index 0000000..206f6d7 --- /dev/null +++ b/ludwig/datasets/configs/ambig_qa.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: ambig_qa +huggingface_dataset_id: ambig_qa +loader: hugging_face.HFLoader +description: | + AmbigQA: open-domain QA with ambiguous questions. Text-in text-out task. +columns: + - name: question + type: text + - name: annotations + type: text +output_features: + - name: annotations + type: text diff --git a/ludwig/datasets/configs/ames_housing.yaml b/ludwig/datasets/configs/ames_housing.yaml new file mode 100644 index 0000000..be4ce34 --- /dev/null +++ b/ludwig/datasets/configs/ames_housing.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: ames_housing +kaggle_competition: house-prices-advanced-regression-techniques +archive_filenames: house-prices-advanced-regression-techniques.zip +train_filenames: train.csv +test_filenames: test.csv +sha256: + house-prices-advanced-regression-techniques.zip: 65f769a9157a2581671957ed08da8a8162d53e67b4e9970ee856b634deb11d9f +description: | + The Ames Housing dataset. + https://www.kaggle.com/c/house-prices-advanced-regression-techniques +output_features: + - name: SalePrice + type: number diff --git a/ludwig/datasets/configs/ami_asr.yaml b/ludwig/datasets/configs/ami_asr.yaml new file mode 100644 index 0000000..4b7f205 --- /dev/null +++ b/ludwig/datasets/configs/ami_asr.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: ami_asr +huggingface_dataset_id: edinburghcstr/ami +huggingface_subsample: ihm +loader: hugging_face.HFLoader +description: | + AMI corpus: meeting audio transcription. 108K train examples. +columns: + - name: text + type: text + - name: audio + type: audio + - name: speaker_id + type: category +output_features: + - name: text + type: text diff --git a/ludwig/datasets/configs/anli.yaml b/ludwig/datasets/configs/anli.yaml new file mode 100644 index 0000000..f1c9cca --- /dev/null +++ b/ludwig/datasets/configs/anli.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: anli +huggingface_dataset_id: facebook/anli +loader: hugging_face.HFLoader +description: | + Adversarial NLI; iteratively adversarial NLI benchmark +columns: + - name: uid + type: text + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category + - name: reason + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/app_reviews.yaml b/ludwig/datasets/configs/app_reviews.yaml new file mode 100644 index 0000000..a34b1a8 --- /dev/null +++ b/ludwig/datasets/configs/app_reviews.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: app_reviews +huggingface_dataset_id: app_reviews +loader: hugging_face.HFLoader +description: | + App Reviews: 288K mobile app reviews with 1-5 star ratings. +columns: + - name: review + type: text + - name: star + type: number +output_features: + - name: star + type: number diff --git a/ludwig/datasets/configs/aqua_rat.yaml b/ludwig/datasets/configs/aqua_rat.yaml new file mode 100644 index 0000000..a296ecc --- /dev/null +++ b/ludwig/datasets/configs/aqua_rat.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: aqua_rat +huggingface_dataset_id: deepmind/aqua_rat +huggingface_subsample: raw +loader: hugging_face.HFLoader +description: | + AQuA-RAT: algebraic word problems 5-way MC with rationales. 97K train examples. +columns: + - name: question + type: text + - name: rationale + type: text + - name: correct + type: category +output_features: + - name: correct + type: category diff --git a/ludwig/datasets/configs/arc_challenge.yaml b/ludwig/datasets/configs/arc_challenge.yaml new file mode 100644 index 0000000..2a83060 --- /dev/null +++ b/ludwig/datasets/configs/arc_challenge.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: arc_challenge +huggingface_dataset_id: allenai/ai2_arc +huggingface_subsample: ARC-Challenge +loader: multiple_choice_loader.ArcLoader +description: | + AI2 ARC-Challenge harder science QA. 4-way multiple choice. +columns: + - name: question + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/arc_easy.yaml b/ludwig/datasets/configs/arc_easy.yaml new file mode 100644 index 0000000..512651e --- /dev/null +++ b/ludwig/datasets/configs/arc_easy.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: arc_easy +huggingface_dataset_id: allenai/ai2_arc +huggingface_subsample: ARC-Easy +loader: multiple_choice_loader.ArcLoader +description: | + AI2 ARC-Easy science QA. 4-way multiple choice. A/B/C/D. +columns: + - name: question + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/arxiv_abstracts_2021.yaml b/ludwig/datasets/configs/arxiv_abstracts_2021.yaml new file mode 100644 index 0000000..cd4a3b7 --- /dev/null +++ b/ludwig/datasets/configs/arxiv_abstracts_2021.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: arxiv_abstracts_2021 +huggingface_dataset_id: gfissore/arxiv-abstracts-2021 +loader: hugging_face.HFLoader +description: | + ArXiv abstracts 2021: predict abstract from title. 2M train examples. +columns: + - name: title + type: text + - name: abstract + type: text +output_features: + - name: abstract + type: text diff --git a/ludwig/datasets/configs/arxiv_summarization.yaml b/ludwig/datasets/configs/arxiv_summarization.yaml new file mode 100644 index 0000000..c91e1ca --- /dev/null +++ b/ludwig/datasets/configs/arxiv_summarization.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: arxiv_summarization +huggingface_dataset_id: ccdv/arxiv-summarization +huggingface_subsample: document +loader: hugging_face.HFLoader +description: | + ArXiv document summarization: predict abstract from full paper text. 203K train examples. +columns: + - name: article + type: text + - name: abstract + type: text +output_features: + - name: abstract + type: text diff --git a/ludwig/datasets/configs/audioset_balanced.yaml b/ludwig/datasets/configs/audioset_balanced.yaml new file mode 100644 index 0000000..2938708 --- /dev/null +++ b/ludwig/datasets/configs/audioset_balanced.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: audioset_balanced +huggingface_dataset_id: agkphysics/AudioSet +huggingface_subsample: balanced +loader: hugging_face.HFLoader +description: | + AudioSet balanced: audio event classification with 527 sound classes from YouTube. 18K train examples. +columns: + - name: audio + type: audio + - name: labels + type: sequence + - name: human_labels + type: sequence +output_features: + - name: labels + type: sequence diff --git a/ludwig/datasets/configs/banking77.yaml b/ludwig/datasets/configs/banking77.yaml new file mode 100644 index 0000000..91cf5c8 --- /dev/null +++ b/ludwig/datasets/configs/banking77.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: banking77 +huggingface_dataset_id: mteb/banking77 +loader: hugging_face.HFLoader +description: | + Banking77; 77-class banking customer intent classification +columns: + - name: text + type: text + - name: label + type: category + - name: label_text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/banking77_legacy.yaml b/ludwig/datasets/configs/banking77_legacy.yaml new file mode 100644 index 0000000..43db079 --- /dev/null +++ b/ludwig/datasets/configs/banking77_legacy.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: banking77_legacy +huggingface_dataset_id: legacy-datasets/banking77 +loader: hugging_face.HFLoader +description: | + Banking77 (legacy) 77-class banking intent classification. 10K train examples. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/bbcnews.yaml b/ludwig/datasets/configs/bbcnews.yaml new file mode 100644 index 0000000..78c7694 --- /dev/null +++ b/ludwig/datasets/configs/bbcnews.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: bbcnews +kaggle_competition: learn-ai-bbc +archive_filenames: learn-ai-bbc.zip +train_filenames: "BBC News Train.csv" +test_filenames: "BBC News Test.csv" +sha256: + learn-ai-bbc.zip: 450dd79c6654248af15d91d94c269fe7e8001effd89389f93c7184aac6699e62 +description: | + BBC News Classification from Kaggle. + https://www.kaggle.com/competitions/learn-ai-bbc/overview +output_features: + - name: Category + type: category diff --git a/ludwig/datasets/configs/bbh.yaml b/ludwig/datasets/configs/bbh.yaml new file mode 100644 index 0000000..45a7682 --- /dev/null +++ b/ludwig/datasets/configs/bbh.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: bbh +huggingface_dataset_id: lukaemon/bbh +huggingface_subsample: boolean_expressions +loader: multiple_choice_loader.BbhLoader +description: | + Big-Bench Hard boolean expressions. Input → target. +columns: + - name: input + type: text + - name: target + type: text +output_features: + - name: target + type: text diff --git a/ludwig/datasets/configs/beans.yaml b/ludwig/datasets/configs/beans.yaml new file mode 100644 index 0000000..fbab0ca --- /dev/null +++ b/ludwig/datasets/configs/beans.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: beans +huggingface_dataset_id: AI-Lab-Makerere/beans +loader: hugging_face.HFLoader +description: | + Beans leaf disease classification; 3 classes: angular leaf spot, bean rust, healthy +columns: + - name: image_file_path + type: text + - name: image + type: image + - name: labels + type: category +output_features: + - name: labels + type: category diff --git a/ludwig/datasets/configs/beavertails.yaml b/ludwig/datasets/configs/beavertails.yaml new file mode 100644 index 0000000..8562024 --- /dev/null +++ b/ludwig/datasets/configs/beavertails.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: beavertails +huggingface_dataset_id: PKU-Alignment/BeaverTails +loader: multilabel_loader.BeaverTailsLoader +description: | + BeaverTails LLM safety: prompt+response → is_safe binary + category. +columns: + - name: prompt + type: text + - name: response + type: text + - name: is_safe + type: binary + - name: category_labels + type: set +output_features: + - name: is_safe + type: binary diff --git a/ludwig/datasets/configs/belebele.yaml b/ludwig/datasets/configs/belebele.yaml new file mode 100644 index 0000000..dcc5f5a --- /dev/null +++ b/ludwig/datasets/configs/belebele.yaml @@ -0,0 +1,34 @@ +version: 1.0 +name: belebele +huggingface_dataset_id: facebook/belebele +huggingface_subsample: eng_Latn +loader: hugging_face.HFLoader +description: 'Belebele multilingual reading comprehension; multiple-choice + + ' +columns: +- name: link + type: text +- name: question_number + type: category +- name: flores_passage + type: text +- name: question + type: text +- name: mc_answer1 + type: text +- name: mc_answer2 + type: text +- name: mc_answer3 + type: text +- name: mc_answer4 + type: text +- name: correct_answer_num + type: text +- name: dialect + type: text +- name: ds + type: text +output_features: +- name: correct_answer_num + type: category diff --git a/ludwig/datasets/configs/belebele_fr.yaml b/ludwig/datasets/configs/belebele_fr.yaml new file mode 100644 index 0000000..ebf1745 --- /dev/null +++ b/ludwig/datasets/configs/belebele_fr.yaml @@ -0,0 +1,34 @@ +version: 1.0 +name: belebele_fr +huggingface_dataset_id: facebook/belebele +huggingface_subsample: fra_Latn +loader: hugging_face.HFLoader +description: 'Belebele French; multilingual reading comprehension + + ' +columns: +- name: link + type: text +- name: question_number + type: category +- name: flores_passage + type: text +- name: question + type: text +- name: mc_answer1 + type: text +- name: mc_answer2 + type: text +- name: mc_answer3 + type: text +- name: mc_answer4 + type: text +- name: correct_answer_num + type: text +- name: dialect + type: text +- name: ds + type: text +output_features: +- name: correct_answer_num + type: category diff --git a/ludwig/datasets/configs/big_patent.yaml b/ludwig/datasets/configs/big_patent.yaml new file mode 100644 index 0000000..f280636 --- /dev/null +++ b/ludwig/datasets/configs/big_patent.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: big_patent +huggingface_dataset_id: NortheasternUniversity/big_patent +huggingface_subsample: a +loader: hugging_face.HFLoader +description: | + BigPatent: patent claim summarization (category A). 1.2M train examples. +columns: + - name: description + type: text + - name: abstract + type: text +output_features: + - name: abstract + type: text diff --git a/ludwig/datasets/configs/bigbench.yaml b/ludwig/datasets/configs/bigbench.yaml new file mode 100644 index 0000000..2ba600f --- /dev/null +++ b/ludwig/datasets/configs/bigbench.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: bigbench +huggingface_dataset_id: tasksource/bigbench +huggingface_subsample: abstract_narrative_understanding +loader: hugging_face.HFLoader +description: | + BigBench: abstract narrative understanding task. 2400 train examples. +columns: + - name: inputs + type: text + - name: targets + type: text +output_features: + - name: targets + type: text diff --git a/ludwig/datasets/configs/billsum.yaml b/ludwig/datasets/configs/billsum.yaml new file mode 100644 index 0000000..1dca59c --- /dev/null +++ b/ludwig/datasets/configs/billsum.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: billsum +huggingface_dataset_id: FiscalNote/billsum +loader: hugging_face.HFLoader +description: | + BillSum; US congressional and California bill summarization +columns: + - name: text + type: text + - name: summary + type: text + - name: title + type: text +output_features: + - name: text + type: text diff --git a/ludwig/datasets/configs/bitext_customer_intent.yaml b/ludwig/datasets/configs/bitext_customer_intent.yaml new file mode 100644 index 0000000..5dd39aa --- /dev/null +++ b/ludwig/datasets/configs/bitext_customer_intent.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: bitext_customer_intent +huggingface_dataset_id: bitext/Bitext-customer-support-llm-chatbot-training-dataset +loader: hugging_face.HFLoader +description: | + Bitext customer support intent detection (26K train, 27 intents) +columns: + - name: instruction + type: text + - name: intent + type: category + - name: response + type: text +output_features: + - name: intent + type: category diff --git a/ludwig/datasets/configs/bitext_customer_support.yaml b/ludwig/datasets/configs/bitext_customer_support.yaml new file mode 100644 index 0000000..5e6f883 --- /dev/null +++ b/ludwig/datasets/configs/bitext_customer_support.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: bitext_customer_support +huggingface_dataset_id: bitext/Bitext-customer-support-llm-chatbot-training-dataset +loader: hugging_face.HFLoader +description: | + Bitext customer support: 27 intents, 10 categories. 26K train examples. +columns: + - name: instruction + type: text + - name: category + type: category + - name: intent + type: category +output_features: + - name: intent + type: category diff --git a/ludwig/datasets/configs/blimp.yaml b/ludwig/datasets/configs/blimp.yaml new file mode 100644 index 0000000..baed0fe --- /dev/null +++ b/ludwig/datasets/configs/blimp.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: blimp +huggingface_dataset_id: nyu-mll/blimp +huggingface_subsample: adjunct_island +loader: misc_loaders.BlimpLoader +description: | + BLiMP: Benchmark of Linguistic Minimal Pairs. Binary grammaticality + classification — given a sentence, predict if it is grammatically acceptable. + Each source row is expanded into two rows (good/bad sentence). +columns: + - name: sentence + type: text + - name: is_grammatical + type: binary +output_features: + - name: is_grammatical + type: binary diff --git a/ludwig/datasets/configs/bnp_claims_management.yaml b/ludwig/datasets/configs/bnp_claims_management.yaml new file mode 100644 index 0000000..1089005 --- /dev/null +++ b/ludwig/datasets/configs/bnp_claims_management.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: bnp_claims_management +kaggle_competition: bnp-paribas-cardif-claims-management +archive_filenames: bnp-paribas-cardif-claims-management.zip +train_filenames: train.csv +test_filenames: test.csv +sha256: + bnp-paribas-cardif-claims-management.zip: c01a11ceae565bc95ec30a1ef4c9ffe4aa27e07d6e433776e90a4d5474f3e95d +description: | + The BNP Paribas Cardif Claims Management dataset. + https://www.kaggle.com/c/bnp-paribas-cardif-claims-management +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/bookprice_prediction.yaml b/ludwig/datasets/configs/bookprice_prediction.yaml new file mode 100644 index 0000000..05ecb72 --- /dev/null +++ b/ludwig/datasets/configs/bookprice_prediction.yaml @@ -0,0 +1,37 @@ +version: 1.0 +name: bookprice_prediction +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_price_of_books/train.csv + - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_price_of_books/test.csv +sha256: + test.csv: 75bcc853efe734a53764127428e005bb9eb7585ad3dc1dce2eb284fa04313c1b + train.csv: dd978b591e623f9c5d4f9ade0f237200597afcad2c6417eb1e764698f1afcfcf +train_filenames: train.csv +test_filenames: test.csv +description: | + Here we explore a database of books of different genres, from thousands of authors. + In this challenge, participants are required to use the dataset to build a + Machine Learning model to predict the price of books based on a given set of features. + https://machinehack.com/hackathons/predict_the_price_of_books/overview +columns: + - name: Title + type: category + - name: Author + type: category + - name: Edition + type: category + - name: Reviews + type: number + - name: Ratings + type: number + - name: Synopsis + type: text + - name: Genre + type: category + - name: BookCategory + type: category + - name: Price + type: number +output_features: + - name: Price + type: number diff --git a/ludwig/datasets/configs/boolq.yaml b/ludwig/datasets/configs/boolq.yaml new file mode 100644 index 0000000..4196ac7 --- /dev/null +++ b/ludwig/datasets/configs/boolq.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: boolq +huggingface_dataset_id: aps/super_glue +huggingface_subsample: boolq +loader: hugging_face.HFLoader +description: | + Boolean Questions; reading comprehension yes/no questions from Google +columns: + - name: question + type: text + - name: passage + type: text + - name: idx + type: category + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/boolq_standalone.yaml b/ludwig/datasets/configs/boolq_standalone.yaml new file mode 100644 index 0000000..258c04c --- /dev/null +++ b/ludwig/datasets/configs/boolq_standalone.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: boolq_standalone +huggingface_dataset_id: google/boolq +loader: hugging_face.HFLoader +description: | + BoolQ standalone; naturally occurring yes/no questions with passage +columns: + - name: question + type: text + - name: answer + type: binary + - name: passage + type: text +output_features: + - name: answer + type: binary diff --git a/ludwig/datasets/configs/bornholm_bitext.yaml b/ludwig/datasets/configs/bornholm_bitext.yaml new file mode 100644 index 0000000..7b8fc75 --- /dev/null +++ b/ludwig/datasets/configs/bornholm_bitext.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: bornholm_bitext +huggingface_dataset_id: mteb/BornholmBitextMining +loader: hugging_face.HFLoader +description: | + Bornholm Bitext Mining: Danish-Bornholmsk low-resource translation. 5785 examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text +output_features: + - name: sentence2 + type: text diff --git a/ludwig/datasets/configs/brazilian_toxic_tweets.yaml b/ludwig/datasets/configs/brazilian_toxic_tweets.yaml new file mode 100644 index 0000000..4b722ed --- /dev/null +++ b/ludwig/datasets/configs/brazilian_toxic_tweets.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: brazilian_toxic_tweets +huggingface_dataset_id: mteb/BrazilianToxicTweetsClassification +loader: hugging_face.HFLoader +description: | + Brazilian toxic tweets binary classification. 8K train examples. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/california_house_price.yaml b/ludwig/datasets/configs/california_house_price.yaml new file mode 100644 index 0000000..6509c29 --- /dev/null +++ b/ludwig/datasets/configs/california_house_price.yaml @@ -0,0 +1,100 @@ +version: 1.0 +name: california_house_price +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/kaggle-california-house-prices/train.csv + - https://automl-mm-bench.s3.amazonaws.com/kaggle-california-house-prices/test.csv +sha256: + test.csv: b5bb9ed6e56cbdd0a410e186a19c6fe137c2ffbb50ba6b0808540434a8123dc6 + train.csv: 907d45804e622fb136a9d55bde97269f421fb9b8f7c9f34416672cf7078ee94b +train_filenames: train.csv +test_filenames: test.csv +description: | + Predict house sale prices based on the house information, such as # of bedrooms, + living areas, locations, near-by schools, and the seller summary. The data consist + of houses sold in California in 2020, with houses in the test dataset sold after + the ones in the training dataset. + https://www.kaggle.com/c/california-house-prices +columns: + - name: Address + type: category + - name: Sold Price + type: number + - name: Summary + type: text + - name: Type + type: category + - name: Year built + type: number + - name: Heating + type: category + - name: Cooling + type: category + - name: Parking + type: category + - name: Lot + type: number + - name: Bedrooms + type: number + - name: Bathrooms + type: number + - name: Full bathrooms + type: number + - name: Total interior livable area + type: number + - name: Total spaces + type: number + - name: Garage spaces + type: number + - name: Region + type: category + - name: Elementary School + type: category + - name: Elementary School Score + type: number + - name: Elementary School Distance + type: number + - name: Middle School + type: category + - name: Middle School Score + type: number + - name: Middle School Distance + type: number + - name: High School + type: category + - name: High School Score + type: number + - name: High School Distance + type: number + - name: Flooring + type: set + - name: Heating features + type: set + - name: Cooling features + type: set + - name: Appliances included + type: set + - name: Laundry features + type: set + - name: Parking features + type: set + - name: Tax assessed value + type: number + - name: Annual tax amount + type: number + - name: Listed On + type: date + - name: Listed Price + type: number + - name: Last Sold On + type: date + - name: Last Sold Price + type: number + - name: City + type: category + - name: Zip + type: category + - name: State + type: category +output_features: + - name: Sold Price + type: number diff --git a/ludwig/datasets/configs/california_housing.yaml b/ludwig/datasets/configs/california_housing.yaml new file mode 100644 index 0000000..1ee2f7f --- /dev/null +++ b/ludwig/datasets/configs/california_housing.yaml @@ -0,0 +1,32 @@ +version: 1.0 +name: california_housing +loader: california_housing.CaliforniaHousingLoader +description: | + California Housing dataset from the 1990 US Census. Predict median house value + for California districts from 8 numerical features (median income, house age, + average rooms, etc.). Standard ML regression benchmark used in the FT-Transformer + paper (Gorishniy et al., NeurIPS 2021). + + Originally from StatLib, available via sklearn.datasets.fetch_california_housing. +columns: + - name: MedInc + type: number + - name: HouseAge + type: number + - name: AveRooms + type: number + - name: AveBedrms + type: number + - name: Population + type: number + - name: AveOccup + type: number + - name: Latitude + type: number + - name: Longitude + type: number + - name: target + type: number +output_features: + - name: target + type: number diff --git a/ludwig/datasets/configs/camseq.yaml b/ludwig/datasets/configs/camseq.yaml new file mode 100644 index 0000000..ce426b4 --- /dev/null +++ b/ludwig/datasets/configs/camseq.yaml @@ -0,0 +1,21 @@ +version: 1.0 +name: camseq +kaggle_dataset_id: carlolepelaars/camseq-semantic-segmentation +archive_filenames: camseq-semantic-segmentation.zip +sha256: + camseq-semantic-segmentation.zip: ea3aeba2661d9b3e3ea406668e7d9240cb2ba0c7e374914bb6d866147faff502 +loader: camseq.CamseqLoader +preserve_paths: + - images + - masks +description: | + CamSeq01 Cambridge Labeled Objects in Video + https://www.kaggle.com/datasets/carlolepelaars/camseq-semantic-segmentation +columns: + - name: image_path + type: image + - name: mask_path + type: image +output_features: + - name: mask_path + type: image diff --git a/ludwig/datasets/configs/cantonese_asr.yaml b/ludwig/datasets/configs/cantonese_asr.yaml new file mode 100644 index 0000000..62427a8 --- /dev/null +++ b/ludwig/datasets/configs/cantonese_asr.yaml @@ -0,0 +1,21 @@ +version: 1.0 +name: cantonese_asr +huggingface_dataset_id: CanCLID/zoengjyutgaai +loader: hugging_face.HFLoader +description: 'Cantonese speech recognition dataset + + ' +columns: +- name: id + type: text +- name: episode_id + type: category +- name: audio + type: audio +- name: audio_duration + type: number +- name: transcription + type: text +output_features: +- name: transcription + type: text diff --git a/ludwig/datasets/configs/ccnews.yaml b/ludwig/datasets/configs/ccnews.yaml new file mode 100644 index 0000000..dc7dd83 --- /dev/null +++ b/ludwig/datasets/configs/ccnews.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: ccnews +huggingface_dataset_id: stanford-oval/ccnews +loader: hugging_face.HFLoader +description: | + CC-News: multilingual web news articles from Common Crawl. 1.9M train examples. +columns: + - name: title + type: text + - name: plain_text + type: text + - name: sitename + type: category +output_features: + - name: sitename + type: category diff --git a/ludwig/datasets/configs/cifar10.yaml b/ludwig/datasets/configs/cifar10.yaml new file mode 100644 index 0000000..5f2543a --- /dev/null +++ b/ludwig/datasets/configs/cifar10.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: cifar10 +huggingface_dataset_id: uoft-cs/cifar10 +loader: hugging_face.HFLoader +description: | + CIFAR-10; 10-class 32x32 image classification +columns: + - name: img + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/cifar100.yaml b/ludwig/datasets/configs/cifar100.yaml new file mode 100644 index 0000000..cb8d853 --- /dev/null +++ b/ludwig/datasets/configs/cifar100.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: cifar100 +huggingface_dataset_id: uoft-cs/cifar100 +loader: hugging_face.HFLoader +description: | + CIFAR-100; 100-class 32x32 image classification +columns: + - name: img + type: image + - name: fine_label + type: category + - name: coarse_label + type: category +output_features: + - name: fine_label + type: category diff --git a/ludwig/datasets/configs/civil_comments.yaml b/ludwig/datasets/configs/civil_comments.yaml new file mode 100644 index 0000000..dadd976 --- /dev/null +++ b/ludwig/datasets/configs/civil_comments.yaml @@ -0,0 +1,26 @@ +version: 1.0 +name: civil_comments +huggingface_dataset_id: google/civil_comments +loader: hugging_face.HFLoader +description: | + Civil Comments toxicity classification; multi-attribute toxicity labels +columns: + - name: text + type: text + - name: toxicity + type: number + - name: severe_toxicity + type: number + - name: obscene + type: number + - name: threat + type: number + - name: insult + type: number + - name: identity_attack + type: number + - name: sexual_explicit + type: number +output_features: + - name: toxicity + type: number diff --git a/ludwig/datasets/configs/climate_fever.yaml b/ludwig/datasets/configs/climate_fever.yaml new file mode 100644 index 0000000..1877c98 --- /dev/null +++ b/ludwig/datasets/configs/climate_fever.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: climate_fever +huggingface_dataset_id: climate_fever +loader: hugging_face.HFLoader +description: | + ClimateFEVER: climate change claim verification dataset. + Labels: SUPPORTS, REFUTES, NOT ENOUGH INFO, DISPUTED. +columns: + - name: claim + type: text + - name: claim_label + type: category +output_features: + - name: claim_label + type: category diff --git a/ludwig/datasets/configs/climate_sentiment.yaml b/ludwig/datasets/configs/climate_sentiment.yaml new file mode 100644 index 0000000..a17c0c1 --- /dev/null +++ b/ludwig/datasets/configs/climate_sentiment.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: climate_sentiment +huggingface_dataset_id: climatebert/climate_sentiment +loader: hugging_face.HFLoader +description: | + ClimateBERT climate-related text sentiment analysis +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/clinc_oos.yaml b/ludwig/datasets/configs/clinc_oos.yaml new file mode 100644 index 0000000..fd681ae --- /dev/null +++ b/ludwig/datasets/configs/clinc_oos.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: clinc_oos +huggingface_dataset_id: clinc/clinc_oos +huggingface_subsample: plus +loader: hugging_face.HFLoader +description: | + CLINC OOS: 150-class intent classification with out-of-scope detection. 10K train examples. +columns: + - name: text + type: text + - name: intent + type: category +output_features: + - name: intent + type: category diff --git a/ludwig/datasets/configs/clue_afqmc.yaml b/ludwig/datasets/configs/clue_afqmc.yaml new file mode 100644 index 0000000..1aa322b --- /dev/null +++ b/ludwig/datasets/configs/clue_afqmc.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: clue_afqmc +huggingface_dataset_id: clue/clue +huggingface_subsample: afqmc +loader: hugging_face.HFLoader +description: | + CLUE AFQMC: Chinese short text similarity for financial domain. 34K train examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/cmrc2018.yaml b/ludwig/datasets/configs/cmrc2018.yaml new file mode 100644 index 0000000..659b95a --- /dev/null +++ b/ludwig/datasets/configs/cmrc2018.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: cmrc2018 +huggingface_dataset_id: hfl/cmrc2018 +loader: hugging_face.HFLoader +description: | + CMRC 2018: Chinese machine reading comprehension (SQuAD-style). 10K train examples. +columns: + - name: context + type: text + - name: question + type: text + - name: answers + type: text +output_features: + - name: answers + type: text diff --git a/ludwig/datasets/configs/cnn_dailymail.yaml b/ludwig/datasets/configs/cnn_dailymail.yaml new file mode 100644 index 0000000..552554c --- /dev/null +++ b/ludwig/datasets/configs/cnn_dailymail.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: cnn_dailymail +huggingface_dataset_id: abisee/cnn_dailymail +huggingface_subsample: 3.0.0 +loader: hugging_face.HFLoader +description: | + CNN/DailyMail news summarization. Article -> highlights. ~300K examples. +columns: + - name: article + type: text + - name: highlights + type: text +output_features: + - name: highlights + type: text diff --git a/ludwig/datasets/configs/cnn_dm_hf.yaml b/ludwig/datasets/configs/cnn_dm_hf.yaml new file mode 100644 index 0000000..ef36b1c --- /dev/null +++ b/ludwig/datasets/configs/cnn_dm_hf.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: cnn_dm_hf +huggingface_dataset_id: abisee/cnn_dailymail +huggingface_subsample: "3.0.0" +loader: hugging_face.HFLoader +description: | + CNN/DailyMail (abisee): news article summarization, 3.0.0 version (287K examples). +columns: + - name: article + type: text + - name: highlights + type: text +output_features: + - name: highlights + type: text diff --git a/ludwig/datasets/configs/code_alpaca.yaml b/ludwig/datasets/configs/code_alpaca.yaml new file mode 100644 index 0000000..6fdc5e1 --- /dev/null +++ b/ludwig/datasets/configs/code_alpaca.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: code_alpaca +download_urls: https://raw.githubusercontent.com/sahil280114/codealpaca/master/data/code_alpaca_20k.json +train_filenames: code_alpaca_20k.json +loader: code_alpaca_loader.CodeAlpacaLoader +description: | + This dataset, created by sahil280114, aims to build and share an instruction-following LLaMA model for code generation. The repo containing + this dataset is fully based on Stanford Alpaca, and only changes the data used for training. +columns: + - name: instruction + type: text + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/code_contests.yaml b/ludwig/datasets/configs/code_contests.yaml new file mode 100644 index 0000000..766d89b --- /dev/null +++ b/ludwig/datasets/configs/code_contests.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: code_contests +huggingface_dataset_id: deepmind/code_contests +loader: hugging_face.HFLoader +description: | + DeepMind code contests: competitive programming problems with difficulty classification. 3.7K train examples. +columns: + - name: name + type: text + - name: description + type: text + - name: source + type: category + - name: difficulty + type: category +output_features: + - name: difficulty + type: category diff --git a/ludwig/datasets/configs/code_defect_detection.yaml b/ludwig/datasets/configs/code_defect_detection.yaml new file mode 100644 index 0000000..fced033 --- /dev/null +++ b/ludwig/datasets/configs/code_defect_detection.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: code_defect_detection +huggingface_dataset_id: google/code_x_glue_cc_defect_detection +loader: hugging_face.HFLoader +description: | + CodeXGLUE: binary defect detection in code functions. 21K train examples. +columns: + - name: func + type: text + - name: target + type: binary +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/code_search_net.yaml b/ludwig/datasets/configs/code_search_net.yaml new file mode 100644 index 0000000..4a873f1 --- /dev/null +++ b/ludwig/datasets/configs/code_search_net.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: code_search_net +huggingface_dataset_id: code-search-net/code_search_net +huggingface_subsample: python +loader: code_loader.CodeSearchNetLoader +description: | + CodeSearchNet Python: function code → docstring (text generation). +columns: + - name: func_code_string + type: text + - name: func_documentation_string + type: text + - name: language + type: category +output_features: + - name: func_documentation_string + type: text diff --git a/ludwig/datasets/configs/codex_thinking.yaml b/ludwig/datasets/configs/codex_thinking.yaml new file mode 100644 index 0000000..9dea6c9 --- /dev/null +++ b/ludwig/datasets/configs/codex_thinking.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: codex_thinking +huggingface_dataset_id: Modotte/CodeX-2M-Thinking +loader: hugging_face.HFLoader +description: | + CodeX 2M: code generation with chain-of-thought reasoning. 2.2M examples. +columns: + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/codexglue_code_to_text.yaml b/ludwig/datasets/configs/codexglue_code_to_text.yaml new file mode 100644 index 0000000..f51e4be --- /dev/null +++ b/ludwig/datasets/configs/codexglue_code_to_text.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: codexglue_code_to_text +huggingface_dataset_id: google/code_x_glue_ct_code_to_text +huggingface_subsample: python +loader: code_loader.CodeXGlueLoader +description: | + CodeXGlue Python code → docstring generation. +columns: + - name: code + type: text + - name: docstring + type: text +output_features: + - name: docstring + type: text diff --git a/ludwig/datasets/configs/coig_cqia.yaml b/ludwig/datasets/configs/coig_cqia.yaml new file mode 100644 index 0000000..d549725 --- /dev/null +++ b/ludwig/datasets/configs/coig_cqia.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: coig_cqia +huggingface_dataset_id: m-a-p/COIG-CQIA +huggingface_subsample: coig_pc +loader: hugging_face.HFLoader +description: | + COIG-CQIA: Chinese instruction dataset with domain/task type. 1111 examples. +columns: + - name: instruction + type: text + - name: output + type: text + - name: task_type + type: category + - name: domain + type: category +output_features: + - name: task_type + type: category diff --git a/ludwig/datasets/configs/cola.yaml b/ludwig/datasets/configs/cola.yaml new file mode 100644 index 0000000..a4015cf --- /dev/null +++ b/ludwig/datasets/configs/cola.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: cola +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: cola +loader: hugging_face.HFLoader +description: | + Corpus of Linguistic Acceptability; grammatically acceptable or not +columns: + - name: sentence + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/commitment_bank.yaml b/ludwig/datasets/configs/commitment_bank.yaml new file mode 100644 index 0000000..5bae461 --- /dev/null +++ b/ludwig/datasets/configs/commitment_bank.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: commitment_bank +huggingface_dataset_id: aps/super_glue +huggingface_subsample: cb +loader: hugging_face.HFLoader +description: | + CommitmentBank; textual entailment with 3-way classification +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: idx + type: category + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/commonsense_qa.yaml b/ludwig/datasets/configs/commonsense_qa.yaml new file mode 100644 index 0000000..78901db --- /dev/null +++ b/ludwig/datasets/configs/commonsense_qa.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: commonsense_qa +huggingface_dataset_id: tau/commonsense_qa +loader: multiple_choice_loader.CommonsenseQALoader +description: | + CommonsenseQA 5-way multiple choice. Question → A/B/C/D/E. +columns: + - name: question + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/compas_recidivism.yaml b/ludwig/datasets/configs/compas_recidivism.yaml new file mode 100644 index 0000000..84bca75 --- /dev/null +++ b/ludwig/datasets/configs/compas_recidivism.yaml @@ -0,0 +1,53 @@ +version: 1.0 +name: compas_recidivism +huggingface_dataset_id: imodels/compas-recidivism +loader: hugging_face.HFLoader +description: 'COMPAS recidivism risk prediction; criminal justice fairness benchmark + + ' +columns: +- name: age + type: number +- name: priors_count + type: number +- name: days_b_screening_arrest + type: number +- name: c_jail_time + type: number +- name: juv_fel_count + type: number +- name: juv_other_count + type: number +- name: juv_misd_count + type: number +- name: c_charge_degree:F + type: number +- name: c_charge_degree:M + type: number +- name: race:African-American + type: number +- name: race:Asian + type: number +- name: race:Caucasian + type: number +- name: race:Hispanic + type: number +- name: race:Native_American + type: number +- name: race:Other + type: number +- name: age_cat:25_-_45 + type: number +- name: age_cat:Greater_than_45 + type: number +- name: age_cat:Less_than_25 + type: number +- name: sex:Female + type: number +- name: sex:Male + type: number +- name: is_recid + type: category +output_features: +- name: is_recid + type: binary diff --git a/ludwig/datasets/configs/connect4.yaml b/ludwig/datasets/configs/connect4.yaml new file mode 100644 index 0000000..8653a49 --- /dev/null +++ b/ludwig/datasets/configs/connect4.yaml @@ -0,0 +1,101 @@ +version: 1.0 +name: connect4 +kaggle_dataset_id: tbrewer/connect-4 +archive_filenames: connect-4.zip +dataset_filenames: c4_game_database.csv +sha256: + connect-4.zip: 46c33c47f2664948a4abe53bafee92a602773f31db615bc8bd239e1f98a3d2cf +description: | + Each row represents the end results of a Connect-4 game. + Columns 1-42 are the positions on the grid from left to right, top to bottom. Each element in these columns represent to player's piece : 1, and -1, 0 marks an empty cell. + Column 43 marks the winner of the game : -1, 1, and 0 for tie games. +columns: + - name: pos_01 + type: number + - name: pos_02 + type: number + - name: pos_03 + type: number + - name: pos_04 + type: number + - name: pos_05 + type: number + - name: pos_06 + type: number + - name: pos_07 + type: number + - name: pos_08 + type: number + - name: pos_09 + type: number + - name: pos_10 + type: number + - name: pos_11 + type: number + - name: pos_12 + type: number + - name: pos_13 + type: number + - name: pos_14 + type: number + - name: pos_15 + type: number + - name: pos_16 + type: number + - name: pos_17 + type: number + - name: pos_18 + type: number + - name: pos_19 + type: number + - name: pos_20 + type: number + - name: pos_21 + type: number + - name: pos_22 + type: number + - name: pos_23 + type: number + - name: pos_24 + type: number + - name: pos_25 + type: number + - name: pos_26 + type: number + - name: pos_27 + type: number + - name: pos_28 + type: number + - name: pos_29 + type: number + - name: pos_30 + type: number + - name: pos_31 + type: number + - name: pos_32 + type: number + - name: pos_33 + type: number + - name: pos_34 + type: number + - name: pos_35 + type: number + - name: pos_36 + type: number + - name: pos_37 + type: number + - name: pos_38 + type: number + - name: pos_39 + type: number + - name: pos_40 + type: number + - name: pos_41 + type: number + - name: pos_42 + type: number + - name: winner + type: number +output_features: + - name: winner + type: category diff --git a/ludwig/datasets/configs/consumer_complaints.yaml b/ludwig/datasets/configs/consumer_complaints.yaml new file mode 100644 index 0000000..6a39632 --- /dev/null +++ b/ludwig/datasets/configs/consumer_complaints.yaml @@ -0,0 +1,50 @@ +version: 1.0 +name: consumer_complaints +kaggle_dataset_id: selener/consumer-complaint-database +archive_filenames: consumer-complaint-database.zip +dataset_filenames: rows.csv +loader: consumer_complaints_loader.ConsumerComplaintsLoader +description: | + The dataset contains different information of complaints that customers have made about a multiple products and + services in the financial sector, such us Credit Reports, Student Loans, Money Transfer, etc. The date of each + complaint ranges from November 2011 to May 2019. +columns: + - name: Date received + type: Date + - name: Product + type: text + - name: Sub-product + type: text + - name: Issue + type: text + - name: Sub-issue + type: text + - name: Consumer complaint narrative + type: text + - name: Company public response + type: text + - name: Company + type: text + - name: State + type: category + - name: ZIP code + type: category + - name: Tags + type: category + - name: Consumer consent provided? + type: text + - name: Submitted via + type: category + - name: Date sent to company + type: date + - name: Company response to consumer + type: text + - name: Timely response? + type: binary + - name: Consumer disputed? + type: binary + - name: Complaint ID + type: number +output_features: + - name: Issue + type: text diff --git a/ludwig/datasets/configs/copa.yaml b/ludwig/datasets/configs/copa.yaml new file mode 100644 index 0000000..05d3504 --- /dev/null +++ b/ludwig/datasets/configs/copa.yaml @@ -0,0 +1,23 @@ +version: 1.0 +name: copa +huggingface_dataset_id: aps/super_glue +huggingface_subsample: copa +loader: hugging_face.HFLoader +description: | + Choice Of Plausible Alternatives; causal commonsense reasoning +columns: + - name: premise + type: text + - name: choice1 + type: text + - name: choice2 + type: text + - name: question + type: text + - name: idx + type: category + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/cord_v2.yaml b/ludwig/datasets/configs/cord_v2.yaml new file mode 100644 index 0000000..e046e15 --- /dev/null +++ b/ludwig/datasets/configs/cord_v2.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: cord_v2 +huggingface_dataset_id: naver-clova-ix/cord-v2 +loader: hugging_face.HFLoader +description: | + CORD v2 receipt understanding: receipt image -> JSON ground truth (text generation). + Korean/English receipts for document AI tasks. +columns: + - name: image + type: image + - name: ground_truth + type: text +output_features: + - name: ground_truth + type: text diff --git a/ludwig/datasets/configs/credit_card_default.yaml b/ludwig/datasets/configs/credit_card_default.yaml new file mode 100644 index 0000000..3936e69 --- /dev/null +++ b/ludwig/datasets/configs/credit_card_default.yaml @@ -0,0 +1,79 @@ +version: 1.0 +name: credit_card_default +huggingface_dataset_id: imodels/credit-card +loader: hugging_face.HFLoader +description: 'Credit card default prediction from payment history + + ' +columns: +- name: limit_bal + type: number +- name: age + type: number +- name: pay_0 + type: number +- name: pay_2 + type: number +- name: pay_3 + type: number +- name: pay_4 + type: number +- name: pay_5 + type: number +- name: pay_6 + type: number +- name: bill_amt1 + type: number +- name: bill_amt2 + type: number +- name: bill_amt3 + type: number +- name: bill_amt4 + type: number +- name: bill_amt5 + type: number +- name: bill_amt6 + type: number +- name: pay_amt1 + type: number +- name: pay_amt2 + type: number +- name: pay_amt3 + type: number +- name: pay_amt4 + type: number +- name: pay_amt5 + type: number +- name: pay_amt6 + type: number +- name: sex:1 + type: number +- name: sex:2 + type: number +- name: education:0 + type: number +- name: education:1 + type: number +- name: education:2 + type: number +- name: education:3 + type: number +- name: education:4 + type: number +- name: education:5 + type: number +- name: education:6 + type: number +- name: marriage:0 + type: number +- name: marriage:1 + type: number +- name: marriage:2 + type: number +- name: marriage:3 + type: number +- name: default.payment.next.month + type: category +output_features: +- name: default.payment.next.month + type: binary diff --git a/ludwig/datasets/configs/creditcard_fraud.yaml b/ludwig/datasets/configs/creditcard_fraud.yaml new file mode 100644 index 0000000..2a72733 --- /dev/null +++ b/ludwig/datasets/configs/creditcard_fraud.yaml @@ -0,0 +1,76 @@ +version: 1.0 +name: creditcard_fraud +kaggle_dataset_id: mlg-ulb/creditcardfraud +archive_filenames: creditcardfraud.zip +sha256: + creditcardfraud.zip: a0360ce715992212e9ac72d8ccdca97f4be87dc1fdf2bed011358f7ab409a28a +loader: creditcard_fraud.CreditCardFraudLoader +description: | + The Machine Learning Group ULB Dataset + https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud +columns: + - name: Time + type: number + - name: V1 + type: number + - name: V2 + type: number + - name: V3 + type: number + - name: V4 + type: number + - name: V5 + type: number + - name: V6 + type: number + - name: V7 + type: number + - name: V8 + type: number + - name: V9 + type: number + - name: V10 + type: number + - name: V11 + type: number + - name: V12 + type: number + - name: V13 + type: number + - name: V14 + type: number + - name: V15 + type: number + - name: V16 + type: number + - name: V17 + type: number + - name: V18 + type: number + - name: V19 + type: number + - name: V20 + type: number + - name: V21 + type: number + - name: V22 + type: number + - name: V23 + type: number + - name: V24 + type: number + - name: V25 + type: number + - name: V26 + type: number + - name: V27 + type: number + - name: V28 + type: number + - name: Amount + type: number + - name: Class + type: number +output_features: + - name: Class + type: binary diff --git a/ludwig/datasets/configs/customer_churn_prediction.yaml b/ludwig/datasets/configs/customer_churn_prediction.yaml new file mode 100644 index 0000000..268fc39 --- /dev/null +++ b/ludwig/datasets/configs/customer_churn_prediction.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: customer_churn_prediction +kaggle_competition: customer-churn-prediction-2020 +archive_filenames: customer-churn-prediction-2020.zip +train_filenames: train.csv +test_filenames: test.csv +sha256: + customer-churn-prediction-2020.zip: fb5cbc787081a6a559592230c657a0520a181447da6eb2adc34a3aebbe8ed9ca +description: | + Dataset from a Kaggle competition that is about predicting whether a customer will change + telecommunications provider, something known as "churning". + https://www.kaggle.com/c/customer-churn-prediction-2020 +output_features: + - name: churn + type: binary diff --git a/ludwig/datasets/configs/customer_reviews.yaml b/ludwig/datasets/configs/customer_reviews.yaml new file mode 100644 index 0000000..6af7d74 --- /dev/null +++ b/ludwig/datasets/configs/customer_reviews.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: customer_reviews +huggingface_dataset_id: SetFit/CR +loader: hugging_face.HFLoader +description: | + Customer Reviews; product review sentiment binary classification +columns: + - name: text + type: text + - name: label + type: category + - name: label_text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/dair_emotion.yaml b/ludwig/datasets/configs/dair_emotion.yaml new file mode 100644 index 0000000..4e43af0 --- /dev/null +++ b/ludwig/datasets/configs/dair_emotion.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: dair_emotion +huggingface_dataset_id: dair-ai/emotion +loader: hugging_face.HFLoader +description: | + DAIR.AI Emotion: 6-class emotion classification from English Twitter messages. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/data_scientist_salary.yaml b/ludwig/datasets/configs/data_scientist_salary.yaml new file mode 100644 index 0000000..ecbc4dc --- /dev/null +++ b/ludwig/datasets/configs/data_scientist_salary.yaml @@ -0,0 +1,32 @@ +version: 1.0 +name: data_scientist_salary +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_data_scientists_salary_in_india_hackathon/train.csv + - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_data_scientists_salary_in_india_hackathon/test.csv +sha256: + test.csv: 244c215f4a03cae4b107e76c7fe94269728450cabf44c943415211ce7d6437df + train.csv: 99d6aa80505ac1311e97f402d5723996119e859c7f3fce261350462148debe3d +train_filenames: train.csv +test_filenames: test.csv +description: | + The training data and test data comprise of 19802 samples and of 6601 samples each from the + Analytics India Annual Salary Study. + https://machinehack.com/hackathons/predict_the_data_scientists_salary_in_india_hackathon/overview +columns: + - name: experience + type: category + - name: job_description + type: text + - name: job_desig + type: category + - name: job_type + type: category + - name: key_skills + type: set + - name: location + type: category + - name: salary + type: category +output_features: + - name: salary + type: category diff --git a/ludwig/datasets/configs/databench_qa.yaml b/ludwig/datasets/configs/databench_qa.yaml new file mode 100644 index 0000000..ee65154 --- /dev/null +++ b/ludwig/datasets/configs/databench_qa.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: databench_qa +huggingface_dataset_id: cardiffnlp/databench +huggingface_subsample: qa +loader: hugging_face.HFLoader +description: | + DataBench: data analysis question-answer type classification. 1830 train examples. +columns: + - name: question + type: text + - name: type + type: category + - name: answer + type: text +output_features: + - name: type + type: category diff --git a/ludwig/datasets/configs/databricks_dolly_15k.yaml b/ludwig/datasets/configs/databricks_dolly_15k.yaml new file mode 100644 index 0000000..320d7d0 --- /dev/null +++ b/ludwig/datasets/configs/databricks_dolly_15k.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: databricks_dolly_15k +huggingface_dataset_id: databricks/databricks-dolly-15k +loader: hugging_face.HFLoader +description: | + Databricks Dolly 15K instruction-response pairs with 8 task categories (QA, summarization, etc.). +columns: + - name: instruction + type: text + - name: context + type: text + - name: response + type: text + - name: category + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/dbpedia.yaml b/ludwig/datasets/configs/dbpedia.yaml new file mode 100644 index 0000000..b0f51c9 --- /dev/null +++ b/ludwig/datasets/configs/dbpedia.yaml @@ -0,0 +1,26 @@ +version: 1.0 +name: dbpedia +download_urls: https://s3.amazonaws.com/fast-ai-nlp/dbpedia_csv.tgz +train_filenames: dbpedia_csv/train.csv +test_filenames: dbpedia_csv/test.csv +sha256: + dbpedia_csv.tgz: 42db5221ddedddb673a4cabcc5f3a7d869714c878bcfe4ba94b29d14aa38e417 +description: | + The DBPedia Ontology dataset. + + Details: + 40,000 training samples and 5,000 testing samples from 14 + nonoverlapping classes from DBpedia 2014. + Dataset source: + Character-level Convolutional Networks for Text Classification + Xiang Zhang et al., 2015 +columns: + - name: label + type: category + - name: title + type: category + - name: content + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/dbpedia_14.yaml b/ludwig/datasets/configs/dbpedia_14.yaml new file mode 100644 index 0000000..4dfe315 --- /dev/null +++ b/ludwig/datasets/configs/dbpedia_14.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: dbpedia_14 +huggingface_dataset_id: fancyzhx/dbpedia_14 +loader: hugging_face.HFLoader +description: | + DBpedia 14 ontology text classification; 14 categories +columns: + - name: label + type: category + - name: title + type: text + - name: content + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/diabetes_readmission.yaml b/ludwig/datasets/configs/diabetes_readmission.yaml new file mode 100644 index 0000000..f27f31b --- /dev/null +++ b/ludwig/datasets/configs/diabetes_readmission.yaml @@ -0,0 +1,313 @@ +version: 1.0 +name: diabetes_readmission +huggingface_dataset_id: imodels/diabetes-readmission +loader: hugging_face.HFLoader +description: 'Diabetes hospital readmission prediction + + ' +columns: +- name: time_in_hospital + type: number +- name: num_lab_procedures + type: number +- name: num_procedures + type: number +- name: num_medications + type: number +- name: number_outpatient + type: number +- name: number_emergency + type: number +- name: number_inpatient + type: number +- name: number_diagnoses + type: number +- name: change + type: number +- name: diabetesMed + type: number +- name: race:AfricanAmerican + type: number +- name: race:Asian + type: number +- name: race:Caucasian + type: number +- name: race:Hispanic + type: number +- name: race:Other + type: number +- name: gender:Female + type: number +- name: gender:Male + type: number +- name: age:70+ + type: number +- name: age:[0-10) + type: number +- name: age:[10-20) + type: number +- name: age:[20-50) + type: number +- name: age:[50-70) + type: number +- name: admission_type_id:Elective + type: number +- name: admission_type_id:Emergency + type: number +- name: admission_type_id:New Born + type: number +- name: admission_type_id:Trauma Center + type: number +- name: discharge_disposition_id:Discharged to Home + type: number +- name: discharge_disposition_id:Other + type: number +- name: admission_source_id:Emergency + type: number +- name: admission_source_id:Other + type: number +- name: admission_source_id:Referral + type: number +- name: admission_source_id:Transfer + type: number +- name: medical_specialty:Cardiology + type: number +- name: medical_specialty:Emergency/Trauma + type: number +- name: medical_specialty:Family/GeneralPractice + type: number +- name: medical_specialty:Gastroenterology + type: number +- name: medical_specialty:Hematology/Oncology + type: number +- name: medical_specialty:InternalMedicine + type: number +- name: medical_specialty:Nephrology + type: number +- name: medical_specialty:ObstetricsandGynecology + type: number +- name: medical_specialty:Orthopedics + type: number +- name: medical_specialty:Other + type: number +- name: medical_specialty:Psychiatry + type: number +- name: medical_specialty:Pulmonology + type: number +- name: medical_specialty:Radiology + type: number +- name: medical_specialty:Surgery-Cardiovascular/Thoracic + type: number +- name: medical_specialty:Surgery-General + type: number +- name: medical_specialty:Urology + type: number +- name: diag_1:Circulatory + type: number +- name: diag_1:Diabetes + type: number +- name: diag_1:Digestive + type: number +- name: diag_1:Genitourinary + type: number +- name: diag_1:Infectious + type: number +- name: diag_1:Injury + type: number +- name: diag_1:Mental + type: number +- name: diag_1:Musculoskeletal + type: number +- name: diag_1:Neoplasms + type: number +- name: diag_1:Non-diabetes endocrine/metabolic + type: number +- name: diag_1:Other + type: number +- name: diag_1:Respiratory + type: number +- name: diag_1:Skin + type: number +- name: diag_2:Circulatory + type: number +- name: diag_2:Diabetes + type: number +- name: diag_2:Digestive + type: number +- name: diag_2:Genitourinary + type: number +- name: diag_2:Infectious + type: number +- name: diag_2:Injury + type: number +- name: diag_2:Mental + type: number +- name: diag_2:Musculoskeletal + type: number +- name: diag_2:Neoplasms + type: number +- name: diag_2:Non-diabetes endocrine/metabolic + type: number +- name: diag_2:Other + type: number +- name: diag_2:Respiratory + type: number +- name: diag_2:Skin + type: number +- name: diag_3:Circulatory + type: number +- name: diag_3:Diabetes + type: number +- name: diag_3:Digestive + type: number +- name: diag_3:Genitourinary + type: number +- name: diag_3:Infectious + type: number +- name: diag_3:Injury + type: number +- name: diag_3:Mental + type: number +- name: diag_3:Musculoskeletal + type: number +- name: diag_3:Neoplasms + type: number +- name: diag_3:Non-diabetes endocrine/metabolic + type: number +- name: diag_3:Other + type: number +- name: diag_3:Respiratory + type: number +- name: diag_3:Skin + type: number +- name: metformin:Down + type: number +- name: metformin:No + type: number +- name: metformin:Steady + type: number +- name: metformin:Up + type: number +- name: repaglinide:Down + type: number +- name: repaglinide:No + type: number +- name: repaglinide:Steady + type: number +- name: repaglinide:Up + type: number +- name: nateglinide:Down + type: number +- name: nateglinide:No + type: number +- name: nateglinide:Steady + type: number +- name: nateglinide:Up + type: number +- name: chlorpropamide:Down + type: number +- name: chlorpropamide:No + type: number +- name: chlorpropamide:Steady + type: number +- name: chlorpropamide:Up + type: number +- name: glimepiride:Down + type: number +- name: glimepiride:No + type: number +- name: glimepiride:Steady + type: number +- name: glimepiride:Up + type: number +- name: glipizide:Down + type: number +- name: glipizide:No + type: number +- name: glipizide:Steady + type: number +- name: glipizide:Up + type: number +- name: glyburide:Down + type: number +- name: glyburide:No + type: number +- name: glyburide:Steady + type: number +- name: glyburide:Up + type: number +- name: pioglitazone:Down + type: number +- name: pioglitazone:No + type: number +- name: pioglitazone:Steady + type: number +- name: pioglitazone:Up + type: number +- name: rosiglitazone:Down + type: number +- name: rosiglitazone:No + type: number +- name: rosiglitazone:Steady + type: number +- name: rosiglitazone:Up + type: number +- name: acarbose:Down + type: number +- name: acarbose:No + type: number +- name: acarbose:Steady + type: number +- name: acarbose:Up + type: number +- name: miglitol:Down + type: number +- name: miglitol:No + type: number +- name: miglitol:Steady + type: number +- name: miglitol:Up + type: number +- name: tolazamide:No + type: number +- name: tolazamide:Steady + type: number +- name: tolazamide:Up + type: number +- name: insulin:Down + type: number +- name: insulin:No + type: number +- name: insulin:Steady + type: number +- name: insulin:Up + type: number +- name: glyburide-metformin:Down + type: number +- name: glyburide-metformin:No + type: number +- name: glyburide-metformin:Steady + type: number +- name: glyburide-metformin:Up + type: number +- name: A1Cresult:>7 + type: number +- name: A1Cresult:>8 + type: number +- name: A1Cresult:None + type: number +- name: A1Cresult:Norm + type: number +- name: max_glu_serum:>200 + type: number +- name: max_glu_serum:>300 + type: number +- name: max_glu_serum:None + type: number +- name: max_glu_serum:Norm + type: number +- name: readmitted + type: category +output_features: +- name: readmitted + type: category diff --git a/ludwig/datasets/configs/dialogsum.yaml b/ludwig/datasets/configs/dialogsum.yaml new file mode 100644 index 0000000..005a087 --- /dev/null +++ b/ludwig/datasets/configs/dialogsum.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: dialogsum +huggingface_dataset_id: knkarthick/dialogsum +loader: hugging_face.HFLoader +description: | + DialogSum dialogue summarization. Dialogue -> summary. ~13K examples. +columns: + - name: dialogue + type: text + - name: summary + type: text +output_features: + - name: summary + type: text diff --git a/ludwig/datasets/configs/docvqa.yaml b/ludwig/datasets/configs/docvqa.yaml new file mode 100644 index 0000000..7349138 --- /dev/null +++ b/ludwig/datasets/configs/docvqa.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: docvqa +huggingface_dataset_id: HuggingFaceM4/the_cauldron +huggingface_subsample: docvqa +loader: vqa_loader.DocVQALoader +description: | + DocVQA: document image + question → answer from the document. +columns: + - name: image_path + type: image + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/dolly_15k.yaml b/ludwig/datasets/configs/dolly_15k.yaml new file mode 100644 index 0000000..734f0c2 --- /dev/null +++ b/ludwig/datasets/configs/dolly_15k.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: dolly_15k +huggingface_dataset_id: databricks/databricks-dolly-15k +loader: hugging_face.HFLoader +description: | + Databricks Dolly 15K: 15K instruction-response pairs with task category labels. +columns: + - name: instruction + type: text + - name: context + type: text + - name: response + type: text + - name: category + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/drop.yaml b/ludwig/datasets/configs/drop.yaml new file mode 100644 index 0000000..eae3541 --- /dev/null +++ b/ludwig/datasets/configs/drop.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: drop +huggingface_dataset_id: ucinlp/drop +loader: hugging_face.HFLoader +description: | + DROP: Discrete Reasoning Over Paragraphs reading comprehension. 77K train examples. +columns: + - name: passage + type: text + - name: question + type: text + - name: answers_spans + type: text +output_features: + - name: answers_spans + type: text diff --git a/ludwig/datasets/configs/duorc.yaml b/ludwig/datasets/configs/duorc.yaml new file mode 100644 index 0000000..d2d466d --- /dev/null +++ b/ludwig/datasets/configs/duorc.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: duorc +huggingface_dataset_id: ibm/duorc +huggingface_subsample: SelfRC +loader: qa_loader.DuoRCLoader +description: | + DuoRC SelfRC: movie plot + question → answer text. +columns: + - name: plot + type: text + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/electricity.yaml b/ludwig/datasets/configs/electricity.yaml new file mode 100644 index 0000000..80732aa --- /dev/null +++ b/ludwig/datasets/configs/electricity.yaml @@ -0,0 +1,22 @@ +version: 1.0 +name: electricity +download_urls: https://raw.githubusercontent.com/nimz/electricity_demand/master/elecdemand.csv +sha256: + elecdemand.csv: 4fd3c8a4b8168f34703b55313c5341f8e8385810a54f1a1cdf6987c1904c9698 +description: | + Electricity demand dataset. Half-hourly electricity demand in Victoria, Australia during 2014, along with + Melbourne temperatures. + + Source textbook: + Forecasting: Principles and Practice + Rob J Hyndman and George Athanasopoulos +columns: + - name: Demand + type: number + - name: WorkDay + type: binary + - name: Temperature + type: number +output_features: + - name: Demand + type: number diff --git a/ludwig/datasets/configs/electricity_tabular.yaml b/ludwig/datasets/configs/electricity_tabular.yaml new file mode 100644 index 0000000..e796bc8 --- /dev/null +++ b/ludwig/datasets/configs/electricity_tabular.yaml @@ -0,0 +1,29 @@ +version: 1.0 +name: electricity_tabular +huggingface_dataset_id: mstz/electricity +loader: hugging_face.HFLoader +description: 'Electricity market price direction classification + + ' +columns: +- name: date + type: number +- name: day + type: category +- name: period + type: number +- name: nswprice + type: number +- name: nswdemand + type: number +- name: vicprice + type: number +- name: vicdemand + type: number +- name: transfer + type: number +- name: is_up + type: category +output_features: +- name: is_up + type: binary diff --git a/ludwig/datasets/configs/emodb.yaml b/ludwig/datasets/configs/emodb.yaml new file mode 100644 index 0000000..d0db56f --- /dev/null +++ b/ludwig/datasets/configs/emodb.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: emodb +huggingface_dataset_id: renumics/emodb +loader: hugging_face.HFLoader +description: | + EMo-DB; Berlin Database of Emotional Speech; 7 emotion classes +columns: + - name: age + type: number + - name: gender + type: category + - name: emotion + type: category + - name: audio + type: audio +output_features: + - name: emotion + type: category diff --git a/ludwig/datasets/configs/emotion.yaml b/ludwig/datasets/configs/emotion.yaml new file mode 100644 index 0000000..2419e13 --- /dev/null +++ b/ludwig/datasets/configs/emotion.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: emotion +huggingface_dataset_id: dair-ai/emotion +loader: hugging_face.HFLoader +description: | + Twitter emotion classification; 6 classes: joy, sadness, anger, fear, surprise, love +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/enron_spam.yaml b/ludwig/datasets/configs/enron_spam.yaml new file mode 100644 index 0000000..db51849 --- /dev/null +++ b/ludwig/datasets/configs/enron_spam.yaml @@ -0,0 +1,24 @@ +version: 1.0 +name: enron_spam +huggingface_dataset_id: SetFit/enron_spam +loader: hugging_face.HFLoader +description: | + Enron Spam; email spam/ham classification +columns: + - name: message_id + type: category + - name: text + type: text + - name: label + type: category + - name: label_text + type: text + - name: subject + type: text + - name: message + type: text + - name: date + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/esc50.yaml b/ludwig/datasets/configs/esc50.yaml new file mode 100644 index 0000000..ea746cc --- /dev/null +++ b/ludwig/datasets/configs/esc50.yaml @@ -0,0 +1,31 @@ +version: 1.0 +name: esc50 +download_urls: + - https://github.com/karoldvl/ESC-50/archive/master.zip +archive_filenames: master.zip +dataset_filenames: ESC-50-master/meta/esc50.csv +preserve_paths: ESC-50-master/audio +loader: esc50.ESC50Loader +description: | + ESC-50: Environmental Sound Classification + + A labeled collection of 2,000 environmental audio recordings for benchmarking + methods of environmental sound classification. The dataset consists of 5-second + clips (44.1 kHz, mono WAV) organized into 50 semantical classes grouped in 5 + major categories (animals, natural soundscapes, human non-speech sounds, + interior/domestic, and exterior/urban). + + Reference: + ESC: Dataset for Environmental Sound Classification + Karol J. Piczak, Proceedings of the 23rd ACM International Conference + on Multimedia, MM '15. + + License: CC BY-NC (ESC-50); CC BY (ESC-10 subset, esc10=True). +columns: + - name: audio_path + type: audio + - name: target + type: category +output_features: + - name: target + type: category diff --git a/ludwig/datasets/configs/europarl_bg_cs.yaml b/ludwig/datasets/configs/europarl_bg_cs.yaml new file mode 100644 index 0000000..5375280 --- /dev/null +++ b/ludwig/datasets/configs/europarl_bg_cs.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_bg_cs +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: bg-cs +loader: translation_loader.EuroparlBgCsLoader +description: | + Europarl: European Parliament proceedings Bulgarian-Czech translation. 400K train pairs. +columns: + - name: bg + type: text + - name: cs + type: text +output_features: + - name: cs + type: text diff --git a/ludwig/datasets/configs/europarl_bg_en.yaml b/ludwig/datasets/configs/europarl_bg_en.yaml new file mode 100644 index 0000000..796b56b --- /dev/null +++ b/ludwig/datasets/configs/europarl_bg_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_bg_en +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: bg-en +loader: translation_loader.EuroparlBgEnLoader +description: | + Europarl: European Parliament proceedings Bulgarian-English translation. +columns: + - name: bg + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/europarl_cs_en.yaml b/ludwig/datasets/configs/europarl_cs_en.yaml new file mode 100644 index 0000000..4e809ea --- /dev/null +++ b/ludwig/datasets/configs/europarl_cs_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_cs_en +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: cs-en +loader: translation_loader.EuroparlCsEnLoader +description: | + Europarl: European Parliament proceedings Czech-English translation. +columns: + - name: cs + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/europarl_da_en.yaml b/ludwig/datasets/configs/europarl_da_en.yaml new file mode 100644 index 0000000..b9118bb --- /dev/null +++ b/ludwig/datasets/configs/europarl_da_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_da_en +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: da-en +loader: translation_loader.EuroparlDaEnLoader +description: | + Europarl: European Parliament proceedings Danish-English translation. +columns: + - name: da + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/europarl_de_en.yaml b/ludwig/datasets/configs/europarl_de_en.yaml new file mode 100644 index 0000000..7bbcace --- /dev/null +++ b/ludwig/datasets/configs/europarl_de_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_de_en +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: de-en +loader: translation_loader.EuroparlDeEnLoader +description: | + Europarl: European Parliament proceedings German-English translation. +columns: + - name: de + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/europarl_el_en.yaml b/ludwig/datasets/configs/europarl_el_en.yaml new file mode 100644 index 0000000..0f51868 --- /dev/null +++ b/ludwig/datasets/configs/europarl_el_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_el_en +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: el-en +loader: translation_loader.EuroparlElEnLoader +description: | + Europarl: European Parliament proceedings Greek-English translation. +columns: + - name: el + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/europarl_en_es.yaml b/ludwig/datasets/configs/europarl_en_es.yaml new file mode 100644 index 0000000..d0737d3 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_es.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_es +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-es +loader: translation_loader.EuroparlEnEsLoader +description: | + Europarl: European Parliament proceedings English-Spanish translation. +columns: + - name: en + type: text + - name: es + type: text +output_features: + - name: es + type: text diff --git a/ludwig/datasets/configs/europarl_en_fr.yaml b/ludwig/datasets/configs/europarl_en_fr.yaml new file mode 100644 index 0000000..8ba69c6 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_fr +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-fr +loader: translation_loader.EuroparlEnFrLoader +description: | + Europarl: European Parliament proceedings English-French translation. +columns: + - name: en + type: text + - name: fr + type: text +output_features: + - name: fr + type: text diff --git a/ludwig/datasets/configs/europarl_en_it.yaml b/ludwig/datasets/configs/europarl_en_it.yaml new file mode 100644 index 0000000..4177d40 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_it.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_it +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-it +loader: translation_loader.EuroparlEnItLoader +description: | + Europarl: European Parliament proceedings English-Italian translation. +columns: + - name: en + type: text + - name: it + type: text +output_features: + - name: it + type: text diff --git a/ludwig/datasets/configs/europarl_en_nl.yaml b/ludwig/datasets/configs/europarl_en_nl.yaml new file mode 100644 index 0000000..92be4d4 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_nl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_nl +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-nl +loader: translation_loader.EuroparlEnNlLoader +description: | + Europarl: European Parliament proceedings English-Dutch translation. +columns: + - name: en + type: text + - name: nl + type: text +output_features: + - name: nl + type: text diff --git a/ludwig/datasets/configs/europarl_en_pl.yaml b/ludwig/datasets/configs/europarl_en_pl.yaml new file mode 100644 index 0000000..dc99213 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_pl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_pl +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-pl +loader: translation_loader.EuroparlEnPlLoader +description: | + Europarl: European Parliament proceedings English-Polish translation. +columns: + - name: en + type: text + - name: pl + type: text +output_features: + - name: pl + type: text diff --git a/ludwig/datasets/configs/europarl_en_pt.yaml b/ludwig/datasets/configs/europarl_en_pt.yaml new file mode 100644 index 0000000..d8a7db5 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_pt.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_pt +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-pt +loader: translation_loader.EuroparlEnPtLoader +description: | + Europarl: European Parliament proceedings English-Portuguese translation. +columns: + - name: en + type: text + - name: pt + type: text +output_features: + - name: pt + type: text diff --git a/ludwig/datasets/configs/europarl_en_ro.yaml b/ludwig/datasets/configs/europarl_en_ro.yaml new file mode 100644 index 0000000..53e1a02 --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_ro.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_ro +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-ro +loader: translation_loader.EuroparlEnRoLoader +description: | + Europarl: European Parliament proceedings English-Romanian translation. +columns: + - name: en + type: text + - name: ro + type: text +output_features: + - name: ro + type: text diff --git a/ludwig/datasets/configs/europarl_en_sv.yaml b/ludwig/datasets/configs/europarl_en_sv.yaml new file mode 100644 index 0000000..cf56deb --- /dev/null +++ b/ludwig/datasets/configs/europarl_en_sv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: europarl_en_sv +huggingface_dataset_id: Helsinki-NLP/europarl +huggingface_subsample: en-sv +loader: translation_loader.EuroparlEnSvLoader +description: | + Europarl: European Parliament proceedings English-Swedish translation. +columns: + - name: en + type: text + - name: sv + type: text +output_features: + - name: sv + type: text diff --git a/ludwig/datasets/configs/eurosat.yaml b/ludwig/datasets/configs/eurosat.yaml new file mode 100644 index 0000000..b18672f --- /dev/null +++ b/ludwig/datasets/configs/eurosat.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: eurosat +huggingface_dataset_id: tanganke/eurosat +loader: hugging_face.HFLoader +description: | + EuroSAT; land use/cover classification from satellite imagery; 10 classes +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/eurosat_rgb.yaml b/ludwig/datasets/configs/eurosat_rgb.yaml new file mode 100644 index 0000000..64d4473 --- /dev/null +++ b/ludwig/datasets/configs/eurosat_rgb.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: eurosat_rgb +huggingface_dataset_id: blanchon/EuroSAT_RGB +loader: hugging_face.HFLoader +description: | + EuroSAT RGB: Sentinel-2 satellite image classification (10 land use classes). 16K train examples. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/factcheck.yaml b/ludwig/datasets/configs/factcheck.yaml new file mode 100644 index 0000000..23a9e4f --- /dev/null +++ b/ludwig/datasets/configs/factcheck.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: factcheck +huggingface_dataset_id: FactCheck-AI/FactCheck +loader: hugging_face.HFLoader +description: | + FactCheck: multilingual fact-checking question ranking dataset. 2M train examples. +columns: + - name: question + type: text + - name: title + type: text + - name: dataset + type: category + - name: language + type: category + - name: rank + type: number +output_features: + - name: rank + type: number diff --git a/ludwig/datasets/configs/fake_job_postings2.yaml b/ludwig/datasets/configs/fake_job_postings2.yaml new file mode 100644 index 0000000..baefd2a --- /dev/null +++ b/ludwig/datasets/configs/fake_job_postings2.yaml @@ -0,0 +1,31 @@ +version: 1.0 +name: fake_job_postings2 +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/fake_job_postings2/train.csv + - https://automl-mm-bench.s3.amazonaws.com/fake_job_postings2/test.csv +sha256: + test.csv: a5296f49129d440434e6274bb892a1320fe1dd4c26d5a1b085786d5ea1133dd8 + train.csv: b6568e415ad49cb7bd23848dfbb8d381f9de590e133a5075abbf4c1a7c7c1711 +train_filenames: train.csv +test_filenames: test.csv +description: | + This dataset contains 18K job descriptions out of which about 800 are fake. + The data consists of both textual information and meta-information about the jobs. + This dataset is "fake_job_postings2" in the AutoGluon paper. + https://www.kaggle.com/datasets/shivamb/real-or-fake-fake-jobposting-prediction +columns: + - name: title + type: category + - name: salary_range + type: category + - name: description + type: text + - name: required_experience + type: category + - name: required_education + type: category + - name: fraudulent + type: binary +output_features: + - name: fraudulent + type: binary diff --git a/ludwig/datasets/configs/fake_news_detection.yaml b/ludwig/datasets/configs/fake_news_detection.yaml new file mode 100644 index 0000000..fc2d368 --- /dev/null +++ b/ludwig/datasets/configs/fake_news_detection.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: fake_news_detection +huggingface_dataset_id: GonzaloA/fake_news +loader: hugging_face.HFLoader +description: | + Fake news detection; real vs fake news articles +columns: + - name: title + type: text + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/farstail_nli.yaml b/ludwig/datasets/configs/farstail_nli.yaml new file mode 100644 index 0000000..56e9464 --- /dev/null +++ b/ludwig/datasets/configs/farstail_nli.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: farstail_nli +huggingface_dataset_id: mteb/FarsTail +loader: hugging_face.HFLoader +description: | + FarsTail: Persian NLI (entailment/neutral/contradiction). 1K test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: labels + type: category +output_features: + - name: labels + type: category diff --git a/ludwig/datasets/configs/fashion_mnist.yaml b/ludwig/datasets/configs/fashion_mnist.yaml new file mode 100644 index 0000000..ddd5e9b --- /dev/null +++ b/ludwig/datasets/configs/fashion_mnist.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: fashion_mnist +huggingface_dataset_id: zalando-datasets/fashion_mnist +loader: hugging_face.HFLoader +description: | + Fashion MNIST; 10-class clothing item classification +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/fever.yaml b/ludwig/datasets/configs/fever.yaml new file mode 100644 index 0000000..60e7e74 --- /dev/null +++ b/ludwig/datasets/configs/fever.yaml @@ -0,0 +1,31 @@ +version: 1.0 +name: fever +download_urls: + - https://fever.ai/download/fever/train.jsonl + - https://fever.ai/download/fever/paper_dev.jsonl + - https://fever.ai/download/fever/paper_test.jsonl +sha256: + train.jsonl: eba7e8f87076753f8494718b9a857827af7bf73e76c9e4b75420207d26e588b6 + paper_test.jsonl: fb7b0280a0adc2302bbb29bfb7af37274fa585de3171bcf908f180642d11d88e + paper_dev.jsonl: 41158707810008747946bf23471e82df53e77a513524b9e3ec1c2e674ef5ef8c +train_filenames: train.jsonl +test_filenames: paper_test.jsonl +validation_filenames: paper_dev.jsonl +column_types: + evidence: str +description: | + FEVER: a Large-scale Dataset for Fact Extraction and VERification +columns: + - name: id + type: category + - name: verifiable + type: category + - name: label + type: category + - name: claim + type: text + - name: evidence + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/fever_gold.yaml b/ludwig/datasets/configs/fever_gold.yaml new file mode 100644 index 0000000..c4519dc --- /dev/null +++ b/ludwig/datasets/configs/fever_gold.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: fever_gold +huggingface_dataset_id: copenlu/fever_gold_evidence +loader: qa_loader.FeverGoldLoader +description: | + FEVER fact verification gold evidence. Claim → SUPPORTS/REFUTES/NOT_ENOUGH_INFO. +columns: + - name: claim + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/few_nerd.yaml b/ludwig/datasets/configs/few_nerd.yaml new file mode 100644 index 0000000..5ed691d --- /dev/null +++ b/ludwig/datasets/configs/few_nerd.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: few_nerd +huggingface_dataset_id: DFKI-SLT/few-nerd +huggingface_subsample: supervised +loader: ner_loader.FewNERDLoader +description: | + Few-NERD fine-grained NER with 8 coarse and 66 fine entity types. +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/financial_phrasebank.yaml b/ludwig/datasets/configs/financial_phrasebank.yaml new file mode 100644 index 0000000..74ea266 --- /dev/null +++ b/ludwig/datasets/configs/financial_phrasebank.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: financial_phrasebank +huggingface_dataset_id: nickmuchi/financial-classification +loader: hugging_face.HFLoader +description: | + Financial classification: sentiment analysis of financial text. + Labels: Bearish, Bullish, Neutral. +columns: + - name: text + type: text + - name: labels + type: category +output_features: + - name: labels + type: category diff --git a/ludwig/datasets/configs/fineweb_edu.yaml b/ludwig/datasets/configs/fineweb_edu.yaml new file mode 100644 index 0000000..2dff5d6 --- /dev/null +++ b/ludwig/datasets/configs/fineweb_edu.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: fineweb_edu +huggingface_dataset_id: HuggingFaceFW/fineweb-edu +huggingface_subsample: sample-10BT +loader: hugging_face.HFLoader +description: | + FineWeb-Edu: 1.3T token high-quality educational web text; 10BT sample subset. + int_score is 0-5 educational quality score. +columns: + - name: text + type: text + - name: int_score + type: number +output_features: + - name: int_score + type: number diff --git a/ludwig/datasets/configs/flashrag_2wikimultihop.yaml b/ludwig/datasets/configs/flashrag_2wikimultihop.yaml new file mode 100644 index 0000000..5b7048e --- /dev/null +++ b/ludwig/datasets/configs/flashrag_2wikimultihop.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: flashrag_2wikimultihop +huggingface_dataset_id: RUC-NLPIR/FlashRAG_datasets +huggingface_subsample: 2wikimultihopqa +loader: hugging_face.HFLoader +description: | + FlashRAG 2WikiMultiHopQA: multi-hop QA. 15K train examples. +columns: + - name: question + type: text + - name: golden_answers + type: text +output_features: + - name: golden_answers + type: text diff --git a/ludwig/datasets/configs/flickr8k.yaml b/ludwig/datasets/configs/flickr8k.yaml new file mode 100644 index 0000000..8539e64 --- /dev/null +++ b/ludwig/datasets/configs/flickr8k.yaml @@ -0,0 +1,29 @@ +version: 1.0 +name: flickr8k +download_urls: + - https://github.com/jbrownlee/Datasets/releases/download/Flickr8k/Flickr8k_Dataset.zip + - https://github.com/jbrownlee/Datasets/releases/download/Flickr8k/Flickr8k_text.zip +dataset_filenames: flickr8k_dataset.csv +preserve_paths: Flicker8k_Dataset +sha256: + Flickr8k_Dataset.zip: 61e4b111d32b24a55b69dafd91f4c3aec07391b7b9217face15dd35d517fe6de + Flickr8k_text.zip: 4992ddc8110e9aa49da5bf698522b0c8f11c448814a488584ee6bf040e5137e7 +loader: flickr8k.Flickr8kLoader +description: | + A new benchmark collection for sentence-based image description and search, + consisting of 8,000 images that are each paired with five different + captions which provide clear descriptions of the salient entities and + events. The images were chosen from six different Flickr groups, and tend + not to contain any well-known people or locations, but were manually + selected to depict a variety of scenes and situations. +output_features: + - name: caption0 + type: text + - name: caption1 + type: text + - name: caption2 + type: text + - name: caption3 + type: text + - name: caption4 + type: text diff --git a/ludwig/datasets/configs/flores_101.yaml b/ludwig/datasets/configs/flores_101.yaml new file mode 100644 index 0000000..4948c75 --- /dev/null +++ b/ludwig/datasets/configs/flores_101.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: flores_101 +huggingface_dataset_id: gsarti/flores_101 +huggingface_revision: refs/convert/parquet +huggingface_data_files: + train: eng/devtest/0000.parquet +loader: hugging_face.HFLoader +description: | + FLoRes-101 English evaluation sentences. Topic classification: predict + the domain (wikinews, wikibooks, wikivoyage) from the sentence text. +columns: + - name: sentence + type: text + - name: topic + type: text + - name: domain + type: category +output_features: + - name: domain + type: category diff --git a/ludwig/datasets/configs/food101.yaml b/ludwig/datasets/configs/food101.yaml new file mode 100644 index 0000000..3b93969 --- /dev/null +++ b/ludwig/datasets/configs/food101.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: food101 +huggingface_dataset_id: ethz/food101 +loader: hugging_face.HFLoader +description: | + Food-101; 101-class food image classification +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/forest_cover.yaml b/ludwig/datasets/configs/forest_cover.yaml new file mode 100644 index 0000000..f50d3ef --- /dev/null +++ b/ludwig/datasets/configs/forest_cover.yaml @@ -0,0 +1,41 @@ +version: 1.0 +name: forest_cover +download_urls: https://archive.ics.uci.edu/ml/machine-learning-databases/covtype/covtype.data.gz +sha256: + covtype.data.gz: 614360d0257557dd1792834a85a1cdebfadc3c4f30b011d56afee7ffb5b15771 +dataset_filenames: covtype.data +loader: forest_cover.ForestCoverLoader +description: | + The Forest Cover Type dataset. + Predicting forest cover type from cartographic variables only. + https://archive.ics.uci.edu/ml/datasets/covertype +columns: + - name: Elevation + type: number + - name: Aspect + type: number + - name: Slope + type: number + - name: Horizontal_Distance_To_Hydrology + type: number + - name: Vertical_Distance_To_Hydrology + type: number + - name: Horizontal_Distance_To_Roadways + type: number + - name: Hillshade_9am + type: number + - name: Hillshade_Noon + type: number + - name: Hillshade_3pm + type: number + - name: Horizontal_Distance_To_Fire_Points + type: number + - name: Wilderness_Area + type: category + - name: Soil_Type + type: category + - name: Cover_Type + type: category +output_features: + - name: Cover_Type + type: category diff --git a/ludwig/datasets/configs/gaia_cepheids.yaml b/ludwig/datasets/configs/gaia_cepheids.yaml new file mode 100644 index 0000000..82a8ab5 --- /dev/null +++ b/ludwig/datasets/configs/gaia_cepheids.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: gaia_cepheids +huggingface_dataset_id: juliensimon/gaia-dr3-cepheids +loader: hugging_face.HFLoader +description: | + Gaia DR3 Cepheids: predict stellar period from photometry. 15K examples. +columns: + - name: period_fundamental_days + type: number + - name: period_1st_overtone_days + type: number + - name: solution_id + type: number +output_features: + - name: period_fundamental_days + type: number diff --git a/ludwig/datasets/configs/gaia_rrlyrae.yaml b/ludwig/datasets/configs/gaia_rrlyrae.yaml new file mode 100644 index 0000000..a4e4d36 --- /dev/null +++ b/ludwig/datasets/configs/gaia_rrlyrae.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: gaia_rrlyrae +huggingface_dataset_id: juliensimon/gaia-dr3-rrlyrae +loader: hugging_face.HFLoader +description: | + Gaia DR3 RR Lyrae: predict fundamental period. 271K examples. +columns: + - name: source_id + type: number + - name: PF + type: number + - name: e_PF + type: number +output_features: + - name: PF + type: number diff --git a/ludwig/datasets/configs/gaia_spectroscopic_binaries.yaml b/ludwig/datasets/configs/gaia_spectroscopic_binaries.yaml new file mode 100644 index 0000000..1e4fc4e --- /dev/null +++ b/ludwig/datasets/configs/gaia_spectroscopic_binaries.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: gaia_spectroscopic_binaries +huggingface_dataset_id: juliensimon/gaia-dr3-spectroscopic-binaries +loader: hugging_face.HFLoader +description: | + Gaia DR3 Spectroscopic Binaries: predict orbital period. 186K examples. +columns: + - name: period_days + type: number + - name: period_days_err + type: number + - name: ra_deg + type: number + - name: dec_deg + type: number +output_features: + - name: period_days + type: number diff --git a/ludwig/datasets/configs/gaia_young_stellar_objects.yaml b/ludwig/datasets/configs/gaia_young_stellar_objects.yaml new file mode 100644 index 0000000..a4fe702 --- /dev/null +++ b/ludwig/datasets/configs/gaia_young_stellar_objects.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: gaia_young_stellar_objects +huggingface_dataset_id: juliensimon/gaia-dr3-young-stellar-objects +loader: hugging_face.HFLoader +description: | + Gaia DR3 Young Stellar Objects: class prediction from photometry. 79K examples. +columns: + - name: ra + type: number + - name: dec + type: number + - name: best_class_score + type: number +output_features: + - name: best_class_score + type: number diff --git a/ludwig/datasets/configs/gaiasky_qa.yaml b/ludwig/datasets/configs/gaiasky_qa.yaml new file mode 100644 index 0000000..15c236b --- /dev/null +++ b/ludwig/datasets/configs/gaiasky_qa.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: gaiasky_qa +huggingface_dataset_id: Langurmonkey/gaiasky-training-dataset +loader: hugging_face.HFLoader +description: | + Gaiasky astronomy Q&A dataset. 3.8K examples. +columns: + - name: instruction + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/germeval18.yaml b/ludwig/datasets/configs/germeval18.yaml new file mode 100644 index 0000000..cdb7140 --- /dev/null +++ b/ludwig/datasets/configs/germeval18.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: germeval18 +huggingface_dataset_id: philschmid/germeval18 +loader: hugging_face.HFLoader +description: | + GermEval 2018: German offensive language detection. Binary and multi-class labels. +columns: + - name: text + type: text + - name: binary + type: binary +output_features: + - name: binary + type: binary diff --git a/ludwig/datasets/configs/go_emotions.yaml b/ludwig/datasets/configs/go_emotions.yaml new file mode 100644 index 0000000..1f3084f --- /dev/null +++ b/ludwig/datasets/configs/go_emotions.yaml @@ -0,0 +1,31 @@ +version: 1.0 +name: go_emotions +huggingface_dataset_id: google-research-datasets/go_emotions +huggingface_subsample: simplified +loader: go_emotions.GoEmotionsLoader +description: | + GoEmotions: Multi-label Emotion Classification + + 54,263 human-labelled Reddit comments annotated with 28 emotion categories + (plus neutral). Each comment may carry one or more emotions, making this a + multi-label classification task. This dataset exercises Ludwig's *set* output + feature: the model receives a plain text comment and must predict the set of + emotion labels that apply. + + Emotion labels (28): + admiration, amusement, anger, annoyance, approval, caring, confusion, + curiosity, desire, disappointment, disapproval, disgust, embarrassment, + excitement, fear, gratitude, grief, joy, love, nervousness, optimism, + pride, realization, relief, remorse, sadness, surprise, neutral + + Reference: + GoEmotions: A Dataset of Fine-Grained Emotions (Demszky et al., ACL 2020) + License: Apache 2.0 +columns: + - name: text + type: text + - name: labels + type: set +output_features: + - name: labels + type: set diff --git a/ludwig/datasets/configs/go_emotions_multiclass.yaml b/ludwig/datasets/configs/go_emotions_multiclass.yaml new file mode 100644 index 0000000..f03edbe --- /dev/null +++ b/ludwig/datasets/configs/go_emotions_multiclass.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: go_emotions_multiclass +huggingface_dataset_id: google-research-datasets/go_emotions +huggingface_subsample: simplified +loader: go_emotions.GoEmotionsLoader +description: | + GoEmotions multi-label emotion (28 classes). Set output. Same as go_emotions but listed separately. +columns: + - name: text + type: text + - name: labels + type: set +output_features: + - name: labels + type: set diff --git a/ludwig/datasets/configs/goemotions.yaml b/ludwig/datasets/configs/goemotions.yaml new file mode 100644 index 0000000..d39877a --- /dev/null +++ b/ludwig/datasets/configs/goemotions.yaml @@ -0,0 +1,27 @@ +version: 1.0 +name: goemotions +download_urls: + - https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/train.tsv + - https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/dev.tsv + - https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/test.tsv +train_filenames: train.tsv +validation_filenames: dev.tsv +test_filenames: test.tsv +sha256: + train.tsv: 1c254a142be5c00e80d819b9ae1bbd36d94b2eeb8f4b1271846508d57e57d9c5 + dev.tsv: 575489c079c9de1097062a01738f998590d6b7ead66dd1c9fd1d2ba01fd8bc62 + test.tsv: 0587b2dd8b27b97352adbfc3fb083d46005c8946657fdc2b1ca8b1cc7f1f8be4 +loader: goemotions.GoEmotionsLoader +description: | + GoEmotions: A Dataset for Fine-Grained Emotion Classification. + https://ai.googleblog.com/2021/10/goemotions-dataset-for-fine-grained.html +columns: + - name: text + type: text + - name: emotion_ids + type: category + - name: comment_id + type: category +output_features: + - name: emotion_ids + type: category diff --git a/ludwig/datasets/configs/goodbooks_books.yaml b/ludwig/datasets/configs/goodbooks_books.yaml new file mode 100644 index 0000000..84794d0 --- /dev/null +++ b/ludwig/datasets/configs/goodbooks_books.yaml @@ -0,0 +1,75 @@ +version: 1.0 +name: goodbooks_books +download_urls: + - https://github.com/zygmuntz/goodbooks-10k/releases/download/v1.0/goodbooks-10k.zip +sha256: + goodbooks-10k.zip: 261b97b56db61f3fb2ce5aadbb13704d30179fcc986c17ace665a0af9ed00731 +dataset_filenames: books.csv +description: | + goodbooks_books is a multimodal dataset of 10K books, taken from the goodreads dataset. + The Goodbooks-10K dataset contains six million ratings for ten thousand most popular (with most ratings) books. + The dataset also contains: + books marked to read by the users + book metadata (author, year, etc.) + tags/shelves/genres + https://github.com/zygmuntz/goodbooks-10k +columns: + - name: book_id + type: category + - name: goodreads_book_id + type: category + - name: best_book_id + type: category + - name: work_id + type: category + - name: books_count + type: number + - name: isbn + type: category + - name: isbn13 + type: category + - name: authors + type: category + - name: original_publication_year + type: category + - name: original_title + type: category + - name: title + type: category + - name: language_code + type: category + - name: average_rating + type: number + - name: ratings_count + type: number + - name: work_ratings_count + type: number + - name: work_text_reviews_count + type: number + - name: ratings_1 + type: number + - name: ratings_2 + type: number + - name: ratings_3 + type: number + - name: ratings_4 + type: number + - name: ratings_5 + type: number + - name: image_url + type: image + - name: small_image_url + type: image +output_features: + - name: average_rating + type: number + - name: ratings_1 + type: number + - name: ratings_2 + type: number + - name: ratings_3 + type: number + - name: ratings_4 + type: number + - name: ratings_5 + type: number diff --git a/ludwig/datasets/configs/google_qa_answer_type_reason_explanation.yaml b/ludwig/datasets/configs/google_qa_answer_type_reason_explanation.yaml new file mode 100644 index 0000000..474321f --- /dev/null +++ b/ludwig/datasets/configs/google_qa_answer_type_reason_explanation.yaml @@ -0,0 +1,104 @@ +version: 1.0 +name: google_qa_answer_type_reason_explanation +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/train.pq + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/dev.pq +sha256: + train.pq: 92274286ffb759c96bfca77001c10eb323b3531db3a0e178813db9b82e80a12a + dev.pq: 2e66450215b94dc404eadc7dde83a1eabad9640d946863c298aa2d42c998ed84 +train_filenames: train.pq +test_filenames: dev.pq +description: | + Google QUEST Q&A Labeling + Improving automated understanding of complex question answer content. + The data for this competition includes questions and answers from various StackExchange properties. + https://www.kaggle.com/c/google-quest-challenge/data + Note: this is the same dataset as `google_quest_qa`. It is duplicated here to have a one-to-one mapping + with the benchmarking datasets in https://arxiv.org/pdf/2111.02705.pdf + In this paper, the column `answer_type_reason_explanation` is used as the output feature. +columns: + - name: qa_id + type: category + - name: question_title + type: text + - name: question_body + type: text + - name: question_user_name + type: category + - name: question_user_page + type: category + - name: answer + type: text + - name: answer_user_name + type: category + - name: answer_user_page + type: category + - name: url + type: category + - name: category + type: category + - name: host + type: category + - name: question_asker_intent_understanding + type: number + - name: question_body_critical + type: number + - name: question_conversational + type: number + - name: question_expect_short_answer + type: number + - name: question_fact_seeking + type: number + - name: question_has_commonly_accepted_answer + type: number + - name: question_interestingness_others + type: number + - name: question_interestingness_self + type: number + - name: question_multi_intent + type: number + - name: question_not_really_a_question + type: number + - name: question_opinion_seeking + type: number + - name: question_type_choice + type: number + - name: question_type_compare + type: number + - name: question_type_consequence + type: number + - name: question_type_definition + type: number + - name: question_type_entity + type: number + - name: question_type_instructions + type: number + - name: question_type_procedure + type: number + - name: question_type_reason_explanation + type: number + - name: question_type_spelling + type: number + - name: question_well_written + type: number + - name: answer_helpful + type: number + - name: answer_level_of_information + type: number + - name: answer_plausible + type: number + - name: answer_relevance + type: number + - name: answer_satisfaction + type: number + - name: answer_type_instructions + type: number + - name: answer_type_procedure + type: number + - name: answer_type_reason_explanation + type: number + - name: answer_well_written + type: number +output_features: + - name: answer_type_reason_explanation + type: number diff --git a/ludwig/datasets/configs/google_qa_question_type_reason_explanation.yaml b/ludwig/datasets/configs/google_qa_question_type_reason_explanation.yaml new file mode 100644 index 0000000..de81cff --- /dev/null +++ b/ludwig/datasets/configs/google_qa_question_type_reason_explanation.yaml @@ -0,0 +1,104 @@ +version: 1.0 +name: google_qa_question_type_reason_explanation +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/train.pq + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/dev.pq +sha256: + train.pq: 92274286ffb759c96bfca77001c10eb323b3531db3a0e178813db9b82e80a12a + dev.pq: 2e66450215b94dc404eadc7dde83a1eabad9640d946863c298aa2d42c998ed84 +train_filenames: train.pq +test_filenames: dev.pq +description: | + Google QUEST Q&A Labeling + Improving automated understanding of complex question answer content. + The data for this competition includes questions and answers from various StackExchange properties. + https://www.kaggle.com/c/google-quest-challenge/data + Note: this is the same dataset as `google_quest_qa`. It is duplicated here to have a one-to-one mapping + with the benchmarking datasets in https://arxiv.org/pdf/2111.02705.pdf + In this paper, the column `question_type_reason_explanation` is used as the output feature. +columns: + - name: qa_id + type: category + - name: question_title + type: text + - name: question_body + type: text + - name: question_user_name + type: category + - name: question_user_page + type: category + - name: answer + type: text + - name: answer_user_name + type: category + - name: answer_user_page + type: category + - name: url + type: category + - name: category + type: category + - name: host + type: category + - name: question_asker_intent_understanding + type: number + - name: question_body_critical + type: number + - name: question_conversational + type: number + - name: question_expect_short_answer + type: number + - name: question_fact_seeking + type: number + - name: question_has_commonly_accepted_answer + type: number + - name: question_interestingness_others + type: number + - name: question_interestingness_self + type: number + - name: question_multi_intent + type: number + - name: question_not_really_a_question + type: number + - name: question_opinion_seeking + type: number + - name: question_type_choice + type: number + - name: question_type_compare + type: number + - name: question_type_consequence + type: number + - name: question_type_definition + type: number + - name: question_type_entity + type: number + - name: question_type_instructions + type: number + - name: question_type_procedure + type: number + - name: question_type_reason_explanation + type: number + - name: question_type_spelling + type: number + - name: question_well_written + type: number + - name: answer_helpful + type: number + - name: answer_level_of_information + type: number + - name: answer_plausible + type: number + - name: answer_relevance + type: number + - name: answer_satisfaction + type: number + - name: answer_type_instructions + type: number + - name: answer_type_procedure + type: number + - name: answer_type_reason_explanation + type: number + - name: answer_well_written + type: number +output_features: + - name: question_type_reason_explanation + type: number diff --git a/ludwig/datasets/configs/google_quest_qa.yaml b/ludwig/datasets/configs/google_quest_qa.yaml new file mode 100644 index 0000000..d73c581 --- /dev/null +++ b/ludwig/datasets/configs/google_quest_qa.yaml @@ -0,0 +1,104 @@ +version: 1.0 +name: google_quest_qa +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/train.pq + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/dev.pq + - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/test.pq +sha256: + test.pq: cb1bb5f32374d83ad4ef7feb4e443c9376cdd919cda40057732ef500e9a4ecf3 + train.pq: 92274286ffb759c96bfca77001c10eb323b3531db3a0e178813db9b82e80a12a + dev.pq: 2e66450215b94dc404eadc7dde83a1eabad9640d946863c298aa2d42c998ed84 +train_filenames: train.pq +validation_filenames: dev.pq +test_filenames: test.pq +description: | + Google QUEST Q&A Labeling + Improving automated understanding of complex question answer content. + The data for this competition includes questions and answers from various StackExchange properties. + https://www.kaggle.com/c/google-quest-challenge/data +columns: + - name: qa_id + type: category + - name: question_title + type: text + - name: question_body + type: text + - name: question_user_name + type: category + - name: question_user_page + type: category + - name: answer + type: text + - name: answer_user_name + type: category + - name: answer_user_page + type: category + - name: url + type: category + - name: category + type: category + - name: host + type: category + - name: question_asker_intent_understanding + type: number + - name: question_body_critical + type: number + - name: question_conversational + type: number + - name: question_expect_short_answer + type: number + - name: question_fact_seeking + type: number + - name: question_has_commonly_accepted_answer + type: number + - name: question_interestingness_others + type: number + - name: question_interestingness_self + type: number + - name: question_multi_intent + type: number + - name: question_not_really_a_question + type: number + - name: question_opinion_seeking + type: number + - name: question_type_choice + type: number + - name: question_type_compare + type: number + - name: question_type_consequence + type: number + - name: question_type_definition + type: number + - name: question_type_entity + type: number + - name: question_type_instructions + type: number + - name: question_type_procedure + type: number + - name: question_type_reason_explanation + type: number + - name: question_type_spelling + type: number + - name: question_well_written + type: number + - name: answer_helpful + type: number + - name: answer_level_of_information + type: number + - name: answer_plausible + type: number + - name: answer_relevance + type: number + - name: answer_satisfaction + type: number + - name: answer_type_instructions + type: number + - name: answer_type_procedure + type: number + - name: answer_type_reason_explanation + type: number + - name: answer_well_written + type: number +output_features: + - name: question_type_reason_explanation + type: category diff --git a/ludwig/datasets/configs/govreport_summarization.yaml b/ludwig/datasets/configs/govreport_summarization.yaml new file mode 100644 index 0000000..8709da0 --- /dev/null +++ b/ludwig/datasets/configs/govreport_summarization.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: govreport_summarization +huggingface_dataset_id: ccdv/govreport-summarization +loader: hugging_face.HFLoader +description: | + GovReport: long government report summarization. 17K train examples. +columns: + - name: report + type: text + - name: summary + type: text +output_features: + - name: summary + type: text diff --git a/ludwig/datasets/configs/graid_bdd.yaml b/ludwig/datasets/configs/graid_bdd.yaml new file mode 100644 index 0000000..9c70066 --- /dev/null +++ b/ludwig/datasets/configs/graid_bdd.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: graid_bdd +huggingface_dataset_id: kd7/graid-bdd +loader: hugging_face.HFLoader +description: | + GrAID-BDD: grounded autonomous driving image QA. 4.6M train examples. +columns: + - name: image + type: image + - name: question + type: text + - name: answer + type: category + - name: question_type + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/gsm8k.yaml b/ludwig/datasets/configs/gsm8k.yaml new file mode 100644 index 0000000..bc9f057 --- /dev/null +++ b/ludwig/datasets/configs/gsm8k.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: gsm8k +huggingface_dataset_id: openai/gsm8k +huggingface_subsample: main +loader: hugging_face.HFLoader +description: | + GSM8K; grade school math word problems with step-by-step solutions +columns: + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/gsm8k_openai.yaml b/ludwig/datasets/configs/gsm8k_openai.yaml new file mode 100644 index 0000000..0deaf13 --- /dev/null +++ b/ludwig/datasets/configs/gsm8k_openai.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: gsm8k_openai +huggingface_dataset_id: openai/gsm8k +huggingface_subsample: main +loader: hugging_face.HFLoader +description: | + GSM8K (OpenAI): grade school math word problems (8.5K problems). +columns: + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/gtsrb.yaml b/ludwig/datasets/configs/gtsrb.yaml new file mode 100644 index 0000000..adc6098 --- /dev/null +++ b/ludwig/datasets/configs/gtsrb.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: gtsrb +huggingface_dataset_id: tanganke/gtsrb +loader: hugging_face.HFLoader +description: | + GTSRB; German Traffic Sign Recognition Benchmark; 43 classes +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/handwritten_crossouts.yaml b/ludwig/datasets/configs/handwritten_crossouts.yaml new file mode 100644 index 0000000..8c3c251 --- /dev/null +++ b/ludwig/datasets/configs/handwritten_crossouts.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: handwritten_crossouts +huggingface_dataset_id: wahlinski/handwritten_cross-outs +loader: hugging_face.HFLoader +description: | + Handwritten cross-outs: classify handwriting correction styles. 22K examples. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/hate_speech18.yaml b/ludwig/datasets/configs/hate_speech18.yaml new file mode 100644 index 0000000..a919d2f --- /dev/null +++ b/ludwig/datasets/configs/hate_speech18.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: hate_speech18 +huggingface_dataset_id: SetFit/hate_speech18 +loader: hugging_face.HFLoader +description: | + Hate Speech 18 (SetFit): binary hate speech detection on online forum data. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/hatespeech_offensive.yaml b/ludwig/datasets/configs/hatespeech_offensive.yaml new file mode 100644 index 0000000..43f9106 --- /dev/null +++ b/ludwig/datasets/configs/hatespeech_offensive.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: hatespeech_offensive +huggingface_dataset_id: tdavidson/hate_speech_offensive +loader: hugging_face.HFLoader +description: | + Hate Speech and Offensive Language: 25K tweets with 3-class labels + (hate speech, offensive but not hate speech, neither). +columns: + - name: tweet + type: text + - name: class + type: category +output_features: + - name: class + type: category diff --git a/ludwig/datasets/configs/hc3.yaml b/ludwig/datasets/configs/hc3.yaml new file mode 100644 index 0000000..5178adf --- /dev/null +++ b/ludwig/datasets/configs/hc3.yaml @@ -0,0 +1,21 @@ +version: 1.0 +name: hc3 +huggingface_dataset_id: Hello-SimpleAI/HC3 +huggingface_revision: refs/convert/parquet +huggingface_data_files: + train: all/train/0000.parquet +loader: misc_loaders.HC3Loader +description: | + Human ChatGPT Comparison Corpus (HC3): binary classification of whether an + answer is human-written (0) or generated by ChatGPT (1). Each source row is + expanded into two rows using the first answer from each list. +columns: + - name: question + type: text + - name: answer + type: text + - name: is_chatgpt + type: binary +output_features: + - name: is_chatgpt + type: binary diff --git a/ludwig/datasets/configs/hc3_chinese.yaml b/ludwig/datasets/configs/hc3_chinese.yaml new file mode 100644 index 0000000..e44df2c --- /dev/null +++ b/ludwig/datasets/configs/hc3_chinese.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: hc3_chinese +huggingface_dataset_id: Hello-SimpleAI/HC3-Chinese +huggingface_revision: refs/convert/parquet +huggingface_data_files: + train: all/train/0000.parquet +loader: misc_loaders.HC3Loader +description: | + HC3-Chinese: Chinese Human ChatGPT Comparison Corpus. Binary classification + of whether an answer is human-written (0) or generated by ChatGPT (1). +columns: + - name: question + type: text + - name: answer + type: text + - name: is_chatgpt + type: binary +output_features: + - name: is_chatgpt + type: binary diff --git a/ludwig/datasets/configs/heart_failure.yaml b/ludwig/datasets/configs/heart_failure.yaml new file mode 100644 index 0000000..e992837 --- /dev/null +++ b/ludwig/datasets/configs/heart_failure.yaml @@ -0,0 +1,37 @@ +version: 1.0 +name: heart_failure +huggingface_dataset_id: mstz/heart_failure +loader: hugging_face.HFLoader +description: 'Heart failure clinical records; death event prediction + + ' +columns: +- name: age + type: category +- name: has_anaemia + type: binary +- name: creatinine_phosphokinase_concentration_in_blood + type: number +- name: has_diabetes + type: binary +- name: heart_ejection_fraction + type: number +- name: has_high_blood_pressure + type: binary +- name: platelets_concentration_in_blood + type: number +- name: serum_creatinine_concentration_in_blood + type: number +- name: serum_sodium_concentration_in_blood + type: number +- name: is_male + type: binary +- name: is_smoker + type: binary +- name: days_in_study + type: category +- name: is_dead + type: category +output_features: +- name: is_dead + type: binary diff --git a/ludwig/datasets/configs/hellaswag.yaml b/ludwig/datasets/configs/hellaswag.yaml new file mode 100644 index 0000000..4680c7c --- /dev/null +++ b/ludwig/datasets/configs/hellaswag.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: hellaswag +huggingface_dataset_id: Rowan/hellaswag +loader: multiple_choice_loader.HellaSwagLoader +description: | + HellaSwag commonsense NLI. Activity + context → correct ending. A/B/C/D. +columns: + - name: activity_label + type: text + - name: ctx + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/helpsteer2.yaml b/ludwig/datasets/configs/helpsteer2.yaml new file mode 100644 index 0000000..511acfb --- /dev/null +++ b/ludwig/datasets/configs/helpsteer2.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: helpsteer2 +huggingface_dataset_id: nvidia/HelpSteer2 +loader: hugging_face.HFLoader +description: | + HelpSteer2: 21K prompt-response pairs with 5 quality attributes rated 0-4. +columns: + - name: prompt + type: text + - name: response + type: text + - name: helpfulness + type: number +output_features: + - name: helpfulness + type: number diff --git a/ludwig/datasets/configs/hermes_function_calling.yaml b/ludwig/datasets/configs/hermes_function_calling.yaml new file mode 100644 index 0000000..2547f41 --- /dev/null +++ b/ludwig/datasets/configs/hermes_function_calling.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: hermes_function_calling +huggingface_dataset_id: NousResearch/hermes-function-calling-v1 +huggingface_subsample: func_calling_singleturn +loader: hugging_face.HFLoader +description: | + Hermes function calling: tool-use conversations with category labels. 1893 train examples. +columns: + - name: category + type: category + - name: subcategory + type: category + - name: task + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/hh_rlhf.yaml b/ludwig/datasets/configs/hh_rlhf.yaml new file mode 100644 index 0000000..9222466 --- /dev/null +++ b/ludwig/datasets/configs/hh_rlhf.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: hh_rlhf +huggingface_dataset_id: Anthropic/hh-rlhf +loader: hugging_face.HFLoader +description: | + Anthropic HH-RLHF: 170K human feedback pairs for helpful/harmless RLHF training. +columns: + - name: chosen + type: text + - name: rejected + type: text +output_features: + - name: chosen + type: text diff --git a/ludwig/datasets/configs/higgs.yaml b/ludwig/datasets/configs/higgs.yaml new file mode 100644 index 0000000..ea185dd --- /dev/null +++ b/ludwig/datasets/configs/higgs.yaml @@ -0,0 +1,77 @@ +version: 1.0 +name: higgs +download_urls: https://archive.ics.uci.edu/ml/machine-learning-databases/00280/HIGGS.csv.gz +sha256: + HIGGS.csv.gz: ea302c18164d4e3d916a1e2e83a9a8d07069fa6ebc7771e4c0540d54e593b698 +column_types: + label: int32 +loader: higgs.HiggsLoader +description: | + The Higgs Boson dataset. + + This is a classification problem to distinguish between a signal process + which produces Higgs bosons and a background process which does not. + + https://archive.ics.uci.edu/ml/datasets/HIGGS +columns: + - name: label + type: binary + - name: lepton_pT + type: number + - name: lepton_eta + type: number + - name: lepton_phi + type: number + - name: missing_energy_magnitude + type: number + - name: missing_energy_phi + type: number + - name: jet_1_pt + type: number + - name: jet_1_eta + type: number + - name: jet_1_phi + type: number + - name: jet_1_b-tag + type: number + - name: jet_2_pt + type: number + - name: jet_2_eta + type: number + - name: jet_2_phi + type: number + - name: jet_2_b-tag + type: number + - name: jet_3_pt + type: number + - name: jet_3_eta + type: number + - name: jet_3_phi + type: number + - name: jet_3_b-tag + type: number + - name: jet_4_pt + type: number + - name: jet_4_eta + type: number + - name: jet_4_phi + type: number + - name: jet_4_b-tag + type: number + - name: m_jj + type: number + - name: m_jjj + type: number + - name: m_lv + type: number + - name: m_jlv + type: number + - name: m_bb + type: number + - name: m_wbb + type: number + - name: m_wwbb + type: number +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/hotpot_qa.yaml b/ludwig/datasets/configs/hotpot_qa.yaml new file mode 100644 index 0000000..0221f4f --- /dev/null +++ b/ludwig/datasets/configs/hotpot_qa.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: hotpot_qa +huggingface_dataset_id: hotpotqa/hotpot_qa +huggingface_subsample: distractor +loader: qa_loader.HotpotQALoader +description: | + HotpotQA multi-hop reasoning QA. Question → answer. +columns: + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/hugging_face.yaml b/ludwig/datasets/configs/hugging_face.yaml new file mode 100644 index 0000000..0fbf423 --- /dev/null +++ b/ludwig/datasets/configs/hugging_face.yaml @@ -0,0 +1,5 @@ +version: 1.0 +name: hugging_face +loader: hugging_face.HFLoader +description: | + Hugging Face Datasets diff --git a/ludwig/datasets/configs/ieee_fraud.yaml b/ludwig/datasets/configs/ieee_fraud.yaml new file mode 100644 index 0000000..d9b1ff3 --- /dev/null +++ b/ludwig/datasets/configs/ieee_fraud.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: ieee_fraud +kaggle_competition: ieee-fraud-detection +archive_filenames: ieee-fraud-detection.zip +sha256: + ieee-fraud-detection.zip: 4cc646da09d0a9b265983ffed775b1f9ee15af5266586df610e04d6adae0b829 +train_filenames: + - train_identity.csv + - train_transaction.csv +test_filenames: + - test_identity.csv + - test_transaction.csv +loader: ieee_fraud.IEEEFraudLoader +description: | + The IEEE-CIS Fraud Detection Dataset + https://www.kaggle.com/c/ieee-fraud-detection/overview. +output_features: + - name: isFraud + type: binary diff --git a/ludwig/datasets/configs/imagenet_100.yaml b/ludwig/datasets/configs/imagenet_100.yaml new file mode 100644 index 0000000..344cc95 --- /dev/null +++ b/ludwig/datasets/configs/imagenet_100.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: imagenet_100 +huggingface_dataset_id: clane9/imagenet-100 +loader: hugging_face.HFLoader +description: | + ImageNet-100: 100-class subset of ImageNet. 126K train examples. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/imbalanced_insurance.yaml b/ludwig/datasets/configs/imbalanced_insurance.yaml new file mode 100644 index 0000000..09e590f --- /dev/null +++ b/ludwig/datasets/configs/imbalanced_insurance.yaml @@ -0,0 +1,40 @@ +version: 1.0 +name: imbalanced_insurance +kaggle_dataset_id: arashnic/imbalanced-data-practice +archive_filenames: imbalanced-data-practice.zip +sha256: + imbalanced-data-practice.zip: 33c7d15cbdb7cc151c1d5e920a8a613b015c19222f90d4eac04ca8cfc5416847 +dataset_filenames: aug_train.csv +loader: split_loaders.RandomSplitLoader +description: | + Health Insurance Cross Sell Prediction + Predict Health Insurance Owners' who will be interested in Vehicle Insurance + https://www.kaggle.com/datasets/arashnic/imbalanced-data-practice +columns: + - name: id + type: category + - name: Gender + type: binary + - name: Age + type: number + - name: Driving_License + type: binary + - name: Region_Code + type: category + - name: Previously_Insured + type: binary + - name: Vehicle_Age + type: category + - name: Vehicle_Damage + type: binary + - name: Annual_Premium + type: number + - name: Policy_Sales_Channel + type: + - name: Vintage + type: + - name: Response + type: +output_features: + - name: Response + type: binary diff --git a/ludwig/datasets/configs/imdb.yaml b/ludwig/datasets/configs/imdb.yaml new file mode 100644 index 0000000..579dac6 --- /dev/null +++ b/ludwig/datasets/configs/imdb.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: imdb +kaggle_dataset_id: lakshmi25npathi/imdb-dataset-of-50k-movie-reviews +archive_filenames: imdb-dataset-of-50k-movie-reviews.zip +sha256: + imdb-dataset-of-50k-movie-reviews.zip: 73a235bc5fc4df57bb5d517afa480fe6bfd4e2afc25dc5e5867fc87f2d25614d +description: | + IMDB dataset having 50K movie reviews for natural language processing or Text analytics. + https://www.kaggle.com/datasets/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews +columns: + - name: review + type: text + - name: sentiment + type: category +output_features: + - name: sentiment + type: binary diff --git a/ludwig/datasets/configs/imdb_genre_prediction.yaml b/ludwig/datasets/configs/imdb_genre_prediction.yaml new file mode 100644 index 0000000..19fb8b9 --- /dev/null +++ b/ludwig/datasets/configs/imdb_genre_prediction.yaml @@ -0,0 +1,42 @@ +version: 1.0 +name: imdb_genre_prediction +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/imdb_genre_prediction/train.csv + - https://automl-mm-bench.s3.amazonaws.com/imdb_genre_prediction/test.csv +sha256: + test.csv: 5bca7b6ca34f4057e2a4920d6034f481055bd03061bb0128c87d6c99a6b4661f + train.csv: b63f1f6fcad17f644d9266891a01d0f0e1187c277ccf6eecb80af72b92b0b621 +train_filenames: train.csv +test_filenames: test.csv +description: | + A data set of 1,000 most popular movies on IMDB in the last 10 years. The data points included are: + Title, Genre, Description, Director, Actors, Year, Runtime, Rating, Votes, Revenue, Metascrore + https://www.kaggle.com/PromptCloudHQ/imdb-data +columns: + - name: Rank + type: number + - name: Title + type: category + - name: Description + type: text + - name: Director + type: category + - name: Actors + type: set + - name: Year + type: category + - name: Runtime (Minutes) + type: number + - name: Rating + type: Number + - name: Votes + type: number + - name: Revenue (Millions) + type: number + - name: Metascore + type: number + - name: Genre_is_Drama + type: binary +output_features: +- name: Genre_is_Drama + type: binary diff --git a/ludwig/datasets/configs/imdb_mteb.yaml b/ludwig/datasets/configs/imdb_mteb.yaml new file mode 100644 index 0000000..13c885d --- /dev/null +++ b/ludwig/datasets/configs/imdb_mteb.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: imdb_mteb +huggingface_dataset_id: mteb/imdb +loader: hugging_face.HFLoader +description: | + IMDB movie review sentiment binary classification (positive/negative). 24K train examples. +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/imdb_sentiment.yaml b/ludwig/datasets/configs/imdb_sentiment.yaml new file mode 100644 index 0000000..46343c1 --- /dev/null +++ b/ludwig/datasets/configs/imdb_sentiment.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: imdb_sentiment +huggingface_dataset_id: stanfordnlp/imdb +loader: hugging_face.HFLoader +description: | + IMDB movie review sentiment; positive/negative binary classification +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/indic_glue.yaml b/ludwig/datasets/configs/indic_glue.yaml new file mode 100644 index 0000000..0bf8ee7 --- /dev/null +++ b/ludwig/datasets/configs/indic_glue.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: indic_glue +huggingface_dataset_id: ai4bharat/indic_glue +huggingface_subsample: actsa-sc.te +loader: hugging_face.HFLoader +description: | + IndicGLUE Telugu sentiment classification (actsa-sc). 4328 train examples. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/insurance_lite.yaml b/ludwig/datasets/configs/insurance_lite.yaml new file mode 100644 index 0000000..f291ef9 --- /dev/null +++ b/ludwig/datasets/configs/insurance_lite.yaml @@ -0,0 +1,33 @@ +version: 1.0 +name: insurance_lite +kaggle_dataset_id: infernape/fast-furious-and-insured +archive_filenames: fast-furious-and-insured.zip +sha256: + fast-furious-and-insured.zip: 3b88ada517aa88d9c9187121d7ef42f4b5539808677a2b0827b989ca0fa19600 +dataset_filenames: Fast_Furious_Insured/train.csv +preserve_paths: Fast_Furious_Insured +loader: insurance_lite.InsuranceLiteLoader +description: | + The dataset consists of parameters such as the images of damaged cars, + the price of the cars and their insurance claim, and the like. + Predict the insurance claim for the cars that are provided in the dataset. +columns: + - name: image_path + type: image + - name: insurance_company + type: category + - name: cost_of_vehicle + type: number + - name: min_coverage + type: number + - name: expiry_date + type: date + - name: max_coverage + type: number + - name: condition + type: binary + - name: amount + type: number +output_features: + - name: amount + type: number diff --git a/ludwig/datasets/configs/intuitive_physics.yaml b/ludwig/datasets/configs/intuitive_physics.yaml new file mode 100644 index 0000000..ccc6cd5 --- /dev/null +++ b/ludwig/datasets/configs/intuitive_physics.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: intuitive_physics +huggingface_dataset_id: worldbenchmark/IntuitivePhysics +loader: hugging_face.HFLoader +description: | + Intuitive Physics: image-based physical intuition classification. 280K train. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/invoice_data.yaml b/ludwig/datasets/configs/invoice_data.yaml new file mode 100644 index 0000000..f73cd5d --- /dev/null +++ b/ludwig/datasets/configs/invoice_data.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: invoice_data +huggingface_dataset_id: katanaml-org/invoices-donut-data-v1 +loader: hugging_face.HFLoader +description: | + Invoice document understanding: invoice image -> JSON ground truth (text generation). + Exercises image input with text output for document AI tasks. +columns: + - name: image + type: image + - name: ground_truth + type: text +output_features: + - name: ground_truth + type: text diff --git a/ludwig/datasets/configs/iris.yaml b/ludwig/datasets/configs/iris.yaml new file mode 100644 index 0000000..aa16fc6 --- /dev/null +++ b/ludwig/datasets/configs/iris.yaml @@ -0,0 +1,22 @@ +version: 1.0 +name: iris +download_urls: https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv +sha256: + iris.csv: 9cc1c345c71bcc9b486b74cbf6063fa66f4bb5e0f603a4b3c3471ec2e5e8e355 +description: | + Iris Dataset + https://archive.ics.uci.edu/ml/datasets/Iris +columns: + - name: sepal_length_cm + type: number + - name: sepal_width_cm + type: number + - name: petal_length_cm + type: number + - name: petal_width_cm + type: number + - name: class + type: category +output_features: + - name: class + type: category diff --git a/ludwig/datasets/configs/iris_sklearn.yaml b/ludwig/datasets/configs/iris_sklearn.yaml new file mode 100644 index 0000000..2a4033c --- /dev/null +++ b/ludwig/datasets/configs/iris_sklearn.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: iris_sklearn +huggingface_dataset_id: scikit-learn/iris +loader: hugging_face.HFLoader +description: | + Iris dataset: classic 3-class flower classification by petal/sepal measurements. +columns: + - name: SepalLengthCm + type: number + - name: SepalWidthCm + type: number + - name: PetalLengthCm + type: number + - name: PetalWidthCm + type: number + - name: Species + type: category +output_features: + - name: Species + type: category diff --git a/ludwig/datasets/configs/irony.yaml b/ludwig/datasets/configs/irony.yaml new file mode 100644 index 0000000..5279839 --- /dev/null +++ b/ludwig/datasets/configs/irony.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: irony +download_urls: https://raw.githubusercontent.com/bwallace/ACL-2014-irony/master/irony-labeled.csv +sha256: + irony-labeled.csv: 11f4d0964bd9c5c8363de2920612f5d926a4e6b3a8ab9187da2c33cfc0fdd02b +description: | + The Reddit Irony dataset. + Source Paper: Humans Require Context to Infer Ironic Intent (so Computers Probably do, too) + Byron C Wallace, Do Kook Choe, Laura Kertz, and Eugene Charniak +columns: + - name: comment_text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/jc_penney_products.yaml b/ludwig/datasets/configs/jc_penney_products.yaml new file mode 100644 index 0000000..d113044 --- /dev/null +++ b/ludwig/datasets/configs/jc_penney_products.yaml @@ -0,0 +1,30 @@ +version: 1.0 +name: jc_penney_products +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/jc_penney_products/train.csv + - https://automl-mm-bench.s3.amazonaws.com/jc_penney_products/test.csv +sha256: + test.csv: 458fb13b07701897fbc0d88481823b90e884e92a42e65eeba816cdf3523b2e85 + train.csv: e9e3d3da627dc544d01f4c27b1d023288c68e55ce2db2593fb7b2268a6b9b020 +train_filenames: train.csv +test_filenames: test.csv +description: | + JCPenney products + 20,000 product listings from JCPenney + https://www.kaggle.com/PromptCloudHQ/all-jc-penny-products +columns: + - name: name_title + type: category + - name: description + type: text + - name: sale_price + type: number + - name: average_product_rating + type: number + - name: brand + type: category + - name: total_number_reviews + type: number +output_features: +- name: sale_price + type: number diff --git a/ludwig/datasets/configs/jigsaw_unintended_bias.yaml b/ludwig/datasets/configs/jigsaw_unintended_bias.yaml new file mode 100644 index 0000000..c68813e --- /dev/null +++ b/ludwig/datasets/configs/jigsaw_unintended_bias.yaml @@ -0,0 +1,111 @@ +version: 1.0 +name: jigsaw_unintended_bias +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias/train.pq + - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias/dev.pq + - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias/test.pq +sha256: + test.pq: e9f3fd6fa83ddea2af8d21e93eb677b2fa5686c9b8ae38e6293f7c3306f66fad + train.pq: 30bedd5bbd5b2277b8bffa4ed3a02ce6ef7c838aa5c1338908b5ad599a6a9888 + dev.pq: 57e1e3a06733fb83ad9ca46839ed8afd7d670e5e5f5c7f0026b748d760457d57 +train_filenames: train.pq +validation_filenames: dev.pq +test_filenames: test.pq +description: | + A dataset labeled for identity mentions and optimizing a metric designed to measure unintended bias. + Disclaimer: The dataset for this competition contains text that may be considered profane, vulgar, or offensive. + https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification +columns: + - name: id + type: category + - name: target + type: binary + - name: comment_text + type: text + - name: severe_toxicity + type: number + - name: obscene + type: number + - name: identity_attack + type: number + - name: insult + type: number + - name: threat + type: number + - name: asian + type: number + - name: atheist + type: number + - name: bisexual + type: number + - name: black + type: number + - name: buddhist + type: number + - name: christian + type: number + - name: female + type: number + - name: heterosexual + type: number + - name: hindu + type: number + - name: homosexual_gay_or_lesbian + type: number + - name: intellectual_or_learning_disability + type: number + - name: jewish + type: number + - name: latino + type: number + - name: male + type: number + - name: muslim + type: number + - name: other_disability + type: number + - name: other_gender + type: number + - name: other_race_or_ethnicity + type: number + - name: other_religion + type: number + - name: other_sexual_orientation + type: number + - name: physical_disability + type: number + - name: psychiatric_or_mental_illness + type: number + - name: transgender + type: number + - name: white + type: number + - name: created_date + type: date + - name: publication_id + type: category + - name: parent_id + type: category + - name: article_id + type: category + - name: rating + type: category + - name: funny + type: number + - name: wow + type: number + - name: sad + type: number + - name: likes + type: number + - name: disagree + type: number + - name: sexual_explicit + type: number + - name: identity_annotator_count + type: number + - name: toxicity_annotator_count + type: number +output_features: +- name: target + type: binary diff --git a/ludwig/datasets/configs/jigsaw_unintended_bias100k.yaml b/ludwig/datasets/configs/jigsaw_unintended_bias100k.yaml new file mode 100644 index 0000000..608b0f8 --- /dev/null +++ b/ludwig/datasets/configs/jigsaw_unintended_bias100k.yaml @@ -0,0 +1,108 @@ +version: 1.0 +name: jigsaw_unintended_bias100k +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias100K/train.pq + - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias100K/test.pq +sha256: + test.pq: f7a0ec60ac89ffdb94919bf95e514057588a444c90ebdcb8ac90dfb0bfec3d48 + train.pq: 48916c037b0a20167f6e9176cc1eedcb0e6ef942beeedb7dc02f19dfebac0229 +train_filenames: train.pq +test_filenames: test.pq +description: | + A dataset labeled for identity mentions and optimizing a metric designed to measure unintended bias. + Disclaimer: The dataset for this competition contains text that may be considered profane, vulgar, or offensive. + https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification +columns: + - name: id + type: category + - name: target + type: binary + - name: comment_text + type: text + - name: severe_toxicity + type: number + - name: obscene + type: number + - name: identity_attack + type: number + - name: insult + type: number + - name: threat + type: number + - name: asian + type: number + - name: atheist + type: number + - name: bisexual + type: number + - name: black + type: number + - name: buddhist + type: number + - name: christian + type: number + - name: female + type: number + - name: heterosexual + type: number + - name: hindu + type: number + - name: homosexual_gay_or_lesbian + type: number + - name: intellectual_or_learning_disability + type: number + - name: jewish + type: number + - name: latino + type: number + - name: male + type: number + - name: muslim + type: number + - name: other_disability + type: number + - name: other_gender + type: number + - name: other_race_or_ethnicity + type: number + - name: other_religion + type: number + - name: other_sexual_orientation + type: number + - name: physical_disability + type: number + - name: psychiatric_or_mental_illness + type: number + - name: transgender + type: number + - name: white + type: number + - name: created_date + type: date + - name: publication_id + type: category + - name: parent_id + type: category + - name: article_id + type: category + - name: rating + type: category + - name: funny + type: number + - name: wow + type: number + - name: sad + type: number + - name: likes + type: number + - name: disagree + type: number + - name: sexual_explicit + type: number + - name: identity_annotator_count + type: number + - name: toxicity_annotator_count + type: number +output_features: +- name: target + type: binary diff --git a/ludwig/datasets/configs/kdd_appetency.yaml b/ludwig/datasets/configs/kdd_appetency.yaml new file mode 100644 index 0000000..e185260 --- /dev/null +++ b/ludwig/datasets/configs/kdd_appetency.yaml @@ -0,0 +1,485 @@ +version: 1.0 +name: kdd_appetency +download_urls: + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train.data.zip + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_test.data.zip + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train_appetency.labels + - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/appetency/stratified_train_idx_appetency.txt + - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/appetency/stratified_test_idx_appetency.txt +sha256: + orange_small_test.data.zip: 440ac8a350144c14f4d6947c096ad675ee84aa27b4b742071662696e333cec53 + orange_small_train.data.zip: 31ccb810bdbb71c16e079326443166dc3dfbf73cd358fc4a4ce7440fb1bc6040 + orange_small_train_appetency.labels: edbfa40e7513804cf25c3f8b3c8f4a6cf5c77116cffc2f87ef770351250a963c + stratified_train_idx_appetency.txt: 9c6bf7da6209653e13d9a1d2ef90e4afafe0ecac0eb843c8025816a445c625d9 + stratified_test_idx_appetency.txt: b80fb8dcf43cd028f4b8affeab65299d580a7e5432ebbe639527dc8177f8764a +dataset_filenames: orange_small_train.data +loader: kdd_loader.KDDAppetencyLoader +description: | + The KDD Cup 2009 Appetency dataset. + https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data +columns: + - name: Var1 + type: number + - name: Var2 + type: number + - name: Var3 + type: number + - name: Var4 + type: number + - name: Var5 + type: number + - name: Var6 + type: number + - name: Var7 + type: number + - name: Var8 + type: number + - name: Var9 + type: number + - name: Var10 + type: number + - name: Var11 + type: number + - name: Var12 + type: number + - name: Var13 + type: number + - name: Var14 + type: number + - name: Var15 + type: number + - name: Var16 + type: number + - name: Var17 + type: number + - name: Var18 + type: number + - name: Var19 + type: number + - name: Var20 + type: number + - name: Var21 + type: number + - name: Var22 + type: number + - name: Var23 + type: number + - name: Var24 + type: number + - name: Var25 + type: number + - name: Var26 + type: number + - name: Var27 + type: number + - name: Var28 + type: number + - name: Var29 + type: number + - name: Var30 + type: number + - name: Var31 + type: number + - name: Var32 + type: number + - name: Var33 + type: number + - name: Var34 + type: number + - name: Var35 + type: number + - name: Var36 + type: number + - name: Var37 + type: number + - name: Var38 + type: number + - name: Var39 + type: number + - name: Var40 + type: number + - name: Var41 + type: number + - name: Var42 + type: number + - name: Var43 + type: number + - name: Var44 + type: number + - name: Var45 + type: number + - name: Var46 + type: number + - name: Var47 + type: number + - name: Var48 + type: number + - name: Var49 + type: number + - name: Var50 + type: number + - name: Var51 + type: number + - name: Var52 + type: number + - name: Var53 + type: number + - name: Var54 + type: number + - name: Var55 + type: number + - name: Var56 + type: number + - name: Var57 + type: number + - name: Var58 + type: number + - name: Var59 + type: number + - name: Var60 + type: number + - name: Var61 + type: number + - name: Var62 + type: number + - name: Var63 + type: number + - name: Var64 + type: number + - name: Var65 + type: number + - name: Var66 + type: number + - name: Var67 + type: number + - name: Var68 + type: number + - name: Var69 + type: number + - name: Var70 + type: number + - name: Var71 + type: number + - name: Var72 + type: number + - name: Var73 + type: number + - name: Var74 + type: number + - name: Var75 + type: number + - name: Var76 + type: number + - name: Var77 + type: number + - name: Var78 + type: number + - name: Var79 + type: number + - name: Var80 + type: number + - name: Var81 + type: number + - name: Var82 + type: number + - name: Var83 + type: number + - name: Var84 + type: number + - name: Var85 + type: number + - name: Var86 + type: number + - name: Var87 + type: number + - name: Var88 + type: number + - name: Var89 + type: number + - name: Var90 + type: number + - name: Var91 + type: number + - name: Var92 + type: number + - name: Var93 + type: number + - name: Var94 + type: number + - name: Var95 + type: number + - name: Var96 + type: number + - name: Var97 + type: number + - name: Var98 + type: number + - name: Var99 + type: number + - name: Var100 + type: number + - name: Var101 + type: number + - name: Var102 + type: number + - name: Var103 + type: number + - name: Var104 + type: number + - name: Var105 + type: number + - name: Var106 + type: number + - name: Var107 + type: number + - name: Var108 + type: number + - name: Var109 + type: number + - name: Var110 + type: number + - name: Var111 + type: number + - name: Var112 + type: number + - name: Var113 + type: number + - name: Var114 + type: number + - name: Var115 + type: number + - name: Var116 + type: number + - name: Var117 + type: number + - name: Var118 + type: number + - name: Var119 + type: number + - name: Var120 + type: number + - name: Var121 + type: number + - name: Var122 + type: number + - name: Var123 + type: number + - name: Var124 + type: number + - name: Var125 + type: number + - name: Var126 + type: number + - name: Var127 + type: number + - name: Var128 + type: number + - name: Var129 + type: number + - name: Var130 + type: number + - name: Var131 + type: number + - name: Var132 + type: number + - name: Var133 + type: number + - name: Var134 + type: number + - name: Var135 + type: number + - name: Var136 + type: number + - name: Var137 + type: number + - name: Var138 + type: number + - name: Var139 + type: number + - name: Var140 + type: number + - name: Var141 + type: number + - name: Var142 + type: number + - name: Var143 + type: number + - name: Var144 + type: number + - name: Var145 + type: number + - name: Var146 + type: number + - name: Var147 + type: number + - name: Var148 + type: number + - name: Var149 + type: number + - name: Var150 + type: number + - name: Var151 + type: number + - name: Var152 + type: number + - name: Var153 + type: number + - name: Var154 + type: number + - name: Var155 + type: number + - name: Var156 + type: number + - name: Var157 + type: number + - name: Var158 + type: number + - name: Var159 + type: number + - name: Var160 + type: number + - name: Var161 + type: number + - name: Var162 + type: number + - name: Var163 + type: number + - name: Var164 + type: number + - name: Var165 + type: number + - name: Var166 + type: number + - name: Var167 + type: number + - name: Var168 + type: number + - name: Var169 + type: number + - name: Var170 + type: number + - name: Var171 + type: number + - name: Var172 + type: number + - name: Var173 + type: number + - name: Var174 + type: number + - name: Var175 + type: number + - name: Var176 + type: number + - name: Var177 + type: number + - name: Var178 + type: number + - name: Var179 + type: number + - name: Var180 + type: number + - name: Var181 + type: number + - name: Var182 + type: number + - name: Var183 + type: number + - name: Var184 + type: number + - name: Var185 + type: number + - name: Var186 + type: number + - name: Var187 + type: number + - name: Var188 + type: number + - name: Var189 + type: number + - name: Var190 + type: number + - name: Var191 + type: category + - name: Var192 + type: category + - name: Var193 + type: category + - name: Var194 + type: category + - name: Var195 + type: category + - name: Var196 + type: category + - name: Var197 + type: category + - name: Var198 + type: category + - name: Var199 + type: category + - name: Var200 + type: category + - name: Var201 + type: category + - name: Var202 + type: category + - name: Var203 + type: category + - name: Var204 + type: category + - name: Var205 + type: category + - name: Var206 + type: category + - name: Var207 + type: category + - name: Var208 + type: category + - name: Var209 + type: number + - name: Var210 + type: category + - name: Var211 + type: category + - name: Var212 + type: category + - name: Var213 + type: category + - name: Var214 + type: category + - name: Var215 + type: category + - name: Var216 + type: category + - name: Var217 + type: category + - name: Var218 + type: category + - name: Var219 + type: category + - name: Var220 + type: category + - name: Var221 + type: category + - name: Var222 + type: category + - name: Var223 + type: category + - name: Var224 + type: category + - name: Var225 + type: category + - name: Var226 + type: category + - name: Var227 + type: category + - name: Var228 + type: category + - name: Var229 + type: category + - name: Var230 + type: number + - name: target + type: binary +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/kdd_churn.yaml b/ludwig/datasets/configs/kdd_churn.yaml new file mode 100644 index 0000000..0abf77b --- /dev/null +++ b/ludwig/datasets/configs/kdd_churn.yaml @@ -0,0 +1,485 @@ +version: 1.0 +name: kdd_churn +download_urls: + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train.data.zip + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_test.data.zip + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train_churn.labels + - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/churn/stratified_train_idx_churn.txt + - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/churn/stratified_test_idx_churn.txt +sha256: + orange_small_test.data.zip: 440ac8a350144c14f4d6947c096ad675ee84aa27b4b742071662696e333cec53 + orange_small_train.data.zip: 31ccb810bdbb71c16e079326443166dc3dfbf73cd358fc4a4ce7440fb1bc6040 + orange_small_train_churn.labels: fe8891cc574bd55a214514e522a5bed1eec2c3f347a49a36e51620009e7b6f5b + stratified_train_idx_churn.txt: 34f9880959ced6f668b25f879fdd388b3826efeca0df03f5a2a5494ce6795406 + stratified_test_idx_churn.txt: 1675a62cd49c43535eedee3b746f65f8c6a4ebd7f4d0da04e442fd658a408042 +dataset_filenames: orange_small_train.data +loader: kdd_loader.KDDChurnLoader +description: | + The KDD Cup 2009 Churn dataset. + https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data +columns: + - name: Var1 + type: number + - name: Var2 + type: number + - name: Var3 + type: number + - name: Var4 + type: number + - name: Var5 + type: number + - name: Var6 + type: number + - name: Var7 + type: number + - name: Var8 + type: number + - name: Var9 + type: number + - name: Var10 + type: number + - name: Var11 + type: number + - name: Var12 + type: number + - name: Var13 + type: number + - name: Var14 + type: number + - name: Var15 + type: number + - name: Var16 + type: number + - name: Var17 + type: number + - name: Var18 + type: number + - name: Var19 + type: number + - name: Var20 + type: number + - name: Var21 + type: number + - name: Var22 + type: number + - name: Var23 + type: number + - name: Var24 + type: number + - name: Var25 + type: number + - name: Var26 + type: number + - name: Var27 + type: number + - name: Var28 + type: number + - name: Var29 + type: number + - name: Var30 + type: number + - name: Var31 + type: number + - name: Var32 + type: number + - name: Var33 + type: number + - name: Var34 + type: number + - name: Var35 + type: number + - name: Var36 + type: number + - name: Var37 + type: number + - name: Var38 + type: number + - name: Var39 + type: number + - name: Var40 + type: number + - name: Var41 + type: number + - name: Var42 + type: number + - name: Var43 + type: number + - name: Var44 + type: number + - name: Var45 + type: number + - name: Var46 + type: number + - name: Var47 + type: number + - name: Var48 + type: number + - name: Var49 + type: number + - name: Var50 + type: number + - name: Var51 + type: number + - name: Var52 + type: number + - name: Var53 + type: number + - name: Var54 + type: number + - name: Var55 + type: number + - name: Var56 + type: number + - name: Var57 + type: number + - name: Var58 + type: number + - name: Var59 + type: number + - name: Var60 + type: number + - name: Var61 + type: number + - name: Var62 + type: number + - name: Var63 + type: number + - name: Var64 + type: number + - name: Var65 + type: number + - name: Var66 + type: number + - name: Var67 + type: number + - name: Var68 + type: number + - name: Var69 + type: number + - name: Var70 + type: number + - name: Var71 + type: number + - name: Var72 + type: number + - name: Var73 + type: number + - name: Var74 + type: number + - name: Var75 + type: number + - name: Var76 + type: number + - name: Var77 + type: number + - name: Var78 + type: number + - name: Var79 + type: number + - name: Var80 + type: number + - name: Var81 + type: number + - name: Var82 + type: number + - name: Var83 + type: number + - name: Var84 + type: number + - name: Var85 + type: number + - name: Var86 + type: number + - name: Var87 + type: number + - name: Var88 + type: number + - name: Var89 + type: number + - name: Var90 + type: number + - name: Var91 + type: number + - name: Var92 + type: number + - name: Var93 + type: number + - name: Var94 + type: number + - name: Var95 + type: number + - name: Var96 + type: number + - name: Var97 + type: number + - name: Var98 + type: number + - name: Var99 + type: number + - name: Var100 + type: number + - name: Var101 + type: number + - name: Var102 + type: number + - name: Var103 + type: number + - name: Var104 + type: number + - name: Var105 + type: number + - name: Var106 + type: number + - name: Var107 + type: number + - name: Var108 + type: number + - name: Var109 + type: number + - name: Var110 + type: number + - name: Var111 + type: number + - name: Var112 + type: number + - name: Var113 + type: number + - name: Var114 + type: number + - name: Var115 + type: number + - name: Var116 + type: number + - name: Var117 + type: number + - name: Var118 + type: number + - name: Var119 + type: number + - name: Var120 + type: number + - name: Var121 + type: number + - name: Var122 + type: number + - name: Var123 + type: number + - name: Var124 + type: number + - name: Var125 + type: number + - name: Var126 + type: number + - name: Var127 + type: number + - name: Var128 + type: number + - name: Var129 + type: number + - name: Var130 + type: number + - name: Var131 + type: number + - name: Var132 + type: number + - name: Var133 + type: number + - name: Var134 + type: number + - name: Var135 + type: number + - name: Var136 + type: number + - name: Var137 + type: number + - name: Var138 + type: number + - name: Var139 + type: number + - name: Var140 + type: number + - name: Var141 + type: number + - name: Var142 + type: number + - name: Var143 + type: number + - name: Var144 + type: number + - name: Var145 + type: number + - name: Var146 + type: number + - name: Var147 + type: number + - name: Var148 + type: number + - name: Var149 + type: number + - name: Var150 + type: number + - name: Var151 + type: number + - name: Var152 + type: number + - name: Var153 + type: number + - name: Var154 + type: number + - name: Var155 + type: number + - name: Var156 + type: number + - name: Var157 + type: number + - name: Var158 + type: number + - name: Var159 + type: number + - name: Var160 + type: number + - name: Var161 + type: number + - name: Var162 + type: number + - name: Var163 + type: number + - name: Var164 + type: number + - name: Var165 + type: number + - name: Var166 + type: number + - name: Var167 + type: number + - name: Var168 + type: number + - name: Var169 + type: number + - name: Var170 + type: number + - name: Var171 + type: number + - name: Var172 + type: number + - name: Var173 + type: number + - name: Var174 + type: number + - name: Var175 + type: number + - name: Var176 + type: number + - name: Var177 + type: number + - name: Var178 + type: number + - name: Var179 + type: number + - name: Var180 + type: number + - name: Var181 + type: number + - name: Var182 + type: number + - name: Var183 + type: number + - name: Var184 + type: number + - name: Var185 + type: number + - name: Var186 + type: number + - name: Var187 + type: number + - name: Var188 + type: number + - name: Var189 + type: number + - name: Var190 + type: number + - name: Var191 + type: category + - name: Var192 + type: category + - name: Var193 + type: category + - name: Var194 + type: category + - name: Var195 + type: category + - name: Var196 + type: category + - name: Var197 + type: category + - name: Var198 + type: category + - name: Var199 + type: category + - name: Var200 + type: category + - name: Var201 + type: category + - name: Var202 + type: category + - name: Var203 + type: category + - name: Var204 + type: category + - name: Var205 + type: category + - name: Var206 + type: category + - name: Var207 + type: category + - name: Var208 + type: category + - name: Var209 + type: number + - name: Var210 + type: category + - name: Var211 + type: category + - name: Var212 + type: category + - name: Var213 + type: category + - name: Var214 + type: category + - name: Var215 + type: category + - name: Var216 + type: category + - name: Var217 + type: category + - name: Var218 + type: category + - name: Var219 + type: category + - name: Var220 + type: category + - name: Var221 + type: category + - name: Var222 + type: category + - name: Var223 + type: category + - name: Var224 + type: category + - name: Var225 + type: category + - name: Var226 + type: category + - name: Var227 + type: category + - name: Var228 + type: category + - name: Var229 + type: category + - name: Var230 + type: number + - name: target + type: binary +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/kdd_upselling.yaml b/ludwig/datasets/configs/kdd_upselling.yaml new file mode 100644 index 0000000..ca2628d --- /dev/null +++ b/ludwig/datasets/configs/kdd_upselling.yaml @@ -0,0 +1,485 @@ +version: 1.0 +name: kdd_upselling +download_urls: + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train.data.zip + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_test.data.zip + - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train_upselling.labels + - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/upselling/stratified_train_idx_upselling.txt + - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/upselling/stratified_test_idx_upselling.txt +sha256: + orange_small_test.data.zip: 440ac8a350144c14f4d6947c096ad675ee84aa27b4b742071662696e333cec53 + orange_small_train.data.zip: 31ccb810bdbb71c16e079326443166dc3dfbf73cd358fc4a4ce7440fb1bc6040 + orange_small_train_upselling.labels: 86effe68394fe1ab21c2d855f74adf70f442990aa95dfe5c97340fc924440e68 + stratified_train_idx_upselling.txt: 659060717872177d607fbb157e8d2142c719912771d1716da11ccdd6ff915a05 + stratified_test_idx_upselling.txt: 64cb66ef559b4ccff096e0d7c150c7d019321ffd6cef2362c195a56c56effcb7 +dataset_filenames: orange_small_train.data +loader: kdd_loader.KDDUpsellingLoader +description: | + The KDD Cup 2009 Upselling dataset. + https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data +columns: + - name: Var1 + type: number + - name: Var2 + type: number + - name: Var3 + type: number + - name: Var4 + type: number + - name: Var5 + type: number + - name: Var6 + type: number + - name: Var7 + type: number + - name: Var8 + type: number + - name: Var9 + type: number + - name: Var10 + type: number + - name: Var11 + type: number + - name: Var12 + type: number + - name: Var13 + type: number + - name: Var14 + type: number + - name: Var15 + type: number + - name: Var16 + type: number + - name: Var17 + type: number + - name: Var18 + type: number + - name: Var19 + type: number + - name: Var20 + type: number + - name: Var21 + type: number + - name: Var22 + type: number + - name: Var23 + type: number + - name: Var24 + type: number + - name: Var25 + type: number + - name: Var26 + type: number + - name: Var27 + type: number + - name: Var28 + type: number + - name: Var29 + type: number + - name: Var30 + type: number + - name: Var31 + type: number + - name: Var32 + type: number + - name: Var33 + type: number + - name: Var34 + type: number + - name: Var35 + type: number + - name: Var36 + type: number + - name: Var37 + type: number + - name: Var38 + type: number + - name: Var39 + type: number + - name: Var40 + type: number + - name: Var41 + type: number + - name: Var42 + type: number + - name: Var43 + type: number + - name: Var44 + type: number + - name: Var45 + type: number + - name: Var46 + type: number + - name: Var47 + type: number + - name: Var48 + type: number + - name: Var49 + type: number + - name: Var50 + type: number + - name: Var51 + type: number + - name: Var52 + type: number + - name: Var53 + type: number + - name: Var54 + type: number + - name: Var55 + type: number + - name: Var56 + type: number + - name: Var57 + type: number + - name: Var58 + type: number + - name: Var59 + type: number + - name: Var60 + type: number + - name: Var61 + type: number + - name: Var62 + type: number + - name: Var63 + type: number + - name: Var64 + type: number + - name: Var65 + type: number + - name: Var66 + type: number + - name: Var67 + type: number + - name: Var68 + type: number + - name: Var69 + type: number + - name: Var70 + type: number + - name: Var71 + type: number + - name: Var72 + type: number + - name: Var73 + type: number + - name: Var74 + type: number + - name: Var75 + type: number + - name: Var76 + type: number + - name: Var77 + type: number + - name: Var78 + type: number + - name: Var79 + type: number + - name: Var80 + type: number + - name: Var81 + type: number + - name: Var82 + type: number + - name: Var83 + type: number + - name: Var84 + type: number + - name: Var85 + type: number + - name: Var86 + type: number + - name: Var87 + type: number + - name: Var88 + type: number + - name: Var89 + type: number + - name: Var90 + type: number + - name: Var91 + type: number + - name: Var92 + type: number + - name: Var93 + type: number + - name: Var94 + type: number + - name: Var95 + type: number + - name: Var96 + type: number + - name: Var97 + type: number + - name: Var98 + type: number + - name: Var99 + type: number + - name: Var100 + type: number + - name: Var101 + type: number + - name: Var102 + type: number + - name: Var103 + type: number + - name: Var104 + type: number + - name: Var105 + type: number + - name: Var106 + type: number + - name: Var107 + type: number + - name: Var108 + type: number + - name: Var109 + type: number + - name: Var110 + type: number + - name: Var111 + type: number + - name: Var112 + type: number + - name: Var113 + type: number + - name: Var114 + type: number + - name: Var115 + type: number + - name: Var116 + type: number + - name: Var117 + type: number + - name: Var118 + type: number + - name: Var119 + type: number + - name: Var120 + type: number + - name: Var121 + type: number + - name: Var122 + type: number + - name: Var123 + type: number + - name: Var124 + type: number + - name: Var125 + type: number + - name: Var126 + type: number + - name: Var127 + type: number + - name: Var128 + type: number + - name: Var129 + type: number + - name: Var130 + type: number + - name: Var131 + type: number + - name: Var132 + type: number + - name: Var133 + type: number + - name: Var134 + type: number + - name: Var135 + type: number + - name: Var136 + type: number + - name: Var137 + type: number + - name: Var138 + type: number + - name: Var139 + type: number + - name: Var140 + type: number + - name: Var141 + type: number + - name: Var142 + type: number + - name: Var143 + type: number + - name: Var144 + type: number + - name: Var145 + type: number + - name: Var146 + type: number + - name: Var147 + type: number + - name: Var148 + type: number + - name: Var149 + type: number + - name: Var150 + type: number + - name: Var151 + type: number + - name: Var152 + type: number + - name: Var153 + type: number + - name: Var154 + type: number + - name: Var155 + type: number + - name: Var156 + type: number + - name: Var157 + type: number + - name: Var158 + type: number + - name: Var159 + type: number + - name: Var160 + type: number + - name: Var161 + type: number + - name: Var162 + type: number + - name: Var163 + type: number + - name: Var164 + type: number + - name: Var165 + type: number + - name: Var166 + type: number + - name: Var167 + type: number + - name: Var168 + type: number + - name: Var169 + type: number + - name: Var170 + type: number + - name: Var171 + type: number + - name: Var172 + type: number + - name: Var173 + type: number + - name: Var174 + type: number + - name: Var175 + type: number + - name: Var176 + type: number + - name: Var177 + type: number + - name: Var178 + type: number + - name: Var179 + type: number + - name: Var180 + type: number + - name: Var181 + type: number + - name: Var182 + type: number + - name: Var183 + type: number + - name: Var184 + type: number + - name: Var185 + type: number + - name: Var186 + type: number + - name: Var187 + type: number + - name: Var188 + type: number + - name: Var189 + type: number + - name: Var190 + type: number + - name: Var191 + type: category + - name: Var192 + type: category + - name: Var193 + type: category + - name: Var194 + type: category + - name: Var195 + type: category + - name: Var196 + type: category + - name: Var197 + type: category + - name: Var198 + type: category + - name: Var199 + type: category + - name: Var200 + type: category + - name: Var201 + type: category + - name: Var202 + type: category + - name: Var203 + type: category + - name: Var204 + type: category + - name: Var205 + type: category + - name: Var206 + type: category + - name: Var207 + type: category + - name: Var208 + type: category + - name: Var209 + type: number + - name: Var210 + type: category + - name: Var211 + type: category + - name: Var212 + type: category + - name: Var213 + type: category + - name: Var214 + type: category + - name: Var215 + type: category + - name: Var216 + type: category + - name: Var217 + type: category + - name: Var218 + type: category + - name: Var219 + type: category + - name: Var220 + type: category + - name: Var221 + type: category + - name: Var222 + type: category + - name: Var223 + type: category + - name: Var224 + type: category + - name: Var225 + type: category + - name: Var226 + type: category + - name: Var227 + type: category + - name: Var228 + type: category + - name: Var229 + type: category + - name: Var230 + type: number + - name: target + type: binary +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/kick_starter_funding.yaml b/ludwig/datasets/configs/kick_starter_funding.yaml new file mode 100644 index 0000000..1a2129c --- /dev/null +++ b/ludwig/datasets/configs/kick_starter_funding.yaml @@ -0,0 +1,38 @@ +version: 1.0 +name: kick_starter_funding +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/kick_starter_funding/train.csv + - https://automl-mm-bench.s3.amazonaws.com/kick_starter_funding/test.csv +sha256: + test.csv: 13c2d4b74ac8d1e258659b5f5fa74526b9d27e305f6c29ad7e853dfeeb01983c + train.csv: 3120b69f30bbc08c68940ab9e5d85d6cc2fbc9a65e8a24c66739179b6a60150e +train_filenames: train.csv +test_filenames: test.csv +description: | + Funding Successful Projects on Kickstarter + Predict if a project will get successfully funded or not using labeled data + https://www.kaggle.com/codename007/funding-successful-projects +columns: + - name: name + type: category + - name: desc + type: text + - name: goal + type: number + - name: keywords + type: category + - name: disable_communication + type: binary + - name: country + type: category + - name: currency + type: category + - name: deadline + type: number + - name: created_at + type: number + - name: final_status + type: binary +output_features: + - name: final_status + type: binary diff --git a/ludwig/datasets/configs/kilt_nq.yaml b/ludwig/datasets/configs/kilt_nq.yaml new file mode 100644 index 0000000..5a9d87b --- /dev/null +++ b/ludwig/datasets/configs/kilt_nq.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: kilt_nq +huggingface_dataset_id: facebook/kilt_tasks +huggingface_subsample: nq +loader: hugging_face.HFLoader +description: | + KILT NQ: Natural Questions in the KILT knowledge-intensive framework. 87K train examples. +columns: + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/klue_sts.yaml b/ludwig/datasets/configs/klue_sts.yaml new file mode 100644 index 0000000..b54a9ad --- /dev/null +++ b/ludwig/datasets/configs/klue_sts.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: klue_sts +huggingface_dataset_id: klue/klue +huggingface_subsample: sts +loader: misc_loaders.KlueStsLoader +description: | + KLUE STS Korean sentence similarity. Sentence pair → score (0-5 regression). +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/klue_topic.yaml b/ludwig/datasets/configs/klue_topic.yaml new file mode 100644 index 0000000..9dfacb8 --- /dev/null +++ b/ludwig/datasets/configs/klue_topic.yaml @@ -0,0 +1,21 @@ +version: 1.0 +name: klue_topic +huggingface_dataset_id: klue/klue +huggingface_subsample: ynat +loader: hugging_face.HFLoader +description: | + KLUE YNAT; Korean news topic classification; 7 categories +columns: + - name: guid + type: text + - name: title + type: text + - name: label + type: category + - name: url + type: text + - name: date + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/kmmlu.yaml b/ludwig/datasets/configs/kmmlu.yaml new file mode 100644 index 0000000..cf4dcfd --- /dev/null +++ b/ludwig/datasets/configs/kmmlu.yaml @@ -0,0 +1,23 @@ +version: 1.0 +name: kmmlu +huggingface_dataset_id: HAERAE-HUB/KMMLU +huggingface_subsample: Accounting +loader: hugging_face.HFLoader +description: | + KMMLU: Korean Massive Multitask Language Understanding, 4-way MC. 45 train examples per subset. +columns: + - name: question + type: text + - name: A + type: text + - name: B + type: text + - name: C + type: text + - name: D + type: text + - name: answer + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/kvasir_vqa.yaml b/ludwig/datasets/configs/kvasir_vqa.yaml new file mode 100644 index 0000000..21d2dcd --- /dev/null +++ b/ludwig/datasets/configs/kvasir_vqa.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: kvasir_vqa +huggingface_dataset_id: SimulaMet/Kvasir-VQA-x1 +loader: hugging_face.HFLoader +description: | + Kvasir-VQA: gastrointestinal endoscopy visual QA. 143K train examples. +columns: + - name: image + type: image + - name: question + type: text + - name: answer + type: category + - name: question_class + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/language_identification.yaml b/ludwig/datasets/configs/language_identification.yaml new file mode 100644 index 0000000..c00a6f5 --- /dev/null +++ b/ludwig/datasets/configs/language_identification.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: language_identification +huggingface_dataset_id: papluca/language-identification +loader: hugging_face.HFLoader +description: | + Language identification; 20 languages from Twitter data +columns: + - name: labels + type: text + - name: text + type: text +output_features: + - name: labels + type: text diff --git a/ludwig/datasets/configs/lex_glue_ecthr.yaml b/ludwig/datasets/configs/lex_glue_ecthr.yaml new file mode 100644 index 0000000..d4b0e8e --- /dev/null +++ b/ludwig/datasets/configs/lex_glue_ecthr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: lex_glue_ecthr +huggingface_dataset_id: coastalcph/lex_glue +huggingface_subsample: ecthr_a +loader: multilabel_loader.LexGlueECtHRLoader +description: | + LexGLUE ECtHR case text → violated ECHR articles (multi-label). Set output. +columns: + - name: text_joined + type: text + - name: labels + type: set +output_features: + - name: labels + type: set diff --git a/ludwig/datasets/configs/lex_glue_eurlex.yaml b/ludwig/datasets/configs/lex_glue_eurlex.yaml new file mode 100644 index 0000000..7f6ce94 --- /dev/null +++ b/ludwig/datasets/configs/lex_glue_eurlex.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: lex_glue_eurlex +huggingface_dataset_id: coastalcph/lex_glue +huggingface_subsample: eurlex +loader: multilabel_loader.LexGlueEURLexLoader +description: | + LexGLUE EuroLex EU documents → EuroVoc concept labels (multi-label). Set output. +columns: + - name: text + type: text + - name: labels + type: set +output_features: + - name: labels + type: set diff --git a/ludwig/datasets/configs/librispeech.yaml b/ludwig/datasets/configs/librispeech.yaml new file mode 100644 index 0000000..b1e7536 --- /dev/null +++ b/ludwig/datasets/configs/librispeech.yaml @@ -0,0 +1,23 @@ +version: 1.0 +name: librispeech +huggingface_dataset_id: openslr/librispeech_asr +huggingface_subsample: clean +loader: hugging_face.HFLoader +description: | + LibriSpeech; English speech recognition from audiobooks; clean 100h split +columns: + - name: file + type: text + - name: audio + type: audio + - name: text + type: text + - name: speaker_id + type: category + - name: chapter_id + type: category + - name: id + type: text +output_features: + - name: text + type: text diff --git a/ludwig/datasets/configs/lmsys_arena.yaml b/ludwig/datasets/configs/lmsys_arena.yaml new file mode 100644 index 0000000..7299ff3 --- /dev/null +++ b/ludwig/datasets/configs/lmsys_arena.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: lmsys_arena +huggingface_dataset_id: lmsys/lmsys-arena-human-preference-55k +loader: hugging_face.HFLoader +description: | + LMSYS Arena: 55K human preference comparisons between LLM responses. +columns: + - name: prompt + type: text + - name: model_a + type: text + - name: model_b + type: text + - name: winner_model_a + type: binary +output_features: + - name: winner_model_a + type: binary diff --git a/ludwig/datasets/configs/m3cot.yaml b/ludwig/datasets/configs/m3cot.yaml new file mode 100644 index 0000000..178a548 --- /dev/null +++ b/ludwig/datasets/configs/m3cot.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: m3cot +huggingface_dataset_id: LightChen2333/M3CoT +loader: hugging_face.HFLoader +description: | + M3CoT: multimodal multi-domain QA category classification. 7K examples. +columns: + - name: question + type: text + - name: choices + type: text + - name: category + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/m_mmlu.yaml b/ludwig/datasets/configs/m_mmlu.yaml new file mode 100644 index 0000000..c97e3b9 --- /dev/null +++ b/ludwig/datasets/configs/m_mmlu.yaml @@ -0,0 +1,23 @@ +version: 1.0 +name: m_mmlu +huggingface_dataset_id: alexandrainst/m_mmlu +huggingface_subsample: ar +loader: hugging_face.HFLoader +description: | + Multilingual MMLU (Arabic): 4-way multiple choice academic questions. 274 train examples. +columns: + - name: instruction + type: text + - name: option_a + type: text + - name: option_b + type: text + - name: option_c + type: text + - name: option_d + type: text + - name: answer + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/map_trace.yaml b/ludwig/datasets/configs/map_trace.yaml new file mode 100644 index 0000000..e87ecda --- /dev/null +++ b/ludwig/datasets/configs/map_trace.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: map_trace +huggingface_dataset_id: google/MapTrace +loader: hugging_face.HFLoader +description: | + MapTrace: map-type image classification (7 categories). 20K examples. +columns: + - name: image + type: image + - name: input + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/math500.yaml b/ludwig/datasets/configs/math500.yaml new file mode 100644 index 0000000..d22ed8f --- /dev/null +++ b/ludwig/datasets/configs/math500.yaml @@ -0,0 +1,22 @@ +version: 1.0 +name: math500 +huggingface_dataset_id: HuggingFaceH4/MATH-500 +loader: hugging_face.HFLoader +description: | + MATH-500 test subset; competition math with step-by-step solutions +columns: + - name: problem + type: text + - name: solution + type: text + - name: answer + type: text + - name: subject + type: text + - name: level + type: category + - name: unique_id + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/mathvista.yaml b/ludwig/datasets/configs/mathvista.yaml new file mode 100644 index 0000000..846b8ca --- /dev/null +++ b/ludwig/datasets/configs/mathvista.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mathvista +huggingface_dataset_id: AI4Math/MathVista +loader: vqa_loader.MathVistaLoader +description: | + MathVista: math reasoning over images. Question → answer. +columns: + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/mbpp.yaml b/ludwig/datasets/configs/mbpp.yaml new file mode 100644 index 0000000..acb881f --- /dev/null +++ b/ludwig/datasets/configs/mbpp.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mbpp +huggingface_dataset_id: google-research-datasets/mbpp +loader: code_loader.MBPPLoader +description: | + MBPP Python problem description → solution code generation. 500 problems. +columns: + - name: text + type: text + - name: code + type: text +output_features: + - name: code + type: text diff --git a/ludwig/datasets/configs/measuring_hate_speech.yaml b/ludwig/datasets/configs/measuring_hate_speech.yaml new file mode 100644 index 0000000..017fdbf --- /dev/null +++ b/ludwig/datasets/configs/measuring_hate_speech.yaml @@ -0,0 +1,296 @@ +version: 1.0 +name: measuring_hate_speech +huggingface_dataset_id: ucberkeley-dlab/measuring-hate-speech +loader: hugging_face.HFLoader +description: | + Measuring Hate Speech; continuous hate speech score regression +columns: + - name: comment_id + type: category + - name: annotator_id + type: category + - name: platform + type: category + - name: sentiment + type: number + - name: respect + type: number + - name: insult + type: number + - name: humiliate + type: number + - name: status + type: number + - name: dehumanize + type: number + - name: violence + type: number + - name: genocide + type: number + - name: attack_defend + type: number + - name: hatespeech + type: number + - name: hate_speech_score + type: number + - name: text + type: text + - name: infitms + type: number + - name: outfitms + type: number + - name: annotator_severity + type: number + - name: std_err + type: number + - name: annotator_infitms + type: number + - name: annotator_outfitms + type: number + - name: hypothesis + type: number + - name: target_race_asian + type: binary + - name: target_race_black + type: binary + - name: target_race_latinx + type: binary + - name: target_race_middle_eastern + type: binary + - name: target_race_native_american + type: binary + - name: target_race_pacific_islander + type: binary + - name: target_race_white + type: binary + - name: target_race_other + type: binary + - name: target_race + type: binary + - name: target_religion_atheist + type: binary + - name: target_religion_buddhist + type: binary + - name: target_religion_christian + type: binary + - name: target_religion_hindu + type: binary + - name: target_religion_jewish + type: binary + - name: target_religion_mormon + type: binary + - name: target_religion_muslim + type: binary + - name: target_religion_other + type: binary + - name: target_religion + type: binary + - name: target_origin_immigrant + type: binary + - name: target_origin_migrant_worker + type: binary + - name: target_origin_specific_country + type: binary + - name: target_origin_undocumented + type: binary + - name: target_origin_other + type: binary + - name: target_origin + type: binary + - name: target_gender_men + type: binary + - name: target_gender_non_binary + type: binary + - name: target_gender_transgender_men + type: binary + - name: target_gender_transgender_unspecified + type: binary + - name: target_gender_transgender_women + type: binary + - name: target_gender_women + type: binary + - name: target_gender_other + type: binary + - name: target_gender + type: binary + - name: target_sexuality_bisexual + type: binary + - name: target_sexuality_gay + type: binary + - name: target_sexuality_lesbian + type: binary + - name: target_sexuality_straight + type: binary + - name: target_sexuality_other + type: binary + - name: target_sexuality + type: binary + - name: target_age_children + type: binary + - name: target_age_teenagers + type: binary + - name: target_age_young_adults + type: binary + - name: target_age_middle_aged + type: binary + - name: target_age_seniors + type: binary + - name: target_age_other + type: binary + - name: target_age + type: binary + - name: target_disability_physical + type: binary + - name: target_disability_cognitive + type: binary + - name: target_disability_neurological + type: binary + - name: target_disability_visually_impaired + type: binary + - name: target_disability_hearing_impaired + type: binary + - name: target_disability_unspecific + type: binary + - name: target_disability_other + type: binary + - name: target_disability + type: binary + - name: target_politics_alt_right + type: binary + - name: target_politics_communist + type: binary + - name: target_politics_conservative + type: binary + - name: target_politics_democrat + type: binary + - name: target_politics_green_party + type: binary + - name: target_politics_leftist + type: binary + - name: target_politics_liberal + type: binary + - name: target_politics_libertarian + type: binary + - name: target_politics_republican + type: binary + - name: target_politics_socialist + type: binary + - name: target_politics_other + type: binary + - name: target_politics + type: binary + - name: annotator_gender + type: text + - name: annotator_trans + type: text + - name: annotator_educ + type: text + - name: annotator_income + type: text + - name: annotator_ideology + type: text + - name: annotator_gender_men + type: binary + - name: annotator_gender_women + type: binary + - name: annotator_gender_non_binary + type: binary + - name: annotator_gender_prefer_not_to_say + type: binary + - name: annotator_gender_self_describe + type: binary + - name: annotator_transgender + type: binary + - name: annotator_cisgender + type: binary + - name: annotator_transgender_prefer_not_to_say + type: binary + - name: annotator_education_some_high_school + type: binary + - name: annotator_education_high_school_grad + type: binary + - name: annotator_education_some_college + type: binary + - name: annotator_education_college_grad_aa + type: binary + - name: annotator_education_college_grad_ba + type: binary + - name: annotator_education_professional_degree + type: binary + - name: annotator_education_masters + type: binary + - name: annotator_education_phd + type: binary + - name: annotator_income_<10k + type: binary + - name: annotator_income_10k-50k + type: binary + - name: annotator_income_50k-100k + type: binary + - name: annotator_income_100k-200k + type: binary + - name: annotator_income_>200k + type: binary + - name: annotator_ideology_extremeley_conservative + type: binary + - name: annotator_ideology_conservative + type: binary + - name: annotator_ideology_slightly_conservative + type: binary + - name: annotator_ideology_neutral + type: binary + - name: annotator_ideology_slightly_liberal + type: binary + - name: annotator_ideology_liberal + type: binary + - name: annotator_ideology_extremeley_liberal + type: binary + - name: annotator_ideology_no_opinion + type: binary + - name: annotator_race_asian + type: binary + - name: annotator_race_black + type: binary + - name: annotator_race_latinx + type: binary + - name: annotator_race_middle_eastern + type: binary + - name: annotator_race_native_american + type: binary + - name: annotator_race_pacific_islander + type: binary + - name: annotator_race_white + type: binary + - name: annotator_race_other + type: binary + - name: annotator_age + type: number + - name: annotator_religion_atheist + type: binary + - name: annotator_religion_buddhist + type: binary + - name: annotator_religion_christian + type: binary + - name: annotator_religion_hindu + type: binary + - name: annotator_religion_jewish + type: binary + - name: annotator_religion_mormon + type: binary + - name: annotator_religion_muslim + type: binary + - name: annotator_religion_nothing + type: binary + - name: annotator_religion_other + type: binary + - name: annotator_sexuality_bisexual + type: binary + - name: annotator_sexuality_gay + type: binary + - name: annotator_sexuality_straight + type: binary + - name: annotator_sexuality_other + type: binary +output_features: + - name: sentiment + type: number diff --git a/ludwig/datasets/configs/medical_flashcards.yaml b/ludwig/datasets/configs/medical_flashcards.yaml new file mode 100644 index 0000000..ffbae38 --- /dev/null +++ b/ludwig/datasets/configs/medical_flashcards.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: medical_flashcards +huggingface_dataset_id: medalpaca/medical_meadow_medical_flashcards +loader: hugging_face.HFLoader +description: | + Medical flashcards: Q&A for medical topics. 34K examples. +columns: + - name: input + type: text + - name: instruction + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/medmcqa.yaml b/ludwig/datasets/configs/medmcqa.yaml new file mode 100644 index 0000000..b1f4800 --- /dev/null +++ b/ludwig/datasets/configs/medmcqa.yaml @@ -0,0 +1,32 @@ +version: 1.0 +name: medmcqa +huggingface_dataset_id: openlifescienceai/medmcqa +loader: hugging_face.HFLoader +description: | + Medical entrance exam QA; 4-choice medical questions +columns: + - name: id + type: text + - name: question + type: text + - name: opa + type: text + - name: opb + type: text + - name: opc + type: text + - name: opd + type: text + - name: cop + type: category + - name: choice_type + type: text + - name: exp + type: text + - name: subject_name + type: text + - name: topic_name + type: text +output_features: + - name: cop + type: category diff --git a/ludwig/datasets/configs/melbourne_airbnb.yaml b/ludwig/datasets/configs/melbourne_airbnb.yaml new file mode 100644 index 0000000..05a556a --- /dev/null +++ b/ludwig/datasets/configs/melbourne_airbnb.yaml @@ -0,0 +1,226 @@ +version: 1.0 +name: melbourne_airbnb +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/airbnb_melbourne/train.pq + - https://automl-mm-bench.s3.amazonaws.com/airbnb_melbourne/test.pq +sha256: + test.pq: 9fe965cfdbd24ee9af7a004a7dc8c4e535a7ffceb722dce00f8ea90a54f95aa9 + train.pq: c158c0f497ef355ba9d5de0de7556f6eb7f9bc343a67c4c681b014f6c7412e48 +train_filenames: train.pq +test_filenames: test.pq +description: | + Melbourne Airbnb Open Data + Detailed and summarized data of Airbnb activity in Melbourne, VIC, Australia + https://www.kaggle.com/tylerx/melbourne-airbnb-open-data +columns: + - name: id + type: number + - name: listing_url + type: category + - name: scrape_id + type: number + - name: last_scraped + type: date + - name: text + type: category + - name: summary + type: text + - name: space + type: text + - name: description + type: text + - name: neighborhood_overview + type: text + - name: notes + type: text + - name: transit + type: text + - name: access + type: text + - name: interaction + type: text + - name: house_rules + type: text + - name: picture_url + type: category + - name: host_id + type: category + - name: host_url + type: category + - name: host_name + type: category + - name: host_since + type: date + - name: host_location + type: category + - name: host_about + type: text + - name: host_response_time + type: category + - name: host_response_rate + type: category + - name: host_is_superhost + type: binary + - name: host_thumbnail_url + type: category + - name: host_picture_url + type: category + - name: host_neighborhood + type: category + - name: host_verifications + type: set + - name: host_has_profile_pic + type: binary + - name: host_identity_verified + type: binary + - name: street + type: category + - name: neighborhood + type: category + - name: city + type: category + - name: suburb + type: category + - name: state + type: category + - name: zipcode + type: category + - name: smart_location + type: category + - name: country_code + type: category + - name: country + type: category + - name: latitude + type: number + - name: longitude + type: number + - name: is_location_exact + type: binary + - name: property_type + type: category + - name: room_type + type: category + - name: accommodates + type: number + - name: bathrooms + type: number + - name: bedrooms + type: number + - name: beds + type: number + - name: bed_type + type: category + - name: amenities + type: set + - name: price + type: number + - name: weekly_price + type: number + - name: monthly_price + type: number + - name: security_deposit + type: number + - name: cleaning_fee + type: number + - name: guests_included + type: number + - name: extra_people + type: number + - name: minimum_nights + type: number + - name: maximum_nights + type: number + - name: calendar_updated + type: category + - name: has_availability + type: binary + - name: availability_30 + type: number + - name: availability_60 + type: number + - name: availability_90 + type: number + - name: availability_365 + type: number + - name: calendar_last_scraped + type: date + - name: number_of_reviews + type: number + - name: first_review + type: date + - name: last_review + type: date + - name: review_scores_rating + type: number + - name: review_scores_accuracy + type: number + - name: review_scores_cleanliness + type: number + - name: review_scores_checkin + type: number + - name: review_scores_communication + type: number + - name: review_scores_location + type: number + - name: review_scores_value + type: number + - name: requires_license + type: binary + - name: license + type: category + - name: instant_bookable + type: binary + - name: cancellation_policy + type: category + - name: require_guest_profile_picture + type: binary + - name: require_guest_phone_verification + type: binary + - name: calculated_host_listings_count + type: number + - name: reviews_per_month + type: number + - name: price_label + type: number + - name: host_verifications_jumio + type: binary + - name: host_verifications_government_id + type: binary + - name: host_verifications_kba + type: binary + - name: host_verifications_zhima_selfie + type: binary + - name: host_verifications_facebook + type: binary + - name: host_verifications_work_email + type: binary + - name: host_verifications_google + type: binary + - name: host_verifications_sesame + type: binary + - name: host_verifications_manual_online + type: binary + - name: host_verifications_manual_offline + type: binary + - name: host_verifications_offline_government_id + type: binary + - name: host_verifications_selfie + type: binary + - name: host_verifications_reviews + type: binary + - name: host_verifications_identity_manual + type: binary + - name: host_verifications_sesame_offline + type: binary + - name: host_verifications_weibo + type: binary + - name: host_verifications_email + type: binary + - name: host_verifications_sent_id + type: binary + - name: host_verifications_phone + type: binary +output_features: + - name: price_label + type: category diff --git a/ludwig/datasets/configs/mercari_price_suggestion.yaml b/ludwig/datasets/configs/mercari_price_suggestion.yaml new file mode 100644 index 0000000..89a0b82 --- /dev/null +++ b/ludwig/datasets/configs/mercari_price_suggestion.yaml @@ -0,0 +1,45 @@ +version: 1.0 +name: mercari_price_suggestion +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion/train.pq + - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion/dev.pq + - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion/test.pq +sha256: + test.pq: 05fed940f5545e6a470ca595d014a02b173fd3362ca5bc5c458d02640b892a57 + train.pq: a0613b77714ebb9f8927cf6bff2092af8143f4a66a64e45e9c3bf9d18604cfe3 + dev.pq: f7284b86adde0354f30ee2c2b7a7a55dc895d202b4291138e807c8f3eaacb6b0 +train_filenames: train.pq +validation_filenames: dev.pq +test_filenames: test.pq +description: | + Predict product price based on details like product category name, brand name, and item condition. + We have converted price to log price by log(1 + price). + https://www.kaggle.com/c/mercari-price-suggestion-challenge +columns: + - name: train_id + type: category + - name: name + type: category + - name: item_condition_id + type: category + - name: category_name + type: category + - name: brand_name + type: category + - name: price + type: number + - name: shipping + type: binary + - name: item_description + type: text + - name: log_price + type: number + - name: cat1 + type: category + - name: cat2 + type: category + - name: cat3 + type: category +output_features: + - name: log_price + type: number diff --git a/ludwig/datasets/configs/mercari_price_suggestion100K.yaml b/ludwig/datasets/configs/mercari_price_suggestion100K.yaml new file mode 100644 index 0000000..e10edd9 --- /dev/null +++ b/ludwig/datasets/configs/mercari_price_suggestion100K.yaml @@ -0,0 +1,42 @@ +version: 1.0 +name: mercari_price_suggestion100K +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion100K/train.pq + - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion100K/test.pq +sha256: + test.pq: 60431577bd6cb433bae287ced2edc7a557497b66b1fe90e2fbec6ffc24bf35eb + train.pq: f60063847d9b828f1e9366eb69fa53774771b53291586d1cce506c931b7173f4 +train_filenames: train.pq +test_filenames: test.pq +description: | + Predict product price based on details like product category name, brand name, and item condition. + We have converted price to log price by log(1 + price). + https://www.kaggle.com/c/mercari-price-suggestion-challenge +columns: + - name: train_id + type: category + - name: name + type: category + - name: item_condition_id + type: category + - name: category_name + type: category + - name: brand_name + type: category + - name: price + type: number + - name: shipping + type: binary + - name: item_description + type: text + - name: log_price + type: number + - name: cat1 + type: category + - name: cat2 + type: category + - name: cat3 + type: category +output_features: + - name: log_price + type: number diff --git a/ludwig/datasets/configs/mercedes_benz_greener.yaml b/ludwig/datasets/configs/mercedes_benz_greener.yaml new file mode 100644 index 0000000..61b4cc9 --- /dev/null +++ b/ludwig/datasets/configs/mercedes_benz_greener.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mercedes_benz_greener +kaggle_competition: mercedes-benz-greener-manufacturing +archive_filenames: mercedes-benz-greener-manufacturing.zip +sha256: + mercedes-benz-greener-manufacturing.zip: 91143716085345a84dc4991b8eb1d5ff80d8aa134930de946b3b24be0f2e5d1a +train_filenames: train.csv +test_filenames: test.csv +description: | + The Mercedes-Benz Greener Manufacturing dataset. + https://www.kaggle.com/c/mercedes-benz-greener-manufacturing +output_features: + - name: y + type: number diff --git a/ludwig/datasets/configs/merit.yaml b/ludwig/datasets/configs/merit.yaml new file mode 100644 index 0000000..abb33a2 --- /dev/null +++ b/ludwig/datasets/configs/merit.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: merit +huggingface_dataset_id: de-Rodrigo/merit +huggingface_subsample: en-digital-line-degradation-seq +loader: hugging_face.HFLoader +description: | + MERIT: historical document image recognition. 7K train examples. +columns: + - name: image + type: image + - name: ground_truth + type: text +output_features: + - name: ground_truth + type: text diff --git a/ludwig/datasets/configs/minds14.yaml b/ludwig/datasets/configs/minds14.yaml new file mode 100644 index 0000000..07548bb --- /dev/null +++ b/ludwig/datasets/configs/minds14.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: minds14 +huggingface_dataset_id: PolyAI/minds14 +huggingface_subsample: en-US +loader: hugging_face.HFLoader +description: | + MINDS-14: multilingual banking intent classification from audio. 8K train examples. +columns: + - name: audio + type: audio + - name: intent_class + type: category + - name: transcription + type: text +output_features: + - name: intent_class + type: category diff --git a/ludwig/datasets/configs/mini_imagenet.yaml b/ludwig/datasets/configs/mini_imagenet.yaml new file mode 100644 index 0000000..713557b --- /dev/null +++ b/ludwig/datasets/configs/mini_imagenet.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mini_imagenet +huggingface_dataset_id: timm/mini-imagenet +loader: hugging_face.HFLoader +description: | + Mini-ImageNet: 100-class image classification subset of ImageNet. 50K train examples. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mls_german.yaml b/ludwig/datasets/configs/mls_german.yaml new file mode 100644 index 0000000..2d2a972 --- /dev/null +++ b/ludwig/datasets/configs/mls_german.yaml @@ -0,0 +1,32 @@ +version: 1.0 +name: mls_german +huggingface_dataset_id: facebook/multilingual_librispeech +huggingface_subsample: german +loader: hugging_face.HFLoader +description: 'Multilingual LibriSpeech German ASR + + ' +columns: +- name: audio + type: audio +- name: original_path + type: text +- name: begin_time + type: number +- name: end_time + type: number +- name: transcript + type: text +- name: audio_duration + type: number +- name: speaker_id + type: text +- name: chapter_id + type: text +- name: file + type: text +- name: id + type: text +output_features: +- name: transcript + type: text diff --git a/ludwig/datasets/configs/mmlu.yaml b/ludwig/datasets/configs/mmlu.yaml new file mode 100644 index 0000000..38138db --- /dev/null +++ b/ludwig/datasets/configs/mmlu.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mmlu +huggingface_dataset_id: cais/mmlu +huggingface_subsample: all +loader: multiple_choice_loader.MmluLoader +description: | + MMLU massive multitask benchmark. 57 tasks, 4-way multiple choice. +columns: + - name: question + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/mmlu_lighteval.yaml b/ludwig/datasets/configs/mmlu_lighteval.yaml new file mode 100644 index 0000000..ddb54c0 --- /dev/null +++ b/ludwig/datasets/configs/mmlu_lighteval.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: mmlu_lighteval +huggingface_dataset_id: lighteval/mmlu +huggingface_subsample: abstract_algebra +loader: hugging_face.HFLoader +description: | + MMLU: Massive Multitask Language Understanding, 57 academic subjects, 4-way MC. 99K auxiliary train. +columns: + - name: question + type: text + - name: subject + type: category + - name: choices + type: text + - name: answer + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/mmlu_pro.yaml b/ludwig/datasets/configs/mmlu_pro.yaml new file mode 100644 index 0000000..0c56f86 --- /dev/null +++ b/ludwig/datasets/configs/mmlu_pro.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: mmlu_pro +huggingface_dataset_id: TIGER-Lab/MMLU-Pro +loader: multiple_choice_loader.MmluProLoader +description: | + MMLU-Pro harder multitask benchmark. 10-way multiple choice. +columns: + - name: cot_content + type: text + - name: question + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/mmsulab.yaml b/ludwig/datasets/configs/mmsulab.yaml new file mode 100644 index 0000000..c33c70b --- /dev/null +++ b/ludwig/datasets/configs/mmsulab.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mmsulab +huggingface_dataset_id: coml/mmsulab +loader: hugging_face.HFLoader +description: | + MMSuLab: multilingual audio speech laboratory data. 1.9M train examples. +columns: + - name: audio + type: audio + - name: iso3 + type: category +output_features: + - name: iso3 + type: category diff --git a/ludwig/datasets/configs/mnist.yaml b/ludwig/datasets/configs/mnist.yaml new file mode 100644 index 0000000..c05380e --- /dev/null +++ b/ludwig/datasets/configs/mnist.yaml @@ -0,0 +1,33 @@ +version: 1.0 +name: mnist +download_urls: + - https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz + - https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz + - https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz + - https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz +sha256: + t10k-images-idx3-ubyte.gz: 8d422c7b0a1c1c79245a5bcf07fe86e33eeafee792b84584aec276f5a2dbc4e6 + train-images-idx3-ubyte.gz: 440fcabf73cc546fa21475e81ea370265605f56be210a4024d2ca8f203523609 + train-labels-idx1-ubyte.gz: 3552534a0a558bbed6aed32b30c495cca23d567ec52cac8be1a0730e8010255c + t10k-labels-idx1-ubyte.gz: f7ae60f92e00ec6debd23a6088c31dbd2371eca3ffa0defaefb259924204aec6 +preserve_paths: + - training + - testing +loader: mnist.MNISTLoader +description: | + The MNIST database of handwritten digits, available from this page, + has a training set of 60,000 examples, and a test set of 10,000 examples. + It is a subset of a larger set available from NIST. The digits have been + size-normalized and centered in a fixed-size image. + It is a good database for people who want to try learning techniques and + pattern recognition methods on real-world data while spending minimal + efforts on preprocessing and formatting. + http://yann.lecun.com/exdb/mnist/ +columns: + - name: image_path + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mnist_ylecun.yaml b/ludwig/datasets/configs/mnist_ylecun.yaml new file mode 100644 index 0000000..bec4286 --- /dev/null +++ b/ludwig/datasets/configs/mnist_ylecun.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mnist_ylecun +huggingface_dataset_id: ylecun/mnist +loader: hugging_face.HFLoader +description: | + MNIST handwritten digit recognition. 60K train examples. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mnli.yaml b/ludwig/datasets/configs/mnli.yaml new file mode 100644 index 0000000..e18af4b --- /dev/null +++ b/ludwig/datasets/configs/mnli.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: mnli +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: mnli +loader: hugging_face.HFLoader +description: | + Multi-Genre Natural Language Inference; premise + hypothesis -> entailment/neutral/contradiction +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mobile_mold.yaml b/ludwig/datasets/configs/mobile_mold.yaml new file mode 100644 index 0000000..de5836e --- /dev/null +++ b/ludwig/datasets/configs/mobile_mold.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mobile_mold +huggingface_dataset_id: nphamdinh/mobilemold +loader: hugging_face.HFLoader +description: | + MobileMold: smartphone food mold binary image classification. 3.5K examples. +columns: + - name: image + type: image + - name: mold + type: binary +output_features: + - name: mold + type: binary diff --git a/ludwig/datasets/configs/moral_stories.yaml b/ludwig/datasets/configs/moral_stories.yaml new file mode 100644 index 0000000..09bef30 --- /dev/null +++ b/ludwig/datasets/configs/moral_stories.yaml @@ -0,0 +1,22 @@ +version: 1.0 +name: moral_stories +huggingface_dataset_id: demelin/moral_stories +huggingface_revision: refs/convert/parquet +huggingface_data_files: + train: cls-action+context+consequence-lexical_bias/train/0000.parquet +loader: hugging_face.HFLoader +description: | + Moral Stories: binary classification of moral vs immoral actions given a norm, + situation, and intention. Label 1 = moral, 0 = immoral. +columns: + - name: norm + type: text + - name: situation + type: text + - name: intention + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/mrpc.yaml b/ludwig/datasets/configs/mrpc.yaml new file mode 100644 index 0000000..783badf --- /dev/null +++ b/ludwig/datasets/configs/mrpc.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: mrpc +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: mrpc +loader: hugging_face.HFLoader +description: | + Microsoft Research Paraphrase Corpus; paraphrase detection +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/msmarco_passage.yaml b/ludwig/datasets/configs/msmarco_passage.yaml new file mode 100644 index 0000000..a09373a --- /dev/null +++ b/ludwig/datasets/configs/msmarco_passage.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: msmarco_passage +huggingface_dataset_id: mteb/msmarco +loader: hugging_face.HFLoader +description: | + MS MARCO passage retrieval; query-passage relevance scoring +columns: + - name: query-id + type: text + - name: corpus-id + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_amazon_polarity.yaml b/ludwig/datasets/configs/mteb_amazon_polarity.yaml new file mode 100644 index 0000000..a388ef8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_polarity.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_amazon_polarity +huggingface_dataset_id: mteb/amazon_polarity +loader: hugging_face.HFLoader +description: | + MTEB Amazon Polarity: binary positive/negative sentiment from Amazon reviews (4M reviews). +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/mteb_amazon_reviews_class_de.yaml b/ludwig/datasets/configs/mteb_amazon_reviews_class_de.yaml new file mode 100644 index 0000000..b408090 --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_reviews_class_de.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_amazon_reviews_class_de +huggingface_dataset_id: mteb/AmazonReviewsClassification +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB Amazon Reviews Classification (German): 5-class star rating classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_amazon_reviews_class_en.yaml b/ludwig/datasets/configs/mteb_amazon_reviews_class_en.yaml new file mode 100644 index 0000000..d3ab6b8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_reviews_class_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_amazon_reviews_class_en +huggingface_dataset_id: mteb/AmazonReviewsClassification +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + MTEB Amazon Reviews Classification (English): 5-class star rating classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_amazon_reviews_class_es.yaml b/ludwig/datasets/configs/mteb_amazon_reviews_class_es.yaml new file mode 100644 index 0000000..10c576c --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_reviews_class_es.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_amazon_reviews_class_es +huggingface_dataset_id: mteb/AmazonReviewsClassification +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + MTEB Amazon Reviews Classification (Spanish): 5-class star rating classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_amazon_reviews_class_fr.yaml b/ludwig/datasets/configs/mteb_amazon_reviews_class_fr.yaml new file mode 100644 index 0000000..869818d --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_reviews_class_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_amazon_reviews_class_fr +huggingface_dataset_id: mteb/AmazonReviewsClassification +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB Amazon Reviews Classification (French): 5-class star rating classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_amazon_reviews_class_ja.yaml b/ludwig/datasets/configs/mteb_amazon_reviews_class_ja.yaml new file mode 100644 index 0000000..4e4f759 --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_reviews_class_ja.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_amazon_reviews_class_ja +huggingface_dataset_id: mteb/AmazonReviewsClassification +huggingface_subsample: ja +loader: hugging_face.HFLoader +description: | + MTEB Amazon Reviews Classification (Japanese): 5-class star rating classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_amazon_reviews_class_zh.yaml b/ludwig/datasets/configs/mteb_amazon_reviews_class_zh.yaml new file mode 100644 index 0000000..3d30197 --- /dev/null +++ b/ludwig/datasets/configs/mteb_amazon_reviews_class_zh.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_amazon_reviews_class_zh +huggingface_dataset_id: mteb/AmazonReviewsClassification +huggingface_subsample: zh +loader: hugging_face.HFLoader +description: | + MTEB Amazon Reviews Classification (Chinese): 5-class star rating classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_biosses.yaml b/ludwig/datasets/configs/mteb_biosses.yaml new file mode 100644 index 0000000..d21ca52 --- /dev/null +++ b/ludwig/datasets/configs/mteb_biosses.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: mteb_biosses +huggingface_dataset_id: mteb/biosses-sts +loader: hugging_face.HFLoader +description: | + MTEB BIOSSES: biomedical sentence similarity benchmark (100 sentence pairs). +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_cyrillic_turkic.yaml b/ludwig/datasets/configs/mteb_cyrillic_turkic.yaml new file mode 100644 index 0000000..0181f06 --- /dev/null +++ b/ludwig/datasets/configs/mteb_cyrillic_turkic.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_cyrillic_turkic +huggingface_dataset_id: mteb/CyrillicTurkicLangClassification +loader: hugging_face.HFLoader +description: | + MTEB Cyrillic Turkic Language Classification: language identification for Cyrillic-script Turkic languages. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_emotion.yaml b/ludwig/datasets/configs/mteb_emotion.yaml new file mode 100644 index 0000000..8082045 --- /dev/null +++ b/ludwig/datasets/configs/mteb_emotion.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: mteb_emotion +huggingface_dataset_id: mteb/emotion +loader: hugging_face.HFLoader +description: | + MTEB emotion task; 6-class emotion from tweets +columns: + - name: text + type: text + - name: label + type: category + - name: label_text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_financial_phrasebank.yaml b/ludwig/datasets/configs/mteb_financial_phrasebank.yaml new file mode 100644 index 0000000..66eac17 --- /dev/null +++ b/ludwig/datasets/configs/mteb_financial_phrasebank.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_financial_phrasebank +huggingface_dataset_id: mteb/FinancialPhrasebankClassification +loader: hugging_face.HFLoader +description: | + MTEB Financial Phrasebank Classification: financial news sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_frenk_en.yaml b/ludwig/datasets/configs/mteb_frenk_en.yaml new file mode 100644 index 0000000..87fc2ac --- /dev/null +++ b/ludwig/datasets/configs/mteb_frenk_en.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_frenk_en +huggingface_dataset_id: mteb/FrenkEnClassification +loader: hugging_face.HFLoader +description: | + MTEB Frenk English Classification: hate speech detection in English. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_frenk_hr.yaml b/ludwig/datasets/configs/mteb_frenk_hr.yaml new file mode 100644 index 0000000..3653831 --- /dev/null +++ b/ludwig/datasets/configs/mteb_frenk_hr.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_frenk_hr +huggingface_dataset_id: mteb/FrenkHrClassification +loader: hugging_face.HFLoader +description: | + MTEB Frenk Croatian Classification: hate speech detection in Croatian. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_frenk_sl.yaml b/ludwig/datasets/configs/mteb_frenk_sl.yaml new file mode 100644 index 0000000..0b23d56 --- /dev/null +++ b/ludwig/datasets/configs/mteb_frenk_sl.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_frenk_sl +huggingface_dataset_id: mteb/FrenkSlClassification +loader: hugging_face.HFLoader +description: | + MTEB Frenk Slovenian Classification: hate speech detection in Slovenian. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_georeview.yaml b/ludwig/datasets/configs/mteb_georeview.yaml new file mode 100644 index 0000000..0144003 --- /dev/null +++ b/ludwig/datasets/configs/mteb_georeview.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_georeview +huggingface_dataset_id: mteb/GeoreviewClassification +loader: hugging_face.HFLoader +description: | + MTEB Georeview Classification: Russian-language geographic review sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_greek_legal.yaml b/ludwig/datasets/configs/mteb_greek_legal.yaml new file mode 100644 index 0000000..fbc700c --- /dev/null +++ b/ludwig/datasets/configs/mteb_greek_legal.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_greek_legal +huggingface_dataset_id: mteb/GreekLegalCodeClassification +loader: hugging_face.HFLoader +description: | + MTEB Greek Legal Code Classification: Greek legal code topic classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_imdb.yaml b/ludwig/datasets/configs/mteb_imdb.yaml new file mode 100644 index 0000000..081eadf --- /dev/null +++ b/ludwig/datasets/configs/mteb_imdb.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_imdb +huggingface_dataset_id: mteb/imdb +loader: hugging_face.HFLoader +description: | + MTEB IMDB: binary movie review sentiment classification. +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/mteb_ita_casehold.yaml b/ludwig/datasets/configs/mteb_ita_casehold.yaml new file mode 100644 index 0000000..b0884e5 --- /dev/null +++ b/ludwig/datasets/configs/mteb_ita_casehold.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_ita_casehold +huggingface_dataset_id: mteb/ItaCaseholdClassification +loader: hugging_face.HFLoader +description: | + MTEB ItaCasehold Classification: Italian legal case holding classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_jd_review.yaml b/ludwig/datasets/configs/mteb_jd_review.yaml new file mode 100644 index 0000000..cb76ed2 --- /dev/null +++ b/ludwig/datasets/configs/mteb_jd_review.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_jd_review +huggingface_dataset_id: mteb/JDReview +loader: hugging_face.HFLoader +description: | + MTEB JDReview: Chinese product review sentiment classification from JD.com. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_kor_sarcasm.yaml b/ludwig/datasets/configs/mteb_kor_sarcasm.yaml new file mode 100644 index 0000000..a93f871 --- /dev/null +++ b/ludwig/datasets/configs/mteb_kor_sarcasm.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_kor_sarcasm +huggingface_dataset_id: mteb/KorSarcasmClassification +loader: hugging_face.HFLoader +description: | + MTEB Korean Sarcasm Classification: sarcasm detection in Korean social media. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_language_class.yaml b/ludwig/datasets/configs/mteb_language_class.yaml new file mode 100644 index 0000000..0083798 --- /dev/null +++ b/ludwig/datasets/configs/mteb_language_class.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_language_class +huggingface_dataset_id: mteb/LanguageClassification +loader: hugging_face.HFLoader +description: | + MTEB Language Classification: language identification from short text. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_af.yaml b/ludwig/datasets/configs/mteb_massive_intent_af.yaml new file mode 100644 index 0000000..ba755d6 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_af.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_af +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "af" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Afrikaans): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_am.yaml b/ludwig/datasets/configs/mteb_massive_intent_am.yaml new file mode 100644 index 0000000..a31fded --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_am.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_am +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "am" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Amharic): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ar.yaml b/ludwig/datasets/configs/mteb_massive_intent_ar.yaml new file mode 100644 index 0000000..7be92c0 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ar.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ar +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: ar +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent (Arabic): 60-class intent classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_az.yaml b/ludwig/datasets/configs/mteb_massive_intent_az.yaml new file mode 100644 index 0000000..7f67836 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_az.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_az +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "az" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Azerbaijani): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_bn.yaml b/ludwig/datasets/configs/mteb_massive_intent_bn.yaml new file mode 100644 index 0000000..6c2bfb6 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_bn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_bn +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "bn" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Bengali): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_cy.yaml b/ludwig/datasets/configs/mteb_massive_intent_cy.yaml new file mode 100644 index 0000000..0fa0619 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_cy.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_cy +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "cy" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Welsh): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_da.yaml b/ludwig/datasets/configs/mteb_massive_intent_da.yaml new file mode 100644 index 0000000..ca492d3 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_da.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_da +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "da" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Danish): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_de.yaml b/ludwig/datasets/configs/mteb_massive_intent_de.yaml new file mode 100644 index 0000000..f1d8f61 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_de.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_de +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent (German): 60-class intent classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_el.yaml b/ludwig/datasets/configs/mteb_massive_intent_el.yaml new file mode 100644 index 0000000..22020b8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_el.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_el +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "el" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Greek): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_en.yaml b/ludwig/datasets/configs/mteb_massive_intent_en.yaml new file mode 100644 index 0000000..4fdeae7 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_en +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent (English): 60-class intent classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_es.yaml b/ludwig/datasets/configs/mteb_massive_intent_es.yaml new file mode 100644 index 0000000..c8cf93e --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_es.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_es +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent (Spanish): 60-class intent classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_fa.yaml b/ludwig/datasets/configs/mteb_massive_intent_fa.yaml new file mode 100644 index 0000000..16f6c16 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_fa.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_fa +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "fa" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Farsi): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_fi.yaml b/ludwig/datasets/configs/mteb_massive_intent_fi.yaml new file mode 100644 index 0000000..33bfbd3 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_fi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_fi +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "fi" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Finnish): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_fr.yaml b/ludwig/datasets/configs/mteb_massive_intent_fr.yaml new file mode 100644 index 0000000..80c2633 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_fr +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent (French): 60-class intent classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_he.yaml b/ludwig/datasets/configs/mteb_massive_intent_he.yaml new file mode 100644 index 0000000..1bc1bcb --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_he.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_he +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "he" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Hebrew): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_hi.yaml b/ludwig/datasets/configs/mteb_massive_intent_hi.yaml new file mode 100644 index 0000000..d595b5a --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_hi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_hi +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "hi" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Hindi): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_hu.yaml b/ludwig/datasets/configs/mteb_massive_intent_hu.yaml new file mode 100644 index 0000000..c27c706 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_hu.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_hu +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "hu" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Hungarian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_hy.yaml b/ludwig/datasets/configs/mteb_massive_intent_hy.yaml new file mode 100644 index 0000000..5e83488 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_hy.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_hy +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "hy" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Armenian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_id.yaml b/ludwig/datasets/configs/mteb_massive_intent_id.yaml new file mode 100644 index 0000000..d40f819 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_id.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_id +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "id" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Indonesian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_is.yaml b/ludwig/datasets/configs/mteb_massive_intent_is.yaml new file mode 100644 index 0000000..27c63c5 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_is.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_is +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "is" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Icelandic): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_it.yaml b/ludwig/datasets/configs/mteb_massive_intent_it.yaml new file mode 100644 index 0000000..88a325e --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_it.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_it +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "it" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Italian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ja.yaml b/ludwig/datasets/configs/mteb_massive_intent_ja.yaml new file mode 100644 index 0000000..1355824 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ja.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ja +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ja" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Japanese): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_jv.yaml b/ludwig/datasets/configs/mteb_massive_intent_jv.yaml new file mode 100644 index 0000000..5a94a3c --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_jv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_jv +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "jv" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Javanese): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ka.yaml b/ludwig/datasets/configs/mteb_massive_intent_ka.yaml new file mode 100644 index 0000000..20f0a6f --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ka.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ka +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ka" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Georgian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_km.yaml b/ludwig/datasets/configs/mteb_massive_intent_km.yaml new file mode 100644 index 0000000..6f11288 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_km.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_km +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "km" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Khmer): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_kn.yaml b/ludwig/datasets/configs/mteb_massive_intent_kn.yaml new file mode 100644 index 0000000..56bb506 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_kn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_kn +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "kn" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Kannada): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ko.yaml b/ludwig/datasets/configs/mteb_massive_intent_ko.yaml new file mode 100644 index 0000000..b6c0eca --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ko.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ko +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ko" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Korean): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_lv.yaml b/ludwig/datasets/configs/mteb_massive_intent_lv.yaml new file mode 100644 index 0000000..634c09d --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_lv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_lv +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "lv" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Latvian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ml.yaml b/ludwig/datasets/configs/mteb_massive_intent_ml.yaml new file mode 100644 index 0000000..824f438 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ml.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ml +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ml" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Malayalam): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_mn.yaml b/ludwig/datasets/configs/mteb_massive_intent_mn.yaml new file mode 100644 index 0000000..e5a927f --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_mn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_mn +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "mn" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Mongolian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ms.yaml b/ludwig/datasets/configs/mteb_massive_intent_ms.yaml new file mode 100644 index 0000000..15c9f2f --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ms.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ms +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ms" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Malay): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_my.yaml b/ludwig/datasets/configs/mteb_massive_intent_my.yaml new file mode 100644 index 0000000..0a443ad --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_my.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_my +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "my" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Burmese): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_nb.yaml b/ludwig/datasets/configs/mteb_massive_intent_nb.yaml new file mode 100644 index 0000000..4d68fe4 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_nb.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_nb +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "nb" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Norwegian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_nl.yaml b/ludwig/datasets/configs/mteb_massive_intent_nl.yaml new file mode 100644 index 0000000..6be9c96 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_nl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_nl +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "nl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Dutch): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_pl.yaml b/ludwig/datasets/configs/mteb_massive_intent_pl.yaml new file mode 100644 index 0000000..09e7674 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_pl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_pl +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "pl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Polish): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_pt.yaml b/ludwig/datasets/configs/mteb_massive_intent_pt.yaml new file mode 100644 index 0000000..1f7775a --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_pt.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_pt +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "pt" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Portuguese): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ro.yaml b/ludwig/datasets/configs/mteb_massive_intent_ro.yaml new file mode 100644 index 0000000..23cd722 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ro.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ro +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ro" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Romanian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ru.yaml b/ludwig/datasets/configs/mteb_massive_intent_ru.yaml new file mode 100644 index 0000000..f2f76a2 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ru.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ru +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ru" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Russian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_sl.yaml b/ludwig/datasets/configs/mteb_massive_intent_sl.yaml new file mode 100644 index 0000000..d6abd4c --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_sl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_sl +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "sl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Slovenian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_sq.yaml b/ludwig/datasets/configs/mteb_massive_intent_sq.yaml new file mode 100644 index 0000000..2813d74 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_sq.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_sq +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "sq" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Albanian): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_sv.yaml b/ludwig/datasets/configs/mteb_massive_intent_sv.yaml new file mode 100644 index 0000000..c997d4a --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_sv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_sv +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "sv" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Swedish): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_sw.yaml b/ludwig/datasets/configs/mteb_massive_intent_sw.yaml new file mode 100644 index 0000000..22a8d4f --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_sw.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_sw +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "sw" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Swahili): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ta.yaml b/ludwig/datasets/configs/mteb_massive_intent_ta.yaml new file mode 100644 index 0000000..92bd4ba --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ta.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ta +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ta" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Tamil): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_te.yaml b/ludwig/datasets/configs/mteb_massive_intent_te.yaml new file mode 100644 index 0000000..18a682b --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_te.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_te +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "te" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Telugu): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_th.yaml b/ludwig/datasets/configs/mteb_massive_intent_th.yaml new file mode 100644 index 0000000..d68d5a4 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_th.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_th +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "th" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Thai): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_tl.yaml b/ludwig/datasets/configs/mteb_massive_intent_tl.yaml new file mode 100644 index 0000000..3fffd23 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_tl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_tl +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "tl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Tagalog): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_tr.yaml b/ludwig/datasets/configs/mteb_massive_intent_tr.yaml new file mode 100644 index 0000000..c58f006 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_tr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_tr +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "tr" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Turkish): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_ur.yaml b/ludwig/datasets/configs/mteb_massive_intent_ur.yaml new file mode 100644 index 0000000..3af130d --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_ur.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_ur +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "ur" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Urdu): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_vi.yaml b/ludwig/datasets/configs/mteb_massive_intent_vi.yaml new file mode 100644 index 0000000..d94d100 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_vi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_vi +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "vi" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Vietnamese): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_zh_cn.yaml b/ludwig/datasets/configs/mteb_massive_intent_zh_cn.yaml new file mode 100644 index 0000000..9a617a4 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_zh_cn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_zh_cn +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "zh-CN" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Chinese (Simplified)): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_intent_zh_tw.yaml b/ludwig/datasets/configs/mteb_massive_intent_zh_tw.yaml new file mode 100644 index 0000000..b8fea75 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_intent_zh_tw.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_intent_zh_tw +huggingface_dataset_id: mteb/massive_intent +huggingface_subsample: "zh-TW" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Intent Classification (Chinese (Traditional)): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_af.yaml b/ludwig/datasets/configs/mteb_massive_scenario_af.yaml new file mode 100644 index 0000000..dd3c3e7 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_af.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_af +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "af" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Afrikaans): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_am.yaml b/ludwig/datasets/configs/mteb_massive_scenario_am.yaml new file mode 100644 index 0000000..0ed47b3 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_am.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_am +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "am" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Amharic): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ar.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ar.yaml new file mode 100644 index 0000000..b6061c3 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ar.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ar +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ar" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Arabic): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_az.yaml b/ludwig/datasets/configs/mteb_massive_scenario_az.yaml new file mode 100644 index 0000000..87c571e --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_az.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_az +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "az" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Azerbaijani): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_bn.yaml b/ludwig/datasets/configs/mteb_massive_scenario_bn.yaml new file mode 100644 index 0000000..84e13bf --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_bn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_bn +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "bn" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Bengali): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_cy.yaml b/ludwig/datasets/configs/mteb_massive_scenario_cy.yaml new file mode 100644 index 0000000..c7a7bb4 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_cy.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_cy +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "cy" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Welsh): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_da.yaml b/ludwig/datasets/configs/mteb_massive_scenario_da.yaml new file mode 100644 index 0000000..7b11d7e --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_da.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_da +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "da" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Danish): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_de.yaml b/ludwig/datasets/configs/mteb_massive_scenario_de.yaml new file mode 100644 index 0000000..6e4a84f --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_de.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_de +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario (German): 18-class scenario classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_el.yaml b/ludwig/datasets/configs/mteb_massive_scenario_el.yaml new file mode 100644 index 0000000..65d0f31 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_el.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_el +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "el" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Greek): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_en.yaml b/ludwig/datasets/configs/mteb_massive_scenario_en.yaml new file mode 100644 index 0000000..0e4f158 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_en +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario (English): 18-class scenario classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_es.yaml b/ludwig/datasets/configs/mteb_massive_scenario_es.yaml new file mode 100644 index 0000000..d66d8d7 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_es.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_es +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario (Spanish): 18-class scenario classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_fa.yaml b/ludwig/datasets/configs/mteb_massive_scenario_fa.yaml new file mode 100644 index 0000000..62cacf1 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_fa.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_fa +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "fa" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Farsi): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_fi.yaml b/ludwig/datasets/configs/mteb_massive_scenario_fi.yaml new file mode 100644 index 0000000..ce08fba --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_fi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_fi +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "fi" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Finnish): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_fr.yaml b/ludwig/datasets/configs/mteb_massive_scenario_fr.yaml new file mode 100644 index 0000000..a855df7 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_fr +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario (French): 18-class scenario classification from voice assistant queries. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_he.yaml b/ludwig/datasets/configs/mteb_massive_scenario_he.yaml new file mode 100644 index 0000000..d4e4422 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_he.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_he +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "he" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Hebrew): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_hi.yaml b/ludwig/datasets/configs/mteb_massive_scenario_hi.yaml new file mode 100644 index 0000000..283cc23 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_hi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_hi +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "hi" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Hindi): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_hu.yaml b/ludwig/datasets/configs/mteb_massive_scenario_hu.yaml new file mode 100644 index 0000000..76648f2 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_hu.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_hu +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "hu" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Hungarian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_hy.yaml b/ludwig/datasets/configs/mteb_massive_scenario_hy.yaml new file mode 100644 index 0000000..4e146b4 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_hy.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_hy +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "hy" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Armenian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_id.yaml b/ludwig/datasets/configs/mteb_massive_scenario_id.yaml new file mode 100644 index 0000000..b7054d9 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_id.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_id +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "id" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Indonesian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_is.yaml b/ludwig/datasets/configs/mteb_massive_scenario_is.yaml new file mode 100644 index 0000000..e915920 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_is.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_is +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "is" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Icelandic): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_it.yaml b/ludwig/datasets/configs/mteb_massive_scenario_it.yaml new file mode 100644 index 0000000..4fe3c67 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_it.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_it +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "it" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Italian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ja.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ja.yaml new file mode 100644 index 0000000..01c87b5 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ja.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ja +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ja" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Japanese): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_jv.yaml b/ludwig/datasets/configs/mteb_massive_scenario_jv.yaml new file mode 100644 index 0000000..e93aa66 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_jv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_jv +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "jv" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Javanese): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ka.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ka.yaml new file mode 100644 index 0000000..0c1228c --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ka.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ka +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ka" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Georgian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_km.yaml b/ludwig/datasets/configs/mteb_massive_scenario_km.yaml new file mode 100644 index 0000000..56e1433 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_km.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_km +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "km" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Khmer): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_kn.yaml b/ludwig/datasets/configs/mteb_massive_scenario_kn.yaml new file mode 100644 index 0000000..2c9df8b --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_kn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_kn +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "kn" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Kannada): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ko.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ko.yaml new file mode 100644 index 0000000..889c820 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ko.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ko +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ko" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Korean): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_lv.yaml b/ludwig/datasets/configs/mteb_massive_scenario_lv.yaml new file mode 100644 index 0000000..967bc32 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_lv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_lv +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "lv" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Latvian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ml.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ml.yaml new file mode 100644 index 0000000..588bc03 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ml.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ml +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ml" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Malayalam): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_mn.yaml b/ludwig/datasets/configs/mteb_massive_scenario_mn.yaml new file mode 100644 index 0000000..a7a8f85 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_mn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_mn +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "mn" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Mongolian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ms.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ms.yaml new file mode 100644 index 0000000..538a173 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ms.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ms +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ms" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Malay): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_my.yaml b/ludwig/datasets/configs/mteb_massive_scenario_my.yaml new file mode 100644 index 0000000..db1d19e --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_my.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_my +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "my" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Burmese): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_nb.yaml b/ludwig/datasets/configs/mteb_massive_scenario_nb.yaml new file mode 100644 index 0000000..ccff772 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_nb.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_nb +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "nb" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Norwegian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_nl.yaml b/ludwig/datasets/configs/mteb_massive_scenario_nl.yaml new file mode 100644 index 0000000..bfc1e96 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_nl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_nl +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "nl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Dutch): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_pl.yaml b/ludwig/datasets/configs/mteb_massive_scenario_pl.yaml new file mode 100644 index 0000000..b39a630 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_pl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_pl +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "pl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Polish): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_pt.yaml b/ludwig/datasets/configs/mteb_massive_scenario_pt.yaml new file mode 100644 index 0000000..c042937 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_pt.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_pt +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "pt" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Portuguese): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ro.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ro.yaml new file mode 100644 index 0000000..dd0ff49 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ro.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ro +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ro" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Romanian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ru.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ru.yaml new file mode 100644 index 0000000..7995532 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ru.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ru +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ru" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Russian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_sl.yaml b/ludwig/datasets/configs/mteb_massive_scenario_sl.yaml new file mode 100644 index 0000000..b5807b7 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_sl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_sl +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "sl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Slovenian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_sq.yaml b/ludwig/datasets/configs/mteb_massive_scenario_sq.yaml new file mode 100644 index 0000000..7bfd476 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_sq.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_sq +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "sq" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Albanian): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_sv.yaml b/ludwig/datasets/configs/mteb_massive_scenario_sv.yaml new file mode 100644 index 0000000..3e1403d --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_sv.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_sv +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "sv" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Swedish): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_sw.yaml b/ludwig/datasets/configs/mteb_massive_scenario_sw.yaml new file mode 100644 index 0000000..1c3f59e --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_sw.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_sw +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "sw" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Swahili): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ta.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ta.yaml new file mode 100644 index 0000000..675967c --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ta.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ta +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ta" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Tamil): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_te.yaml b/ludwig/datasets/configs/mteb_massive_scenario_te.yaml new file mode 100644 index 0000000..d062036 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_te.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_te +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "te" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Telugu): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_th.yaml b/ludwig/datasets/configs/mteb_massive_scenario_th.yaml new file mode 100644 index 0000000..13442a8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_th.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_th +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "th" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Thai): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_tl.yaml b/ludwig/datasets/configs/mteb_massive_scenario_tl.yaml new file mode 100644 index 0000000..ce06be2 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_tl.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_tl +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "tl" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Tagalog): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_tr.yaml b/ludwig/datasets/configs/mteb_massive_scenario_tr.yaml new file mode 100644 index 0000000..bc0622d --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_tr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_tr +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "tr" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Turkish): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_ur.yaml b/ludwig/datasets/configs/mteb_massive_scenario_ur.yaml new file mode 100644 index 0000000..733f8d7 --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_ur.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_ur +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "ur" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Urdu): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_vi.yaml b/ludwig/datasets/configs/mteb_massive_scenario_vi.yaml new file mode 100644 index 0000000..15a6e2b --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_vi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_vi +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "vi" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Vietnamese): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_zh_cn.yaml b/ludwig/datasets/configs/mteb_massive_scenario_zh_cn.yaml new file mode 100644 index 0000000..82f14ed --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_zh_cn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_zh_cn +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: zh-CN +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario (Chinese Simplified): 18-class scenario classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_massive_scenario_zh_tw.yaml b/ludwig/datasets/configs/mteb_massive_scenario_zh_tw.yaml new file mode 100644 index 0000000..dbf2ede --- /dev/null +++ b/ludwig/datasets/configs/mteb_massive_scenario_zh_tw.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_massive_scenario_zh_tw +huggingface_dataset_id: mteb/massive_scenario +huggingface_subsample: "zh-TW" +loader: hugging_face.HFLoader +description: | + MTEB MASSIVE Scenario Classification (Chinese (Traditional)): task-oriented dialog scenario prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_domain_de.yaml b/ludwig/datasets/configs/mteb_mtop_domain_de.yaml new file mode 100644 index 0000000..2cc40a8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_domain_de.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_domain_de +huggingface_dataset_id: mteb/MTOPDomainClassification +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB MTOP Domain Classification (German): task-oriented dialog domain prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_domain_en.yaml b/ludwig/datasets/configs/mteb_mtop_domain_en.yaml new file mode 100644 index 0000000..54846f9 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_domain_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_domain_en +huggingface_dataset_id: mteb/MTOPDomainClassification +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + MTEB MTOP Domain Classification (English): task-oriented dialog domain prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_domain_es.yaml b/ludwig/datasets/configs/mteb_mtop_domain_es.yaml new file mode 100644 index 0000000..1363944 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_domain_es.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_domain_es +huggingface_dataset_id: mteb/MTOPDomainClassification +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + MTEB MTOP Domain Classification (Spanish): task-oriented dialog domain prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_domain_fr.yaml b/ludwig/datasets/configs/mteb_mtop_domain_fr.yaml new file mode 100644 index 0000000..e6e7605 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_domain_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_domain_fr +huggingface_dataset_id: mteb/MTOPDomainClassification +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB MTOP Domain Classification (French): task-oriented dialog domain prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_domain_hi.yaml b/ludwig/datasets/configs/mteb_mtop_domain_hi.yaml new file mode 100644 index 0000000..eb9ddb8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_domain_hi.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_domain_hi +huggingface_dataset_id: mteb/MTOPDomainClassification +huggingface_subsample: hi +loader: hugging_face.HFLoader +description: | + MTEB MTOP Domain Classification (Hindi): task-oriented dialog domain prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_domain_th.yaml b/ludwig/datasets/configs/mteb_mtop_domain_th.yaml new file mode 100644 index 0000000..8b22937 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_domain_th.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_domain_th +huggingface_dataset_id: mteb/MTOPDomainClassification +huggingface_subsample: th +loader: hugging_face.HFLoader +description: | + MTEB MTOP Domain Classification (Thai): task-oriented dialog domain prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_intent_de2.yaml b/ludwig/datasets/configs/mteb_mtop_intent_de2.yaml new file mode 100644 index 0000000..c904d52 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_intent_de2.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_intent_de2 +huggingface_dataset_id: mteb/MTOPIntentClassification +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB MTOP Intent Classification (German): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_intent_en.yaml b/ludwig/datasets/configs/mteb_mtop_intent_en.yaml new file mode 100644 index 0000000..da60739 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_intent_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_intent_en +huggingface_dataset_id: mteb/MTOPIntentClassification +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + MTEB MTOP Intent Classification (English): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_intent_es2.yaml b/ludwig/datasets/configs/mteb_mtop_intent_es2.yaml new file mode 100644 index 0000000..30976af --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_intent_es2.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_intent_es2 +huggingface_dataset_id: mteb/MTOPIntentClassification +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + MTEB MTOP Intent Classification (Spanish): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_intent_fr2.yaml b/ludwig/datasets/configs/mteb_mtop_intent_fr2.yaml new file mode 100644 index 0000000..d7553bb --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_intent_fr2.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_intent_fr2 +huggingface_dataset_id: mteb/MTOPIntentClassification +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB MTOP Intent Classification (French): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_intent_hi2.yaml b/ludwig/datasets/configs/mteb_mtop_intent_hi2.yaml new file mode 100644 index 0000000..50ac10b --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_intent_hi2.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_intent_hi2 +huggingface_dataset_id: mteb/MTOPIntentClassification +huggingface_subsample: hi +loader: hugging_face.HFLoader +description: | + MTEB MTOP Intent Classification (Hindi): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_mtop_intent_th2.yaml b/ludwig/datasets/configs/mteb_mtop_intent_th2.yaml new file mode 100644 index 0000000..adb4934 --- /dev/null +++ b/ludwig/datasets/configs/mteb_mtop_intent_th2.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_mtop_intent_th2 +huggingface_dataset_id: mteb/MTOPIntentClassification +huggingface_subsample: th +loader: hugging_face.HFLoader +description: | + MTEB MTOP Intent Classification (Thai): task-oriented dialog intent prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_multilingual_sentiment.yaml b/ludwig/datasets/configs/mteb_multilingual_sentiment.yaml new file mode 100644 index 0000000..a36bbdd --- /dev/null +++ b/ludwig/datasets/configs/mteb_multilingual_sentiment.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_multilingual_sentiment +huggingface_dataset_id: mteb/MultilingualSentiment +loader: hugging_face.HFLoader +description: | + MTEB Multilingual Sentiment Classification: multilingual product review sentiment. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_naija_senti_hau.yaml b/ludwig/datasets/configs/mteb_naija_senti_hau.yaml new file mode 100644 index 0000000..50cad53 --- /dev/null +++ b/ludwig/datasets/configs/mteb_naija_senti_hau.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_naija_senti_hau +huggingface_dataset_id: mteb/NaijaSenti +huggingface_subsample: hau +loader: hugging_face.HFLoader +description: | + MTEB NaijaSenti (Hausa): Nigerian language sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_naija_senti_ibo.yaml b/ludwig/datasets/configs/mteb_naija_senti_ibo.yaml new file mode 100644 index 0000000..d57ecaa --- /dev/null +++ b/ludwig/datasets/configs/mteb_naija_senti_ibo.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_naija_senti_ibo +huggingface_dataset_id: mteb/NaijaSenti +huggingface_subsample: ibo +loader: hugging_face.HFLoader +description: | + MTEB NaijaSenti (Igbo): Nigerian language sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_naija_senti_pcm.yaml b/ludwig/datasets/configs/mteb_naija_senti_pcm.yaml new file mode 100644 index 0000000..1dde7ea --- /dev/null +++ b/ludwig/datasets/configs/mteb_naija_senti_pcm.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_naija_senti_pcm +huggingface_dataset_id: mteb/NaijaSenti +huggingface_subsample: pcm +loader: hugging_face.HFLoader +description: | + MTEB NaijaSenti (Nigerian Pidgin): Nigerian language sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_naija_senti_yor.yaml b/ludwig/datasets/configs/mteb_naija_senti_yor.yaml new file mode 100644 index 0000000..50a0347 --- /dev/null +++ b/ludwig/datasets/configs/mteb_naija_senti_yor.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_naija_senti_yor +huggingface_dataset_id: mteb/NaijaSenti +huggingface_subsample: yor +loader: hugging_face.HFLoader +description: | + MTEB NaijaSenti (Yoruba): Nigerian language sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_nepali_news.yaml b/ludwig/datasets/configs/mteb_nepali_news.yaml new file mode 100644 index 0000000..8d3542b --- /dev/null +++ b/ludwig/datasets/configs/mteb_nepali_news.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_nepali_news +huggingface_dataset_id: mteb/NepaliNewsClassification +loader: hugging_face.HFLoader +description: | + MTEB Nepali News Classification: news category classification in Nepali. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_nordic_lang.yaml b/ludwig/datasets/configs/mteb_nordic_lang.yaml new file mode 100644 index 0000000..5d88713 --- /dev/null +++ b/ludwig/datasets/configs/mteb_nordic_lang.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_nordic_lang +huggingface_dataset_id: mteb/NordicLangClassification +loader: hugging_face.HFLoader +description: | + MTEB Nordic Language Classification: language identification for Nordic languages. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_online_shopping.yaml b/ludwig/datasets/configs/mteb_online_shopping.yaml new file mode 100644 index 0000000..a404f74 --- /dev/null +++ b/ludwig/datasets/configs/mteb_online_shopping.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_online_shopping +huggingface_dataset_id: mteb/OnlineShopping +loader: hugging_face.HFLoader +description: | + MTEB OnlineShopping: Chinese online shopping review sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_poem_sentiment.yaml b/ludwig/datasets/configs/mteb_poem_sentiment.yaml new file mode 100644 index 0000000..c6c9cf8 --- /dev/null +++ b/ludwig/datasets/configs/mteb_poem_sentiment.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_poem_sentiment +huggingface_dataset_id: mteb/PoemSentimentClassification +loader: hugging_face.HFLoader +description: | + MTEB Poem Sentiment Classification: sentiment classification of poem verses. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_sensitive_topics.yaml b/ludwig/datasets/configs/mteb_sensitive_topics.yaml new file mode 100644 index 0000000..8accee0 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sensitive_topics.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_sensitive_topics +huggingface_dataset_id: mteb/SensitiveTopicsClassification +loader: hugging_face.HFLoader +description: | + MTEB Sensitive Topics Classification: sensitive topic detection in text. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_sentiment_hindi.yaml b/ludwig/datasets/configs/mteb_sentiment_hindi.yaml new file mode 100644 index 0000000..f4ca294 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sentiment_hindi.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_sentiment_hindi +huggingface_dataset_id: mteb/SentimentAnalysisHindi +loader: hugging_face.HFLoader +description: | + MTEB Sentiment Analysis Hindi: sentiment classification of Hindi text. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_sts17_ar.yaml b/ludwig/datasets/configs/mteb_sts17_ar.yaml new file mode 100644 index 0000000..b6e2eeb --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts17_ar.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts17_ar +huggingface_dataset_id: mteb/sts17-crosslingual-sts +huggingface_subsample: ar-ar +loader: hugging_face.HFLoader +description: | + MTEB STS17 (Arabic-Arabic): Arabic semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts17_de.yaml b/ludwig/datasets/configs/mteb_sts17_de.yaml new file mode 100644 index 0000000..b823e49 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts17_de.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts17_de +huggingface_dataset_id: mteb/sts17-crosslingual-sts +huggingface_subsample: en-de +loader: hugging_face.HFLoader +description: | + MTEB STS17 (German-English): cross-lingual semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts17_en.yaml b/ludwig/datasets/configs/mteb_sts17_en.yaml new file mode 100644 index 0000000..4c77d88 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts17_en.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts17_en +huggingface_dataset_id: mteb/sts17-crosslingual-sts +huggingface_subsample: en-en +loader: hugging_face.HFLoader +description: | + MTEB STS17 (English-English): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts17_es.yaml b/ludwig/datasets/configs/mteb_sts17_es.yaml new file mode 100644 index 0000000..53f1230 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts17_es.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts17_es +huggingface_dataset_id: mteb/sts17-crosslingual-sts +huggingface_subsample: es-en +loader: hugging_face.HFLoader +description: | + MTEB STS17 (Spanish-English): cross-lingual semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts17_fr.yaml b/ludwig/datasets/configs/mteb_sts17_fr.yaml new file mode 100644 index 0000000..ecacc52 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts17_fr.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts17_fr +huggingface_dataset_id: mteb/sts17-crosslingual-sts +huggingface_subsample: fr-en +loader: hugging_face.HFLoader +description: | + MTEB STS17 (French-English): cross-lingual semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_ar.yaml b/ludwig/datasets/configs/mteb_sts22_ar.yaml new file mode 100644 index 0000000..3e94bac --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_ar.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_ar +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: ar +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Arabic): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_de.yaml b/ludwig/datasets/configs/mteb_sts22_de.yaml new file mode 100644 index 0000000..b1fd9ca --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_de.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_de +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB STS22 (German): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_de_en.yaml b/ludwig/datasets/configs/mteb_sts22_de_en.yaml new file mode 100644 index 0000000..6584c85 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_de_en.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_de_en +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: de-en +loader: hugging_face.HFLoader +description: | + MTEB STS22 (German-English cross-lingual): semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_de_fr.yaml b/ludwig/datasets/configs/mteb_sts22_de_fr.yaml new file mode 100644 index 0000000..c49f802 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_de_fr.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_de_fr +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: de-fr +loader: hugging_face.HFLoader +description: | + MTEB STS22 (German-French cross-lingual): semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_en.yaml b/ludwig/datasets/configs/mteb_sts22_en.yaml new file mode 100644 index 0000000..2688450 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_en.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_en +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + MTEB STS22 (English): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_es.yaml b/ludwig/datasets/configs/mteb_sts22_es.yaml new file mode 100644 index 0000000..85d5594 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_es.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_es +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Spanish): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_es_en.yaml b/ludwig/datasets/configs/mteb_sts22_es_en.yaml new file mode 100644 index 0000000..f033eef --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_es_en.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_es_en +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: es-en +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Spanish-English cross-lingual): semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_es_it.yaml b/ludwig/datasets/configs/mteb_sts22_es_it.yaml new file mode 100644 index 0000000..61cd41a --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_es_it.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_es_it +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: es-it +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Spanish-Italian cross-lingual): semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_fr.yaml b/ludwig/datasets/configs/mteb_sts22_fr.yaml new file mode 100644 index 0000000..a14c939 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_fr.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_fr +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB STS22 (French): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_it.yaml b/ludwig/datasets/configs/mteb_sts22_it.yaml new file mode 100644 index 0000000..9ac76b3 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_it.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_it +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: it +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Italian): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_pl.yaml b/ludwig/datasets/configs/mteb_sts22_pl.yaml new file mode 100644 index 0000000..d696795 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_pl.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_pl +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: pl +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Polish): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_pl_en.yaml b/ludwig/datasets/configs/mteb_sts22_pl_en.yaml new file mode 100644 index 0000000..085c5eb --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_pl_en.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_pl_en +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: pl-en +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Polish-English cross-lingual): semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_ru.yaml b/ludwig/datasets/configs/mteb_sts22_ru.yaml new file mode 100644 index 0000000..65cfc71 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_ru.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_ru +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: ru +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Russian): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_tr.yaml b/ludwig/datasets/configs/mteb_sts22_tr.yaml new file mode 100644 index 0000000..5741f2c --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_tr.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_tr +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: tr +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Turkish): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_zh.yaml b/ludwig/datasets/configs/mteb_sts22_zh.yaml new file mode 100644 index 0000000..ee7d273 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_zh.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_zh +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: zh +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Chinese): semantic textual similarity regression. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_sts22_zh_en.yaml b/ludwig/datasets/configs/mteb_sts22_zh_en.yaml new file mode 100644 index 0000000..e63a674 --- /dev/null +++ b/ludwig/datasets/configs/mteb_sts22_zh_en.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: mteb_sts22_zh_en +huggingface_dataset_id: mteb/sts22-crosslingual-sts +huggingface_subsample: zh-en +loader: hugging_face.HFLoader +description: | + MTEB STS22 (Chinese-English cross-lingual): semantic textual similarity. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_stsbenchmark.yaml b/ludwig/datasets/configs/mteb_stsbenchmark.yaml new file mode 100644 index 0000000..7d1a82d --- /dev/null +++ b/ludwig/datasets/configs/mteb_stsbenchmark.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: mteb_stsbenchmark +huggingface_dataset_id: mteb/stsbenchmark-sts +loader: hugging_face.HFLoader +description: | + MTEB STSBenchmark: STS Benchmark semantic similarity (8K sentence pairs, scores 0-5). +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/mteb_swiss_judgement_de.yaml b/ludwig/datasets/configs/mteb_swiss_judgement_de.yaml new file mode 100644 index 0000000..8cefaf4 --- /dev/null +++ b/ludwig/datasets/configs/mteb_swiss_judgement_de.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_swiss_judgement_de +huggingface_dataset_id: mteb/SwissJudgementClassification +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + MTEB Swiss Judgement Classification (German): Swiss Federal Supreme Court judgement prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_swiss_judgement_fr.yaml b/ludwig/datasets/configs/mteb_swiss_judgement_fr.yaml new file mode 100644 index 0000000..c247441 --- /dev/null +++ b/ludwig/datasets/configs/mteb_swiss_judgement_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_swiss_judgement_fr +huggingface_dataset_id: mteb/SwissJudgementClassification +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + MTEB Swiss Judgement Classification (French): Swiss Federal Supreme Court judgement prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_swiss_judgement_it.yaml b/ludwig/datasets/configs/mteb_swiss_judgement_it.yaml new file mode 100644 index 0000000..ba1cb80 --- /dev/null +++ b/ludwig/datasets/configs/mteb_swiss_judgement_it.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: mteb_swiss_judgement_it +huggingface_dataset_id: mteb/SwissJudgementClassification +huggingface_subsample: it +loader: hugging_face.HFLoader +description: | + MTEB Swiss Judgement Classification (Italian): Swiss Federal Supreme Court judgement prediction. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_tnews.yaml b/ludwig/datasets/configs/mteb_tnews.yaml new file mode 100644 index 0000000..bcb8ae0 --- /dev/null +++ b/ludwig/datasets/configs/mteb_tnews.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_tnews +huggingface_dataset_id: mteb/TNews +loader: hugging_face.HFLoader +description: | + MTEB TNews: Chinese news topic classification dataset. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_toxic_convo.yaml b/ludwig/datasets/configs/mteb_toxic_convo.yaml new file mode 100644 index 0000000..2404469 --- /dev/null +++ b/ludwig/datasets/configs/mteb_toxic_convo.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_toxic_convo +huggingface_dataset_id: mteb/toxic_conversations_50k +loader: hugging_face.HFLoader +description: | + MTEB Toxic Conversations 50K: binary toxicity classification. +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/mteb_turkish_product.yaml b/ludwig/datasets/configs/mteb_turkish_product.yaml new file mode 100644 index 0000000..a65d014 --- /dev/null +++ b/ludwig/datasets/configs/mteb_turkish_product.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_turkish_product +huggingface_dataset_id: mteb/TurkishProductSentimentClassification +loader: hugging_face.HFLoader +description: | + MTEB Turkish Product Sentiment Classification: Turkish product review sentiment. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_tweet_sentiment.yaml b/ludwig/datasets/configs/mteb_tweet_sentiment.yaml new file mode 100644 index 0000000..5c40808 --- /dev/null +++ b/ludwig/datasets/configs/mteb_tweet_sentiment.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_tweet_sentiment +huggingface_dataset_id: mteb/tweet_sentiment_extraction +loader: hugging_face.HFLoader +description: | + MTEB Tweet Sentiment Extraction: 3-class tweet sentiment classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_tweet_topic.yaml b/ludwig/datasets/configs/mteb_tweet_topic.yaml new file mode 100644 index 0000000..720214d --- /dev/null +++ b/ludwig/datasets/configs/mteb_tweet_topic.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_tweet_topic +huggingface_dataset_id: mteb/TweetTopicSingleClassification +loader: hugging_face.HFLoader +description: | + MTEB Tweet Topic Single Classification: single-label topic classification of tweets. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_waimai.yaml b/ludwig/datasets/configs/mteb_waimai.yaml new file mode 100644 index 0000000..385c99f --- /dev/null +++ b/ludwig/datasets/configs/mteb_waimai.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_waimai +huggingface_dataset_id: mteb/Waimai +loader: hugging_face.HFLoader +description: | + MTEB Waimai: Chinese food delivery review sentiment classification. +columns: + - name: label + type: category + - name: text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/mteb_yahoo_answers.yaml b/ludwig/datasets/configs/mteb_yahoo_answers.yaml new file mode 100644 index 0000000..8799a99 --- /dev/null +++ b/ludwig/datasets/configs/mteb_yahoo_answers.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: mteb_yahoo_answers +huggingface_dataset_id: mteb/YahooAnswersTopicsClassification +loader: hugging_face.HFLoader +description: | + MTEB Yahoo Answers Topics Classification: topic classification of Yahoo Answers questions. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/multi30k.yaml b/ludwig/datasets/configs/multi30k.yaml new file mode 100644 index 0000000..c4315b7 --- /dev/null +++ b/ludwig/datasets/configs/multi30k.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: multi30k +huggingface_dataset_id: bentrevett/multi30k +loader: hugging_face.HFLoader +description: | + Multi30k: English-German image caption translation. 29K train pairs. +columns: + - name: en + type: text + - name: de + type: text +output_features: + - name: de + type: text diff --git a/ludwig/datasets/configs/multi_nli.yaml b/ludwig/datasets/configs/multi_nli.yaml new file mode 100644 index 0000000..ef143b7 --- /dev/null +++ b/ludwig/datasets/configs/multi_nli.yaml @@ -0,0 +1,30 @@ +version: 1.0 +name: multi_nli +huggingface_dataset_id: nyu-mll/multi_nli +loader: hugging_face.HFLoader +description: | + Multi-Genre NLI; 10 diverse genres, 3-way NLI +columns: + - name: promptID + type: category + - name: pairID + type: text + - name: premise + type: text + - name: premise_binary_parse + type: text + - name: premise_parse + type: text + - name: hypothesis + type: text + - name: hypothesis_binary_parse + type: text + - name: hypothesis_parse + type: text + - name: genre + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/multinerd.yaml b/ludwig/datasets/configs/multinerd.yaml new file mode 100644 index 0000000..ef0cf1f --- /dev/null +++ b/ludwig/datasets/configs/multinerd.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: multinerd +huggingface_dataset_id: Babelscape/multinerd +loader: ner_loader.MultiNERDLoader +description: | + MultiNERD multilingual NER. 31 fine-grained entity types. CC BY 4.0. +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/multirc.yaml b/ludwig/datasets/configs/multirc.yaml new file mode 100644 index 0000000..1b8cf37 --- /dev/null +++ b/ludwig/datasets/configs/multirc.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: multirc +huggingface_dataset_id: aps/super_glue +huggingface_subsample: multirc +loader: misc_loaders.MultiRCLoader +description: | + SuperGLUE MultiRC: paragraph + question + answer → binary (answer correct?). +columns: + - name: paragraph + type: text + - name: question + type: text + - name: answer + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/multiun_ar_en.yaml b/ludwig/datasets/configs/multiun_ar_en.yaml new file mode 100644 index 0000000..450b34a --- /dev/null +++ b/ludwig/datasets/configs/multiun_ar_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: multiun_ar_en +huggingface_dataset_id: Helsinki-NLP/multiun +huggingface_subsample: ar-en +loader: translation_loader.MultiUNArEnLoader +description: | + MultiUN Arabic-English United Nations document translation. 9.8M train pairs. +columns: + - name: ar + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/mushroom_edibility.yaml b/ludwig/datasets/configs/mushroom_edibility.yaml new file mode 100644 index 0000000..4d63f77 --- /dev/null +++ b/ludwig/datasets/configs/mushroom_edibility.yaml @@ -0,0 +1,62 @@ +version: 1.0 +name: mushroom_edibility +download_urls: http://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.data +sha256: + agaricus-lepiota.data: e65d082030501a3ebcbcd7c9f7c71aa9d28fdfff463bf4cf4716a3fe13ac360e +train_filenames: agaricus-lepiota.data +description: | + This data set includes descriptions of hypothetical samples corresponding + to 23 species of gilled mushrooms in the Agaricus and Lepiota Family (pp. 500-525). + Each species is identified as definitely edible, definitely poisonous, + or of unknown edibility and not recommended. This latter class was combined with + the poisonous one. +columns: + - name: class + type: category + - name: cap-shape + type: category + - name: cap-surface + type: category + - name: cap-color + type: category + - name: bruises? + type: category + - name: odor + type: category + - name: gill-attachment + type: category + - name: gill-spacing + type: category + - name: gill-size + type: category + - name: gill-color + type: category + - name: stalk-shape + type: category + - name: stalk-root + type: category + - name: stalk-surface-above-ring + type: category + - name: stalk-surface-below-ring + type: category + - name: stalk-color-above-ring + type: category + - name: stalk-color-below-ring + type: category + - name: veil-type + type: category + - name: veil-color + type: category + - name: ring-number + type: category + - name: ring-type + type: category + - name: spore-print-color + type: category + - name: population + type: category + - name: habitat + type: category +output_features: + - name: class + type: category diff --git a/ludwig/datasets/configs/natural_questions.yaml b/ludwig/datasets/configs/natural_questions.yaml new file mode 100644 index 0000000..df29695 --- /dev/null +++ b/ludwig/datasets/configs/natural_questions.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: natural_questions +huggingface_dataset_id: google-research-datasets/natural_questions +loader: qa_loader.NaturalQuestionsLoader +description: | + Natural Questions: real Google search questions with Wikipedia answers. + ~307K training examples. Question -> short answer text. +columns: + - name: question_text + type: text + - name: answer_text + type: text +output_features: + - name: answer_text + type: text diff --git a/ludwig/datasets/configs/natural_questions_hard_negatives.yaml b/ludwig/datasets/configs/natural_questions_hard_negatives.yaml new file mode 100644 index 0000000..0426081 --- /dev/null +++ b/ludwig/datasets/configs/natural_questions_hard_negatives.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: natural_questions_hard_negatives +huggingface_dataset_id: sentence-transformers/natural-questions +loader: hugging_face.HFLoader +description: | + Natural Questions hard negatives for retrieval/ranking +columns: + - name: query + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/naturelm_audio.yaml b/ludwig/datasets/configs/naturelm_audio.yaml new file mode 100644 index 0000000..9a796b3 --- /dev/null +++ b/ludwig/datasets/configs/naturelm_audio.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: naturelm_audio +huggingface_dataset_id: EarthSpeciesProject/NatureLM-audio-training +huggingface_subsample: NatureLM-audio-training +loader: hugging_face.HFLoader +description: | + NatureLM Audio: wildlife/nature audio instruction following. 26M train examples. +columns: + - name: audio + type: audio + - name: instruction_text + type: text + - name: source_dataset + type: category +output_features: + - name: source_dataset + type: category diff --git a/ludwig/datasets/configs/naval.yaml b/ludwig/datasets/configs/naval.yaml new file mode 100644 index 0000000..27e7a6b --- /dev/null +++ b/ludwig/datasets/configs/naval.yaml @@ -0,0 +1,50 @@ +version: 1.0 +name: naval +download_urls: http://archive.ics.uci.edu/ml/machine-learning-databases/00316/UCI%20CBM%20Dataset.zip +sha256: + UCI%20CBM%20Dataset.zip: 91a3815da80b5ab7e2d5b82ac82f1c2cbf89182c7a65bcdf240db1e014423cb9 +dataset_filenames: UCI CBM Dataset/data.txt +loader: naval.NavalLoader +description: | + Condition Based Maintenance of Naval Propulsion Plants Data Set + http://archive.ics.uci.edu/ml/datasets/condition+based+maintenance+of+naval+propulsion+plants +columns: + - name: lp + type: number + - name: v + type: number + - name: gtt + type: number + - name: gtn + type: number + - name: ggn + type: number + - name: ts + type: number + - name: tp + type: number + - name: t48 + type: number + - name: t1 + type: number + - name: t2 + type: number + - name: p48 + type: number + - name: p1 + type: number + - name: p2 + type: number + - name: pexh + type: number + - name: tic + type: number + - name: mf + type: number + - name: gtcdsc + type: number + - name: gttdsc + type: number +output_features: +- name: gtcdsc + type: number diff --git a/ludwig/datasets/configs/naver_news_summary.yaml b/ludwig/datasets/configs/naver_news_summary.yaml new file mode 100644 index 0000000..baf973a --- /dev/null +++ b/ludwig/datasets/configs/naver_news_summary.yaml @@ -0,0 +1,24 @@ +version: 1.0 +name: naver_news_summary +huggingface_dataset_id: daekeun-ml/naver-news-summarization-ko +loader: hugging_face.HFLoader +description: | + Naver News Korean summarization dataset +columns: + - name: date + type: text + - name: category + type: text + - name: press + type: text + - name: title + type: text + - name: document + type: text + - name: link + type: text + - name: summary + type: text +output_features: + - name: category + type: text diff --git a/ludwig/datasets/configs/nemotron_pii.yaml b/ludwig/datasets/configs/nemotron_pii.yaml new file mode 100644 index 0000000..70f077b --- /dev/null +++ b/ludwig/datasets/configs/nemotron_pii.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: nemotron_pii +huggingface_dataset_id: nvidia/Nemotron-PII +loader: hugging_face.HFLoader +description: | + Nemotron PII: document classification by domain type. 100K examples. +columns: + - name: text + type: text + - name: domain + type: category + - name: document_type + type: category +output_features: + - name: domain + type: category diff --git a/ludwig/datasets/configs/nemotron_safety.yaml b/ludwig/datasets/configs/nemotron_safety.yaml new file mode 100644 index 0000000..d9a9d76 --- /dev/null +++ b/ludwig/datasets/configs/nemotron_safety.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: nemotron_safety +huggingface_dataset_id: nvidia/Nemotron-Safety-Guard-Dataset-v3 +loader: hugging_face.HFLoader +description: | + Nemotron Safety Guard v3: prompt/response safety classification. 451K examples. +columns: + - name: prompt + type: text + - name: response + type: text + - name: prompt_label + type: category + - name: response_label + type: category +output_features: + - name: prompt_label + type: category diff --git a/ludwig/datasets/configs/news_category.yaml b/ludwig/datasets/configs/news_category.yaml new file mode 100644 index 0000000..4d060e7 --- /dev/null +++ b/ludwig/datasets/configs/news_category.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: news_category +huggingface_dataset_id: heegyu/news-category-dataset +loader: hugging_face.HFLoader +description: | + News Category Dataset: 210K Huffington Post headlines with 42 categories. +columns: + - name: headline + type: text + - name: short_description + type: text + - name: category + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/news_channel.yaml b/ludwig/datasets/configs/news_channel.yaml new file mode 100644 index 0000000..9a8b8f8 --- /dev/null +++ b/ludwig/datasets/configs/news_channel.yaml @@ -0,0 +1,56 @@ +version: 1.0 +name: news_channel +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/news_channel/train.csv + - https://automl-mm-bench.s3.amazonaws.com/news_channel/test.csv +sha256: + test.csv: d48e7261dce69964eb1163c89e05261b8732c676b10de9b40339b2d95559c9c3 + train.csv: 46e433fcf070ec684cfaf30bada482a73637e8dd954edc3e1fe860de8e661055 +train_filenames: train.csv +test_filenames: test.csv +description: | + Online News Popularity Data Set + This dataset summarizes a heterogeneous set of features about articles + published by Mashable in a period of two years. The goal is to predict + the number of shares in social networks (popularity). + https://archive.ics.uci.edu/ml/datasets/online+news+popularity +columns: # Most lot of these columns have a leading space + - name: n_tokens_content + type: number + - name: n_unique_tokens + type: number + - name: n_non_stop_words + type: number + - name: n_non_stop_unique_tokens + type: number + - name: num_hrefs + type: number + - name: num_self_hrefs + type: number + - name: num_imgs + type: number + - name: num_videos + type: number + - name: average_token_length + type: number + - name: num_keywords + type: number + - name: global_subjectivity + type: number + - name: global_sentiment_polarity + type: number + - name: global_rate_positive_words + type: number + - name: global_rate_negative_words + type: number + - name: rate_positive_words + type: number + - name: rate_negative_words + type: number + - name: article_title + type: text + - name: channel + type: category +output_features: +- name: channel + type: category diff --git a/ludwig/datasets/configs/news_popularity2.yaml b/ludwig/datasets/configs/news_popularity2.yaml new file mode 100644 index 0000000..84e76d6 --- /dev/null +++ b/ludwig/datasets/configs/news_popularity2.yaml @@ -0,0 +1,30 @@ +version: 1.0 +name: news_popularity2 +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/news_popularity2/train.csv + - https://automl-mm-bench.s3.amazonaws.com/news_popularity2/test.csv +sha256: + test.csv: 276effa981456e187fb1fc07abd8556d240e1a110fc5c096f2ad75a4082d1ccb + train.csv: 3673a07b87dbe09a9073e5ab83241681f561984269a9dc5411018fd9bca70b71 +train_filenames: train.csv +test_filenames: test.csv +description: | + Online News Popularity Data Set + This dataset summarizes a heterogeneous set of features about articles + published by Mashable in a period of two years. The goal is to predict + the number of shares in social networks (popularity). + https://archive.ics.uci.edu/ml/datasets/online+news+popularity +columns: + - name: n_tokens_content + type: number + - name: average_token_length + type: number + - name: num_keywords + type: number + - name: log_shares + type: number + - name: article_title + type: text +output_features: +- name: log_shares + type: number diff --git a/ludwig/datasets/configs/newyorker_caption_contest.yaml b/ludwig/datasets/configs/newyorker_caption_contest.yaml new file mode 100644 index 0000000..26fee4a --- /dev/null +++ b/ludwig/datasets/configs/newyorker_caption_contest.yaml @@ -0,0 +1,31 @@ +version: 1.0 +name: newyorker_caption_contest +huggingface_dataset_id: jmhessel/newyorker_caption_contest +huggingface_subsample: matching +loader: newyorker_caption_contest.NewYorkerCaptionContestLoader +description: | + New Yorker Caption Contest — Multimodal Image+Text Classification + + ~9,800 New Yorker cartoon panels, each paired with five candidate captions + (A–E). The task is to identify the winning caption from the set. This + dataset exercises Ludwig's multimodal input (image + text) with a + categorical output feature. + + Features: + image — cartoon panel (saved as JPEG by this loader) + image_description — brief text description of the cartoon (~173 chars) + label — winning caption letter (A / B / C / D / E) + + Reference: + Do Androids Laugh at Electric Sheep? (Hessel et al., ACL 2023) + License: CC BY 4.0 +columns: + - name: image_path + type: image + - name: image_description + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/nli_zh_all.yaml b/ludwig/datasets/configs/nli_zh_all.yaml new file mode 100644 index 0000000..a72722a --- /dev/null +++ b/ludwig/datasets/configs/nli_zh_all.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: nli_zh_all +huggingface_dataset_id: shibing624/nli-zh-all +huggingface_revision: refs/convert/parquet +huggingface_data_files: + train: plain_text/train/0000.parquet +loader: hugging_face.HFLoader +description: | + NLI-ZH: Chinese Natural Language Inference dataset merging multiple Chinese + NLI corpora. Sentence pair classification (entailment/neutral/contradiction). +columns: + - name: text1 + type: text + - name: text2 + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/no_robots.yaml b/ludwig/datasets/configs/no_robots.yaml new file mode 100644 index 0000000..7fb1e3b --- /dev/null +++ b/ludwig/datasets/configs/no_robots.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: no_robots +huggingface_dataset_id: HuggingFaceH4/no_robots +loader: hugging_face.HFLoader +description: | + No Robots: 10K high-quality instruction-following conversations with category labels. +columns: + - name: prompt + type: text + - name: category + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/noshow_appointments.yaml b/ludwig/datasets/configs/noshow_appointments.yaml new file mode 100644 index 0000000..b0acb1b --- /dev/null +++ b/ludwig/datasets/configs/noshow_appointments.yaml @@ -0,0 +1,43 @@ +version: 1.0 +name: noshow_appointments +kaggle_dataset_id: joniarroba/noshowappointments +archive_filenames: noshowappointments.zip +sha256: + noshowappointments.zip: 4b4f258837029bd4e61ed4c9bab2ce8a3b8a299d1a4f5bdabcc98967d5e29a43 +loader: split_loaders.RandomSplitLoader +description: | + 110.527 medical appointments its 14 associated variables (characteristics). + The most important one if the patient show-up or no-show to the appointment. + https://www.kaggle.com/datasets/joniarroba/noshowappointments +columns: + - name: PatientId + type: category + - name: AppointmentID + type: category + - name: Gender + type: binary + - name: ScheduledDay + type: date + - name: AppointmentDay + type: date + - name: Age + type: number + - name: Neighbourhood + type: category + - name: Scholarship + type: binary + - name: Hipertension + type: binary + - name: Diabetes + type: binary + - name: Alcoholism + type: binary + - name: Handcap + type: binary + - name: SMS_received + type: binary + - name: No-show + type: binary +output_features: + - name: No-show + type: binary diff --git a/ludwig/datasets/configs/nq_open.yaml b/ludwig/datasets/configs/nq_open.yaml new file mode 100644 index 0000000..dba28d5 --- /dev/null +++ b/ludwig/datasets/configs/nq_open.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: nq_open +huggingface_dataset_id: google-research-datasets/nq_open +huggingface_subsample: nq_open +loader: hugging_face.HFLoader +description: | + Natural Questions Open: open-domain QA with Wikipedia answers. 88K train examples. +columns: + - name: question + type: text + - name: answer + type: sequence +output_features: + - name: answer + type: sequence diff --git a/ludwig/datasets/configs/numerai28pt6.yaml b/ludwig/datasets/configs/numerai28pt6.yaml new file mode 100644 index 0000000..734ab13 --- /dev/null +++ b/ludwig/datasets/configs/numerai28pt6.yaml @@ -0,0 +1,56 @@ +version: 1.0 +name: numerai28pt6 +kaggle_dataset_id: numerai/encrypted-stock-market-data-from-numerai +archive_filenames: encrypted-stock-market-data-from-numerai.zip +sha256: + encrypted-stock-market-data-from-numerai.zip: cc0714c5f4c8ac6b212f7569641c5110bd2296547af434cba77184ebb03f304b +description: | + Encrypted Stock Market Data from Numerai dataset from Kaggle. +columns: + - name: feature1 + type: number + - name: feature2 + type: number + - name: feature3 + type: number + - name: feature4 + type: number + - name: feature5 + type: number + - name: feature6 + type: number + - name: feature7 + type: number + - name: feature8 + type: number + - name: feature9 + type: number + - name: feature10 + type: number + - name: feature11 + type: number + - name: feature12 + type: number + - name: feature13 + type: number + - name: feature14 + type: number + - name: feature15 + type: number + - name: feature16 + type: number + - name: feature17 + type: number + - name: feature18 + type: number + - name: feature19 + type: number + - name: feature20 + type: number + - name: feature21 + type: number + - name: target + type: binary +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/numinamath.yaml b/ludwig/datasets/configs/numinamath.yaml new file mode 100644 index 0000000..265a40a --- /dev/null +++ b/ludwig/datasets/configs/numinamath.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: numinamath +huggingface_dataset_id: nlile/NuminaMath-1.5-RL-Verifiable +loader: hugging_face.HFLoader +description: | + NuminaMath: competition math problems with verified answers. 131K train examples. +columns: + - name: problem + type: text + - name: solution + type: text + - name: problem_type + type: category + - name: answer + type: text +output_features: + - name: problem_type + type: category diff --git a/ludwig/datasets/configs/oasst1.yaml b/ludwig/datasets/configs/oasst1.yaml new file mode 100644 index 0000000..2c1e656 --- /dev/null +++ b/ludwig/datasets/configs/oasst1.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: oasst1 +huggingface_dataset_id: OpenAssistant/oasst1 +loader: hugging_face.HFLoader +description: | + OpenAssistant OASST1: 161K messages from 35K conversation trees; role classification. +columns: + - name: text + type: text + - name: role + type: category + - name: lang + type: category +output_features: + - name: role + type: category diff --git a/ludwig/datasets/configs/ohsumed_7400.yaml b/ludwig/datasets/configs/ohsumed_7400.yaml new file mode 100644 index 0000000..1ecc584 --- /dev/null +++ b/ludwig/datasets/configs/ohsumed_7400.yaml @@ -0,0 +1,23 @@ +version: 1.0 +name: ohsumed_7400 +kaggle_dataset_id: weipengfei/ohr8r52 +archive_filenames: ohr8r52.zip +sha256: + ohr8r52.zip: 93c7a8817a32b994d93267506ad766281764ba9382e3f4f9d978544cebab6ca4 +train_filenames: oh/oh-train-stemmed.csv +validation_filenames: oh/oh-dev-stemmed.csv +test_filenames: oh/oh-test-stemmed.csv +description: | + Ohsumed corpus is extracted from MEDLINE database. MEDLINE is designed for multi-label classification, we remove the + text with two or more labels. + https://www.kaggle.com/datasets/weipengfei/ohr8r52 +columns: + - name: text + type: text + - name: edge + type: text + - name: intent + type: category +output_features: + - name: intent + type: category diff --git a/ludwig/datasets/configs/ohsumed_cmu.yaml b/ludwig/datasets/configs/ohsumed_cmu.yaml new file mode 100644 index 0000000..883e3e4 --- /dev/null +++ b/ludwig/datasets/configs/ohsumed_cmu.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: ohsumed_cmu +download_urls: http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/ohsumed-allcats-6.zip +sha256: + ohsumed-allcats-6.zip: 3f2f6c4e27faaac1c8dc179a121bed92d6adbdf91a1e11d2d124f7bd963798da +description: | + OHSUMED is a well-known medical abstracts dataset. It contains 348,566 references, + and is still used for research and development. + + This is a subset of OHSUMED containing 6 categories, from this CMU course: + http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/ +columns: + - name: text + type: text + - name: class + type: category +output_features: + - name: class + type: category diff --git a/ludwig/datasets/configs/openbookqa.yaml b/ludwig/datasets/configs/openbookqa.yaml new file mode 100644 index 0000000..b10c429 --- /dev/null +++ b/ludwig/datasets/configs/openbookqa.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: openbookqa +huggingface_dataset_id: allenai/openbookqa +huggingface_subsample: main +loader: multiple_choice_loader.OpenBookQALoader +description: | + OpenBookQA elementary science 4-way multiple choice. +columns: + - name: question_stem + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/openfake.yaml b/ludwig/datasets/configs/openfake.yaml new file mode 100644 index 0000000..0823466 --- /dev/null +++ b/ludwig/datasets/configs/openfake.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: openfake +huggingface_dataset_id: ComplexDataLab/OpenFake +huggingface_subsample: core +loader: hugging_face.HFLoader +description: | + OpenFake: AI-generated vs real image binary classification. +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/opus100_en_es.yaml b/ludwig/datasets/configs/opus100_en_es.yaml new file mode 100644 index 0000000..bf0b4a6 --- /dev/null +++ b/ludwig/datasets/configs/opus100_en_es.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: opus100_en_es +huggingface_dataset_id: Helsinki-NLP/opus-100 +huggingface_subsample: en-es +loader: translation_loader.Opus100EnEsLoader +description: | + OPUS-100 English-Spanish parallel corpus. ~1M sentence pairs. +columns: + - name: en + type: text + - name: es + type: text +output_features: + - name: es + type: text diff --git a/ludwig/datasets/configs/opus100_en_fr.yaml b/ludwig/datasets/configs/opus100_en_fr.yaml new file mode 100644 index 0000000..a825fbf --- /dev/null +++ b/ludwig/datasets/configs/opus100_en_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: opus100_en_fr +huggingface_dataset_id: Helsinki-NLP/opus-100 +huggingface_subsample: en-fr +loader: translation_loader.Opus100EnFrLoader +description: | + OPUS-100 English-French parallel corpus. ~1M sentence pairs. +columns: + - name: en + type: text + - name: fr + type: text +output_features: + - name: fr + type: text diff --git a/ludwig/datasets/configs/opus_books_en_fr.yaml b/ludwig/datasets/configs/opus_books_en_fr.yaml new file mode 100644 index 0000000..ed0f33e --- /dev/null +++ b/ludwig/datasets/configs/opus_books_en_fr.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: opus_books_en_fr +huggingface_dataset_id: Helsinki-NLP/opus_books +huggingface_subsample: en-fr +loader: translation_loader.OpusBooksEnFrLoader +description: | + OPUS Books English-French literary translations. +columns: + - name: en + type: text + - name: fr + type: text +output_features: + - name: fr + type: text diff --git a/ludwig/datasets/configs/or_bench.yaml b/ludwig/datasets/configs/or_bench.yaml new file mode 100644 index 0000000..106e078 --- /dev/null +++ b/ludwig/datasets/configs/or_bench.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: or_bench +huggingface_dataset_id: bench-llm/or-bench +huggingface_subsample: or-bench-80k +loader: hugging_face.HFLoader +description: | + OR-Bench: over-refusal benchmark prompt classification. 80K examples. +columns: + - name: prompt + type: text + - name: category + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/orca_dpo_pairs.yaml b/ludwig/datasets/configs/orca_dpo_pairs.yaml new file mode 100644 index 0000000..271cf1e --- /dev/null +++ b/ludwig/datasets/configs/orca_dpo_pairs.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: orca_dpo_pairs +huggingface_dataset_id: Intel/orca_dpo_pairs +loader: hugging_face.HFLoader +description: | + Intel Orca DPO Pairs: preference dataset for direct preference optimization. +columns: + - name: question + type: text + - name: chosen + type: text + - name: rejected + type: text +output_features: + - name: chosen + type: text diff --git a/ludwig/datasets/configs/orca_math.yaml b/ludwig/datasets/configs/orca_math.yaml new file mode 100644 index 0000000..1d2a488 --- /dev/null +++ b/ludwig/datasets/configs/orca_math.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: orca_math +huggingface_dataset_id: microsoft/orca-math-word-problems-200k +loader: hugging_face.HFLoader +description: | + OrcaMath: 200K math word problems with solutions. +columns: + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/otto_group_product.yaml b/ludwig/datasets/configs/otto_group_product.yaml new file mode 100644 index 0000000..38c561f --- /dev/null +++ b/ludwig/datasets/configs/otto_group_product.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: otto_group_product +kaggle_competition: otto-group-product-classification-challenge +archive_filenames: otto-group-product-classification-challenge.zip +sha256: + otto-group-product-classification-challenge.zip: 81d1fa5805036772b7a2a2425311fdc7b1568af4fbb42f0ec8f9661d0d21ce42 +train_filenames: train.csv +test_filenames: test.csv +description: | + The Otto Group Product Classification Challenge + https://www.kaggle.com/c/otto-group-product-classification-challenge/overview +output_features: + - name: target + type: category diff --git a/ludwig/datasets/configs/oxford_pets.yaml b/ludwig/datasets/configs/oxford_pets.yaml new file mode 100644 index 0000000..308e568 --- /dev/null +++ b/ludwig/datasets/configs/oxford_pets.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: oxford_pets +huggingface_dataset_id: timm/oxford-iiit-pet +loader: hugging_face.HFLoader +description: | + Oxford-IIIT Pet; 37 pet breed classification +columns: + - name: image + type: image + - name: label + type: category + - name: image_id + type: text + - name: label_cat_dog + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/path_vqa.yaml b/ludwig/datasets/configs/path_vqa.yaml new file mode 100644 index 0000000..5cc44cb --- /dev/null +++ b/ludwig/datasets/configs/path_vqa.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: path_vqa +huggingface_dataset_id: flaviagiammarino/path-vqa +loader: hugging_face.HFLoader +description: | + PathVQA: pathology visual QA (yes/no + open). 20K train examples. +columns: + - name: image + type: image + - name: question + type: text + - name: answer + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/paws.yaml b/ludwig/datasets/configs/paws.yaml new file mode 100644 index 0000000..cd14d79 --- /dev/null +++ b/ludwig/datasets/configs/paws.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: paws +huggingface_dataset_id: google-research-datasets/paws +huggingface_subsample: labeled_final +loader: hugging_face.HFLoader +description: | + Paraphrase Adversaries from Word Scrambling; challenging paraphrase detection +columns: + - name: id + type: category + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/paws_x.yaml b/ludwig/datasets/configs/paws_x.yaml new file mode 100644 index 0000000..8d0c8f2 --- /dev/null +++ b/ludwig/datasets/configs/paws_x.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: paws_x +huggingface_dataset_id: google-research-datasets/paws-x +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + PAWS-X multilingual paraphrase identification. 49K train examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/peoples_speech.yaml b/ludwig/datasets/configs/peoples_speech.yaml new file mode 100644 index 0000000..8082925 --- /dev/null +++ b/ludwig/datasets/configs/peoples_speech.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: peoples_speech +huggingface_dataset_id: MLCommons/peoples_speech +huggingface_subsample: clean +loader: hugging_face.HFLoader +description: | + People's Speech; 30K-hour diverse English ASR +columns: + - name: id + type: text + - name: audio + type: audio + - name: duration_ms + type: number + - name: text + type: text +output_features: + - name: text + type: text diff --git a/ludwig/datasets/configs/persuasion.yaml b/ludwig/datasets/configs/persuasion.yaml new file mode 100644 index 0000000..53a63c7 --- /dev/null +++ b/ludwig/datasets/configs/persuasion.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: persuasion +huggingface_dataset_id: Anthropic/persuasion +loader: hugging_face.HFLoader +description: | + Anthropic Persuasion: rate how persuasive arguments are for various claims. +columns: + - name: claim + type: text + - name: argument + type: text + - name: persuasiveness_metric + type: number +output_features: + - name: persuasiveness_metric + type: number diff --git a/ludwig/datasets/configs/phinc.yaml b/ludwig/datasets/configs/phinc.yaml new file mode 100644 index 0000000..86c9076 --- /dev/null +++ b/ludwig/datasets/configs/phinc.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: phinc +huggingface_dataset_id: LingoIITGN/PHINC +loader: hugging_face.HFLoader +description: | + PHINC: Hindi-English parallel corpus. 13K train pairs. +columns: + - name: Sentence + type: text + - name: English_Translation + type: text +output_features: + - name: English_Translation + type: text diff --git a/ludwig/datasets/configs/pii_masking.yaml b/ludwig/datasets/configs/pii_masking.yaml new file mode 100644 index 0000000..b2c7087 --- /dev/null +++ b/ludwig/datasets/configs/pii_masking.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: pii_masking +huggingface_dataset_id: ai4privacy/pii-masking-300k +loader: ner_loader.PIIMaskingLoader +description: | + PII masking: source text → BIO labels for PII tokens. +columns: + - name: source_text + type: text + - name: bio_labels + type: sequence +output_features: + - name: bio_labels + type: sequence diff --git a/ludwig/datasets/configs/poem_sentiment.yaml b/ludwig/datasets/configs/poem_sentiment.yaml new file mode 100644 index 0000000..cc725df --- /dev/null +++ b/ludwig/datasets/configs/poem_sentiment.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: poem_sentiment +huggingface_dataset_id: google-research-datasets/poem_sentiment +loader: hugging_face.HFLoader +description: | + Poem Sentiment: verse-level sentiment classification from English poetry. + Labels: negative, positive, no impact, mixed. +columns: + - name: verse_text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/poem_sentiment_hf.yaml b/ludwig/datasets/configs/poem_sentiment_hf.yaml new file mode 100644 index 0000000..080860c --- /dev/null +++ b/ludwig/datasets/configs/poem_sentiment_hf.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: poem_sentiment_hf +huggingface_dataset_id: google-research-datasets/poem_sentiment +loader: hugging_face.HFLoader +description: | + Poem Sentiment (Google Research): verse-level sentiment from English poetry. +columns: + - name: verse_text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/poker_hand.yaml b/ludwig/datasets/configs/poker_hand.yaml new file mode 100644 index 0000000..f81ce7a --- /dev/null +++ b/ludwig/datasets/configs/poker_hand.yaml @@ -0,0 +1,44 @@ +version: 1.0 +name: poker_hand +download_urls: + - http://archive.ics.uci.edu/ml/machine-learning-databases/poker/poker-hand-training-true.data + - http://archive.ics.uci.edu/ml/machine-learning-databases/poker/poker-hand-testing.data +train_filenames: poker-hand-training-true.data +test_filenames: poker-hand-testing.data +sha256: + poker-hand-testing.data: 3cd75958e19dd321ed5ca3f7f154c0f6aad544aab9f37731ac545b5f66b232c7 + poker-hand-training-true.data: 37becdf87d5f8cbf2b91d6471e965a25b86cb4a6d878c0f94a4025969fca464f +description: | + Each record is an example of a hand consisting of five playing cards + drawn from a standard deck of 52. Each card is described using two + attributes (suit and rank), for a total of 10 predictive attributes. + There is one Class attribute that describes the "Poker Hand". The + order of cards is important, which is why there are 480 possible + Royal Flush hands as compared to 4. + https://archive.ics.uci.edu/ml/datasets/Poker+Hand +columns: + - name: S1 + type: number + - name: C1 + type: number + - name: S2 + type: number + - name: C2 + type: number + - name: S3 + type: number + - name: C3 + type: number + - name: S4 + type: number + - name: C4 + type: number + - name: S5 + type: number + - name: C5 + type: number + - name: hand + type: category +output_features: + - name: hand + type: category diff --git a/ludwig/datasets/configs/porto_seguro_safe_driver.yaml b/ludwig/datasets/configs/porto_seguro_safe_driver.yaml new file mode 100644 index 0000000..a68d61d --- /dev/null +++ b/ludwig/datasets/configs/porto_seguro_safe_driver.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: porto_seguro_safe_driver +kaggle_competition: porto-seguro-safe-driver-prediction +archive_filenames: porto-seguro-safe-driver-prediction.zip +sha256: + porto-seguro-safe-driver-prediction.zip: 53dd7b67b9b3df088c4e0814cba7317d3bc8f76094c726471c8f91e84f61ccdc +train_filenames: train.csv +test_filenames: test.csv +description: | + Predict the probability that an auto insurance policy holder files a claim. + https://www.kaggle.com/competitions/porto-seguro-safe-driver-prediction +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/product_sentiment_machine_hack.yaml b/ludwig/datasets/configs/product_sentiment_machine_hack.yaml new file mode 100644 index 0000000..599ccfd --- /dev/null +++ b/ludwig/datasets/configs/product_sentiment_machine_hack.yaml @@ -0,0 +1,27 @@ +version: 1.0 +name: product_sentiment_machine_hack +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/machine_hack_product_sentiment/train.csv + - https://automl-mm-bench.s3.amazonaws.com/machine_hack_product_sentiment/dev.csv +sha256: + dev.csv: 33adff4dba7d9322397b398900c20f678d3fffc5d87b0ea825d9aa497a343150 + train.csv: 85a229e162b6d8c4839d1b27f834c36ae5e244fd027534fe62a888d4f536f0ef +train_filenames: train.csv +test_filenames: dev.csv +description: | + We challenge the machinehackers community to develop a machine learning model + to accurately classify various products into 4 different classes of sentiments + based on the raw text review provided by the user. + https://www.machinehack.com/hackathons/product_sentiment_classification_weekend_hackathon_19/overview +columns: + - name: Text_ID + type: category + - name: Product_Description + type: text + - name: Product_Type + type: category + - name: Sentiment + type: category +output_features: +- name: Sentiment + type: category diff --git a/ludwig/datasets/configs/protein.yaml b/ludwig/datasets/configs/protein.yaml new file mode 100644 index 0000000..823279e --- /dev/null +++ b/ludwig/datasets/configs/protein.yaml @@ -0,0 +1,32 @@ +version: 1.0 +name: protein +download_urls: http://archive.ics.uci.edu/ml/machine-learning-databases/00265/CASP.csv +sha256: + CASP.csv: 4277cfcb4e91a181746cbc654f001b57951c9e6a80f4f795fdb5c807e0848f40 +description: | + Physicochemical Properties of Protein Tertiary Structure Data Set. + https://archive.ics.uci.edu/ml/datasets/Physicochemical+Properties+of+Protein+Tertiary+Structure +columns: + - name: RMSD + type: number + - name: F1 + type: number + - name: F2 + type: number + - name: F3 + type: number + - name: F4 + type: number + - name: F5 + type: number + - name: F6 + type: number + - name: F7 + type: number + - name: F8 + type: number + - name: F9 + type: number +output_features: + - name: RMSD + type: number diff --git a/ludwig/datasets/configs/pubmed_qa.yaml b/ludwig/datasets/configs/pubmed_qa.yaml new file mode 100644 index 0000000..b7ee166 --- /dev/null +++ b/ludwig/datasets/configs/pubmed_qa.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: pubmed_qa +huggingface_dataset_id: qiaojin/PubMedQA +huggingface_subsample: pqa_labeled +loader: qa_loader.PubMedQALoader +description: | + PubMedQA biomedical QA. Context + question → yes/no/maybe. +columns: + - name: context_text + type: text + - name: question + type: text + - name: final_decision + type: category +output_features: + - name: final_decision + type: category diff --git a/ludwig/datasets/configs/pubmed_summarization.yaml b/ludwig/datasets/configs/pubmed_summarization.yaml new file mode 100644 index 0000000..4b53384 --- /dev/null +++ b/ludwig/datasets/configs/pubmed_summarization.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: pubmed_summarization +huggingface_dataset_id: ccdv/pubmed-summarization +loader: hugging_face.HFLoader +description: | + PubMed biomedical document summarization. 120K train examples. +columns: + - name: article + type: text + - name: abstract + type: text +output_features: + - name: abstract + type: text diff --git a/ludwig/datasets/configs/python_code_instructions.yaml b/ludwig/datasets/configs/python_code_instructions.yaml new file mode 100644 index 0000000..2e4911d --- /dev/null +++ b/ludwig/datasets/configs/python_code_instructions.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: python_code_instructions +huggingface_dataset_id: iamtarun/python_code_instructions_18k_alpaca +loader: hugging_face.HFLoader +description: | + Python code generation from instructions. 18K examples. +columns: + - name: instruction + type: text + - name: input + type: text + - name: output + type: text +output_features: + - name: output + type: text diff --git a/ludwig/datasets/configs/qasc.yaml b/ludwig/datasets/configs/qasc.yaml new file mode 100644 index 0000000..4cdde22 --- /dev/null +++ b/ludwig/datasets/configs/qasc.yaml @@ -0,0 +1,20 @@ +version: 1.0 +name: qasc +huggingface_dataset_id: allenai/qasc +loader: hugging_face.HFLoader +description: | + QASC: 8-way MC QA with science facts. 8134 train examples. +columns: + - name: question + type: text + - name: fact1 + type: text + - name: fact2 + type: text + - name: formatted_question + type: text + - name: answerKey + type: category +output_features: + - name: answerKey + type: category diff --git a/ludwig/datasets/configs/qnli.yaml b/ludwig/datasets/configs/qnli.yaml new file mode 100644 index 0000000..9f07971 --- /dev/null +++ b/ludwig/datasets/configs/qnli.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: qnli +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: qnli +loader: hugging_face.HFLoader +description: | + Question-answering NLI; whether context sentence contains answer to question +columns: + - name: question + type: text + - name: sentence + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/qqp.yaml b/ludwig/datasets/configs/qqp.yaml new file mode 100644 index 0000000..4995878 --- /dev/null +++ b/ludwig/datasets/configs/qqp.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: qqp +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: qqp +loader: hugging_face.HFLoader +description: | + Quora Question Pairs; whether two questions are semantically equivalent +columns: + - name: question1 + type: text + - name: question2 + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/race.yaml b/ludwig/datasets/configs/race.yaml new file mode 100644 index 0000000..ffd2212 --- /dev/null +++ b/ludwig/datasets/configs/race.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: race +huggingface_dataset_id: ehovy/race +huggingface_subsample: all +loader: hugging_face.HFLoader +description: | + RACE: large-scale reading comprehension from Chinese English exams. 87K train examples. 4-way MC. +columns: + - name: article + type: text + - name: question + type: text + - name: options + type: text + - name: answer + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/ravnursson_asr.yaml b/ludwig/datasets/configs/ravnursson_asr.yaml new file mode 100644 index 0000000..c7de7d8 --- /dev/null +++ b/ludwig/datasets/configs/ravnursson_asr.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: ravnursson_asr +huggingface_dataset_id: carlosdanielhernandezmena/ravnursson_asr +huggingface_subsample: ravnursson_asr +loader: hugging_face.HFLoader +description: | + Ravnursson: Faroese audio speech recognition dataset. 65K train examples. +columns: + - name: audio + type: audio + - name: normalized_text + type: text + - name: gender + type: category + - name: dialect + type: category +output_features: + - name: normalized_text + type: text diff --git a/ludwig/datasets/configs/rendered_sst2.yaml b/ludwig/datasets/configs/rendered_sst2.yaml new file mode 100644 index 0000000..d31e871 --- /dev/null +++ b/ludwig/datasets/configs/rendered_sst2.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: rendered_sst2 +huggingface_dataset_id: nateraw/rendered-sst2 +loader: hugging_face.HFLoader +description: | + Rendered SST2; sentiment classification from rendered text images +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/repid.yaml b/ludwig/datasets/configs/repid.yaml new file mode 100644 index 0000000..4697de4 --- /dev/null +++ b/ludwig/datasets/configs/repid.yaml @@ -0,0 +1,39 @@ +version: 1.0 +name: repid +huggingface_dataset_id: vsevolodpl/REPID +loader: hugging_face.HFLoader +description: | + REPID: Retinal Perceptual Image Dataset — tabular perceptual quality metrics + (MOS, MSE, color/chroma/lightness stats) for image pairs. +columns: + - name: mos + type: number + - name: mse + type: number + - name: hue_left + type: number + - name: hue_right + type: number + - name: std_left + type: number + - name: std_right + type: number + - name: chroma_u_left + type: number + - name: chroma_u_right + type: number + - name: chroma_v_left + type: number + - name: chroma_v_right + type: number + - name: lightness_left + type: number + - name: lightness_right + type: number + - name: saturation_left + type: number + - name: saturation_right + type: number +output_features: + - name: mos + type: number diff --git a/ludwig/datasets/configs/reuters_cmu.yaml b/ludwig/datasets/configs/reuters_cmu.yaml new file mode 100644 index 0000000..98ca234 --- /dev/null +++ b/ludwig/datasets/configs/reuters_cmu.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: reuters_cmu +download_urls: http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/reuters-allcats-6.zip +sha256: + reuters-allcats-6.zip: 304ae223f9ca35f7ce9066c9d31558c06ed5c72cd91faa885f82b928b2aa6f34 +description: | + Reuters-21578 is a well-known newswire dataset containing 21,578 documents. + + This is a subset of Reuters-21578 using only 6 categories, from this CMU course: + http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/ +columns: + - name: text + type: text + - name: class + type: category +output_features: + - name: class + type: category diff --git a/ludwig/datasets/configs/reuters_r8.yaml b/ludwig/datasets/configs/reuters_r8.yaml new file mode 100644 index 0000000..0bac5b4 --- /dev/null +++ b/ludwig/datasets/configs/reuters_r8.yaml @@ -0,0 +1,21 @@ +version: 1.0 +name: reuters_r8 +kaggle_dataset_id: weipengfei/ohr8r52 +archive_filenames: ohr8r52.zip +sha256: + ohr8r52.zip: 93c7a8817a32b994d93267506ad766281764ba9382e3f4f9d978544cebab6ca4 +train_filenames: r8/r8-train-stemmed.csv +validation_filenames: r8/r8-dev-stemmed.csv +test_filenames: r8/r8-test-stemmed.csv +description: | + Reuters R8 subset of Reuters 21578 dataset from Kaggle. +columns: + - name: text + type: text + - name: edge + type: text + - name: intent + type: category +output_features: + - name: intent + type: category diff --git a/ludwig/datasets/configs/reward_bench.yaml b/ludwig/datasets/configs/reward_bench.yaml new file mode 100644 index 0000000..4eca42b --- /dev/null +++ b/ludwig/datasets/configs/reward_bench.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: reward_bench +huggingface_dataset_id: allenai/reward-bench +loader: hugging_face.HFLoader +description: | + RewardBench: preference evaluation - predict which model response is chosen/preferred. +columns: + - name: prompt + type: text + - name: chosen + type: text + - name: rejected + type: text + - name: subset + type: category +output_features: + - name: subset + type: category diff --git a/ludwig/datasets/configs/rossman_store_sales.yaml b/ludwig/datasets/configs/rossman_store_sales.yaml new file mode 100644 index 0000000..c361b21 --- /dev/null +++ b/ludwig/datasets/configs/rossman_store_sales.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: rossman_store_sales +kaggle_competition: rossmann-store-sales +archive_filenames: rossmann-store-sales.zip +sha256: + rossmann-store-sales.zip: 52ce715e02dc70cac16b14548580d656997f5d43ce3544220d5e574d26483cf3 +loader: rossman_store_sales.RossmanStoreSalesLoader +description: | + The Rossmann Store Sales dataset. + Using the time split from the catboost benchmark + https://github.com/catboost/benchmarks/tree/master/kaggle/rossmann-store-sales + that is used in the TabNet paper, + because the test set does not contain sales ground truth. +output_features: + - name: Sales + type: number diff --git a/ludwig/datasets/configs/rotten_tomatoes.yaml b/ludwig/datasets/configs/rotten_tomatoes.yaml new file mode 100644 index 0000000..b4e9e6c --- /dev/null +++ b/ludwig/datasets/configs/rotten_tomatoes.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: rotten_tomatoes +huggingface_dataset_id: cornell-movie-review-data/rotten_tomatoes +loader: hugging_face.HFLoader +description: | + Rotten Tomatoes movie review sentiment; positive/negative +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/rte.yaml b/ludwig/datasets/configs/rte.yaml new file mode 100644 index 0000000..32d7fa5 --- /dev/null +++ b/ludwig/datasets/configs/rte.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: rte +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: rte +loader: hugging_face.HFLoader +description: | + Recognizing Textual Entailment; entailment vs not-entailment +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/samsum.yaml b/ludwig/datasets/configs/samsum.yaml new file mode 100644 index 0000000..12f2727 --- /dev/null +++ b/ludwig/datasets/configs/samsum.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: samsum +huggingface_dataset_id: knkarthick/samsum +loader: hugging_face.HFLoader +description: | + SAMSum: dialogue summarization. 14K train examples. +columns: + - name: dialogue + type: text + - name: summary + type: text +output_features: + - name: summary + type: text diff --git a/ludwig/datasets/configs/santander_customer_satisfaction.yaml b/ludwig/datasets/configs/santander_customer_satisfaction.yaml new file mode 100644 index 0000000..e3c6c96 --- /dev/null +++ b/ludwig/datasets/configs/santander_customer_satisfaction.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: santander_customer_satisfaction +kaggle_competition: santander-customer-satisfaction +archive_filenames: santander-customer-satisfaction.zip +sha256: + santander-customer-satisfaction.zip: d4c2d068d8041af168d82d0eef7ad0b53ddd1d7fca9aba4e5d88fa1f957ee594 +train_filenames: train.csv +test_filenames: test.csv +description: | + Santander Customer Satisfaction Prediction. + https://www.kaggle.com/c/santander-customer-satisfaction/overview +output_features: + - name: TARGET + type: binary diff --git a/ludwig/datasets/configs/santander_customer_transaction.yaml b/ludwig/datasets/configs/santander_customer_transaction.yaml new file mode 100644 index 0000000..1fcbd62 --- /dev/null +++ b/ludwig/datasets/configs/santander_customer_transaction.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: santander_customer_transaction +kaggle_competition: santander-customer-transaction-prediction +archive_filenames: santander-customer-transaction-prediction.zip +sha256: + santander-customer-transaction-prediction.zip: b3a56d036b493a9cf0695018c968baba1ba7ef8c39d842cc5626e72f13c0ec69 +train_filenames: train.csv +test_filenames: test.csv +description: | + Santander Customer Transaction Prediction. + https://www.kaggle.com/c/santander-customer-transaction-prediction/overview +output_features: + - name: target + type: binary diff --git a/ludwig/datasets/configs/santander_value_prediction.yaml b/ludwig/datasets/configs/santander_value_prediction.yaml new file mode 100644 index 0000000..5b932dc --- /dev/null +++ b/ludwig/datasets/configs/santander_value_prediction.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: santander_value_prediction +kaggle_competition: santander-value-prediction-challenge +archive_filenames: santander-value-prediction-challenge.zip +sha256: + santander-value-prediction-challenge.zip: a8b44a0403bff6ab42f2bd1da8d9cbaf98f1fd4b9ea7a86e47491ac996384bf4 +train_filenames: train.csv +loader: santander_value_prediction.SantanderValuePredictionLoader +description: | + The Santander Value Prediction Challenge dataset. + https://www.kaggle.com/c/santander-value-prediction-challenge +output_features: + - name: target + type: number diff --git a/ludwig/datasets/configs/sarcastic_headlines.yaml b/ludwig/datasets/configs/sarcastic_headlines.yaml new file mode 100644 index 0000000..03af5aa --- /dev/null +++ b/ludwig/datasets/configs/sarcastic_headlines.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: sarcastic_headlines +train_filenames: Sarcasm_Headlines_Dataset.json +archive_filenames: news-headlines-dataset-for-sarcasm-detection.zip +sha256: + news-headlines-dataset-for-sarcasm-detection.zip: 3728f0fbce563536c3c67ab92e343e3ebcdc5cf1feaf4980c3abd4e54109eb51 +kaggle_dataset_id: rmisra/news-headlines-dataset-for-sarcasm-detection +description: A dataset to determine if a news headline is sarcastic or serious. +loader: sarcastic_headlines.SarcasticHeadlinesLoader +columns: + - name: article_link + type: category + - name: headline + type: text + - name: is_sarcastic + type: binary +output_features: + - name: is_sarcastic + type: binary diff --git a/ludwig/datasets/configs/sarcos.yaml b/ludwig/datasets/configs/sarcos.yaml new file mode 100644 index 0000000..8ab1e5b --- /dev/null +++ b/ludwig/datasets/configs/sarcos.yaml @@ -0,0 +1,78 @@ +version: 1.0 +name: sarcos +download_urls: + - http://www.gaussianprocess.org/gpml/data/sarcos_inv.mat + - http://www.gaussianprocess.org/gpml/data/sarcos_inv_test.mat +sha256: + sarcos_inv_test.mat: 161a59b5c3b4f4b404584323f181607b2acbe620eb134dc720760dc3f38f5cec + sarcos_inv.mat: b8a249733253ba6097372fedee7696833fcf30de42037d5b4a7227f21a6d1d97 +train_filenames: sarcos_inv.mat +test_filenames: sarcos_inv_test.mat +loader: sarcos.SarcosLoader +description: | + The data relates to an inverse dynamics problem for a seven + degrees-of-freedom SARCOS anthropomorphic robot arm. + The task is to map from a 21-dimensional input space + (7 joint positions, 7 joint velocities, 7 joint accelerations) + to the corresponding 7 joint torques. + http://gaussianprocess.org/gpml/data/ +columns: + - name: position_1 + type: number + - name: position_2 + type: number + - name: position_3 + type: number + - name: position_4 + type: number + - name: position_5 + type: number + - name: position_6 + type: number + - name: position_7 + type: number + - name: velocity_1 + type: number + - name: velocity_2 + type: number + - name: velocity_3 + type: number + - name: velocity_4 + type: number + - name: velocity_5 + type: number + - name: velocity_6 + type: number + - name: velocity_7 + type: number + - name: acceleration_1 + type: number + - name: acceleration_2 + type: number + - name: acceleration_3 + type: number + - name: acceleration_4 + type: number + - name: acceleration_5 + type: number + - name: acceleration_6 + type: number + - name: acceleration_7 + type: number + - name: torque_1 + type: number + - name: torque_2 + type: number + - name: torque_3 + type: number + - name: torque_4 + type: number + - name: torque_5 + type: number + - name: torque_6 + type: number + - name: torque_7 + type: number +output_features: +- name: torque_1 + type: number diff --git a/ludwig/datasets/configs/scandisent.yaml b/ludwig/datasets/configs/scandisent.yaml new file mode 100644 index 0000000..e5b7427 --- /dev/null +++ b/ludwig/datasets/configs/scandisent.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: scandisent +huggingface_dataset_id: timpal0l/scandisent +loader: hugging_face.HFLoader +description: | + ScandiSent: Nordic language sentiment classification. 50K train examples. +columns: + - name: text + type: text + - name: label + type: number + - name: language + type: category +output_features: + - name: label + type: number diff --git a/ludwig/datasets/configs/scienceqa_vqa.yaml b/ludwig/datasets/configs/scienceqa_vqa.yaml new file mode 100644 index 0000000..78470cc --- /dev/null +++ b/ludwig/datasets/configs/scienceqa_vqa.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: scienceqa_vqa +huggingface_dataset_id: derek-thomas/ScienceQA +loader: vqa_loader.ScienceQAImageLoader +description: | + ScienceQA: science question + optional lecture → multiple choice answer. +columns: + - name: question + type: text + - name: choices_text + type: text + - name: answer_label + type: category +output_features: + - name: answer_label + type: category diff --git a/ludwig/datasets/configs/sciq.yaml b/ludwig/datasets/configs/sciq.yaml new file mode 100644 index 0000000..b20d8fb --- /dev/null +++ b/ludwig/datasets/configs/sciq.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sciq +huggingface_dataset_id: allenai/sciq +loader: misc_loaders.SciqLoader +description: | + SciQ science QA with support text. Support + question → correct_answer (text). +columns: + - name: support + type: text + - name: question + type: text + - name: correct_answer + type: text +output_features: + - name: correct_answer + type: text diff --git a/ludwig/datasets/configs/scitail.yaml b/ludwig/datasets/configs/scitail.yaml new file mode 100644 index 0000000..4a626d7 --- /dev/null +++ b/ludwig/datasets/configs/scitail.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: scitail +huggingface_dataset_id: allenai/scitail +huggingface_subsample: dgem_format +loader: hugging_face.HFLoader +description: | + SciTail; science textual entailment from multiple-choice questions +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: text + - name: hypothesis_graph_structure + type: text +output_features: + - name: label + type: text diff --git a/ludwig/datasets/configs/scotus_classification.yaml b/ludwig/datasets/configs/scotus_classification.yaml new file mode 100644 index 0000000..29920ab --- /dev/null +++ b/ludwig/datasets/configs/scotus_classification.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: scotus_classification +huggingface_dataset_id: coastalcph/lex_glue +huggingface_subsample: scotus +loader: hugging_face.HFLoader +description: | + LexGLUE SCOTUS; US Supreme Court opinion issue area classification +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/setfit_ag_news.yaml b/ludwig/datasets/configs/setfit_ag_news.yaml new file mode 100644 index 0000000..317f667 --- /dev/null +++ b/ludwig/datasets/configs/setfit_ag_news.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: setfit_ag_news +huggingface_dataset_id: SetFit/ag_news +loader: hugging_face.HFLoader +description: | + SetFit AG News: 4-class news topic classification (World, Sports, Business, Sci/Tech). +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/setfit_amazon_polarity.yaml b/ludwig/datasets/configs/setfit_amazon_polarity.yaml new file mode 100644 index 0000000..533bd39 --- /dev/null +++ b/ludwig/datasets/configs/setfit_amazon_polarity.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: setfit_amazon_polarity +huggingface_dataset_id: SetFit/amazon_polarity +loader: hugging_face.HFLoader +description: | + SetFit Amazon Polarity: binary positive/negative sentiment from Amazon reviews. +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/setfit_emotion.yaml b/ludwig/datasets/configs/setfit_emotion.yaml new file mode 100644 index 0000000..da54fad --- /dev/null +++ b/ludwig/datasets/configs/setfit_emotion.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: setfit_emotion +huggingface_dataset_id: SetFit/emotion +loader: hugging_face.HFLoader +description: | + SetFit Emotion: 6-class emotion classification from English Twitter messages. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/setfit_mrpc.yaml b/ludwig/datasets/configs/setfit_mrpc.yaml new file mode 100644 index 0000000..06dcf64 --- /dev/null +++ b/ludwig/datasets/configs/setfit_mrpc.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: setfit_mrpc +huggingface_dataset_id: SetFit/mrpc +loader: hugging_face.HFLoader +description: | + SetFit MRPC: Microsoft Research Paraphrase Corpus, binary paraphrase detection. +columns: + - name: text1 + type: text + - name: text2 + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/setfit_sst2.yaml b/ludwig/datasets/configs/setfit_sst2.yaml new file mode 100644 index 0000000..e36b2de --- /dev/null +++ b/ludwig/datasets/configs/setfit_sst2.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: setfit_sst2 +huggingface_dataset_id: SetFit/sst2 +loader: hugging_face.HFLoader +description: | + SetFit SST-2: Stanford Sentiment Treebank binary sentiment (SetFit format). +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/setfit_subj.yaml b/ludwig/datasets/configs/setfit_subj.yaml new file mode 100644 index 0000000..5d151a7 --- /dev/null +++ b/ludwig/datasets/configs/setfit_subj.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: setfit_subj +huggingface_dataset_id: SetFit/subj +loader: hugging_face.HFLoader +description: | + SetFit SUBJ: subjectivity detection (subjective vs objective sentences). +columns: + - name: text + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/setfit_yelp_review.yaml b/ludwig/datasets/configs/setfit_yelp_review.yaml new file mode 100644 index 0000000..45f56a6 --- /dev/null +++ b/ludwig/datasets/configs/setfit_yelp_review.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: setfit_yelp_review +huggingface_dataset_id: SetFit/yelp_review_full +loader: hugging_face.HFLoader +description: | + SetFit Yelp Review Full: 5-class star rating classification from Yelp reviews. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/setimes_bg_bs.yaml b/ludwig/datasets/configs/setimes_bg_bs.yaml new file mode 100644 index 0000000..b153032 --- /dev/null +++ b/ludwig/datasets/configs/setimes_bg_bs.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: setimes_bg_bs +huggingface_dataset_id: community-datasets/setimes +huggingface_subsample: bg-bs +loader: translation_loader.SetimesBgBsLoader +description: | + SETimes: South-East European Times Bulgarian-Bosnian translation. 136K train pairs. +columns: + - name: bg + type: text + - name: bs + type: text +output_features: + - name: bs + type: text diff --git a/ludwig/datasets/configs/sib200.yaml b/ludwig/datasets/configs/sib200.yaml new file mode 100644 index 0000000..86ec169 --- /dev/null +++ b/ludwig/datasets/configs/sib200.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sib200 +huggingface_dataset_id: mteb/sib200 +loader: hugging_face.HFLoader +description: | + SIB-200: multilingual 7-class topic classification in 205 languages. 143K train examples. +columns: + - name: text + type: text + - name: category + type: category + - name: lang + type: category +output_features: + - name: category + type: category diff --git a/ludwig/datasets/configs/sickr.yaml b/ludwig/datasets/configs/sickr.yaml new file mode 100644 index 0000000..31ee696 --- /dev/null +++ b/ludwig/datasets/configs/sickr.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sickr +huggingface_dataset_id: mteb/sickr-sts +loader: hugging_face.HFLoader +description: | + SICK-R: sentences involving compositional knowledge. 9927 test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sms_spam.yaml b/ludwig/datasets/configs/sms_spam.yaml new file mode 100644 index 0000000..fe73002 --- /dev/null +++ b/ludwig/datasets/configs/sms_spam.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: sms_spam +huggingface_dataset_id: ucirvine/sms_spam +loader: hugging_face.HFLoader +description: | + SMS spam detection: ham or spam binary classification. 5574 examples. +columns: + - name: sms + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/snli.yaml b/ludwig/datasets/configs/snli.yaml new file mode 100644 index 0000000..b0a6cf4 --- /dev/null +++ b/ludwig/datasets/configs/snli.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: snli +huggingface_dataset_id: stanfordnlp/snli +loader: hugging_face.HFLoader +description: | + Stanford NLI; premise/hypothesis pairs -> entailment/neutral/contradiction +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/speech_massive.yaml b/ludwig/datasets/configs/speech_massive.yaml new file mode 100644 index 0000000..64d0464 --- /dev/null +++ b/ludwig/datasets/configs/speech_massive.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: speech_massive +huggingface_dataset_id: FBK-MT/Speech-MASSIVE +huggingface_subsample: all +loader: hugging_face.HFLoader +description: | + Speech-MASSIVE: multilingual audio intent classification. 23K train examples. +columns: + - name: utt + type: text + - name: audio + type: audio + - name: intent_idx + type: category + - name: scenario + type: category +output_features: + - name: intent_idx + type: category diff --git a/ludwig/datasets/configs/spotify_tracks.yaml b/ludwig/datasets/configs/spotify_tracks.yaml new file mode 100644 index 0000000..d203362 --- /dev/null +++ b/ludwig/datasets/configs/spotify_tracks.yaml @@ -0,0 +1,24 @@ +version: 1.0 +name: spotify_tracks +huggingface_dataset_id: maharshipandya/spotify-tracks-dataset +loader: hugging_face.HFLoader +description: | + Spotify tracks: predict genre from audio features. 114K examples. +columns: + - name: track_name + type: text + - name: artists + type: text + - name: popularity + type: number + - name: danceability + type: number + - name: energy + type: number + - name: valence + type: number + - name: track_genre + type: category +output_features: + - name: track_genre + type: category diff --git a/ludwig/datasets/configs/squad.yaml b/ludwig/datasets/configs/squad.yaml new file mode 100644 index 0000000..c44cbd1 --- /dev/null +++ b/ludwig/datasets/configs/squad.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: squad +huggingface_dataset_id: rajpurkar/squad +loader: qa_loader.SquadLoader +description: | + SQuAD v1.1 extractive QA. Context + question → answer text. 87K examples. +columns: + - name: context + type: text + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/squad_v2.yaml b/ludwig/datasets/configs/squad_v2.yaml new file mode 100644 index 0000000..3f1968d --- /dev/null +++ b/ludwig/datasets/configs/squad_v2.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: squad_v2 +huggingface_dataset_id: rajpurkar/squad_v2 +loader: qa_loader.SquadV2Loader +description: | + SQuAD v2 extractive QA with unanswerable questions. 130K examples. +columns: + - name: context + type: text + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/sst2.yaml b/ludwig/datasets/configs/sst2.yaml new file mode 100644 index 0000000..f49d7b4 --- /dev/null +++ b/ludwig/datasets/configs/sst2.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: sst2 +huggingface_dataset_id: stanfordnlp/sst2 +loader: hugging_face.HFLoader +description: | + The SST2 dataset (Stanford Sentiment Treebank, binary). + + Loaded from HuggingFace (stanfordnlp/sst2) since the original Stanford + download URL (nlp.stanford.edu) is no longer publicly accessible. + + Labels: 0=negative, 1=positive. +columns: + - name: sentence + type: text + - name: label + type: binary +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/sst2_hf.yaml b/ludwig/datasets/configs/sst2_hf.yaml new file mode 100644 index 0000000..5d8df05 --- /dev/null +++ b/ludwig/datasets/configs/sst2_hf.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sst2_hf +huggingface_dataset_id: stanfordnlp/sst2 +loader: hugging_face.HFLoader +description: | + Stanford Sentiment Treebank 2-class (HF canonical version) +columns: + - name: idx + type: category + - name: sentence + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/sst3.yaml b/ludwig/datasets/configs/sst3.yaml new file mode 100644 index 0000000..b202386 --- /dev/null +++ b/ludwig/datasets/configs/sst3.yaml @@ -0,0 +1,21 @@ +version: 1.0 +name: sst3 +huggingface_dataset_id: tweet_eval +huggingface_subsample: sentiment +loader: hugging_face.HFLoader +description: | + Three-class sentiment dataset (negative/neutral/positive). + + Previously used the Stanford SST3 split (Stanford URL is no longer accessible). + Now uses tweet_eval/sentiment from HuggingFace as a functionally equivalent + 3-class sentiment benchmark. + + Labels: 0=negative, 1=neutral, 2=positive. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/sst5.yaml b/ludwig/datasets/configs/sst5.yaml new file mode 100644 index 0000000..024e235 --- /dev/null +++ b/ludwig/datasets/configs/sst5.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: sst5 +huggingface_dataset_id: SetFit/sst5 +loader: hugging_face.HFLoader +description: | + The SST5 dataset (Stanford Sentiment Treebank, 5-class). + + Loaded from HuggingFace (SetFit/sst5) since the original Stanford + download URL (nlp.stanford.edu) is no longer publicly accessible. + + Labels: 0=very negative, 1=negative, 2=neutral, 3=positive, 4=very positive. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/sst5_setfit.yaml b/ludwig/datasets/configs/sst5_setfit.yaml new file mode 100644 index 0000000..3c7a8f2 --- /dev/null +++ b/ludwig/datasets/configs/sst5_setfit.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sst5_setfit +huggingface_dataset_id: SetFit/sst5 +loader: hugging_face.HFLoader +description: | + SST-5 fine-grained sentiment (SetFit version); 5 classes +columns: + - name: text + type: text + - name: label + type: category + - name: label_text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/stackoverflow_posts.yaml b/ludwig/datasets/configs/stackoverflow_posts.yaml new file mode 100644 index 0000000..16be560 --- /dev/null +++ b/ludwig/datasets/configs/stackoverflow_posts.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: stackoverflow_posts +huggingface_dataset_id: mikex86/stackoverflow-posts +loader: hugging_face.HFLoader +description: | + Stack Overflow posts: predict question score from title and body. 58M train examples. +columns: + - name: Title + type: text + - name: Body + type: text + - name: Score + type: number +output_features: + - name: Score + type: number diff --git a/ludwig/datasets/configs/stanford_cars.yaml b/ludwig/datasets/configs/stanford_cars.yaml new file mode 100644 index 0000000..15e095a --- /dev/null +++ b/ludwig/datasets/configs/stanford_cars.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: stanford_cars +huggingface_dataset_id: tanganke/stanford_cars +loader: hugging_face.HFLoader +description: | + Stanford Cars; 196-class fine-grained car make/model/year classification +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/stocks_daily_price.yaml b/ludwig/datasets/configs/stocks_daily_price.yaml new file mode 100644 index 0000000..ed9a216 --- /dev/null +++ b/ludwig/datasets/configs/stocks_daily_price.yaml @@ -0,0 +1,22 @@ +version: 1.0 +name: stocks_daily_price +huggingface_dataset_id: paperswithbacktest/Stocks-Daily-Price +loader: hugging_face.HFLoader +description: | + Daily stock OHLCV: predict close price from symbol and features. 25M examples. +columns: + - name: symbol + type: category + - name: open + type: number + - name: high + type: number + - name: low + type: number + - name: volume + type: number + - name: close + type: number +output_features: + - name: close + type: number diff --git a/ludwig/datasets/configs/sts12.yaml b/ludwig/datasets/configs/sts12.yaml new file mode 100644 index 0000000..68f2112 --- /dev/null +++ b/ludwig/datasets/configs/sts12.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sts12 +huggingface_dataset_id: mteb/sts12-sts +loader: hugging_face.HFLoader +description: | + STS 2012: semantic textual similarity benchmark. 2234 train examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts13.yaml b/ludwig/datasets/configs/sts13.yaml new file mode 100644 index 0000000..17629ef --- /dev/null +++ b/ludwig/datasets/configs/sts13.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sts13 +huggingface_dataset_id: mteb/sts13-sts +loader: hugging_face.HFLoader +description: | + STS 2013: semantic textual similarity. 1500 test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts14.yaml b/ludwig/datasets/configs/sts14.yaml new file mode 100644 index 0000000..e8b4986 --- /dev/null +++ b/ludwig/datasets/configs/sts14.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sts14 +huggingface_dataset_id: mteb/sts14-sts +loader: hugging_face.HFLoader +description: | + STS 2014: semantic textual similarity. 3750 test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts15.yaml b/ludwig/datasets/configs/sts15.yaml new file mode 100644 index 0000000..1ad86f4 --- /dev/null +++ b/ludwig/datasets/configs/sts15.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sts15 +huggingface_dataset_id: mteb/sts15-sts +loader: hugging_face.HFLoader +description: | + STS 2015: semantic textual similarity. 3000 test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts16.yaml b/ludwig/datasets/configs/sts16.yaml new file mode 100644 index 0000000..9a567a7 --- /dev/null +++ b/ludwig/datasets/configs/sts16.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: sts16 +huggingface_dataset_id: mteb/sts16-sts +loader: hugging_face.HFLoader +description: | + STS 2016: semantic textual similarity. 1186 test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts17.yaml b/ludwig/datasets/configs/sts17.yaml new file mode 100644 index 0000000..53a939d --- /dev/null +++ b/ludwig/datasets/configs/sts17.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: sts17 +huggingface_dataset_id: mteb/sts17-crosslingual-sts +loader: hugging_face.HFLoader +description: | + STS 2017: cross-lingual STS. 5346 test examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number + - name: lang + type: category +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts22.yaml b/ludwig/datasets/configs/sts22.yaml new file mode 100644 index 0000000..265df62 --- /dev/null +++ b/ludwig/datasets/configs/sts22.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: sts22 +huggingface_dataset_id: mteb/sts22-crosslingual-sts +loader: hugging_face.HFLoader +description: | + STS 2022: cross-lingual semantic textual similarity. 4.6K train examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number + - name: lang + type: category +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/sts_benchmark.yaml b/ludwig/datasets/configs/sts_benchmark.yaml new file mode 100644 index 0000000..cb05472 --- /dev/null +++ b/ludwig/datasets/configs/sts_benchmark.yaml @@ -0,0 +1,24 @@ +version: 1.0 +name: sts_benchmark +huggingface_dataset_id: mteb/stsbenchmark-sts +loader: hugging_face.HFLoader +description: | + STS Benchmark; sentence pair semantic similarity scoring +columns: + - name: genre + type: text + - name: dataset + type: text + - name: year + type: text + - name: sid + type: text + - name: score + type: number + - name: sentence1 + type: text + - name: sentence2 + type: text +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/stsb.yaml b/ludwig/datasets/configs/stsb.yaml new file mode 100644 index 0000000..05e17bc --- /dev/null +++ b/ludwig/datasets/configs/stsb.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: stsb +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: stsb +loader: hugging_face.HFLoader +description: | + Semantic Textual Similarity Benchmark; similarity score 0-5 +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: number + - name: idx + type: category +output_features: + - name: label + type: number diff --git a/ludwig/datasets/configs/stsb_de.yaml b/ludwig/datasets/configs/stsb_de.yaml new file mode 100644 index 0000000..71c0286 --- /dev/null +++ b/ludwig/datasets/configs/stsb_de.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: stsb_de +huggingface_dataset_id: PhilipMay/stsb_multi_mt +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + STS Benchmark German (machine translated). 5749 train examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: similarity_score + type: number +output_features: + - name: similarity_score + type: number diff --git a/ludwig/datasets/configs/stsb_sentencetransformers.yaml b/ludwig/datasets/configs/stsb_sentencetransformers.yaml new file mode 100644 index 0000000..781728f --- /dev/null +++ b/ludwig/datasets/configs/stsb_sentencetransformers.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: stsb_sentencetransformers +huggingface_dataset_id: sentence-transformers/stsb +loader: hugging_face.HFLoader +description: | + STS Benchmark: semantic textual similarity scoring (0-5 scale). 5.7K train examples. +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: score + type: number +output_features: + - name: score + type: number diff --git a/ludwig/datasets/configs/student_performance.yaml b/ludwig/datasets/configs/student_performance.yaml new file mode 100644 index 0000000..dc4c0a6 --- /dev/null +++ b/ludwig/datasets/configs/student_performance.yaml @@ -0,0 +1,27 @@ +version: 1.0 +name: student_performance +huggingface_dataset_id: mstz/student_performance +loader: hugging_face.HFLoader +description: 'Student performance regression; predict final grade from demographics + + ' +columns: +- name: is_male + type: binary +- name: ethnicity + type: text +- name: parental_level_of_education + type: category +- name: has_standard_lunch + type: binary +- name: has_completed_preparation_test + type: binary +- name: reading_score + type: category +- name: writing_score + type: category +- name: has_passed_math_exam + type: category +output_features: +- name: has_passed_math_exam + type: binary diff --git a/ludwig/datasets/configs/sun397.yaml b/ludwig/datasets/configs/sun397.yaml new file mode 100644 index 0000000..fdc7e84 --- /dev/null +++ b/ludwig/datasets/configs/sun397.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: sun397 +huggingface_dataset_id: tanganke/sun397 +loader: hugging_face.HFLoader +description: | + SUN397; scene understanding; 397 scene categories +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/superglue_rte.yaml b/ludwig/datasets/configs/superglue_rte.yaml new file mode 100644 index 0000000..8a7faaa --- /dev/null +++ b/ludwig/datasets/configs/superglue_rte.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: superglue_rte +huggingface_dataset_id: aps/super_glue +huggingface_subsample: rte +loader: hugging_face.HFLoader +description: | + SuperGLUE version of Recognizing Textual Entailment +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: idx + type: category + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/svhn.yaml b/ludwig/datasets/configs/svhn.yaml new file mode 100644 index 0000000..71c2f06 --- /dev/null +++ b/ludwig/datasets/configs/svhn.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: svhn +huggingface_dataset_id: ufldl-stanford/svhn +huggingface_subsample: cropped_digits +loader: hugging_face.HFLoader +description: | + SVHN; Street View House Numbers digit classification +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/synthetic_fraud.yaml b/ludwig/datasets/configs/synthetic_fraud.yaml new file mode 100644 index 0000000..4cb9d90 --- /dev/null +++ b/ludwig/datasets/configs/synthetic_fraud.yaml @@ -0,0 +1,35 @@ +version: 1.0 +name: synthetic_fraud +kaggle_dataset_id: ealaxi/paysim1 +archive_filenames: paysim1.zip +sha256: + paysim1.zip: f7eef9ffad5cfa64a034143a5c9b30491d189420b273d5ad5723ca40b596613d +description: | + The Synthetic Financial Datasets For Fraud Detection dataset. + https://www.kaggle.com/ealaxi/paysim1 +columns: + - name: step + type: category + - name: type + type: category + - name: amount + type: number + - name: nameOrig + type: category + - name: oldbalanceOrg + type: number + - name: newbalanceOrig + type: number + - name: nameDest + type: category + - name: oldbalanceDest + type: number + - name: newbalanceDest + type: number + - name: isFraud + type: binary + - name: isFlaggedFraud + type: binary +output_features: + - name: isFraud + type: binary diff --git a/ludwig/datasets/configs/synthia.yaml b/ludwig/datasets/configs/synthia.yaml new file mode 100644 index 0000000..96bb0a3 --- /dev/null +++ b/ludwig/datasets/configs/synthia.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: synthia +huggingface_dataset_id: agicommies/synthia +loader: hugging_face.HFLoader +description: | + SYNTHIA: synthetic instructional examples across 20 academic fields. 1.2M examples. +columns: + - name: field + type: category + - name: subject + type: text + - name: explanation + type: text +output_features: + - name: field + type: category diff --git a/ludwig/datasets/configs/tadabur.yaml b/ludwig/datasets/configs/tadabur.yaml new file mode 100644 index 0000000..b4abad4 --- /dev/null +++ b/ludwig/datasets/configs/tadabur.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: tadabur +huggingface_dataset_id: FaisaI/tadabur +loader: hugging_face.HFLoader +description: | + Tadabur: Arabic Quran audio with reciter, surah, and ayah classification. 409K examples. +columns: + - name: audio + type: audio + - name: reciter_id + type: category + - name: surah_id + type: category +output_features: + - name: reciter_id + type: category diff --git a/ludwig/datasets/configs/taix_ray.yaml b/ludwig/datasets/configs/taix_ray.yaml new file mode 100644 index 0000000..d644318 --- /dev/null +++ b/ludwig/datasets/configs/taix_ray.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: taix_ray +huggingface_dataset_id: TLAIM/TAIX-Ray +loader: hugging_face.HFLoader +description: | + TAIX-Ray: thoracic X-ray study - predict patient sex from age and other metadata. 137K train examples. +columns: + - name: Age + type: number + - name: Sex + type: category +output_features: + - name: Sex + type: category diff --git a/ludwig/datasets/configs/talkingdata_adtrack_fraud.yaml b/ludwig/datasets/configs/talkingdata_adtrack_fraud.yaml new file mode 100644 index 0000000..0db7bae --- /dev/null +++ b/ludwig/datasets/configs/talkingdata_adtrack_fraud.yaml @@ -0,0 +1,13 @@ +version: 1.0 +name: talkingdata_adtrack_fraud +kaggle_competition: talkingdata-adtracking-fraud-detection +archive_filenames: talkingdata-adtracking-fraud-detection.zip +sha256: + talkingdata-adtracking-fraud-detection.zip: 4441bea984e936db153aba30627b222cb1685021efb887bd22d78771fb793735 +train_filenames: train.csv +description: | + TalkingData AdTracking Fraud Detection Challenge. + https://www.kaggle.com/competitions/talkingdata-adtracking-fraud-detection/overview +output_features: + - name: is_attributed + type: binary diff --git a/ludwig/datasets/configs/telco_customer_churn.yaml b/ludwig/datasets/configs/telco_customer_churn.yaml new file mode 100644 index 0000000..2883c00 --- /dev/null +++ b/ludwig/datasets/configs/telco_customer_churn.yaml @@ -0,0 +1,58 @@ +version: 1.0 +name: telco_customer_churn +kaggle_dataset_id: blastchar/telco-customer-churn +archive_filenames: telco-customer-churn.zip +dataset_filenames: WA_Fn-UseC_-Telco-Customer-Churn.csv +sha256: + telco-customer-churn.zip: cf7e6dcd8a238ecaa841a7d133142525453992d8d5e3ef6d1e5f0d359e7bf444 +description: | + The Telco customer churn data contains information about a fictional telco company + that provided home phone and Internet services to customers. Each row represents a + customer, each column contains customer’s attributes described on the column Metadata. + https://www.kaggle.com/datasets/blastchar/telco-customer-churn +columns: + - name: customerID + type: category + - name: gender + type: binary + - name: SeniorCitizen + type: binary + - name: Partner + type: binary + - name: Dependents + type: binary + - name: tenure + type: number + - name: PhoneService + type: binary + - name: MultipleLines + type: category + - name: InternetService + type: category + - name: OnlineSecurity + type: category + - name: OnlineBackup + type: category + - name: DeviceProtection + type: category + - name: TechSupport + type: category + - name: StreamingTV + type: category + - name: StreamingMovies + type: category + - name: Contract + type: category + - name: PaperlessBilling + type: binary + - name: PaymentMethod + type: category + - name: MonthlyCharges + type: number + - name: TotalCharges + type: number + - name: Churn + type: binary +output_features: + - name: Churn + type: binary diff --git a/ludwig/datasets/configs/temperature.yaml b/ludwig/datasets/configs/temperature.yaml new file mode 100644 index 0000000..70a2dce --- /dev/null +++ b/ludwig/datasets/configs/temperature.yaml @@ -0,0 +1,88 @@ +version: 1.0 +name: temperature +kaggle_dataset_id: selfishgene/historical-hourly-weather-data +archive_filenames: historical-hourly-weather-data.zip +sha256: + historical-hourly-weather-data.zip: db40ffce67318f366115b82a6f693d6dc82c808f23514e2ddae56c0434f606d7 +dataset_filenames: temperature.csv +description: | + Hourly temperature dataset from Kaggle + https://www.kaggle.com/selfishgene/historical-hourly-weather-data +columns: + - name: datetime + type: date + - name: Vancouver + type: number + - name: Portland + type: number + - name: San Francisco + type: number + - name: Seattle + type: number + - name: Los Angeles + type: number + - name: San Diego + type: number + - name: Las Vegas + type: number + - name: Phoenix + type: number + - name: Albuquerque + type: number + - name: Denver + type: number + - name: San Antonio + type: number + - name: Dallas + type: number + - name: Houston + type: number + - name: Kansas City + type: number + - name: Minneapolis + type: number + - name: Saint Louis + type: number + - name: Chicago + type: number + - name: Nashville + type: number + - name: Indianapolis + type: number + - name: Atlanta + type: number + - name: Detroit + type: number + - name: Jacksonville + type: number + - name: Charlotte + type: number + - name: Miami + type: number + - name: Pittsburgh + type: number + - name: Toronto + type: number + - name: Philadelphia + type: number + - name: New York + type: number + - name: Montreal + type: number + - name: Boston + type: number + - name: Beersheba + type: number + - name: Tel Aviv District + type: number + - name: Eilat + type: number + - name: Haifa + type: number + - name: Nahariyya + type: number + - name: Jerusalem + type: number +output_features: + - name: San Francisco + type: number diff --git a/ludwig/datasets/configs/textvqa.yaml b/ludwig/datasets/configs/textvqa.yaml new file mode 100644 index 0000000..760ab0a --- /dev/null +++ b/ludwig/datasets/configs/textvqa.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: textvqa +huggingface_dataset_id: HuggingFaceM4/the_cauldron +huggingface_subsample: textvqa +loader: vqa_loader.TextVQALoader +description: | + TextVQA: image with text + question → answer requiring reading the text. +columns: + - name: image_path + type: image + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/tiny_imagenet.yaml b/ludwig/datasets/configs/tiny_imagenet.yaml new file mode 100644 index 0000000..1ddccfb --- /dev/null +++ b/ludwig/datasets/configs/tiny_imagenet.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: tiny_imagenet +huggingface_dataset_id: zh-plus/tiny-imagenet +loader: hugging_face.HFLoader +description: | + Tiny ImageNet; 200-class 64x64 subset of ImageNet +columns: + - name: image + type: image + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/titanic.yaml b/ludwig/datasets/configs/titanic.yaml new file mode 100644 index 0000000..f9e23b7 --- /dev/null +++ b/ludwig/datasets/configs/titanic.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: titanic +kaggle_competition: titanic +archive_filenames: titanic.zip +sha256: + titanic.zip: bb1bda464cc6819d412b41d34be69fd89d26b372dc24c09421c3dbca1b0dbe9f +train_filenames: train.csv +test_filenames: test.csv +description: | + The Titanic dataset: use machine learning to create a model + that predicts which passengers survived the Titanic shipwreck. + https://www.kaggle.com/c/titanic +output_features: + - name: Survived + type: binary diff --git a/ludwig/datasets/configs/tobacco_document.yaml b/ludwig/datasets/configs/tobacco_document.yaml new file mode 100644 index 0000000..232c969 --- /dev/null +++ b/ludwig/datasets/configs/tobacco_document.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: tobacco_document +huggingface_dataset_id: anirudh1112/corrected-tobacco-dataset-with-ocr +loader: hugging_face.HFLoader +description: | + Tobacco document image classification with OCR text. 2.2K examples. +columns: + - name: image + type: image + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tofu.yaml b/ludwig/datasets/configs/tofu.yaml new file mode 100644 index 0000000..4683b0e --- /dev/null +++ b/ludwig/datasets/configs/tofu.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tofu +huggingface_dataset_id: locuslab/TOFU +huggingface_subsample: full +loader: hugging_face.HFLoader +description: | + TOFU: Fictitious Unlearning. Fictional author QA dataset for LLM unlearning research. 4K train examples. +columns: + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/toxic_chat.yaml b/ludwig/datasets/configs/toxic_chat.yaml new file mode 100644 index 0000000..a66f78b --- /dev/null +++ b/ludwig/datasets/configs/toxic_chat.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: toxic_chat +huggingface_dataset_id: lmsys/toxic-chat +huggingface_subsample: toxicchat0124 +loader: hugging_face.HFLoader +description: | + LMSys ToxicChat: human toxicity annotation for LLM conversations. +columns: + - name: user_input + type: text + - name: model_output + type: text + - name: toxicity + type: binary +output_features: + - name: toxicity + type: binary diff --git a/ludwig/datasets/configs/trivia_qa.yaml b/ludwig/datasets/configs/trivia_qa.yaml new file mode 100644 index 0000000..3d5ba4f --- /dev/null +++ b/ludwig/datasets/configs/trivia_qa.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: trivia_qa +huggingface_dataset_id: mandarjoshi/trivia_qa +huggingface_subsample: rc +loader: qa_loader.TriviaQALoader +description: | + TriviaQA reading comprehension. Question → answer. +columns: + - name: question + type: text + - name: answer_text + type: text +output_features: + - name: answer_text + type: text diff --git a/ludwig/datasets/configs/truthful_qa.yaml b/ludwig/datasets/configs/truthful_qa.yaml new file mode 100644 index 0000000..23a270b --- /dev/null +++ b/ludwig/datasets/configs/truthful_qa.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: truthful_qa +huggingface_dataset_id: truthfulqa/truthful_qa +huggingface_subsample: generation +loader: misc_loaders.TruthfulQALoader +description: | + TruthfulQA: question → best truthful answer. 817 questions. +columns: + - name: question + type: text + - name: best_answer + type: text + - name: category + type: category +output_features: + - name: best_answer + type: text diff --git a/ludwig/datasets/configs/tweet_eval_emoji.yaml b/ludwig/datasets/configs/tweet_eval_emoji.yaml new file mode 100644 index 0000000..1f31102 --- /dev/null +++ b/ludwig/datasets/configs/tweet_eval_emoji.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweet_eval_emoji +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: emoji +loader: hugging_face.HFLoader +description: | + TweetEval emoji: predict emoji from tweet text. 20-class classification. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweet_sentiment_extraction.yaml b/ludwig/datasets/configs/tweet_sentiment_extraction.yaml new file mode 100644 index 0000000..19510a3 --- /dev/null +++ b/ludwig/datasets/configs/tweet_sentiment_extraction.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: tweet_sentiment_extraction +huggingface_dataset_id: mteb/tweet_sentiment_extraction +loader: hugging_face.HFLoader +description: | + Tweet sentiment extraction; positive/negative/neutral +columns: + - name: id + type: text + - name: text + type: text + - name: label + type: category + - name: label_text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweeteval_emotion.yaml b/ludwig/datasets/configs/tweeteval_emotion.yaml new file mode 100644 index 0000000..4cca893 --- /dev/null +++ b/ludwig/datasets/configs/tweeteval_emotion.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweeteval_emotion +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: emotion +loader: hugging_face.HFLoader +description: | + TweetEval emotion classification; 4 classes +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweeteval_hate.yaml b/ludwig/datasets/configs/tweeteval_hate.yaml new file mode 100644 index 0000000..4abec5f --- /dev/null +++ b/ludwig/datasets/configs/tweeteval_hate.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweeteval_hate +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: hate +loader: hugging_face.HFLoader +description: | + TweetEval hate speech detection +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweeteval_irony.yaml b/ludwig/datasets/configs/tweeteval_irony.yaml new file mode 100644 index 0000000..7ba538e --- /dev/null +++ b/ludwig/datasets/configs/tweeteval_irony.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweeteval_irony +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: irony +loader: hugging_face.HFLoader +description: | + TweetEval irony detection +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweeteval_offensive.yaml b/ludwig/datasets/configs/tweeteval_offensive.yaml new file mode 100644 index 0000000..9a98b3e --- /dev/null +++ b/ludwig/datasets/configs/tweeteval_offensive.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweeteval_offensive +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: offensive +loader: hugging_face.HFLoader +description: | + TweetEval offensive language detection +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweeteval_sentiment.yaml b/ludwig/datasets/configs/tweeteval_sentiment.yaml new file mode 100644 index 0000000..aefb077 --- /dev/null +++ b/ludwig/datasets/configs/tweeteval_sentiment.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweeteval_sentiment +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: sentiment +loader: hugging_face.HFLoader +description: | + TweetEval sentiment; positive/negative/neutral tweet classification +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/tweeteval_stance.yaml b/ludwig/datasets/configs/tweeteval_stance.yaml new file mode 100644 index 0000000..f2d9646 --- /dev/null +++ b/ludwig/datasets/configs/tweeteval_stance.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: tweeteval_stance +huggingface_dataset_id: cardiffnlp/tweet_eval +huggingface_subsample: stance_abortion +loader: hugging_face.HFLoader +description: | + TweetEval stance detection; against/in favor/neutral +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/twitter_bots.yaml b/ludwig/datasets/configs/twitter_bots.yaml new file mode 100644 index 0000000..6ee654f --- /dev/null +++ b/ludwig/datasets/configs/twitter_bots.yaml @@ -0,0 +1,59 @@ +version: 1.0 +name: twitter_bots +kaggle_dataset_id: danieltreiman/twitter-human-bots-dataset +archive_filenames: twitter-human-bots-dataset.zip +dataset_filenames: twitter_human_bots_dataset.csv +sha256: + twitter-human-bots-dataset.zip: 16ffaad719ebb9688231844a80f92901c5efb1ff96eafeb869dc5de07b323cdd +preserve_paths: + - profile_images + - profile_background_images +description: | + A dataset for Twitter Bot account detection. + https://www.kaggle.com/datasets/davidmartngutirrez/twitter-bots-accounts +columns: + - name: created_at + type: date + - name: default_profile + type: binary + - name: default_profile_image + type: binary + - name: description + type: text + - name: favourites_count + type: number + - name: followers_count + type: number + - name: friends_count + type: number + - name: geo_enabled + type: binary + - name: id + type: category + - name: lang + type: category + - name: location + type: category + - name: profile_background_image_url + type: category + - name: profile_image_url + type: category + - name: screen_name + type: category + - name: statuses_count + type: number + - name: verified + type: binary + - name: average_tweets_per_day + type: number + - name: account_age_days + type: number + - name: account_type + type: category + - name: profile_image_path + type: image + - name: profile_background_image_path + type: image +output_features: + - name: account_type + type: binary diff --git a/ludwig/datasets/configs/twitter_financial_news_topic.yaml b/ludwig/datasets/configs/twitter_financial_news_topic.yaml new file mode 100644 index 0000000..1d34fd9 --- /dev/null +++ b/ludwig/datasets/configs/twitter_financial_news_topic.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: twitter_financial_news_topic +huggingface_dataset_id: zeroshot/twitter-financial-news-topic +loader: hugging_face.HFLoader +description: | + Twitter financial news 20-class topic classification. 17K train examples. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/universal_dependencies.yaml b/ludwig/datasets/configs/universal_dependencies.yaml new file mode 100644 index 0000000..99cd86a --- /dev/null +++ b/ludwig/datasets/configs/universal_dependencies.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: universal_dependencies +huggingface_dataset_id: commul/universal_dependencies +huggingface_subsample: en_ewt +loader: hugging_face.HFLoader +description: | + Universal Dependencies English EWT: POS tagging with UPOS tags. 12K train sentences. +columns: + - name: text + type: text + - name: upos + type: sequence +output_features: + - name: upos + type: sequence diff --git a/ludwig/datasets/configs/voxpopuli.yaml b/ludwig/datasets/configs/voxpopuli.yaml new file mode 100644 index 0000000..b31a113 --- /dev/null +++ b/ludwig/datasets/configs/voxpopuli.yaml @@ -0,0 +1,28 @@ +version: 1.0 +name: voxpopuli +huggingface_dataset_id: facebook/voxpopuli +huggingface_subsample: en +loader: hugging_face.HFLoader +description: 'VoxPopuli; European Parliament speech ASR; 23 languages + + ' +columns: +- name: audio_id + type: text +- name: language + type: category +- name: audio + type: audio +- name: raw_text + type: text +- name: normalized_text + type: text +- name: gender + type: text +- name: speaker_id + type: text +- name: accent + type: text +output_features: +- name: normalized_text + type: text diff --git a/ludwig/datasets/configs/vqa_rad.yaml b/ludwig/datasets/configs/vqa_rad.yaml new file mode 100644 index 0000000..785500d --- /dev/null +++ b/ludwig/datasets/configs/vqa_rad.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: vqa_rad +huggingface_dataset_id: flaviagiammarino/vqa-rad +loader: hugging_face.HFLoader +description: | + VQA-RAD; radiology visual question answering +columns: + - name: image + type: image + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/vqav2.yaml b/ludwig/datasets/configs/vqav2.yaml new file mode 100644 index 0000000..fa5b1b5 --- /dev/null +++ b/ludwig/datasets/configs/vqav2.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: vqav2 +huggingface_dataset_id: HuggingFaceM4/the_cauldron +huggingface_subsample: vqav2 +loader: vqa_loader.VQAv2Loader +description: | + VQAv2: image + question → free-form answer. +columns: + - name: image_path + type: image + - name: question + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/vukuzenzele.yaml b/ludwig/datasets/configs/vukuzenzele.yaml new file mode 100644 index 0000000..fd0becb --- /dev/null +++ b/ludwig/datasets/configs/vukuzenzele.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: vukuzenzele +huggingface_dataset_id: dsfsi/vukuzenzele-sentence-aligned +huggingface_subsample: afr-eng +loader: hugging_face.HFLoader +description: | + Vukuzenzele: Afrikaans-English sentence pairs. 2.7K train examples. +columns: + - name: afr + type: text + - name: eng + type: text +output_features: + - name: eng + type: text diff --git a/ludwig/datasets/configs/walmart_recruiting.yaml b/ludwig/datasets/configs/walmart_recruiting.yaml new file mode 100644 index 0000000..c6feba5 --- /dev/null +++ b/ludwig/datasets/configs/walmart_recruiting.yaml @@ -0,0 +1,13 @@ +version: 1.0 +name: walmart_recruiting +kaggle_competition: walmart-recruiting-trip-type-classification +archive_filenames: walmart-recruiting-trip-type-classification.zip +sha256: + walmart-recruiting-trip-type-classification.zip: 4c0ad71034d0b907e018adcb00c7b2835d2c30abe770fde5ce8719d7b89d4de6 +train_filenames: train.csv +description: | + Walmart Recruiting: Trip Type Classification + https://www.kaggle.com/c/walmart-recruiting-trip-type-classification +output_features: + - name: TripType + type: category diff --git a/ludwig/datasets/configs/web_questions.yaml b/ludwig/datasets/configs/web_questions.yaml new file mode 100644 index 0000000..db60668 --- /dev/null +++ b/ludwig/datasets/configs/web_questions.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: web_questions +huggingface_dataset_id: stanfordnlp/web_questions +loader: hugging_face.HFLoader +description: | + WebQuestions: Freebase-grounded factoid QA. 3.8K train examples. +columns: + - name: question + type: text + - name: answers + type: text +output_features: + - name: answers + type: text diff --git a/ludwig/datasets/configs/wic.yaml b/ludwig/datasets/configs/wic.yaml new file mode 100644 index 0000000..f9267e5 --- /dev/null +++ b/ludwig/datasets/configs/wic.yaml @@ -0,0 +1,29 @@ +version: 1.0 +name: wic +huggingface_dataset_id: aps/super_glue +huggingface_subsample: wic +loader: hugging_face.HFLoader +description: | + Word-in-Context; word sense disambiguation as binary classification +columns: + - name: word + type: text + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: start1 + type: category + - name: start2 + type: category + - name: end1 + type: category + - name: end2 + type: category + - name: idx + type: category + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/wiki_qa.yaml b/ludwig/datasets/configs/wiki_qa.yaml new file mode 100644 index 0000000..1e25ef1 --- /dev/null +++ b/ludwig/datasets/configs/wiki_qa.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: wiki_qa +huggingface_dataset_id: microsoft/wiki_qa +loader: hugging_face.HFLoader +description: | + WikiQA: answer sentence selection (relevant/not-relevant). 20K train examples. +columns: + - name: question + type: text + - name: answer + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/wikiann.yaml b/ludwig/datasets/configs/wikiann.yaml new file mode 100644 index 0000000..934c463 --- /dev/null +++ b/ludwig/datasets/configs/wikiann.yaml @@ -0,0 +1,28 @@ +version: 1.0 +name: wikiann +huggingface_dataset_id: wikiann +huggingface_subsample: en +loader: wikiann.WikiANNLoader +description: | + WikiANN (PAN-X) Named Entity Recognition — English + + A multilingual NER dataset constructed from Wikipedia annotations. + Each sentence is represented as a list of tokens with corresponding IOB2 + NER tags: O (outside), B-PER/I-PER (person), B-ORG/I-ORG (organisation), + B-LOC/I-LOC (location). + + This dataset exercises Ludwig's sequence output feature: the model receives + a plain text sentence and must produce a space-separated sequence of NER + tag strings that aligns with the whitespace-tokenised input. + + Reference: + Massively Multilingual Transfer for NER (Pan et al., ACL 2019) + License: CC BY-SA 4.0 +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/wikiann_de.yaml b/ludwig/datasets/configs/wikiann_de.yaml new file mode 100644 index 0000000..ca2037f --- /dev/null +++ b/ludwig/datasets/configs/wikiann_de.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wikiann_de +huggingface_dataset_id: unimelb-nlp/wikiann +huggingface_subsample: de +loader: ner_loader.WikiannDeLoader +description: | + WikiANN German NER. IOB2 tags: O, B-PER, I-PER, B-ORG, I-ORG, B-LOC, I-LOC. +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/wikiann_en.yaml b/ludwig/datasets/configs/wikiann_en.yaml new file mode 100644 index 0000000..994950b --- /dev/null +++ b/ludwig/datasets/configs/wikiann_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wikiann_en +huggingface_dataset_id: unimelb-nlp/wikiann +huggingface_subsample: en +loader: ner_loader.WikiannEnLoader +description: | + WikiANN English NER. IOB2 tags: O, B-PER, I-PER, B-ORG, I-ORG, B-LOC, I-LOC. +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/wikiann_zh.yaml b/ludwig/datasets/configs/wikiann_zh.yaml new file mode 100644 index 0000000..264a6f7 --- /dev/null +++ b/ludwig/datasets/configs/wikiann_zh.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wikiann_zh +huggingface_dataset_id: unimelb-nlp/wikiann +huggingface_subsample: zh +loader: ner_loader.WikiannZhLoader +description: | + WikiANN Chinese NER. IOB2 tags: O, B-PER, I-PER, B-ORG, I-ORG, B-LOC, I-LOC. +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/wikiart.yaml b/ludwig/datasets/configs/wikiart.yaml new file mode 100644 index 0000000..a01ebe3 --- /dev/null +++ b/ludwig/datasets/configs/wikiart.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: wikiart +huggingface_dataset_id: huggan/wikiart +loader: hugging_face.HFLoader +description: | + WikiArt; artwork style classification across 27 art styles +columns: + - name: image + type: image + - name: artist + type: category + - name: genre + type: category + - name: style + type: category +output_features: + - name: artist + type: category diff --git a/ludwig/datasets/configs/wildchat.yaml b/ludwig/datasets/configs/wildchat.yaml new file mode 100644 index 0000000..ff28a49 --- /dev/null +++ b/ludwig/datasets/configs/wildchat.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: wildchat +huggingface_dataset_id: allenai/WildChat +loader: hugging_face.HFLoader +description: | + WildChat: 570K real ChatGPT user interactions; language + toxicity classification. +columns: + - name: model + type: text + - name: language + type: category + - name: toxic + type: binary +output_features: + - name: language + type: category diff --git a/ludwig/datasets/configs/wine_reviews.yaml b/ludwig/datasets/configs/wine_reviews.yaml new file mode 100644 index 0000000..5ca0f78 --- /dev/null +++ b/ludwig/datasets/configs/wine_reviews.yaml @@ -0,0 +1,30 @@ +version: 1.0 +name: wine_reviews +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/wine_reviews/train.csv + - https://automl-mm-bench.s3.amazonaws.com/wine_reviews/test.csv +sha256: + test.csv: c862d1af572659406ab39356a25c7d5e9b7c8570a89e069311fca1abb6bf1849 + train.csv: c54101bb07571a3df0723e93a5f7c48123dd792b316396db4404a04bcf1809cb +train_filenames: train.csv +test_filenames: test.csv +description: | + Wine Reviews + 130k wine reviews with variety, location, winery, price, and description + https://www.kaggle.com/datasets/zynicide/wine-reviews +columns: + - name: country + type: category + - name: description + type: text + - name: points + type: number + - name: price + type: number + - name: province + type: category + - name: variety + type: category +output_features: + - name: points + type: number diff --git a/ludwig/datasets/configs/winobias.yaml b/ludwig/datasets/configs/winobias.yaml new file mode 100644 index 0000000..8bfba2a --- /dev/null +++ b/ludwig/datasets/configs/winobias.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: winobias +huggingface_dataset_id: uclanlp/wino_bias +huggingface_subsample: type1_anti +loader: ner_loader.WinobiasLoader +description: | + WinoBias coreference gender bias. Sentence → label. +columns: + - name: sentence + type: text + - name: ner_tags + type: sequence +output_features: + - name: ner_tags + type: sequence diff --git a/ludwig/datasets/configs/winograd_schema.yaml b/ludwig/datasets/configs/winograd_schema.yaml new file mode 100644 index 0000000..4727ca7 --- /dev/null +++ b/ludwig/datasets/configs/winograd_schema.yaml @@ -0,0 +1,25 @@ +version: 1.0 +name: winograd_schema +huggingface_dataset_id: aps/super_glue +huggingface_subsample: wsc.fixed +loader: hugging_face.HFLoader +description: | + Winograd Schema Challenge; pronoun coreference resolution +columns: + - name: text + type: text + - name: span1_index + type: category + - name: span2_index + type: category + - name: span1_text + type: text + - name: span2_text + type: text + - name: idx + type: category + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/winogrande.yaml b/ludwig/datasets/configs/winogrande.yaml new file mode 100644 index 0000000..edd81d1 --- /dev/null +++ b/ludwig/datasets/configs/winogrande.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: winogrande +huggingface_dataset_id: allenai/winogrande +huggingface_subsample: winogrande_xl +loader: hugging_face.HFLoader +description: | + WinoGrande; large-scale Winograd schema challenge +columns: + - name: sentence + type: text + - name: option1 + type: text + - name: option2 + type: text + - name: answer + type: text +output_features: + - name: answer + type: text diff --git a/ludwig/datasets/configs/winogrande_hf.yaml b/ludwig/datasets/configs/winogrande_hf.yaml new file mode 100644 index 0000000..b2d501a --- /dev/null +++ b/ludwig/datasets/configs/winogrande_hf.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: winogrande_hf +huggingface_dataset_id: allenai/winogrande +huggingface_subsample: winogrande_xl +loader: hugging_face.HFLoader +description: | + WinoGrande (allenai): large-scale commonsense reasoning benchmark (273K examples). +columns: + - name: sentence + type: text + - name: option1 + type: text + - name: option2 + type: text + - name: answer + type: category +output_features: + - name: answer + type: category diff --git a/ludwig/datasets/configs/wmt14_de_en.yaml b/ludwig/datasets/configs/wmt14_de_en.yaml new file mode 100644 index 0000000..d05cc9e --- /dev/null +++ b/ludwig/datasets/configs/wmt14_de_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wmt14_de_en +huggingface_dataset_id: wmt/wmt14 +huggingface_subsample: de-en +loader: translation_loader.Wmt14DeEnLoader +description: | + WMT14 German-English news translation. ~4.5M sentence pairs. +columns: + - name: de + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/wmt15.yaml b/ludwig/datasets/configs/wmt15.yaml new file mode 100644 index 0000000..fa2d6c2 --- /dev/null +++ b/ludwig/datasets/configs/wmt15.yaml @@ -0,0 +1,13 @@ +version: 1.0 +name: wmt15 +kaggle_dataset_id: dhruvildave/en-fr-translation-dataset +archive_filenames: en-fr-translation-dataset.zip +sha256: + en-fr-translation-dataset.zip: 5fb911b327f2f36ea32315b4754f6aef95e6830562eec7054d31d614dd53d93c +description: | + French/English parallel texts for training translation models. + Over 22.5 million sentences in French and English. + https://www.kaggle.com/dhruvildave/en-fr-translation-dataset +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/wmt16_de_en.yaml b/ludwig/datasets/configs/wmt16_de_en.yaml new file mode 100644 index 0000000..a9d2457 --- /dev/null +++ b/ludwig/datasets/configs/wmt16_de_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wmt16_de_en +huggingface_dataset_id: wmt/wmt16 +huggingface_subsample: de-en +loader: translation_loader.Wmt16DeEnLoader +description: | + WMT16 German-English news translation. +columns: + - name: de + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/wmt19_de_en.yaml b/ludwig/datasets/configs/wmt19_de_en.yaml new file mode 100644 index 0000000..7695b84 --- /dev/null +++ b/ludwig/datasets/configs/wmt19_de_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wmt19_de_en +huggingface_dataset_id: wmt/wmt19 +huggingface_subsample: de-en +loader: translation_loader.Wmt19DeEnLoader +description: | + WMT19 German-English news translation. +columns: + - name: de + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/wmt_t2t_de_en.yaml b/ludwig/datasets/configs/wmt_t2t_de_en.yaml new file mode 100644 index 0000000..a16ae62 --- /dev/null +++ b/ludwig/datasets/configs/wmt_t2t_de_en.yaml @@ -0,0 +1,15 @@ +version: 1.0 +name: wmt_t2t_de_en +huggingface_dataset_id: wmt/wmt_t2t +huggingface_subsample: de-en +loader: translation_loader.Wmt14DeEnLoader +description: | + WMT T2T German-English news translation. 4.6M train sentence pairs. +columns: + - name: de + type: text + - name: en + type: text +output_features: + - name: en + type: text diff --git a/ludwig/datasets/configs/wnli.yaml b/ludwig/datasets/configs/wnli.yaml new file mode 100644 index 0000000..31f0783 --- /dev/null +++ b/ludwig/datasets/configs/wnli.yaml @@ -0,0 +1,19 @@ +version: 1.0 +name: wnli +huggingface_dataset_id: nyu-mll/glue +huggingface_subsample: wnli +loader: hugging_face.HFLoader +description: | + Winograd NLI; pronoun reference coreference classification +columns: + - name: sentence1 + type: text + - name: sentence2 + type: text + - name: label + type: category + - name: idx + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/women_clothing_review.yaml b/ludwig/datasets/configs/women_clothing_review.yaml new file mode 100644 index 0000000..dcf1000 --- /dev/null +++ b/ludwig/datasets/configs/women_clothing_review.yaml @@ -0,0 +1,38 @@ +version: 1.0 +name: women_clothing_review +download_urls: + - https://automl-mm-bench.s3.amazonaws.com/women_clothing_review/train.pq + - https://automl-mm-bench.s3.amazonaws.com/women_clothing_review/test.pq +sha256: + test.pq: 477de72fe7e672ef87e1eca00de312f55ba884a9b80fbd04fa79c0d0159e5593 + train.pq: 1b3d248397cee76a6ccff814560f29ae3d66eeb26a6e97ac0837e021629bc740 +train_filenames: train.pq +test_filenames: test.pq +description: | + Women's E-Commerce Clothing Reviews + 23,000 Customer Reviews and Ratings + https://www.kaggle.com/nicapotato/womens-ecommerce-clothing-reviews +columns: + - name: Clothing ID + type: category + - name: Age + type: number + - name: Title + type: text + - name: Review Text + type: text + - name: Rating + type: number + - name: Recommended IND + type: binary + - name: Positive Feedback Count + type: number + - name: Division Name + type: category + - name: Department Name + type: category + - name: Class Name + type: category +output_features: + - name: Rating + type: number diff --git a/ludwig/datasets/configs/world_speech_asr.yaml b/ludwig/datasets/configs/world_speech_asr.yaml new file mode 100644 index 0000000..58aa1b1 --- /dev/null +++ b/ludwig/datasets/configs/world_speech_asr.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: world_speech_asr +huggingface_dataset_id: disco-eth/WorldSpeech +huggingface_subsample: ca_fr +loader: hugging_face.HFLoader +description: | + WorldSpeech: multilingual audio quality with CER measurement. 1.2M train examples. +columns: + - name: audio + type: audio + - name: human_transcript + type: text + - name: cer + type: number +output_features: + - name: cer + type: number diff --git a/ludwig/datasets/configs/xnli.yaml b/ludwig/datasets/configs/xnli.yaml new file mode 100644 index 0000000..3cc61ce --- /dev/null +++ b/ludwig/datasets/configs/xnli.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: xnli +huggingface_dataset_id: facebook/xnli +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + XNLI cross-lingual NLI. Premise+hypothesis -> entailment/neutral/contradiction. +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/xnli_de.yaml b/ludwig/datasets/configs/xnli_de.yaml new file mode 100644 index 0000000..a45abe1 --- /dev/null +++ b/ludwig/datasets/configs/xnli_de.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: xnli_de +huggingface_dataset_id: xnli +huggingface_subsample: de +loader: hugging_face.HFLoader +description: | + XNLI German: cross-lingual natural language inference, German split. + Labels: entailment, neutral, contradiction. +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/xnli_en.yaml b/ludwig/datasets/configs/xnli_en.yaml new file mode 100644 index 0000000..a3044b4 --- /dev/null +++ b/ludwig/datasets/configs/xnli_en.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: xnli_en +huggingface_dataset_id: xnli +huggingface_subsample: en +loader: hugging_face.HFLoader +description: | + XNLI English: cross-lingual natural language inference, English split. + Labels: entailment, neutral, contradiction. +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/xnli_es.yaml b/ludwig/datasets/configs/xnli_es.yaml new file mode 100644 index 0000000..cd73664 --- /dev/null +++ b/ludwig/datasets/configs/xnli_es.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: xnli_es +huggingface_dataset_id: xnli +huggingface_subsample: es +loader: hugging_face.HFLoader +description: | + XNLI Spanish: cross-lingual natural language inference, Spanish split. +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/xnli_fr.yaml b/ludwig/datasets/configs/xnli_fr.yaml new file mode 100644 index 0000000..282715b --- /dev/null +++ b/ludwig/datasets/configs/xnli_fr.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: xnli_fr +huggingface_dataset_id: xnli +huggingface_subsample: fr +loader: hugging_face.HFLoader +description: | + XNLI French: cross-lingual natural language inference, French split. + Labels: entailment, neutral, contradiction. +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/xnli_zh.yaml b/ludwig/datasets/configs/xnli_zh.yaml new file mode 100644 index 0000000..485bb1d --- /dev/null +++ b/ludwig/datasets/configs/xnli_zh.yaml @@ -0,0 +1,17 @@ +version: 1.0 +name: xnli_zh +huggingface_dataset_id: xnli +huggingface_subsample: zh +loader: hugging_face.HFLoader +description: | + XNLI Chinese: cross-lingual natural language inference, Chinese split. +columns: + - name: premise + type: text + - name: hypothesis + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/xsum.yaml b/ludwig/datasets/configs/xsum.yaml new file mode 100644 index 0000000..9c1bab6 --- /dev/null +++ b/ludwig/datasets/configs/xsum.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: xsum +huggingface_dataset_id: EdinburghNLP/xsum +loader: hugging_face.HFLoader +description: | + XSum BBC news summarization. Document -> one-sentence summary. ~200K examples. +columns: + - name: document + type: text + - name: summary + type: text +output_features: + - name: summary + type: text diff --git a/ludwig/datasets/configs/xsum_hf.yaml b/ludwig/datasets/configs/xsum_hf.yaml new file mode 100644 index 0000000..6516ef7 --- /dev/null +++ b/ludwig/datasets/configs/xsum_hf.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: xsum_hf +huggingface_dataset_id: EdinburghNLP/xsum +loader: hugging_face.HFLoader +description: | + XSum (Edinburgh NLP): extreme summarization of BBC news articles (226K examples). +columns: + - name: document + type: text + - name: summary + type: text +output_features: + - name: summary + type: text diff --git a/ludwig/datasets/configs/yahoo_answers.yaml b/ludwig/datasets/configs/yahoo_answers.yaml new file mode 100644 index 0000000..9689f97 --- /dev/null +++ b/ludwig/datasets/configs/yahoo_answers.yaml @@ -0,0 +1,30 @@ +version: 1.0 +name: yahoo_answers +download_urls: https://s3.amazonaws.com/fast-ai-nlp/yahoo_answers_csv.tgz +sha256: + yahoo_answers_csv.tgz: 2d4277855faf8b35259009425fa8f7fe1888b5644b47165508942d000f4c96ae +train_filenames: yahoo_answers_csv/train.csv +test_filenames: yahoo_answers_csv/test.csv +description: | + The Yahoo Answers dataset + Details: + The 10 largest main categories from the Yahoo! Answers \ + Comprehensive Questions and Answers version 1.0 dataset. \ + Each class contains 140,000 training samples and 5,000 \ + testing samples. + Dataset source: + Character-level Convolutional Networks for Text Classification + Xiang Zhang et al., 2015 + https://arxiv.org/abs/1509.01626 +columns: + - name: label + type: category + - name: question_title + type: text + - name: question + type: text + - name: best_answer + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/yahoo_answers_topics.yaml b/ludwig/datasets/configs/yahoo_answers_topics.yaml new file mode 100644 index 0000000..690cced --- /dev/null +++ b/ludwig/datasets/configs/yahoo_answers_topics.yaml @@ -0,0 +1,18 @@ +version: 1.0 +name: yahoo_answers_topics +huggingface_dataset_id: community-datasets/yahoo_answers_topics +loader: hugging_face.HFLoader +description: | + Yahoo Answers 10-class topic classification. 1.4M train examples. +columns: + - name: question_title + type: text + - name: question_content + type: text + - name: best_answer + type: text + - name: topic + type: category +output_features: + - name: topic + type: category diff --git a/ludwig/datasets/configs/yelp_polarity.yaml b/ludwig/datasets/configs/yelp_polarity.yaml new file mode 100644 index 0000000..75c89a1 --- /dev/null +++ b/ludwig/datasets/configs/yelp_polarity.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: yelp_polarity +huggingface_dataset_id: fancyzhx/yelp_polarity +loader: hugging_face.HFLoader +description: | + Yelp binary sentiment classification: 1 (negative) or 2 (positive). 560K train examples. +columns: + - name: text + type: text + - name: label + type: category +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/yelp_review_full.yaml b/ludwig/datasets/configs/yelp_review_full.yaml new file mode 100644 index 0000000..c8cd926 --- /dev/null +++ b/ludwig/datasets/configs/yelp_review_full.yaml @@ -0,0 +1,14 @@ +version: 1.0 +name: yelp_review_full +huggingface_dataset_id: Yelp/yelp_review_full +loader: hugging_face.HFLoader +description: | + Yelp review 5-star rating classification +columns: + - name: label + type: category + - name: text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/yelp_review_polarity.yaml b/ludwig/datasets/configs/yelp_review_polarity.yaml new file mode 100644 index 0000000..8660f56 --- /dev/null +++ b/ludwig/datasets/configs/yelp_review_polarity.yaml @@ -0,0 +1,24 @@ +version: 1.0 +name: yelp_review_polarity +download_urls: https://s3.amazonaws.com/fast-ai-nlp/yelp_review_polarity_csv.tgz +sha256: + yelp_review_polarity_csv.tgz: 528f22e286cad085948acbc3bea7e58188416546b0e364d0ae4ca0ce666abe35 +train_filenames: yelp_review_polarity_csv/train.csv +test_filenames: yelp_review_polarity_csv/test.csv +description: | + The Yelp Polarity dataset + Details: + 1,569,264 samples from the Yelp Dataset Challenge 2015. + This subset has 280,000 training samples and 19,000 test samples + in each polarity. + Dataset source: + Character-level Convolutional Networks for Text Classification + Xiang Zhang et al., 2015 +columns: + - name: label + type: binary + - name: text + type: text +output_features: + - name: label + type: binary diff --git a/ludwig/datasets/configs/yelp_reviews.yaml b/ludwig/datasets/configs/yelp_reviews.yaml new file mode 100644 index 0000000..78b76d5 --- /dev/null +++ b/ludwig/datasets/configs/yelp_reviews.yaml @@ -0,0 +1,24 @@ +version: 1.0 +name: yelp_reviews +download_urls: https://s3.amazonaws.com/fast-ai-nlp/yelp_review_full_csv.tgz +sha256: + yelp_review_full_csv.tgz: 56006b0a17a370f1e366504b1f2c3e3754e4a3dda17d3e718a885c552869a559 +train_filenames: yelp_review_full_csv/train.csv +test_filenames: yelp_review_full_csv/test.csv +description: | + The Yelp Reviews dataset + Details: + 1,569,264 samples from the Yelp Dataset Challenge 2015. + This subset has 130,000 training samples and 10,000 + testing samples in each star rating. + Dataset source: + Character-level Convolutional Networks for Text Classification + Xiang Zhang et al., 2015 +columns: + - name: label + type: category + - name: text + type: text +output_features: + - name: label + type: category diff --git a/ludwig/datasets/configs/yosemite.yaml b/ludwig/datasets/configs/yosemite.yaml new file mode 100644 index 0000000..e0c65fa --- /dev/null +++ b/ludwig/datasets/configs/yosemite.yaml @@ -0,0 +1,16 @@ +version: 1.0 +name: yosemite +download_urls: https://raw.githubusercontent.com/ourownstory/neuralprophet-data/main/datasets_raw/yosemite_temps.csv +sha256: + yosemite_temps.csv: c0ec9f2cb4bbf0bc53f7bfd2e39f88ae21e43b7b8912b2d1eb8185055f9510e2 +description: | + Yosemite temperatures dataset. + As found in https://github.com/ourownstory/neural_prophet +columns: + - name: ds + type: date + - name: y + type: number +output_features: + - name: y + type: number diff --git a/ludwig/datasets/dataset_config.py b/ludwig/datasets/dataset_config.py new file mode 100644 index 0000000..10fb3b9 --- /dev/null +++ b/ludwig/datasets/dataset_config.py @@ -0,0 +1,105 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from dataclasses import dataclass, field + +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class DatasetFallbackMirror: + # Name of the mirror + name: str + + # List of paths to download from. Must map 1:1 to DatasetConfig.download_urls or to the archive_filenames + # that we get from Kaggle. + download_paths: str | list[str] + + +@dataclass_json +@dataclass +class DatasetConfig: + """The configuration of a Ludwig dataset.""" + + # The version of the dataset. + version: str + + # The name of the dataset. Make this a valid python module name, should not contain spaces or dashes. + name: str + + # The readable description of the dataset + description: str = "" + + # Fallback mirrors. Paths must be in local/remote filesystems. + fallback_mirrors: list[DatasetFallbackMirror] | None = None + + # Optional. The (suggested) output features for this dataset. Helps users discover new datasets and filter for + # relevance to a specific machine learning setting. + output_features: list[dict] = field(default_factory=list) + + # The kaggle competition this dataset belongs to, or None if this dataset is not hosted by a Kaggle competition. + kaggle_competition: str | None = None + + # The kaggle dataset ID, or None if this dataset if not hosted by Kaggle. + kaggle_dataset_id: str | None = None + + # The list of URLs to download. + download_urls: str | list[str] = field(default_factory=list) + + # The list of file archives which will be downloaded. If download_urls contains a filename with extension, for + # example https://domain.com/archive.zip, then archive_filenames does not need to be specified. + archive_filenames: str | list[str] = field(default_factory=list) + + # The names of files in the dataset (after extraction). Glob-style patterns are supported, see + # https://docs.python.org/3/library/glob.html + dataset_filenames: str | list[str] = field(default_factory=list) + + # If the dataset contains separate files for training, testing, or validation. Glob-style patterns are supported, + # see https://docs.python.org/3/library/glob.html + train_filenames: str | list[str] = field(default_factory=list) + validation_filenames: str | list[str] = field(default_factory=list) + test_filenames: str | list[str] = field(default_factory=list) + + # If the dataset contains additional referenced files or directories (ex. images or audio) list them here and they + # will be copied to the same location as the processed dataset. Glob-style patterns are supported, + # see https://docs.python.org/3/library/glob.html + preserve_paths: str | list[str] = field(default_factory=list) + + # Optionally verify integrity of the dataset by providing sha256 checksums for important files. Maps filename to + # sha256 digest. Use `sha256sum ` on linux, `shasum -a 256 ` on Mac to get checksums. + # If verification fails, loading the dataset will fail with a ValueError. + # If no sha256 digests are in the config, a warning is logged and the dataset will load without verification. + sha256: dict[str, str] = field(default_factory=dict) + + # List of column names, for datasets which do not have column names. If specified, will override the column names + # already present in the dataset. + columns: list[dict] = field(default_factory=list) + + # Optional dictionary which maps column name to column type. Column's will be converted to the requested type, or + # will be inferred from the dataset by default. + column_types: dict[str, str] = field(default_factory=dict) + + # The loader module and class to use, relative to ludwig.datasets.loaders. Only change this if the dataset requires + # processing which is not handled by the default loader. + loader: str = "dataset_loader.DatasetLoader" + + # The OpenML task ID, or None if this dataset is not hosted on OpenML. + openml_task_id: int | None = None + + # HuggingFace dataset path (e.g. "stanfordnlp/sst2"), used by HFLoader. + huggingface_dataset_id: str | None = None + + # HuggingFace dataset subset/configuration name (e.g. "sentiment" for tweet_eval). + huggingface_subsample: str | None = None diff --git a/ludwig/datasets/kaggle.py b/ludwig/datasets/kaggle.py new file mode 100644 index 0000000..1c636f3 --- /dev/null +++ b/ludwig/datasets/kaggle.py @@ -0,0 +1,46 @@ +import os +from contextlib import contextmanager + +from ludwig.utils.fs_utils import upload_output_directory + + +def create_kaggle_client(): + # Need to import here to prevent Kaggle from authenticating on import + from kaggle import api + + return api + + +@contextmanager +def update_env(**kwargs): + override_env = {k: v for k, v in kwargs.items() if v is not None} + old = os.environ.copy() + try: + os.environ.update(override_env) + yield + finally: + os.environ = old + + +def download_kaggle_dataset( + download_directory: str, + kaggle_dataset_id: str | None = None, + kaggle_competition: str | None = None, + kaggle_username: str | None = None, + kaggle_key: str | None = None, +): + """Download all files in a kaggle dataset. One of kaggle_dataset_id, + + If the user has not specified creds in the kaggle.json file we lookup the passed in username and the api key and + perform authentication. + """ + with update_env(KAGGLE_USERNAME=kaggle_username, KAGGLE_KEY=kaggle_key): + # Call authenticate explicitly to pick up new credentials if necessary + api = create_kaggle_client() + api.authenticate() + with upload_output_directory(download_directory) as (tmpdir, _): + if kaggle_competition: + api.competition_download_files(kaggle_competition, path=tmpdir) + else: + api.dataset_download_files(kaggle_dataset_id, path=tmpdir) + return [os.path.join(download_directory, f) for f in os.listdir(download_directory)] diff --git a/ludwig/datasets/loaders/__init__.py b/ludwig/datasets/loaders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/datasets/loaders/adult_census_income.py b/ludwig/datasets/loaders/adult_census_income.py new file mode 100644 index 0000000..ac4aa24 --- /dev/null +++ b/ludwig/datasets/loaders/adult_census_income.py @@ -0,0 +1,31 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class AdultCensusIncomeLoader(DatasetLoader): + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + if file_path.endswith(".test"): + # The test file contains the line "|1x3 Cross validator" before the CSV content. + return pd.read_csv(file_path, skiprows=1) + return super().load_file_to_dataframe(file_path) + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + processed_df["income"] = processed_df["income"].str.rstrip(".") + processed_df["income"] = processed_df["income"].str.strip() + return processed_df diff --git a/ludwig/datasets/loaders/agnews.py b/ludwig/datasets/loaders/agnews.py new file mode 100644 index 0000000..1aca890 --- /dev/null +++ b/ludwig/datasets/loaders/agnews.py @@ -0,0 +1,34 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class AGNewsLoader(DatasetLoader): + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + # Maps class_index to class name. + class_names = ["", "world", "sports", "business", "sci_tech"] + # Adds new column 'class' by mapping class indexes to strings. + processed_df["class"] = processed_df.class_index.apply(lambda i: class_names[i]) + # Agnews has no validation split, only train and test (0, 2). For convenience, we'll designate the first 5% of + # each class from the training set as the validation set. + val_set_n = int((len(processed_df) * 0.05) // len(class_names)) # rows from each class in validation set. + for ci in range(1, 5): + # For each class, reassign the first val_set_n rows of the training set to validation set. + train_rows = processed_df[(processed_df.split == 0) & (processed_df.class_index == ci)].index + processed_df.loc[train_rows[:val_set_n], "split"] = 1 + return processed_df diff --git a/ludwig/datasets/loaders/allstate_claims_severity.py b/ludwig/datasets/loaders/allstate_claims_severity.py new file mode 100644 index 0000000..ec8bfd2 --- /dev/null +++ b/ludwig/datasets/loaders/allstate_claims_severity.py @@ -0,0 +1,31 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class AllstateClaimsSeverityLoader(DatasetLoader): + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + if os.path.basename(file_path) == "train.csv": + # train.csv has been updated with quoted test rows at the end; don't load these, only load the original + # training set. + return pd.read_csv(file_path, nrows=188319) + if os.path.basename(file_path) == "test.csv": + # we limit the loaded rows for the same reason as the training set. + return pd.read_csv(file_path, nrows=125547) + super().load_file_to_dataframe(file_path) diff --git a/ludwig/datasets/loaders/california_housing.py b/ludwig/datasets/loaders/california_housing.py new file mode 100644 index 0000000..05af44c --- /dev/null +++ b/ludwig/datasets/loaders/california_housing.py @@ -0,0 +1,33 @@ +"""California Housing dataset loader. + +Wraps sklearn.datasets.fetch_california_housing to provide the standard ML regression benchmark through Ludwig's dataset +infrastructure. +""" + +import numpy as np +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class CaliforniaHousingLoader(DatasetLoader): + def load(self, kaggle_username=None, kaggle_key=None, split=False): + """Load California Housing directly from sklearn.""" + from sklearn.datasets import fetch_california_housing + + data = fetch_california_housing() + df = pd.DataFrame(data.data, columns=data.feature_names) + df["target"] = data.target + + # Add deterministic split column (70/15/15) + n = len(df) + rng = np.random.RandomState(42) + indices = rng.permutation(n) + splits = np.zeros(n, dtype=int) + splits[indices[int(n * 0.7) : int(n * 0.85)]] = 1 + splits[indices[int(n * 0.85) :]] = 2 + df["split"] = splits + + if split: + return self.split(df) + return df diff --git a/ludwig/datasets/loaders/camseq.py b/ludwig/datasets/loaders/camseq.py new file mode 100644 index 0000000..d7c72d7 --- /dev/null +++ b/ludwig/datasets/loaders/camseq.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023 Aizen Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader +from ludwig.utils.fs_utils import makedirs + + +class CamseqLoader(DatasetLoader): + def transform_files(self, file_paths: list[str]) -> list[str]: + if not os.path.exists(self.processed_dataset_dir): + os.makedirs(self.processed_dataset_dir) + + # move images and masks into separate directories + source_dir = self.raw_dataset_dir + images_dir = os.path.join(source_dir, "images") + masks_dir = os.path.join(source_dir, "masks") + makedirs(images_dir, exist_ok=True) + makedirs(masks_dir, exist_ok=True) + + data_files = [] + for f in os.listdir(source_dir): + if f.endswith("_L.png"): # masks + dest_file = os.path.join(masks_dir, f) + elif f.endswith(".png"): # images + dest_file = os.path.join(images_dir, f) + else: + continue + source_file = os.path.join(source_dir, f) + os.replace(source_file, dest_file) + data_files.append(dest_file) + + return super().transform_files(data_files) + + def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame: + """Creates a dataframe of image paths and mask paths.""" + images_dir = os.path.join(self.processed_dataset_dir, "images") + masks_dir = os.path.join(self.processed_dataset_dir, "masks") + images = [] + masks = [] + for f in os.listdir(images_dir): + images.append(os.path.join(images_dir, f)) + mask_f = f[:-4] + "_L.png" + masks.append(os.path.join(masks_dir, mask_f)) + + return pd.DataFrame({"image_path": images, "mask_path": masks}) diff --git a/ludwig/datasets/loaders/code_alpaca_loader.py b/ludwig/datasets/loaders/code_alpaca_loader.py new file mode 100644 index 0000000..f378ef9 --- /dev/null +++ b/ludwig/datasets/loaders/code_alpaca_loader.py @@ -0,0 +1,27 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class CodeAlpacaLoader(DatasetLoader): + """The Code Alpaca dataset.""" + + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + df = pd.read_json(file_path) + return df diff --git a/ludwig/datasets/loaders/code_loader.py b/ludwig/datasets/loaders/code_loader.py new file mode 100644 index 0000000..067355d --- /dev/null +++ b/ludwig/datasets/loaders/code_loader.py @@ -0,0 +1,68 @@ +"""Loaders for code/programming datasets where token lists need joining.""" + +from __future__ import annotations + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +def _join_tokens(val) -> str: + if hasattr(val, "__iter__") and not isinstance(val, str): + return " ".join(str(t) for t in val) + return str(val) + + +class CodeSearchNetLoader(HFLoader): + """CodeSearchNet — func_code_string + func_documentation_string.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # Use the pre-joined string columns if available + keep = [] + for col in ["func_code_string", "func_documentation_string", "language"]: + if col in df.columns: + keep.append(col) + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class CodeXGlueLoader(HFLoader): + """CodeXGlue code-to-text — code → docstring.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + if "code_tokens" in df.columns: + df["code"] = df["code_tokens"].apply(_join_tokens) + if "docstring_tokens" in df.columns: + df["docstring"] = df["docstring_tokens"].apply(_join_tokens) + keep = ["code", "docstring"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class MBPPLoader(HFLoader): + """Mostly Basic Python Problems — prompt → code (text generation).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + keep = ["text", "code"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) diff --git a/ludwig/datasets/loaders/consumer_complaints_loader.py b/ludwig/datasets/loaders/consumer_complaints_loader.py new file mode 100644 index 0000000..9c96b7a --- /dev/null +++ b/ludwig/datasets/loaders/consumer_complaints_loader.py @@ -0,0 +1,45 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class ConsumerComplaintsLoader(DatasetLoader): + """The Consumer Complaints dataset.""" + + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + + consumer_complaints_df = pd.read_csv(file_path) + consumer_complaints_df = preprocess_df(consumer_complaints_df) + + return consumer_complaints_df + + +def preprocess_df(df): + """Preprocesses the dataframe. + + - Remove all rows with missing values in the following columns: + - Consumer complaint narrative + - Issue + - Product + + Args: + df (pd.DataFrame): The dataframe to preprocess. + + Returns: + pd.DataFrame: The preprocessed dataframe. + """ + return df.dropna(subset=["Consumer complaint narrative", "Issue", "Product"]) diff --git a/ludwig/datasets/loaders/creditcard_fraud.py b/ludwig/datasets/loaders/creditcard_fraud.py new file mode 100644 index 0000000..d8fffdb --- /dev/null +++ b/ludwig/datasets/loaders/creditcard_fraud.py @@ -0,0 +1,28 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class CreditCardFraudLoader(DatasetLoader): + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + # Train/Test split like https://www.kaggle.com/competitions/1056lab-fraud-detection-in-credit-card/overview + processed_df = processed_df.sort_values(by=["Time"]) + processed_df.loc[:198365, "split"] = 0 + processed_df.loc[198365:, "split"] = 2 + processed_df.split = processed_df.split.astype(int) + return processed_df diff --git a/ludwig/datasets/loaders/dataset_loader.py b/ludwig/datasets/loaders/dataset_loader.py new file mode 100644 index 0000000..9c5216c --- /dev/null +++ b/ludwig/datasets/loaders/dataset_loader.py @@ -0,0 +1,553 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from __future__ import annotations + +import glob +import hashlib +import logging +import os +import shutil +import urllib +from enum import Enum +from urllib.parse import urlparse + +import pandas as pd +from tqdm import tqdm + +from ludwig.api_annotations import DeveloperAPI, PublicAPI +from ludwig.constants import SPLIT +from ludwig.datasets.archives import extract_archive, is_archive, list_archive +from ludwig.datasets.dataset_config import DatasetConfig, DatasetFallbackMirror +from ludwig.datasets.kaggle import download_kaggle_dataset +from ludwig.datasets.utils import model_configs_for_dataset +from ludwig.utils.fs_utils import get_default_cache_location, get_fs_and_path +from ludwig.utils.strings_utils import make_safe_filename + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +class TqdmUpTo(tqdm): + """Provides progress bar for `urlretrieve`. + + Taken from: https://gist.github.com/leimao/37ff6e990b3226c2c9670a2cd1e4a6f5 + """ + + def update_to(self, b=1, bsize=1, tsize=None): + """Update progress bar. + + Args: + b: Number of blocks transferred so far. + bsize: Size of each block (in tqdm units). + tsize: Total size (in tqdm units). If None, remains unchanged. + """ + if tsize is not None: + self.total = tsize + self.update(b * bsize - self.n) # will also set self.n = b * bsize + + +def _list_of_strings(list_or_string: str | list[str]) -> list[str]: + """Helper function to accept single string or lists in config.""" + return [list_or_string] if isinstance(list_or_string, str) else list_or_string + + +def _glob_multiple(pathnames: list[str], root_dir: str | None = None, recursive: bool = True) -> set[str]: + """Recursive glob multiple patterns, returns set of matches. + + Note: glob's root_dir argument was added in python 3.10, not using it for compatibility. + """ + if root_dir: + pathnames = [os.path.join(root_dir, p) for p in pathnames] + return set().union(*[glob.glob(p, recursive=recursive) for p in pathnames]) + + +def _sha256_digest(file_path) -> str: + """Returns the sha256 digest for the specified file.""" + hash = hashlib.sha256() + buffer = bytearray(hash.block_size * 1024) # Attempts to read in multiples of the hash block size (64KB). + mv = memoryview(buffer) + with open(file_path, "rb", buffering=0) as f: + for bytes_read in iter(lambda: f.readinto(mv), 0): + hash.update(mv[:bytes_read]) + return hash.hexdigest() + + +@PublicAPI +class DatasetState(int, Enum): + """The state of the dataset.""" + + NOT_LOADED = 0 + DOWNLOADED = 1 + EXTRACTED = 2 + TRANSFORMED = 3 + + +@PublicAPI +class DatasetLoader: + """Base class that defines the default pipeline for loading a ludwig dataset. + + Clients will typically call load(), which processes the dataset according to the config. + + A dataset is processed in 4 phases: + 1. Download - The dataset files are downloaded to the cache. + 2. Verify - Hashes of downloaded files are verified. + 3. Extract - The dataset files are extracted from an archive (may be a no-op if data is not archived). + 4. Transform - The dataset is transformed into a format usable for training and is ready to load. + a. Transform Files (Files -> Files) + b. Load Dataframe (Files -> DataFrame) + c. Transform Dataframe (DataFrame -> DataFrame) + d. Save Processed (DataFrame -> File) + + The download and extract phases are run for each URL based on the URL type and file extension. After extraction, the + full set of downloaded and extracted files are collected and passed as a list to the transform stage. + + The transform phase offers customization points for datasets which require preprocessing before they are usable for + training. + """ + + def __init__(self, config: DatasetConfig, cache_dir: str | None = None): + """Constructor.""" + self.config = config + self.cache_dir = cache_dir if cache_dir else get_default_cache_location() + + @property + def name(self): + """The name of the dataset.""" + return self.config.name + + @property + def version(self): + """The version of the dataset.""" + return self.config.version + + @property + def is_kaggle_dataset(self) -> bool: + return self.config.kaggle_dataset_id or self.config.kaggle_competition + + @property + def download_dir(self) -> str: + """Directory where all dataset artifacts are saved.""" + return os.path.join(self.cache_dir, f"{self.name}_{self.version}") + + @property + def raw_dataset_dir(self) -> str: + """Save path for raw data downloaded from the web.""" + return os.path.join(self.download_dir, "raw") + + @property + def processed_dataset_dir(self) -> str: + """Save path for processed data.""" + return os.path.join(self.download_dir, "processed") + + @property + def processed_dataset_filename(self) -> str: + """Filename for processed data.""" + return f"{make_safe_filename(self.config.name)}.parquet" + + @property + def processed_dataset_path(self) -> str: + """Save path to processed dataset file.""" + return os.path.join(self.processed_dataset_dir, self.processed_dataset_filename) + + @property + def processed_temp_dir(self) -> str: + """Save path for processed temp data.""" + return os.path.join(self.download_dir, "_processed") + + @property + def state(self) -> DatasetState: + """Dataset state.""" + if os.path.exists(self.processed_dataset_path): + return DatasetState.TRANSFORMED + if all([os.path.exists(os.path.join(self.raw_dataset_dir, filename)) for filename in self.download_filenames]): + archive_filenames = [f for f in self.download_filenames if is_archive(f)] + if archive_filenames: + # Check to see if archive has been extracted. + extracted_files = [ + f for a in archive_filenames for f in list_archive(os.path.join(self.raw_dataset_dir, a)) + ] + if all(os.path.exists(os.path.join(self.raw_dataset_dir, ef)) for ef in extracted_files): + return DatasetState.EXTRACTED + else: + return DatasetState.DOWNLOADED + # If none of the dataset download files are archives, skip extraction phase. + return DatasetState.EXTRACTED + return DatasetState.NOT_LOADED + + @property + def download_urls(self) -> list[str]: + return _list_of_strings(self.config.download_urls) + + @property + def download_filenames(self) -> list[str]: + """Filenames for downloaded files inferred from download_urls.""" + if self.config.archive_filenames: + return _list_of_strings(self.config.archive_filenames) + return [os.path.basename(urlparse(url).path) for url in self.download_urls] + + @staticmethod + def get_mirror_download_paths(mirror: DatasetFallbackMirror): + """Filenames for downloaded files inferred from mirror download_paths.""" + return _list_of_strings(mirror.download_paths) + + def get_mirror_download_filenames(self, mirror: DatasetFallbackMirror): + """Filenames for downloaded files inferred from mirror download_paths.""" + if self.config.archive_filenames: + return _list_of_strings(self.config.archive_filenames) + return [os.path.basename(path) for path in mirror.download_paths] + + def description(self) -> str: + """Returns human-readable description of the dataset.""" + return f"{self.config.name} {self.config.version}\n{self.config.description}" + + @property + def model_configs(self) -> dict[str, dict]: + """Returns a dictionary of built-in model configs for this dataset.""" + return model_configs_for_dataset(self.config.name) + + @property + def best_model_config(self) -> dict | None: + """Returns the best built-in model config for this dataset, or None.""" + return self.model_configs.get("best") + + @property + def default_model_config(self) -> dict | None: + """Returns the default built-in model config for this dataset. + + This is a good first model which should train in under 10m on a current laptop without GPU acceleration. + """ + return self.model_configs.get("default") + + def _get_preserved_paths(self, root_dir=None): + """Gets list of files to preserve when exporting dataset, not including self.processed_dataset_path. + + Returns paths relative to the dataset root directory. + """ + root_dir = root_dir if root_dir else self.processed_dataset_dir + preserved_paths = _glob_multiple(_list_of_strings(self.config.preserve_paths), root_dir=root_dir) + return [os.path.relpath(p, start=root_dir) for p in preserved_paths] + + def export(self, output_directory: str) -> None: + """Exports the dataset (and any files required by it) into the specified directory.""" + self._download_and_process() + os.makedirs(output_directory, exist_ok=True) + shutil.copy2(self.processed_dataset_path, os.path.join(output_directory, self.processed_dataset_filename)) + preserve_paths = self._get_preserved_paths() + for relative_path in preserve_paths: + source = os.path.join(self.processed_dataset_dir, relative_path) + destination = os.path.join(output_directory, relative_path) + if os.path.isdir(source): + shutil.copytree(source, destination, symlinks=False, dirs_exist_ok=True) + else: + shutil.copy2(source, destination) + + def _download_and_process(self, kaggle_username: str | None = None, kaggle_key: str | None = None): + """Loads the dataset, downloaded and processing it if needed. + + If dataset is already processed, does nothing. + """ + if self.state == DatasetState.NOT_LOADED: + try: + self.download(kaggle_username=kaggle_username, kaggle_key=kaggle_key) + except Exception as e: + logger.warning( + f"Finding fallback mirrors to download the dataset. Downloading from " + f"the original source failed with the following error {e}." + ) + if not self.config.fallback_mirrors: + raise RuntimeError(f"Failed to download dataset {self.config.name}. Original error: {e}") from e + else: + try: + self.download_from_fallback_mirrors() + except Exception as fallback_e: + raise RuntimeError( + f"Failed to download dataset {self.config.name}. " + f"Original error: {e}. " + f"Fallback also failed: {fallback_e}" + ) from e + self.verify() + if self.state == DatasetState.DOWNLOADED: + # Extract dataset + try: + self.extract() + except Exception: + logger.exception("Failed to extract dataset") + if self.state == DatasetState.EXTRACTED: + # Transform dataset + try: + self.transform() + except Exception: + logger.exception("Failed to transform dataset") + + def load( + self, kaggle_username: str | None = None, kaggle_key: str | None = None, split: bool = False + ) -> pd.DataFrame | list[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """Loads the dataset, downloaded and processing it if needed. + + Note: This method is also responsible for splitting the data, returning a single dataframe if split=False, and a + 3-tuple of train, val, test if split=True. + + Args: + kaggle_username: Username on Kaggle platform. + kaggle_key: Dataset key on Kaggle platform. + split: Splits dataset along 'split' column if present. The split column should always have values + 0: train, 1: validation, 2: test. + """ + self._download_and_process(kaggle_username=kaggle_username, kaggle_key=kaggle_key) + if self.state == DatasetState.TRANSFORMED: + dataset_df = self.load_transformed_dataset() + if split: + return self.split(dataset_df) + else: + return dataset_df + + def download(self, kaggle_username: str | None = None, kaggle_key: str | None = None) -> list[str]: + if not os.path.exists(self.raw_dataset_dir): + os.makedirs(self.raw_dataset_dir) + if self.is_kaggle_dataset: + return download_kaggle_dataset( + self.raw_dataset_dir, + kaggle_dataset_id=self.config.kaggle_dataset_id, + kaggle_competition=self.config.kaggle_competition, + kaggle_username=kaggle_username, + kaggle_key=kaggle_key, + ) + else: + for url, filename in zip(self.download_urls, self.download_filenames): + downloaded_file_path = os.path.join(self.raw_dataset_dir, filename) + with TqdmUpTo(unit="B", unit_scale=True, unit_divisor=1024, miniters=1, desc=filename) as t: + urllib.request.urlretrieve(url, downloaded_file_path, t.update_to) + + def download_from_fallback_mirrors(self): + for mirror in self.config.fallback_mirrors: + logger.info(f"Attempting download from mirror {mirror.name}.") + try: + download_paths = self.get_mirror_download_paths(mirror) + filenames = self.get_mirror_download_filenames(mirror) + for path, filename in zip(download_paths, filenames): + downloaded_file_path = os.path.join(self.raw_dataset_dir, filename) + with TqdmUpTo(unit="B", unit_scale=True, unit_divisor=1024, miniters=1, desc=filename): + fs, path = get_fs_and_path(path) + fs.get(path, downloaded_file_path) + return + except Exception: + logger.exception(f"Download from mirror `{mirror.name}` failed.") + + def verify(self) -> None: + """Verifies checksums for dataset.""" + for filename, sha256sum in self.config.sha256.items(): + digest = _sha256_digest(os.path.join(self.raw_dataset_dir, filename)) + if digest != sha256sum: + raise ValueError(f"Checksum mismatch for file {filename} of {self.config.name} dataset") + if not self.config.sha256: + logger.warning(f"No sha256 digest provided for dataset {self.config.name}, cannot verify.") + logger.info("Contents:") + for filename in os.listdir(self.raw_dataset_dir): + path = os.path.join(self.raw_dataset_dir, filename) + if not os.path.isdir(path): + digest = _sha256_digest(path) + logger.info(f" {filename}: {digest}") + + def extract(self) -> list[str]: + extracted_files = set() + for download_filename in self.download_filenames: + download_path = os.path.join(self.raw_dataset_dir, download_filename) + if is_archive(download_path): + extracted_files.update(extract_archive(download_path)) + # If the archive contains archives, extract those too. For example, bnp_claims_management. + archive_contents = extracted_files.copy() + for extracted_file in archive_contents: + extracted_path = os.path.join(self.raw_dataset_dir, extracted_file) + if is_archive(extracted_path): + try: + extracted_files.update(extract_archive(extracted_path)) + except RuntimeError as e: + logger.warning(f"Error extracting {extracted_file}" + str(e)) + return list(extracted_files) + + def transform(self) -> None: + data_filenames = [ + os.path.join(self.raw_dataset_dir, f) for f in os.listdir(self.raw_dataset_dir) if not is_archive(f) + ] + transformed_files = self.transform_files(data_filenames) + unprocessed_dataframe = self.load_unprocessed_dataframe(transformed_files) + transformed_dataframe = self.transform_dataframe(unprocessed_dataframe) + self.save_processed(transformed_dataframe) + + def transform_files(self, file_paths: list[str]) -> list[str]: + """Transform data files before loading to dataframe. + + Subclasses should override this method to process files before loading dataframe, calling the base class + implementation after transformation if the results of transformation are needed by preserve_paths. + """ + data_files = [p for p in file_paths if not os.path.isdir(p)] + if not os.path.exists(self.processed_dataset_dir): + os.makedirs(self.processed_dataset_dir) + # Moves any preserved paths (ex. image directories) into processed directory to avoid unnecessary copy. + for rel_path in self._get_preserved_paths(self.raw_dataset_dir): + source_path = os.path.join(self.raw_dataset_dir, rel_path) + dest_path = os.path.join(self.processed_dataset_dir, rel_path) + if os.path.exists(source_path) and not os.path.exists(dest_path): + os.replace(source_path, dest_path) + return data_files + + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe. + + Subclasses may override this method to support other input formats (json, jsonl, tsv, csv, parquet) + """ + file_extension = os.path.splitext(file_path)[-1].lower() + if file_extension == ".json": + return pd.read_json(file_path) + elif file_extension == ".jsonl": + return pd.read_json(file_path, lines=True) + elif file_extension == ".tsv": + return pd.read_table(file_path) + elif file_extension in {".csv", ".data"}: + return pd.read_csv(file_path) + elif file_extension in {".parquet", ".pq", ".pqt"}: + return pd.read_parquet(file_path) + else: + raise ValueError(f"Unsupported dataset file type: {file_extension}") + + def load_files_to_dataframe(self, file_paths: list[str], root_dir=None) -> pd.DataFrame: + """Loads a file or list of files and returns a dataframe. + + Subclasses may override this method to change the loader's behavior for groups of files. + """ + if root_dir: + file_paths = [os.path.join(root_dir, path) for path in file_paths] + dataframes = [self.load_file_to_dataframe(path) for path in file_paths] + try: + if self.config.columns: + column_names = [column["name"] for column in self.config.columns] + + set_cols_dfs = [] + for df in dataframes: + # Split column is not included in configs, add in if pre-set split is present + cols = list(column_names) + if SPLIT in df.columns and SPLIT not in cols: + cols.append(SPLIT) + + # If the dataframe already has named columns (parquet / CSV with header), + # filter by name rather than renaming positionally. This avoids silently + # mapping the wrong column to a name when the file is a labeled split that + # has a different column set from the train split (e.g. unlabeled test files). + if any(c in df.columns for c in cols): + existing = [c for c in cols if c in df.columns] + df = df[existing] + cols = existing + elif len(cols) != len(df.columns): + # Positional file (no header): trim extra columns like "Unnamed: 0". + existing = cols[: len(df.columns)] + df = df.iloc[:, : len(existing)] + cols = existing + set_cols_dfs.append(df.set_axis(cols, axis=1)) + return pd.concat(set_cols_dfs, ignore_index=True) + else: + return pd.concat(dataframes, ignore_index=True) + except ValueError as e: + logger.warning(f"Error setting column names: {e}") + return pd.concat(dataframes, ignore_index=True) + + def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame: + """Load dataset files into a dataframe. + + Will use the list of data files in the dataset directory as a default if all of config's dataset_filenames, + train_filenames, validation_filenames, test_filenames are empty. + """ + dataset_paths = _glob_multiple(_list_of_strings(self.config.dataset_filenames), root_dir=self.raw_dataset_dir) + train_paths = _glob_multiple(_list_of_strings(self.config.train_filenames), root_dir=self.raw_dataset_dir) + validation_paths = _glob_multiple( + _list_of_strings(self.config.validation_filenames), root_dir=self.raw_dataset_dir + ) + test_paths = _glob_multiple(_list_of_strings(self.config.test_filenames), root_dir=self.raw_dataset_dir) + if self.config.name == "hugging_face": + dataframes = self._get_dataframe_with_fixed_splits_from_hf() + else: + dataframes = self._get_dataframe_with_fixed_splits( + train_paths, validation_paths, test_paths, dataset_paths, file_paths + ) + return pd.concat(dataframes, ignore_index=True) + + def _get_dataframe_with_fixed_splits_from_hf(self): + dataframes = [] + splits = ["train", "validation", "test"] + data_dict = self.load_hf_to_dict( + self.config.huggingface_dataset_id, self.config.huggingface_subset + ) # This function is defined in the Hugging Face dataloader + for split_type in splits: + if split_type in data_dict: + # We don't have to do anything if split not in data_dict because we just concatenate the dataframes + # in the end anyway. + data_dict[split_type][SPLIT] = splits.index(split_type) # Add "split" column (0, 1, or 2) + dataframes.append(data_dict[split_type]) + return dataframes + + def _get_dataframe_with_fixed_splits(self, train_paths, validation_paths, test_paths, dataset_paths, file_paths): + dataframes = [] + if len(train_paths) > 0: + train_df = self.load_files_to_dataframe(train_paths) + train_df[SPLIT] = 0 + dataframes.append(train_df) + if len(validation_paths) > 0: + validation_df = self.load_files_to_dataframe(validation_paths) + validation_df[SPLIT] = 1 + dataframes.append(validation_df) + if len(test_paths) > 0: + test_df = self.load_files_to_dataframe(test_paths) + test_df[SPLIT] = 2 + dataframes.append(test_df) + # If we have neither train/validation/test files nor dataset_paths in the config, + # use data files in root dir. + if len(dataset_paths) == len(dataframes) == 0: + dataset_paths = file_paths + if len(dataset_paths) > 0: + dataframes.append(self.load_files_to_dataframe(dataset_paths)) + return dataframes + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + """Transforms a dataframe of the entire dataset. + + Subclasses should override this method if transformation of the dataframe is needed. + """ + for column_name, type in self.config.column_types.items(): + dataframe[column_name] = dataframe[column_name].astype(type) + return dataframe + + def save_processed(self, dataframe: pd.DataFrame) -> None: + """Saves transformed dataframe as a flat file ludwig can load for training.""" + if not os.path.exists(self.processed_dataset_dir): + os.makedirs(self.processed_dataset_dir) + dataframe.to_parquet(self.processed_dataset_path, engine="pyarrow") + + def load_transformed_dataset(self) -> pd.DataFrame: + """Load processed dataset into a dataframe.""" + return pd.read_parquet(self.processed_dataset_path) + + def get_mtime(self) -> float: + """Last modified time of the processed dataset after downloading successfully.""" + return os.path.getmtime(self.processed_dataset_path) + + @staticmethod + def split(dataset: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + if SPLIT in dataset: + dataset[SPLIT] = pd.to_numeric(dataset[SPLIT]) + training_set = dataset[dataset[SPLIT] == 0].drop(columns=[SPLIT]) + val_set = dataset[dataset[SPLIT] == 1].drop(columns=[SPLIT]) + test_set = dataset[dataset[SPLIT] == 2].drop(columns=[SPLIT]) + return training_set, test_set, val_set + else: + raise ValueError(f"The dataset does not a '{SPLIT}' column, load with `split=False`") diff --git a/ludwig/datasets/loaders/esc50.py b/ludwig/datasets/loaders/esc50.py new file mode 100644 index 0000000..908c1b3 --- /dev/null +++ b/ludwig/datasets/loaders/esc50.py @@ -0,0 +1,28 @@ +import os + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class ESC50Loader(DatasetLoader): + """ESC-50 Environmental Sound Classification dataset. + + After extraction the archive contains: + ESC-50-master/audio/*.wav — 2,000 WAV clips + ESC-50-master/meta/esc50.csv — metadata (filename, fold, target, category, …) + + The loader rewrites the ``filename`` column into an ``audio_path`` column with + paths relative to the processed dataset directory (where ``preserve_paths`` + copies the audio subdirectory). + """ + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + df = super().transform_dataframe(dataframe) + # Build relative paths so the smoke-test can prepend processed_dataset_dir. + df["audio_path"] = df["filename"].apply( + lambda fn: os.path.join("ESC-50-master", "audio", os.path.basename(str(fn))) + ) + # Use integer fold (1-5) as the split column: fold 5 → test, fold 4 → val, rest → train. + df["split"] = df["fold"].apply(lambda f: 2 if f == 5 else (1 if f == 4 else 0)) + return df diff --git a/ludwig/datasets/loaders/ethos_binary.py b/ludwig/datasets/loaders/ethos_binary.py new file mode 100644 index 0000000..ba79fc4 --- /dev/null +++ b/ludwig/datasets/loaders/ethos_binary.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class EthosBinaryLoader(DatasetLoader): + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + # This dataset uses ; seperator instead of , + return pd.read_csv(file_path, sep=";") + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + # convert float labels (0.0, 1.0) to binary labels + processed_df["isHate"] = processed_df["isHate"] >= 0.5 + processed_df["isHate"] = processed_df["isHate"].astype(int) + return processed_df diff --git a/ludwig/datasets/loaders/flickr8k.py b/ludwig/datasets/loaders/flickr8k.py new file mode 100644 index 0000000..dd1cfbd --- /dev/null +++ b/ludwig/datasets/loaders/flickr8k.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os +import re +from collections import defaultdict + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class Flickr8kLoader(DatasetLoader): + def transform_files(self, file_paths: list[str]) -> list[str]: + # create a dictionary matching image_path --> list of captions + image_to_caption = defaultdict(list) + with open(f"{self.raw_dataset_dir}/Flickr8k.token.txt") as captions_file: + image_to_caption = defaultdict(list) + for line in captions_file: + line = line.split("#") + # the regex is to format the string to fit properly in a csv + line[1] = line[1].strip("\n01234.\t ") + line[1] = re.sub('"', '""', line[1]) + line[1] = '"' + line[1] + '"' + image_to_caption[line[0]].append(line[1]) + # create csv file with 7 columns: image_path, 5 captions, and split + with open(os.path.join(self.raw_dataset_dir, "flickr8k_dataset.csv"), "w") as output_file: + output_file.write("image_path,caption0,caption1,caption2,") + output_file.write("caption3,caption4,split\n") + splits = ["train", "dev", "test"] + for i in range(len(splits)): + split = splits[i] + with open(f"{self.raw_dataset_dir}/Flickr_8k.{split}Images.txt") as split_file: + for image_name in split_file: + image_name = image_name.strip("\n") + if image_name in image_to_caption: + output_file.write( + "{},{},{},{},{},{},{}\n".format( + # Note: image folder is named Flicker8k_Dataset + f"{self.raw_dataset_dir}/Flicker8k_Dataset/{image_name}", + *image_to_caption[image_name], + i, + ) + ) + return super().transform_files(file_paths) diff --git a/ludwig/datasets/loaders/forest_cover.py b/ludwig/datasets/loaders/forest_cover.py new file mode 100644 index 0000000..c19a794 --- /dev/null +++ b/ludwig/datasets/loaders/forest_cover.py @@ -0,0 +1,148 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import pandas as pd +from sklearn.model_selection import train_test_split + +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + +_RAW_COLUMNS = ( + [ + "Elevation", + "Aspect", + "Slope", + "Horizontal_Distance_To_Hydrology", + "Vertical_Distance_To_Hydrology", + "Horizontal_Distance_To_Roadways", + "Hillshade_9am", + "Hillshade_Noon", + "Hillshade_3pm", + "Horizontal_Distance_To_Fire_Points", + ] + + [f"Wilderness_Area_{i}" for i in range(1, 5)] + + [f"Soil_Type_{i}" for i in range(1, 41)] + + ["Cover_Type"] +) + + +class ForestCoverLoader(DatasetLoader): + def __init__(self, config: DatasetConfig, cache_dir: str | None = None, use_tabnet_split=True): + super().__init__(config, cache_dir=cache_dir) + self.use_tabnet_split = use_tabnet_split + + def load_file_to_dataframe(self, file_path: str, **kwargs) -> pd.DataFrame: + """Read the headerless covtype.data file and assign raw column names.""" + return pd.read_csv(file_path, header=None, names=_RAW_COLUMNS) + + def load_files_to_dataframe(self, file_paths: list[str], root_dir=None) -> pd.DataFrame: + """Bypass base-class column-name reassignment — raw file already has correct names.""" + import os + + if root_dir: + file_paths = [os.path.join(root_dir, p) for p in file_paths] + frames = [self.load_file_to_dataframe(p) for p in file_paths] + return pd.concat(frames, ignore_index=True) + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + df = super().transform_dataframe(dataframe) + # Elevation quantitative meters Elevation in meters + # Aspect quantitative azimuth Aspect in degrees azimuth + # Slope quantitative degrees Slope in degrees + # Horizontal_Distance_To_Hydrology quantitative meters Horz Dist to nearest surface water features + # Vertical_Distance_To_Hydrology quantitative meters Vert Dist to nearest surface water features + # Horizontal_Distance_To_Roadways quantitative meters Horz Dist to nearest roadway + # Hillshade_9am quantitative 0 to 255 index Hillshade index at 9am, summer solstice + # Hillshade_Noon quantitative 0 to 255 index Hillshade index at noon, summer soltice + # Hillshade_3pm quantitative 0 to 255 index Hillshade index at 3pm, summer solstice + # Horizontal_Distance_To_Fire_Points quantitative meters Horz Dist to nearest wildfire ignition points + # Wilderness_Area (4 binary columns) qualitative 0 (absence) or 1 (presence) Wilderness area designation + # Soil_Type (40 binary columns) qualitative 0 (absence) or 1 (presence) Soil Type designation + # Cover_Type (7 types) integer 1 to 7 Forest Cover Type designation + + # Map the 40 soil types to a single integer instead of 40 binary columns + st_cols = [ + "Soil_Type_1", + "Soil_Type_2", + "Soil_Type_3", + "Soil_Type_4", + "Soil_Type_5", + "Soil_Type_6", + "Soil_Type_7", + "Soil_Type_8", + "Soil_Type_9", + "Soil_Type_10", + "Soil_Type_11", + "Soil_Type_12", + "Soil_Type_13", + "Soil_Type_14", + "Soil_Type_15", + "Soil_Type_16", + "Soil_Type_17", + "Soil_Type_18", + "Soil_Type_19", + "Soil_Type_20", + "Soil_Type_21", + "Soil_Type_22", + "Soil_Type_23", + "Soil_Type_24", + "Soil_Type_25", + "Soil_Type_26", + "Soil_Type_27", + "Soil_Type_28", + "Soil_Type_29", + "Soil_Type_30", + "Soil_Type_31", + "Soil_Type_32", + "Soil_Type_33", + "Soil_Type_34", + "Soil_Type_35", + "Soil_Type_36", + "Soil_Type_37", + "Soil_Type_38", + "Soil_Type_39", + "Soil_Type_40", + ] + st_vals = [] + for _, row in df[st_cols].iterrows(): + st_vals.append(row.to_numpy().nonzero()[0].item(0)) + df = df.drop(columns=st_cols) + df["Soil_Type"] = st_vals + + # Map the 4 wilderness areas to a single integer + # instead of 4 binary columns + wa_cols = ["Wilderness_Area_1", "Wilderness_Area_2", "Wilderness_Area_3", "Wilderness_Area_4"] + wa_vals = [] + for _, row in df[wa_cols].iterrows(): + wa_vals.append(row.to_numpy().nonzero()[0].item(0)) + df = df.drop(columns=wa_cols) + df["Wilderness_Area"] = wa_vals + + if not self.use_tabnet_split: + # first 11340 records used for training data subset + # next 3780 records used for validation data subset + # last 565892 records used for testing data subset + df["split"] = [0] * 11340 + [1] * 3780 + [2] * 565892 + else: + # Split used in the tabNet paper + # https://github.com/google-research/google-research/blob/master/tabnet/download_prepare_covertype.py + train_val_indices, test_indices = train_test_split(range(len(df)), test_size=0.2, random_state=0) + train_indices, val_indices = train_test_split(train_val_indices, test_size=0.2 / 0.6, random_state=0) + + df["split"] = 0 + df.loc[val_indices, "split"] = 1 + df.loc[test_indices, "split"] = 2 + + return df diff --git a/ludwig/datasets/loaders/go_emotions.py b/ludwig/datasets/loaders/go_emotions.py new file mode 100644 index 0000000..650f427 --- /dev/null +++ b/ludwig/datasets/loaders/go_emotions.py @@ -0,0 +1,72 @@ +import ast + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + +# 28-class emotion vocabulary (index → label string). +_EMOTION_LABELS = [ + "admiration", + "amusement", + "anger", + "annoyance", + "approval", + "caring", + "confusion", + "curiosity", + "desire", + "disappointment", + "disapproval", + "disgust", + "embarrassment", + "excitement", + "fear", + "gratitude", + "grief", + "joy", + "love", + "nervousness", + "optimism", + "pride", + "realization", + "relief", + "remorse", + "sadness", + "surprise", + "neutral", +] + + +class GoEmotionsLoader(HFLoader): + """GoEmotions multi-label emotion classification dataset. + + The HuggingFace 'simplified' split stores ``labels`` as a list of integer + indices. Ludwig's set feature expects a space-separated string of category + names, so this loader maps the integer ids to emotion label strings. + """ + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + def _map_ids(ids): + if isinstance(ids, str): + try: + ids = ast.literal_eval(ids) + except (ValueError, SyntaxError): + return ids + # Covers list, tuple, and numpy arrays + try: + return " ".join(_EMOTION_LABELS[int(i)] if int(i) < len(_EMOTION_LABELS) else str(i) for i in ids) + except TypeError: + return str(ids) + + df["labels"] = df["labels"].apply(_map_ids) + keep = ["text", "labels"] + (["split"] if "split" in df.columns else []) + return df[keep] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + df = super().load(split=False) + return self._transform(df) diff --git a/ludwig/datasets/loaders/goemotions.py b/ludwig/datasets/loaders/goemotions.py new file mode 100644 index 0000000..f5f4b8f --- /dev/null +++ b/ludwig/datasets/loaders/goemotions.py @@ -0,0 +1,25 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class GoEmotionsLoader(DatasetLoader): + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + # Format emotion IDs as space-delimited string (Set). + processed_df["emotion_ids"] = processed_df["emotion_ids"].apply(lambda e_id: " ".join(e_id.split(","))) + return processed_df diff --git a/ludwig/datasets/loaders/higgs.py b/ludwig/datasets/loaders/higgs.py new file mode 100644 index 0000000..dd816cd --- /dev/null +++ b/ludwig/datasets/loaders/higgs.py @@ -0,0 +1,45 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import pandas as pd + +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class HiggsLoader(DatasetLoader): + def __init__(self, config: DatasetConfig, cache_dir: str | None = None, add_validation_set=True): + super().__init__(config, cache_dir) + self.add_validation_set = add_validation_set + + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + return pd.read_csv(file_path, header=None) + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + n = len(processed_df) + # The HIGGS CSV may contain fewer rows than the canonical 11M depending on + # which version was downloaded — derive split boundaries from actual length. + if self.add_validation_set: + n_test = min(n // 3, max(1, int(n * 0.05))) + n_val = min(n // 3, max(1, int(n * 0.05))) + n_train = max(0, n - n_test - n_val) + processed_df["split"] = [0] * n_train + [1] * n_val + [2] * n_test + else: + n_test = min(n // 2, max(1, int(n * 0.05))) + n_train = max(0, n - n_test) + processed_df["split"] = [0] * n_train + [2] * n_test + return processed_df diff --git a/ludwig/datasets/loaders/hugging_face.py b/ludwig/datasets/loaders/hugging_face.py new file mode 100644 index 0000000..57e9bfd --- /dev/null +++ b/ludwig/datasets/loaders/hugging_face.py @@ -0,0 +1,99 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from __future__ import annotations + +import logging + +import datasets +import pandas as pd + +from ludwig.constants import TEST, TRAIN, VALIDATION +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + +SPLITS = [TRAIN, VALIDATION, TEST] +logger = logging.getLogger(__name__) + + +class HFLoader(DatasetLoader): + """HFLoader differs from all other DatasetLoaders because of how it loads data through the Hugging Face + datasets API instead of saving any files to the cache. + + The config for HFLoader contains two unique parameters, huggingface_dataset_id and huggingface_subsample, that + identify which dataset and which subsample of that dataset to load in. + """ + + @staticmethod + def load_hf_to_dict(hf_id: str | None = None, hf_subsample: str | None = None) -> dict[str, pd.DataFrame]: + """Returns a map of split -> pd.DataFrame for the given HF dataset. + + Args: + hf_id: path to dataset on HuggingFace platform. + hf_subsample: name of dataset configuration on HuggingFace platform. + """ + dataset_dict: dict[str, datasets.Dataset] = datasets.load_dataset(path=hf_id, name=hf_subsample) + pandas_dict = {} + for split in dataset_dict: + # Convert from HF DatasetDict type to a dictionary of pandas dataframes + pandas_dict[split] = dataset_dict[split].to_pandas() + return pandas_dict + + # TODO(Alex): Standardize load() signature as interface method in DatasetLoader and adhere to it in all subclasses. + def load( + self, hf_id: str | None = None, hf_subsample: str | None = None, split: bool = False + ) -> pd.DataFrame | list[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """When load() is called, HFLoader calls the datasets API to return all of the data in a HuggingFace + DatasetDict, converts it to a dictionary of pandas dataframes, and returns either three dataframes + containing train, validation, and test data or one dataframe that is the concatenation of all three + depending on whether `split` is set to True or False. + + Note that some datasets may not provide a validation set or a test set. In this case: + - If split is True, the DataFrames corresponding to the missing sets are initialized to be empty + - If split is False, the "split" column in the resulting DataFrame will reflect the fact that there is no + validation/test split (i.e., there will be no 1s/2s) + + A train set should always be provided by Hugging Face. + + :param hf_id: (str) path to dataset on HuggingFace platform; falls back to config.huggingface_dataset_id + :param hf_subsample: (str) name of dataset configuration on HuggingFace platform; falls back to config + :param split: directive for how to interpret if dataset contains validation or test set. + """ + # Fall back to config values so that HFLoader works with the standard load(split=True) call pattern. + if hf_id is None: + hf_id = getattr(self.config, "huggingface_dataset_id", None) + if hf_subsample is None: + hf_subsample = getattr(self.config, "huggingface_subsample", None) + if hf_id is None: + raise ValueError( + "No HuggingFace dataset ID provided. Set config.huggingface_dataset_id or pass hf_id= to load()." + ) + pandas_dict = self.load_hf_to_dict( + hf_id=hf_id, + hf_subsample=hf_subsample, + ) + if split: # For each split, either return the appropriate dataframe or an empty dataframe + for spl in SPLITS: + if spl not in pandas_dict: + logger.warning(f"No {spl} set found in provided Hugging Face dataset. Skipping {spl} set.") + train_df = pandas_dict[TRAIN] if TRAIN in pandas_dict else pd.DataFrame() + validation_df = pandas_dict[VALIDATION] if VALIDATION in pandas_dict else pd.DataFrame() + test_df = pandas_dict[TEST] if TEST in pandas_dict else pd.DataFrame() + + return train_df, validation_df, test_df + else: + dataset_list = [] + for spl in pandas_dict: + pandas_dict[spl]["split"] = SPLITS.index(spl) # Add a column containing 0s, 1s, and 2s denoting splits + dataset_list.append(pandas_dict[spl]) + return pd.concat(dataset_list) diff --git a/ludwig/datasets/loaders/ieee_fraud.py b/ludwig/datasets/loaders/ieee_fraud.py new file mode 100644 index 0000000..b6b97d6 --- /dev/null +++ b/ludwig/datasets/loaders/ieee_fraud.py @@ -0,0 +1,44 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class IEEEFraudLoader(DatasetLoader): + """The IEEE-CIS Fraud Detection Dataset https://www.kaggle.com/c/ieee-fraud-detection/overview.""" + + def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame: + """Load dataset files into a dataframe.""" + train_files = {"train_identity.csv", "train_transaction.csv"} + test_files = {"test_identity.csv", "test_transaction.csv"} + + train_dfs, test_dfs = {}, {} + + for filename in train_files.union(test_files): + split_name = os.path.splitext(filename)[0] + file_df = self.load_file_to_dataframe(os.path.join(self.raw_dataset_dir, filename)) + if filename in train_files: + train_dfs[split_name] = file_df + elif filename in test_files: + test_dfs[split_name] = file_df + + # Merge on TransactionID + final_train = pd.merge( + train_dfs["train_transaction"], train_dfs["train_identity"], on="TransactionID", how="left" + ) + return final_train diff --git a/ludwig/datasets/loaders/insurance_lite.py b/ludwig/datasets/loaders/insurance_lite.py new file mode 100644 index 0000000..0570904 --- /dev/null +++ b/ludwig/datasets/loaders/insurance_lite.py @@ -0,0 +1,32 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class InsuranceLiteLoader(DatasetLoader): + """Health Insurance Cross Sell Prediction Predict Health Insurance Owners' who will be interested in Vehicle + Insurance https://www.kaggle.com/datasets/arashnic/imbalanced-data-practice.""" + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + df = super().transform_dataframe(dataframe) + # Make image paths relative to dataset root directory + df["image_path"] = df["image_path"].apply( + lambda x: os.path.join("Fast_Furious_Insured", "trainImages", os.path.basename(x)) + ) + return df diff --git a/ludwig/datasets/loaders/kdd_loader.py b/ludwig/datasets/loaders/kdd_loader.py new file mode 100644 index 0000000..de11f89 --- /dev/null +++ b/ludwig/datasets/loaders/kdd_loader.py @@ -0,0 +1,159 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd + +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class KDDCup2009Loader(DatasetLoader): + def __init__(self, config: DatasetConfig, cache_dir: str | None = None, task_name="", include_test_download=False): + super().__init__(config, cache_dir=cache_dir) + self.task_name = task_name + self.include_test_download = include_test_download + + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + return pd.read_csv(file_path, sep="\t") + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + train_df = super().transform_dataframe(dataframe) + train_df = process_categorical_features(train_df, categorical_features) + train_df = process_number_features(train_df, categorical_features) + + targets = ( + pd.read_csv(os.path.join(self.raw_dataset_dir, f"orange_small_train_{self.task_name}.labels"), header=None)[ + 0 + ] + .astype(str) + .apply(lambda x: "true" if x == "1" else "false") + ) + + train_idcs = pd.read_csv( + os.path.join(self.raw_dataset_dir, f"stratified_train_idx_{self.task_name}.txt"), header=None + )[0] + + val_idcs = pd.read_csv( + os.path.join(self.raw_dataset_dir, f"stratified_test_idx_{self.task_name}.txt"), header=None + )[0] + + processed_train_df = train_df.iloc[train_idcs].copy() + processed_train_df["target"] = targets.iloc[train_idcs] + processed_train_df["split"] = 0 + + processed_val_df = train_df.iloc[val_idcs].copy() + processed_val_df["target"] = targets.iloc[val_idcs] + processed_val_df["split"] = 1 + + if self.include_test_download: + test_df = self.load_file_to_dataframe(os.path.join(self.raw_dataset_dir, "orange_small_test.data")) + test_df["target"] = "" # no ground truth labels for test download + test_df["split"] = 2 + df = pd.concat([processed_train_df, processed_val_df, test_df]) + else: + df = pd.concat([processed_train_df, processed_val_df]) + + return df + + +def process_categorical_features(df, categorical_features): + for i in categorical_features: + df.iloc[:, i].fillna("", inplace=True) + return df + + +def process_number_features(df, categorical_features): + for i, column in enumerate(df.columns): + if i not in categorical_features: + df[column].astype(float, copy=False) + return df + + +categorical_features = { + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225, + 226, + 227, + 228, +} + + +class KDDAppetencyLoader(KDDCup2009Loader): + """The KDD Cup 2009 Appetency dataset. + + https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data + """ + + def __init__(self, config: DatasetConfig, cache_dir: str | None = None, include_test_download=False): + super().__init__( + config, cache_dir=cache_dir, task_name="appetency", include_test_download=include_test_download + ) + + +class KDDChurnLoader(KDDCup2009Loader): + """The KDD Cup 2009 Churn dataset. + + https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data + """ + + def __init__(self, config: DatasetConfig, cache_dir: str | None = None, include_test_download=False): + super().__init__(config, cache_dir=cache_dir, task_name="churn", include_test_download=include_test_download) + + +class KDDUpsellingLoader(KDDCup2009Loader): + """The KDD Cup 2009 Upselling dataset. + + https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data + """ + + def __init__(self, config: DatasetConfig, cache_dir: str | None = None, include_test_download=False): + super().__init__( + config, cache_dir=cache_dir, task_name="upselling", include_test_download=include_test_download + ) diff --git a/ludwig/datasets/loaders/misc_loaders.py b/ludwig/datasets/loaders/misc_loaders.py new file mode 100644 index 0000000..72b332e --- /dev/null +++ b/ludwig/datasets/loaders/misc_loaders.py @@ -0,0 +1,140 @@ +"""Miscellaneous loaders for datasets that don't fit other base patterns.""" + +from __future__ import annotations + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +class KlueStsLoader(HFLoader): + """KLUE STS — sentence pair → similarity score (regression).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # labels is a dict {'label': float, 'real-label': float} + df["score"] = df["labels"].apply(lambda v: float(v.get("label", 0.0)) if isinstance(v, dict) else float(v)) + keep = ["sentence1", "sentence2", "score"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class MultiRCLoader(HFLoader): + """SuperGLUE MultiRC — paragraph + question + answer → label (0/1).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # idx is a nested dict; we don't need it + keep = ["paragraph", "question", "answer", "label"] + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class TruthfulQALoader(HFLoader): + """TruthfulQA multiple choice — question → best_answer category.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # mc1_targets / mc2_targets are dicts; just use best_answer as text + keep = ["question", "best_answer", "category"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class GiftEvalLoader(HFLoader): + """GiftEval Pretrain — time-series forecasting: use freq + item_id as features.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # target is a list of floats; use the mean as a scalar regression target + if "target" in df.columns: + df["target_mean"] = df["target"].apply( + lambda v: float(sum(v) / len(v)) if hasattr(v, "__iter__") and len(v) > 0 else 0.0 + ) + keep = ["freq", "item_id", "target_mean"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class HC3Loader(HFLoader): + """HC3 — expand each (question, human_answers, chatgpt_answers) row into two rows + for binary classification: detect if an answer is human-written (0) or ChatGPT (1). + Takes the first element from each answer list. + """ + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + + def first(lst): + if isinstance(lst, list) and lst: + return str(lst[0]) + return "" + + human = pd.DataFrame( + { + "question": df["question"], + "answer": df["human_answers"].apply(first), + "is_chatgpt": 0, + } + ) + chatgpt = pd.DataFrame( + { + "question": df["question"], + "answer": df["chatgpt_answers"].apply(first), + "is_chatgpt": 1, + } + ) + return pd.concat([human, chatgpt], ignore_index=True) + + +class BlimpLoader(HFLoader): + """BLiMP — reshape minimal pairs into binary grammaticality classification. + + Each source row has (sentence_good, sentence_bad). We expand to two rows + each with a single `sentence` column and a binary `is_grammatical` label. + """ + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + good = df[["sentence_good"]].rename(columns={"sentence_good": "sentence"}) + good["is_grammatical"] = 1 + bad = df[["sentence_bad"]].rename(columns={"sentence_bad": "sentence"}) + bad["is_grammatical"] = 0 + return pd.concat([good, bad], ignore_index=True) + + +class SciqLoader(HFLoader): + """SciQ — support + question + distractor/correct_answer → correct answer (text).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + keep = ["support", "question", "correct_answer"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) diff --git a/ludwig/datasets/loaders/mnist.py b/ludwig/datasets/loaders/mnist.py new file mode 100644 index 0000000..9d30d68 --- /dev/null +++ b/ludwig/datasets/loaders/mnist.py @@ -0,0 +1,125 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os +import struct +from multiprocessing.pool import ThreadPool + +import numpy as np +import pandas as pd +import torch + +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader +from ludwig.utils.fs_utils import makedirs + +logger = logging.getLogger(__name__) +NUM_LABELS = 10 + + +class MNISTLoader(DatasetLoader): + def __init__(self, config: DatasetConfig, cache_dir: str | None = None): + try: + from torchvision.io import write_png + + self.write_png = write_png + except ImportError: + logger.error( + "torchvision is not installed. " + "In order to install all image feature dependencies run " + "pip install ludwig[image]" + ) + raise + super().__init__(config, cache_dir) + + def transform_files(self, file_paths: list[str]) -> list[str]: + for dataset in ["training", "testing"]: + labels, images = self.read_source_dataset(dataset, self.raw_dataset_dir) + self.write_output_dataset(labels, images, os.path.join(self.raw_dataset_dir, dataset)) + return super().transform_files(file_paths) + + def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame: + """Load dataset files into a dataframe.""" + return self.output_training_and_test_data() + + def read_source_dataset(self, dataset="training", path="."): + """Create a directory for training and test and extract all the images and labels to this destination. + + :args: dataset (str) : the label for the dataset path (str): the raw dataset path + :returns: A tuple of the label for the image, the file array, the size and rows and columns for the image + """ + if dataset == "training": + fname_img = os.path.join(path, "train-images-idx3-ubyte") + fname_lbl = os.path.join(path, "train-labels-idx1-ubyte") + elif dataset == "testing": + fname_img = os.path.join(path, "t10k-images-idx3-ubyte") + fname_lbl = os.path.join(path, "t10k-labels-idx1-ubyte") + else: + raise ValueError("dataset must be 'testing' or 'training'") + + with open(fname_lbl, "rb") as flbl: + struct.unpack(">II", flbl.read(8)) + lbl = np.frombuffer(flbl.read(), dtype=np.uint8) + + with open(fname_img, "rb") as fimg: + magic_nr, size, rows, cols = struct.unpack(">IIII", fimg.read(16)) + img = np.frombuffer(fimg.read(), dtype=np.uint8) + img = img.reshape((size, rows, cols)) + + return lbl, img + + def write_output_dataset(self, labels, images, output_dir): + """Create output directories where we write out the images. + + :args: labels (str) : the labels for the image data (np.array) : the binary array corresponding to the image + output_dir (str) : the output directory that we need to write to path (str): the raw dataset path + :returns: A tuple of the label for the image, the file array, the size and rows and columns for the image + """ + # create child image output directories + output_dirs = [os.path.join(output_dir, str(i)) for i in range(NUM_LABELS)] + + for output_dir in output_dirs: + makedirs(output_dir, exist_ok=True) + + def write_processed_image(t): + i, label = t + output_filename = os.path.join(output_dirs[label], str(i) + ".png") + torch_image = torch.from_numpy(images[i].copy()).view(1, 28, 28) + self.write_png(torch_image, output_filename) + + # write out image data + tasks = list(enumerate(labels)) + pool = ThreadPool(NUM_LABELS) + pool.map(write_processed_image, tasks) + pool.close() + pool.join() + + def output_training_and_test_data(self): + """Creates a combined (training and test) dataframe by iterating through all the images and labels.""" + dataframes = [] + for name in ["training", "testing"]: + labels = [] + paths = [] + splits = [] + for i in range(NUM_LABELS): + label_dir = f"{name}/{i}" + img_dir = os.path.join(self.processed_dataset_dir, label_dir) + for file in os.listdir(img_dir): + if file.endswith(".png"): + labels.append(str(i)) + paths.append(os.path.join(img_dir, file)) + splits.append(0 if name == "training" else 2) + dataframes.append(pd.DataFrame({"image_path": paths, "label": labels, "split": splits})) + return pd.concat(dataframes, ignore_index=True) diff --git a/ludwig/datasets/loaders/multilabel_loader.py b/ludwig/datasets/loaders/multilabel_loader.py new file mode 100644 index 0000000..2b8db3e --- /dev/null +++ b/ludwig/datasets/loaders/multilabel_loader.py @@ -0,0 +1,142 @@ +"""Base loader for multi-label classification datasets where labels are stored +as a list of integers or strings. Ludwig's set feature expects a +space-separated string of label names. +""" + +from __future__ import annotations + +import ast + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +def _coerce_to_list(val) -> list: + if isinstance(val, str): + try: + val = ast.literal_eval(val) + except (ValueError, SyntaxError): + return val.split() + if hasattr(val, "__iter__") and not isinstance(val, (str, dict)): + return list(val) + return [val] + + +class MultiLabelLoader(HFLoader): + """Convert a list-of-labels column to a space-separated string. + + Subclasses set: + labels_col – column containing the label list + label_names – list mapping int index → label string (or None to use as-is) + out_col – output column name + """ + + labels_col: str = "labels" + label_names: list[str] | None = None + out_col: str = "labels" + + def _map_label(self, idx) -> str: + if self.label_names is not None: + try: + return self.label_names[int(idx)] + except (IndexError, ValueError): + return str(idx) + return str(idx) + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df[self.out_col] = df[self.labels_col].apply(lambda v: " ".join(self._map_label(x) for x in _coerce_to_list(v))) + keep = [c for c in df.columns if c not in (self.labels_col,) or c == self.out_col] + return df[keep] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +# LexGLUE ECtHR — 10 article labels (int indices) +_ECTHR_LABELS = [ + "Art. 2", + "Art. 3", + "Art. 5", + "Art. 6", + "Art. 8", + "Art. 9", + "Art. 10", + "Art. 11", + "Art. 14", + "Art. P1-1", +] + + +class LexGlueECtHRLoader(HFLoader): + """LexGLUE ECtHR — text (list of paragraphs joined) → label set.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # text is a list of paragraphs + df["text_joined"] = df["text"].apply( + lambda v: ( + " ".join(str(p) for p in _coerce_to_list(v)) + if hasattr(v, "__iter__") and not isinstance(v, str) + else str(v) + ) + ) + df["labels"] = df["labels"].apply( + lambda v: " ".join( + _ECTHR_LABELS[int(i)] if int(i) < len(_ECTHR_LABELS) else str(i) for i in _coerce_to_list(v) + ) + ) + keep = ["text_joined", "labels"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class LexGlueEURLexLoader(HFLoader): + """LexGLUE EuroVoc — text → space-separated EuroVoc concept IDs.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["labels"] = df["labels"].apply(lambda v: " ".join(str(x) for x in _coerce_to_list(v))) + keep = ["text", "labels"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class BeaverTailsLoader(HFLoader): + """BeaverTails safety — response + prompt → is_safe binary, category string.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + if "category" in df.columns and df["category"].dtype == object: + # category is a dict of {label: bool} — find active labels + df["category_labels"] = df["category"].apply( + lambda c: " ".join(k for k, v in c.items() if v) if isinstance(c, dict) else str(c) + ) + keep = ( + ["prompt", "response", "is_safe", "category_labels"] + if "category_labels" in df.columns + else ["prompt", "response", "is_safe"] + ) + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) diff --git a/ludwig/datasets/loaders/multiple_choice_loader.py b/ludwig/datasets/loaders/multiple_choice_loader.py new file mode 100644 index 0000000..1c11171 --- /dev/null +++ b/ludwig/datasets/loaders/multiple_choice_loader.py @@ -0,0 +1,167 @@ +"""Base loader for multiple-choice QA datasets. + +Multiple-choice datasets store answer options as a list or dict. +This loader linearizes them into a single ``choices_text`` column +(format: "A: ... B: ... C: ... D: ...") alongside the question, +and maps the correct answer to a letter label (A/B/C/D/E). +""" + +from __future__ import annotations + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + +_LABELS = list("ABCDEFGHIJ") + + +def _linearize_choices(choices) -> str: + """Convert a list or dict of choices to 'A: x B: y ...' string.""" + if isinstance(choices, dict): + # Format: {'text': [...], 'label': [...]} (ARC, OpenBookQA) + texts = choices.get("text", choices.get("choices", [])) + labels = choices.get("label", _LABELS[: len(texts)]) + return " ".join(f"{lbl}: {t}" for lbl, t in zip(labels, texts)) + if hasattr(choices, "__iter__") and not isinstance(choices, str): + return " ".join(f"{_LABELS[i]}: {t}" for i, t in enumerate(choices)) + return str(choices) + + +def _answer_to_int(answer, choices) -> int: + """Convert an answer label/index to 0-based int.""" + if isinstance(answer, int): + return answer + s = str(answer).strip() + if s in _LABELS: + return _LABELS.index(s) + # try numeric string + try: + return int(s) + except ValueError: + pass + # try matching against choices text + if hasattr(choices, "__iter__") and not isinstance(choices, (str, dict)): + for i, c in enumerate(choices): + if str(c).strip() == s: + return i + return 0 + + +class MultipleChoiceLoader(HFLoader): + """Flatten multiple-choice QA datasets into question + choices_text + label columns. + + Subclasses must set: + question_col – column with the question text + choices_col – column with the answer choices (list or dict) + answer_col – column with the correct answer (letter or index) + context_col – optional additional context column (or None) + """ + + question_col: str = "question" + choices_col: str = "choices" + answer_col: str = "answer" + context_col: str | None = None + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["choices_text"] = df[self.choices_col].apply(_linearize_choices) + + # Map answer to letter (A/B/C/D) + def _to_letter(row): + idx = _answer_to_int(row[self.answer_col], row.get(self.choices_col)) + return _LABELS[idx] if idx < len(_LABELS) else "A" + + df["answer_label"] = df.apply(_to_letter, axis=1) + + keep = [] + if self.context_col and self.context_col in df.columns: + keep.append(self.context_col) + keep += [self.question_col, "choices_text", "answer_label"] + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +# ── Per-dataset subclasses ──────────────────────────────────────────────────── + + +class HellaSwagLoader(MultipleChoiceLoader): + question_col = "ctx" + choices_col = "endings" + answer_col = "label" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["choices_text"] = df["endings"].apply(_linearize_choices) + + def _to_letter(label): + try: + return _LABELS[int(label)] + except (ValueError, IndexError): + return "A" + + df["answer_label"] = df["label"].apply(_to_letter) + keep = ["activity_label", "ctx", "choices_text", "answer_label"] + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + +class CommonsenseQALoader(MultipleChoiceLoader): + question_col = "question" + choices_col = "choices" + answer_col = "answerKey" + + +class ArcLoader(MultipleChoiceLoader): + question_col = "question" + choices_col = "choices" + answer_col = "answerKey" + + +class OpenBookQALoader(MultipleChoiceLoader): + question_col = "question_stem" + choices_col = "choices" + answer_col = "answerKey" + context_col = None + + +class MmluLoader(MultipleChoiceLoader): + question_col = "question" + choices_col = "choices" + answer_col = "answer" + + +class MmluProLoader(MultipleChoiceLoader): + question_col = "question" + choices_col = "options" + answer_col = "answer" + context_col = "cot_content" + + +class ScienceQALoader(MultipleChoiceLoader): + question_col = "question" + choices_col = "choices" + answer_col = "answer" + context_col = "lecture" + + +class BbhLoader(HFLoader): + """Big-Bench Hard — input/target are plain strings, no choices.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + keep = ["input", "target"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) diff --git a/ludwig/datasets/loaders/naval.py b/ludwig/datasets/loaders/naval.py new file mode 100644 index 0000000..37c8163 --- /dev/null +++ b/ludwig/datasets/loaders/naval.py @@ -0,0 +1,23 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class NavalLoader(DatasetLoader): + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + return pd.read_csv(file_path, header=None, sep=" ") diff --git a/ludwig/datasets/loaders/ner_loader.py b/ludwig/datasets/loaders/ner_loader.py new file mode 100644 index 0000000..21cee66 --- /dev/null +++ b/ludwig/datasets/loaders/ner_loader.py @@ -0,0 +1,175 @@ +"""Base loader for token-level NER / sequence-labeling datasets. + +HuggingFace NER datasets store tokens and tags as lists (or numpy arrays). +This loader joins them into space-separated strings that Ludwig's +text/sequence features expect. +""" + +from __future__ import annotations + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +def _to_str_list(val) -> list[str]: + """Coerce any iterable (list, tuple, numpy array) to a list of strings.""" + if hasattr(val, "__iter__") and not isinstance(val, str): + return [str(v) for v in val] + return str(val).split() + + +def _to_int_list(val) -> list[int]: + if hasattr(val, "__iter__") and not isinstance(val, str): + return [int(v) for v in val] + s = str(val).strip().strip("[]") + return [int(t) for t in s.split() if t] + + +class NERLoader(HFLoader): + """Convert token/tag lists to space-separated strings. + + Subclasses set: + tokens_col – column with the token list + tags_col – column with integer tag indices + tag_labels – list mapping index → string label + out_sentence_col – output column name for the sentence + out_tags_col – output column name for the tag sequence + """ + + tokens_col: str = "tokens" + tags_col: str = "ner_tags" + tag_labels: list[str] = [] + out_sentence_col: str = "sentence" + out_tags_col: str = "ner_tags" + + def _map_tag(self, t: int) -> str: + if self.tag_labels and t < len(self.tag_labels): + return self.tag_labels[t] + return str(t) + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df[self.out_sentence_col] = df[self.tokens_col].apply(lambda v: " ".join(_to_str_list(v))) + df[self.out_tags_col] = df[self.tags_col].apply(lambda v: " ".join(self._map_tag(t) for t in _to_int_list(v))) + keep = [self.out_sentence_col, self.out_tags_col] + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +# ── Per-dataset subclasses ──────────────────────────────────────────────────── + +_WIKIANN_TAGS = ["O", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC"] + + +class WikiannEnLoader(NERLoader): + tags_col = "ner_tags" + tag_labels = _WIKIANN_TAGS + + +class WikiannDeLoader(NERLoader): + tags_col = "ner_tags" + tag_labels = _WIKIANN_TAGS + + +class WikiannZhLoader(NERLoader): + tags_col = "ner_tags" + tag_labels = _WIKIANN_TAGS + + +# MultiNERD: 31-class NER (PER, ORG, LOC, ANIM, BIO, CEL, DIS, EVE, FOOD, +# INST, MEDIA, MYTH, PLANT, TIME, VEHI + B-/I- prefixes + O) +_MULTINERD_TAGS = [ + "O", + "B-PER", + "I-PER", + "B-ORG", + "I-ORG", + "B-LOC", + "I-LOC", + "B-ANIM", + "I-ANIM", + "B-BIO", + "I-BIO", + "B-CEL", + "I-CEL", + "B-DIS", + "I-DIS", + "B-EVE", + "I-EVE", + "B-FOOD", + "I-FOOD", + "B-INST", + "I-INST", + "B-MEDIA", + "I-MEDIA", + "B-MYTH", + "I-MYTH", + "B-PLANT", + "I-PLANT", + "B-TIME", + "I-TIME", + "B-VEHI", + "I-VEHI", +] + + +class MultiNERDLoader(NERLoader): + tag_labels = _MULTINERD_TAGS + + +# FewNERD: fine-grained NER — tags are already string labels in this dataset, +# but can also be integer indices. We'll keep them as-is strings. +class FewNERDLoader(HFLoader): + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["sentence"] = df["tokens"].apply(lambda v: " ".join(_to_str_list(v))) + df["ner_tags"] = df["ner_tags"].apply(lambda v: " ".join(str(t) for t in _to_int_list(v))) + keep = ["sentence", "ner_tags"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class AcronymIdentificationLoader(NERLoader): + tags_col = "labels" + tag_labels = ["O", "B-long", "I-long", "B-short", "I-short"] + out_tags_col = "labels" + + +class PIIMaskingLoader(HFLoader): + """PII Masking — keep source_text and mbert_bio_labels (space-joined).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["bio_labels"] = df["mbert_bio_labels"].apply(lambda v: " ".join(_to_str_list(v))) + keep = ["source_text", "bio_labels"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class WinobiasLoader(NERLoader): + """WinoBias — tokens → sentence + ner_tags (space-joined int tags).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["sentence"] = df["tokens"].apply(lambda v: " ".join(_to_str_list(v))) + df["ner_tags"] = df["ner_tags"].apply(lambda v: " ".join(str(t) for t in _to_int_list(v))) + keep = ["sentence", "ner_tags"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] diff --git a/ludwig/datasets/loaders/newyorker_caption_contest.py b/ludwig/datasets/loaders/newyorker_caption_contest.py new file mode 100644 index 0000000..42d87a3 --- /dev/null +++ b/ludwig/datasets/loaders/newyorker_caption_contest.py @@ -0,0 +1,63 @@ +import os + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + +_SPLITS = {"train": 0, "validation": 1, "test": 2} + + +class NewYorkerCaptionContestLoader(HFLoader): + """New Yorker Caption Contest (matching config) — multimodal image+text classification. + + The HuggingFace dataset stores images as PIL objects. This loader saves them + as JPEG files under ``/images/`` (with caching) and + returns a DataFrame with absolute ``image_path`` and ``image_description`` + columns alongside a ``label`` target column. + """ + + def _save_images(self, df: pd.DataFrame, split_name: str) -> pd.Series: + """Save PIL images to disk and return a Series of absolute paths.""" + img_dir = os.path.join(self.processed_dataset_dir, "images", split_name) + os.makedirs(img_dir, exist_ok=True) + + paths = [] + for idx, row in df.iterrows(): + img_path = os.path.join(img_dir, f"{idx}.jpg") + if not os.path.exists(img_path): + img = row["image"] + if hasattr(img, "save"): + img.save(img_path, format="JPEG") + elif isinstance(img, dict) and "bytes" in img: + import io + + from PIL import Image + + Image.open(io.BytesIO(img["bytes"])).convert("RGB").save(img_path, format="JPEG") + paths.append(img_path) + return pd.Series(paths, index=df.index) + + def _transform(self, df: pd.DataFrame, split_name: str = "train") -> pd.DataFrame: + df = df.copy().reset_index(drop=True) + df["image_path"] = self._save_images(df, split_name) + keep = ["image_path", "image_description", "label"] + if "split" in df.columns: + keep.append("split") + return df[keep] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return ( + self._transform(train, "train"), + self._transform(val, "validation"), + self._transform(test, "test"), + ) + df = super().load(split=False) + # super() adds integer split column; reconstruct per-split name for image dirs + result_parts = [] + for split_int, split_name in [(0, "train"), (1, "validation"), (2, "test")]: + part = df[df["split"] == split_int] + if not part.empty: + result_parts.append(self._transform(part, split_name)) + return pd.concat(result_parts).reset_index(drop=True) diff --git a/ludwig/datasets/loaders/openml_loader.py b/ludwig/datasets/loaders/openml_loader.py new file mode 100644 index 0000000..1070d4d --- /dev/null +++ b/ludwig/datasets/loaders/openml_loader.py @@ -0,0 +1,195 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from __future__ import annotations + +import logging +import os + +import numpy as np +import pandas as pd + +from ludwig.constants import SPLIT +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + +logger = logging.getLogger(__name__) + + +class OpenMLLoader(DatasetLoader): + """Loads any OpenML task by task ID. + + Unlike other DatasetLoaders, OpenMLLoader bypasses the usual download/extract/transform pipeline and fetches + data directly via the OpenML Python API. The processed dataset is cached as a Parquet file so that subsequent + calls skip the network round-trip. + + The task ID can be provided either in the DatasetConfig (``config.openml_task_id``) or passed directly to + :meth:`load` via the ``openml_task_id`` keyword argument (which takes precedence). + """ + + def load( + self, openml_task_id: int | None = None, split: bool = False + ) -> pd.DataFrame | tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """Load an OpenML task, downloading and caching it if necessary. + + :param openml_task_id: (int, optional) OpenML task ID. Overrides ``self.config.openml_task_id`` when given. + :param split: (bool) If True, return a 3-tuple of (train_df, val_df, test_df). If False (default), return a + single DataFrame with a ``split`` column (0=train, 1=validation, 2=test). + :returns: A single DataFrame or a 3-tuple of DataFrames depending on the value of ``split``. + """ + task_id = openml_task_id if openml_task_id is not None else self.config.openml_task_id + if task_id is None: + raise ValueError("No OpenML task ID provided. Set config.openml_task_id or pass openml_task_id= to load().") + + if os.path.exists(self.processed_dataset_path): + # Validate that the cached file was written for this task ID. + import pyarrow.parquet as pq + + cached_meta = pq.read_metadata(self.processed_dataset_path).metadata or {} + cached_task_id = cached_meta.get(b"openml_task_id", b"").decode() + if cached_task_id and int(cached_task_id) != int(task_id): + logger.warning(f"Cached file was for task {cached_task_id}, but requested task {task_id}. Re-fetching.") + os.remove(self.processed_dataset_path) + df = self._fetch_and_cache(task_id) + else: + logger.info(f"Loading cached OpenML task {task_id} from {self.processed_dataset_path}") + df = pd.read_parquet(self.processed_dataset_path) + else: + df = self._fetch_and_cache(task_id) + + if split: + df[SPLIT] = pd.to_numeric(df[SPLIT]) + train_df = df[df[SPLIT] == 0].drop(columns=[SPLIT]).reset_index(drop=True) + val_df = df[df[SPLIT] == 1].drop(columns=[SPLIT]).reset_index(drop=True) + test_df = df[df[SPLIT] == 2].drop(columns=[SPLIT]).reset_index(drop=True) + return train_df, val_df, test_df + return df + + def _fetch_and_cache(self, task_id: int) -> pd.DataFrame: + """Download the OpenML task, build a DataFrame with a split column, and save it to Parquet. + + :param task_id: (int) OpenML task ID. + :returns: The combined DataFrame (with ``split`` column). + """ + try: + import openml + except ImportError: + raise ImportError("openml package is required: pip install openml") + + logger.info(f"Fetching OpenML task {task_id} …") + task = openml.tasks.get_task(task_id) + dataset = task.get_dataset() + + X, y, categorical_indicator, attribute_names = dataset.get_data(target=task.target_name) + + # Newer openml versions already return X as a pd.DataFrame; older versions return a numpy array. + if isinstance(X, pd.DataFrame): + df = X.copy() + else: + df = pd.DataFrame(X, columns=attribute_names if attribute_names else list(range(X.shape[1]))) + + # Ensure the target column is present under its original name. + target_col = task.target_name if task.target_name else "target" + if isinstance(y, pd.Series): + df[target_col] = y.values + else: + df[target_col] = y + + df = self._assign_splits(df, task) + + os.makedirs(self.processed_dataset_dir, exist_ok=True) + logger.info(f"Saving processed OpenML task {task_id} to {self.processed_dataset_path}") + import pyarrow as pa + import pyarrow.parquet as pq + + table = pa.Table.from_pandas(df, preserve_index=False) + existing_meta = table.schema.metadata or {} + table = table.replace_schema_metadata({**existing_meta, b"openml_task_id": str(task_id).encode()}) + pq.write_table(table, self.processed_dataset_path) + return df + + @staticmethod + def _assign_splits(df: pd.DataFrame, task) -> pd.DataFrame: + """Assign a ``split`` column (0=train, 1=val, 2=test) to *df*. + + Tries to use the OpenML task's predefined train/test split (fold 0). If the split is unavailable, falls back to + a random 70/10/20 split. + + :param df: Combined feature + target DataFrame (no split column yet). + :param task: OpenML task object. + :returns: The same DataFrame with a ``split`` column added. + """ + n = len(df) + split_col = np.full(n, 0, dtype=np.int8) # Default: all train + + try: + train_indices, test_indices = task.get_train_test_split_indices(fold=0) + split_col[test_indices] = 2 + + # Carve a validation set out of the training indices (≈12.5 % of train → ~10 % of total). + rng = np.random.default_rng(seed=42) + val_size = max(1, int(0.125 * len(train_indices))) + val_indices = rng.choice(train_indices, size=val_size, replace=False) + split_col[val_indices] = 1 + + logger.info( + f"Used OpenML predefined split: {(split_col == 0).sum()} train, " + f"{(split_col == 1).sum()} val, {(split_col == 2).sum()} test." + ) + except Exception as exc: + logger.warning(f"Could not retrieve OpenML predefined split ({exc}). Falling back to random 70/10/20.") + rng = np.random.default_rng(seed=42) + indices = rng.permutation(n) + train_end = int(0.70 * n) + val_end = int(0.80 * n) + split_col[indices[train_end:val_end]] = 1 + split_col[indices[val_end:]] = 2 + + df = df.copy() + df[SPLIT] = split_col + return df + + +def openml_suite_loaders(suite_id: int, cache_dir: str | None = None) -> list[OpenMLLoader]: + """Return a list of :class:`OpenMLLoader` instances for all tasks in an OpenML benchmark suite. + + Well-known suite IDs: + + * **99** – OpenML-CC18 (72 classification tasks) + * **269** – OpenML regression suite + * **271** – OpenML-CTR23 (35 regression tasks) + * **337** – OpenML-AutoML (classification) + + :param suite_id: (int) OpenML benchmark suite (study) ID. + :param cache_dir: (str, optional) Directory used to cache downloaded datasets. Defaults to the Ludwig cache + location when *None*. + :returns: A list of :class:`OpenMLLoader` instances, one per task in the suite. + """ + try: + import openml + except ImportError: + raise ImportError("openml package is required: pip install openml") + + suite = openml.study.get_suite(suite_id) + loaders: list[OpenMLLoader] = [] + for task_id in suite.tasks: + config = DatasetConfig( + name=f"openml_task_{task_id}", + version="1", + description=f"OpenML task {task_id} from suite {suite_id}", + openml_task_id=task_id, + loader="openml_loader.OpenMLLoader", + ) + loaders.append(OpenMLLoader(config=config, cache_dir=cache_dir)) + return loaders diff --git a/ludwig/datasets/loaders/qa_loader.py b/ludwig/datasets/loaders/qa_loader.py new file mode 100644 index 0000000..10b14ff --- /dev/null +++ b/ludwig/datasets/loaders/qa_loader.py @@ -0,0 +1,172 @@ +"""Loaders for extractive and open-domain QA datasets. + +Most HF QA datasets store answers as ``{'text': [...], 'answer_start': [...]}``. +These loaders flatten that to a single string (first answer text, or empty string +for unanswerable questions). +""" + +from __future__ import annotations + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +def _extract_first_answer(answers) -> str: + """Return the first answer text from various answer formats.""" + if isinstance(answers, dict): + texts = answers.get("text", answers.get("answer", [])) + if texts: + first = texts[0] if isinstance(texts, list) else texts + return str(first) + return "" + if hasattr(answers, "__iter__") and not isinstance(answers, str): + items = list(answers) + return str(items[0]) if items else "" + return str(answers) + + +class SquadLoader(HFLoader): + """SQuAD v1/v2 extractive QA — context + question → answer text.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["answer"] = df["answers"].apply(_extract_first_answer) + # For SQuAD v2 unanswerable questions, answer is "" + keep = ["context", "question", "answer"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class SquadV2Loader(SquadLoader): + pass + + +class DuoRCLoader(HFLoader): + """DuoRC SelfRC — plot + question → answer (first from list).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["answer"] = df["answers"].apply(_extract_first_answer) + keep = ["plot", "question", "answer"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class HotpotQALoader(HFLoader): + """HotpotQA — question → answer (ignore supporting facts for simplicity).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + keep = ["question", "answer"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class TriviaQALoader(HFLoader): + """TriviaQA — question → first answer alias.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["answer_text"] = df["answer"].apply( + lambda a: str(a.get("value", "")) if isinstance(a, dict) else _extract_first_answer(a) + ) + keep = ["question", "answer_text"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class PubMedQALoader(HFLoader): + """PubMedQA — context (joined) + question → yes/no/maybe label.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # context is a dict with 'contexts' list + df["context_text"] = df["context"].apply( + lambda c: " ".join(c.get("contexts", [])) if isinstance(c, dict) else str(c) + ) + keep = ["context_text", "question", "final_decision"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class FeverGoldLoader(HFLoader): + """FEVER gold evidence — claim → verdict (SUPPORTS/REFUTES/NEI).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # evidence is a list of lists — just keep the claim and label + keep = ["claim", "label"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class NaturalQuestionsLoader(HFLoader): + """Natural Questions simplified — question + short answer.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # NQ has very complex nested structure; use just question_text and annotations + if "question" in df.columns and isinstance(df["question"].iloc[0], dict): + df["question_text"] = df["question"].apply(lambda q: q.get("text", "") if isinstance(q, dict) else str(q)) + elif "question_text" in df.columns: + pass + else: + df["question_text"] = df.get("question", df.iloc[:, 0]).astype(str) + + if "annotations" in df.columns: + + def _nq_answer(a): + if not isinstance(a, dict): + return "" + short = a.get("short_answers", []) + if not short: + return "" + first = short[0] + if not first: + return "" + texts = first.get("text", []) if isinstance(first, dict) else [] + return texts[0] if texts else "" + + df["answer_text"] = df["annotations"].apply(_nq_answer) + else: + df["answer_text"] = "" + + keep = ["question_text", "answer_text"] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) diff --git a/ludwig/datasets/loaders/rossman_store_sales.py b/ludwig/datasets/loaders/rossman_store_sales.py new file mode 100644 index 0000000..8cd1590 --- /dev/null +++ b/ludwig/datasets/loaders/rossman_store_sales.py @@ -0,0 +1,102 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import calendar +import os + +import numpy as np +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class RossmanStoreSalesLoader(DatasetLoader): + """The Rossmann Store Sales dataset.""" + + def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame: + """Load dataset files into a dataframe.""" + + stores_df = pd.read_csv(os.path.join(self.raw_dataset_dir, "store.csv")) + + train_df = pd.read_csv(os.path.join(self.raw_dataset_dir, "train.csv"), low_memory=False) + train_df = preprocess_df(train_df, stores_df) + + train_df["split"] = -1 + train_df.loc[train_df["Year"] == 2014, "split"] = 0 + train_df.loc[train_df["Year"] == 2015, "split"] = 2 + train_df.drop(train_df[train_df["split"] == -1].index, inplace=True) + return train_df + + +def preprocess_dates(df): + # Make integer Year,Month,Day columns instead of Date + dates = np.array([[int(v) for v in s.split("-")] for s in df["Date"]]) + df = df.drop(["Date"], axis=1) + df["Year"] = dates[:, 0] + df["Month"] = dates[:, 1] + df["Day"] = dates[:, 2] + return df + + +month_abbrs = calendar.month_abbr[1:] +month_abbrs[8] = "Sept" + + +def preprocess_stores(df, stores_df): + # join data in df with stores df + df = df.join(stores_df, on="Store", rsuffix="_right") + df = df.drop(["Store_right"], axis=1) + + promo2_start_months = [(s.split(",") if not pd.isnull(s) else []) for s in df["PromoInterval"]] + + for month_abbr in month_abbrs: + df["Promo2Start_" + month_abbr] = np.array( + [(1 if month_abbr in s else 0) for s in promo2_start_months], dtype=np.int8 + ) + df = df.drop(["PromoInterval"], axis=1) + + return df + + +int_columns = [ + "Store", + "DayOfWeek", + "Sales", + "Customers", + "Open", + "Promo", + "SchoolHoliday", + "Year", + "Month", + "Day", + "CompetitionDistance", + "CompetitionOpenSinceMonth", + "CompetitionOpenSinceYear", + "Promo2", + "Promo2SinceWeek", + "Promo2SinceYear", +] + + +def preprocess_df(df, stores_df): + df = preprocess_dates(df) + df = preprocess_stores(df, stores_df) + + for column in int_columns: + df[column] = pd.to_numeric(df[column].fillna(0), downcast="integer") + + df["StateHoliday"] = df["StateHoliday"].astype(str) + df.loc[df["StateHoliday"] == "0", "StateHoliday"] = "No" + + return df diff --git a/ludwig/datasets/loaders/santander_value_prediction.py b/ludwig/datasets/loaders/santander_value_prediction.py new file mode 100644 index 0000000..01c8a1c --- /dev/null +++ b/ludwig/datasets/loaders/santander_value_prediction.py @@ -0,0 +1,31 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class SantanderValuePredictionLoader(DatasetLoader): + """The Santander Value Prediction Challenge dataset. + + https://www.kaggle.com/c/santander-value-prediction-challenge + """ + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + # Ensure feature column names are strings (some are numeric); keep special names as is + processed_df.columns = ["C" + str(col) for col in processed_df.columns] + processed_df.rename(columns={"CID": "ID", "Ctarget": "target", "Csplit": "split"}, inplace=True) + return processed_df diff --git a/ludwig/datasets/loaders/sarcastic_headlines.py b/ludwig/datasets/loaders/sarcastic_headlines.py new file mode 100644 index 0000000..dac6f8b --- /dev/null +++ b/ludwig/datasets/loaders/sarcastic_headlines.py @@ -0,0 +1,24 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import pandas as pd + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class SarcasticHeadlinesLoader(DatasetLoader): + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + return pd.read_json(file_path, lines=True) diff --git a/ludwig/datasets/loaders/sarcos.py b/ludwig/datasets/loaders/sarcos.py new file mode 100644 index 0000000..1fc68f4 --- /dev/null +++ b/ludwig/datasets/loaders/sarcos.py @@ -0,0 +1,61 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd +from scipy.io import loadmat + +from ludwig.datasets.loaders.dataset_loader import DatasetLoader +from ludwig.utils.fs_utils import open_file + + +class SarcosLoader(DatasetLoader): + """The Sarcos dataset. + + Details: + The data relates to an inverse dynamics problem for a seven + degrees-of-freedom SARCOS anthropomorphic robot arm. The + task is to map from a 21-dimensional input space (7 joint + positions, 7 joint velocities, 7 joint accelerations) to the + corresponding 7 joint torques. There are 44,484 training + examples and 4,449 test examples. The first 21 columns are + the input variables, and the 22nd column is used as the target + variable. + + Dataset source: + Locally Weighted Projection RegressionL: An O(n) Algorithm for + Incremental Real Time Learning in High Dimensional Space, + S. Vijayakumar and S. Schaal, Proc ICML 2000. + http://www.gaussianprocess.org/gpml/data/ + """ + + def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame: + """Loads a file into a dataframe.""" + with open_file(file_path) as f: + mat = loadmat(f) + file_df = pd.DataFrame(mat[os.path.basename(file_path).split(".")[0]]) + return file_df + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + processed_df = super().transform_dataframe(dataframe) + columns = [] + columns += [f"position_{i}" for i in range(1, 8)] + columns += [f"velocity_{i}" for i in range(1, 8)] + columns += [f"acceleration_{i}" for i in range(1, 8)] + columns += [f"torque_{i}" for i in range(1, 8)] + columns += ["split"] + + processed_df.columns = columns + return processed_df diff --git a/ludwig/datasets/loaders/split_loaders.py b/ludwig/datasets/loaders/split_loaders.py new file mode 100644 index 0000000..f83f605 --- /dev/null +++ b/ludwig/datasets/loaders/split_loaders.py @@ -0,0 +1,34 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import numpy as np +import pandas as pd + +from ludwig.constants import SPLIT +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class RandomSplitLoader(DatasetLoader): + """Adds a random split column to the dataset, with fixed proportions of: + + train: 70% + validation: 10% + test: 20% + . + """ + + def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame: + df = super().transform_dataframe(dataframe) + df[SPLIT] = np.random.choice(3, len(df), p=(0.7, 0.1, 0.2)).astype(np.int8) + return df diff --git a/ludwig/datasets/loaders/sst.py b/ludwig/datasets/loaders/sst.py new file mode 100644 index 0000000..b9ad223 --- /dev/null +++ b/ludwig/datasets/loaders/sst.py @@ -0,0 +1,382 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os + +import pandas as pd + +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + + +class SSTLoader(DatasetLoader): + """The SST dataset. + + This dataset is constructed using the Stanford Sentiment Treebank Dataset. + This dataset contains binary labels (positive or negative) for each sample. + + The original dataset specified 5 labels: + very negative, negative, neutral, positive, very positive with + the following cutoffs: + [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0] + """ + + def __init__( + self, + config: DatasetConfig, + cache_dir: str | None = None, + include_subtrees=False, + discard_neutral=False, + convert_parentheses=True, + remove_duplicates=False, + ): + super().__init__(config, cache_dir=cache_dir) + self.include_subtrees = include_subtrees + self.discard_neutral = discard_neutral + self.convert_parentheses = convert_parentheses + self.remove_duplicates = remove_duplicates + + @staticmethod + def get_sentiment_label(id2sent, phrase_id): + raise NotImplementedError + + def transform_files(self, file_paths: list[str]) -> list[str]: + # maybe this should be + + """Load dataset files into a dataframe.""" + sentences_df = pd.read_csv( + os.path.join(self.raw_dataset_dir, "stanfordSentimentTreebank/datasetSentences.txt"), + sep="\t", + ) + + sentences_df["sentence"] = sentences_df["sentence"].apply(format_text) + + datasplit_df = pd.read_csv( + os.path.join(self.raw_dataset_dir, "stanfordSentimentTreebank/datasetSplit.txt"), sep="," + ) + + phrase2id = {} + with open(os.path.join(self.raw_dataset_dir, "stanfordSentimentTreebank/dictionary.txt")) as f: + Lines = f.readlines() + for line in Lines: + if line: + split_line = line.split("|") + phrase = split_line[0] + phrase2id[phrase] = int(split_line[1]) + + id2sent = {} + with open(os.path.join(self.raw_dataset_dir, "stanfordSentimentTreebank/sentiment_labels.txt")) as f: + Lines = f.readlines() + for line in Lines: + if line: + split_line = line.split("|") + try: + id2sent[int(split_line[0])] = float(split_line[1]) + except ValueError: + pass + + trees_pointers = None + trees_phrases = None + + if self.include_subtrees: + trees_pointers = [] + with open(os.path.join(self.raw_dataset_dir, "stanfordSentimentTreebank/STree.txt")) as f: + Lines = f.readlines() + for line in Lines: + if line: + trees_pointers.append([int(s.strip()) for s in line.split("|")]) + + trees_phrases = [] + with open(os.path.join(self.raw_dataset_dir, "stanfordSentimentTreebank/SOStr.txt")) as f: + Lines = f.readlines() + for line in Lines: + if line: + trees_phrases.append([s.strip() for s in line.split("|")]) + + splits = {"train": 1, "test": 2, "dev": 3} + + generated_csv_filenames = [] + for split_name, split_id in splits.items(): + sentence_idcs = get_sentence_idcs_in_split(datasplit_df, split_id) + + pairs = [] + if split_name == "train" and self.include_subtrees: + phrases = [] + for sentence_idx in sentence_idcs: + # trees_pointers and trees_phrases are 0 indexed + # while sentence_idx starts from 1 + # so we need to decrease sentence_idx value + sentence_idx -= 1 + subtrees = sentence_subtrees(sentence_idx, trees_pointers, trees_phrases) + + sentence_idx += 1 + sentence_phrase = list(sentences_df[sentences_df["sentence_index"] == sentence_idx]["sentence"])[0] + + sentence_phrase = convert_parentheses(sentence_phrase) + label = self.get_sentiment_label(id2sent, phrase2id[sentence_phrase]) + # filter @ sentence level + # For SST-2, check subtrees only if sentence is not neutral + if not self.discard_neutral or label != -1: + for phrase in subtrees: + label = self.get_sentiment_label(id2sent, phrase2id[phrase]) + if not self.discard_neutral or label != -1: + if not self.convert_parentheses: + phrase = convert_parentheses_back(phrase) + phrase = phrase.replace("\xa0", " ") + pairs.append([phrase, label]) + else: + phrases = get_sentences_with_idcs(sentences_df, sentence_idcs) + for phrase in phrases: + phrase = convert_parentheses(phrase) + label = self.get_sentiment_label(id2sent, phrase2id[phrase]) + if not self.discard_neutral or label != -1: + if not self.convert_parentheses: + phrase = convert_parentheses_back(phrase) + phrase = phrase.replace("\xa0", " ") + pairs.append([phrase, label]) + + final_csv = pd.DataFrame(pairs) + final_csv.columns = ["sentence", "label"] + if self.remove_duplicates: + final_csv = final_csv.drop_duplicates(subset=["sentence"]) + csv_filename = os.path.join(self.raw_dataset_dir, f"{split_name}.csv") + generated_csv_filenames.append(csv_filename) + final_csv.to_csv(csv_filename, index=False) + + return super().transform_files(generated_csv_filenames) + + +class SST2Loader(SSTLoader): + """The SST2 dataset. + + This dataset is constructed using the Stanford Sentiment Treebank Dataset. + This dataset contains binary labels (positive or negative) for each sample. + + The original dataset specified 5 labels: + very negative, negative, neutral, positive, very positive with + the following cutoffs: + [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0] + + In the construction of this dataset, we remove all neutral phrases + and assign a negative label if the original rating falls + into the following range: [0, 0.4] and a positive label + if the original rating is between (0.6, 1.0]. + """ + + def __init__( + self, + config: DatasetConfig, + cache_dir: str | None = None, + include_subtrees=False, + convert_parentheses=True, + remove_duplicates=False, + ): + super().__init__( + config, + cache_dir=cache_dir, + include_subtrees=include_subtrees, + discard_neutral=True, + convert_parentheses=convert_parentheses, + remove_duplicates=remove_duplicates, + ) + + def get_sentiment_label(self, id2sent, phrase_id): + sentiment = id2sent[phrase_id] + if sentiment <= 0.4: # negative + return 0 + elif sentiment > 0.6: # positive + return 1 + return -1 # neutral + + +class SST3Loader(SSTLoader): + """The SST3 dataset. + + This dataset is constructed using the Stanford Sentiment Treebank Dataset. + This dataset contains five labels (very negative, negative, neutral, + positive, very positive) for each sample. + + In the original dataset, the 5 labels: very negative, negative, neutral, positive, + and very positive have the following cutoffs: + [0, 0.4], (0.4, 0.6], (0.6, 1.0] + + This class pulls in an array of mixins for different types of functionality + which belongs in the workflow for ingesting and transforming + training data into a destination dataframe that can be use by Ludwig. + """ + + def __init__( + self, + config: DatasetConfig, + cache_dir: str | None = None, + include_subtrees=False, + convert_parentheses=True, + remove_duplicates=False, + ): + super().__init__( + config, + cache_dir=cache_dir, + include_subtrees=include_subtrees, + convert_parentheses=convert_parentheses, + remove_duplicates=remove_duplicates, + ) + + def get_sentiment_label(self, id2sent, phrase_id): + sentiment = id2sent[phrase_id] + if sentiment <= 0.4: + return "negative" + elif sentiment <= 0.6: + return "neutral" + elif sentiment <= 1.0: + return "positive" + return "neutral" + + +class SST5Loader(SSTLoader): + """The SST5 dataset. + + This dataset is constructed using the Stanford Sentiment Treebank Dataset. + This dataset contains five labels (very negative, negative, neutral, + positive, very positive) for each sample. + + In the original dataset, the 5 labels: very negative, negative, neutral, positive, + and very positive have the following cutoffs: + [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0] + + This class pulls in an array of mixins for different types of functionality + which belongs in the workflow for ingesting and transforming + training data into a destination dataframe that can be use by Ludwig. + """ + + def __init__( + self, + config: DatasetConfig, + cache_dir: str | None = None, + include_subtrees=False, + convert_parentheses=True, + remove_duplicates=False, + ): + super().__init__( + config, + cache_dir=cache_dir, + include_subtrees=include_subtrees, + convert_parentheses=convert_parentheses, + remove_duplicates=remove_duplicates, + ) + + def get_sentiment_label(self, id2sent, phrase_id): + sentiment = id2sent[phrase_id] + if sentiment <= 0.2: + return "very_negative" + elif sentiment <= 0.4: + return "negative" + elif sentiment <= 0.6: + return "neutral" + elif sentiment <= 0.8: + return "positive" + elif sentiment <= 1.0: + return "very_positive" + return "neutral" + + +def format_text(text: str): + """Formats text by decoding into utf-8.""" + return " ".join([w.encode("latin1").decode("utf-8") for w in text.strip().split(" ")]) + + +def convert_parentheses(text: str): + """Replaces -LRB- and -RRB- tokens present in SST with ( and )""" + return text.replace("-LRB-", "(").replace("-RRB-", ")") + + +def convert_parentheses_back(text: str): + """Replaces ( and ) tokens with -LRB- and -RRB-""" + return text.replace("(", "-LRB-").replace(")", "-RRB-") + + +def get_sentence_idcs_in_split(datasplit: pd.DataFrame, split_id: int): + """Given a dataset split is (1 for train, 2 for test, 3 for dev), returns the set of corresponding sentence + indices in sentences_df.""" + return set(datasplit[datasplit["splitset_label"] == split_id]["sentence_index"]) + + +def get_sentences_with_idcs(sentences: pd.DataFrame, sentences_idcs: set[int]): + """Given a set of sentence indices, returns the corresponding sentences texts in sentences.""" + criterion = sentences["sentence_index"].map(lambda x: x in sentences_idcs) + return sentences[criterion]["sentence"].tolist() + + +def sentence_subtrees(sentence_idx, trees_pointers, trees_phrases): + tree_pointers = trees_pointers[sentence_idx] + tree_phrases = trees_phrases[sentence_idx] + tree = SSTTree(tree_pointers, tree_phrases) + return tree.subtrees() + + +def visit_postorder(node, visit_list): + if node: + visit_postorder(node.left, visit_list) + visit_postorder(node.right, visit_list) + visit_list.append(node.val) + + +class SSTTree: + class Node: + def __init__(self, key, val=None): + self.left = None + self.right = None + self.key = key + self.val = val + + def create_node(self, parent, i): + if self.nodes[i] is not None: + # already created + return + self.nodes[i] = self.Node(i) + + if parent[i] == -1: + # is root + self.root = self.nodes[i] + return + + if self.nodes[parent[i]] is None: + # parent not yet created + self.create_node(parent, parent[i]) + + # assign current node to parent + parent = self.nodes[parent[i]] + if parent.left is None: + parent.left = self.nodes[i] + else: + parent.right = self.nodes[i] + + def create_tree(self, parents, tree_phrases): + n = len(parents) + self.nodes = [None for i in range(n)] + self.root = [None] + for i in range(n): + self.create_node(parents, i) + for i, phrase in enumerate(tree_phrases): + self.nodes[i].val = phrase + for node in self.nodes: + if node.val is None: + node.val = " ".join((node.left.val, node.right.val)) + + def __init__(self, tree_pointers, tree_phrases): + self.create_tree([int(elem) - 1 for elem in tree_pointers], tree_phrases) + + def subtrees(self): + visit_list = [] + visit_postorder(self.root, visit_list) + return visit_list diff --git a/ludwig/datasets/loaders/translation_loader.py b/ludwig/datasets/loaders/translation_loader.py new file mode 100644 index 0000000..6f3944e --- /dev/null +++ b/ludwig/datasets/loaders/translation_loader.py @@ -0,0 +1,159 @@ +"""Base loader for HuggingFace translation datasets. + +HF translation datasets store source and target sentences in a single +``translation`` column as a dict: ``{'en': '...', 'de': '...'}``. +This loader splits that into two text columns named after the language codes. +""" + +from __future__ import annotations + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +class TranslationLoader(HFLoader): + """Flatten HF translation dict → two separate text columns. + + Subclasses set ``source_lang`` and ``target_lang`` as class attributes. + """ + + source_lang: str = "en" + target_lang: str = "de" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + src, tgt = self.source_lang, self.target_lang + + def _extract(row, lang): + val = row.get("translation", row) if isinstance(row, dict) else {} + if isinstance(val, dict): + return str(val.get(lang, "")) + return str(val) + + if "translation" in df.columns: + df[src] = df["translation"].apply(lambda x: _extract(x, src)) + df[tgt] = df["translation"].apply(lambda x: _extract(x, tgt)) + df = df.drop(columns=["translation"]) + + keep = [src, tgt] + (["split"] if "split" in df.columns else []) + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +# ── Per-pair subclasses ─────────────────────────────────────────────────────── + + +class Opus100EnFrLoader(TranslationLoader): + source_lang = "en" + target_lang = "fr" + + +class Opus100EnEsLoader(TranslationLoader): + source_lang = "en" + target_lang = "es" + + +class Wmt14DeEnLoader(TranslationLoader): + source_lang = "de" + target_lang = "en" + + +class Wmt16DeEnLoader(TranslationLoader): + source_lang = "de" + target_lang = "en" + + +class Wmt19DeEnLoader(TranslationLoader): + source_lang = "de" + target_lang = "en" + + +class OpusBooksEnFrLoader(TranslationLoader): + source_lang = "en" + target_lang = "fr" + + +class MultiUNArEnLoader(TranslationLoader): + source_lang = "ar" + target_lang = "en" + + +class EuroparlBgCsLoader(TranslationLoader): + source_lang = "bg" + target_lang = "cs" + + +class EuroparlBgEnLoader(TranslationLoader): + source_lang = "bg" + target_lang = "en" + + +class EuroparlCsEnLoader(TranslationLoader): + source_lang = "cs" + target_lang = "en" + + +class EuroparlDaEnLoader(TranslationLoader): + source_lang = "da" + target_lang = "en" + + +class EuroparlDeEnLoader(TranslationLoader): + source_lang = "de" + target_lang = "en" + + +class EuroparlElEnLoader(TranslationLoader): + source_lang = "el" + target_lang = "en" + + +class EuroparlEnEsLoader(TranslationLoader): + source_lang = "en" + target_lang = "es" + + +class EuroparlEnFrLoader(TranslationLoader): + source_lang = "en" + target_lang = "fr" + + +class EuroparlEnItLoader(TranslationLoader): + source_lang = "en" + target_lang = "it" + + +class EuroparlEnNlLoader(TranslationLoader): + source_lang = "en" + target_lang = "nl" + + +class EuroparlEnPlLoader(TranslationLoader): + source_lang = "en" + target_lang = "pl" + + +class EuroparlEnPtLoader(TranslationLoader): + source_lang = "en" + target_lang = "pt" + + +class EuroparlEnRoLoader(TranslationLoader): + source_lang = "en" + target_lang = "ro" + + +class EuroparlEnSvLoader(TranslationLoader): + source_lang = "en" + target_lang = "sv" + + +class SetimesBgBsLoader(TranslationLoader): + source_lang = "bg" + target_lang = "bs" diff --git a/ludwig/datasets/loaders/vqa_loader.py b/ludwig/datasets/loaders/vqa_loader.py new file mode 100644 index 0000000..d83a9e6 --- /dev/null +++ b/ludwig/datasets/loaders/vqa_loader.py @@ -0,0 +1,184 @@ +"""Loaders for Visual Question Answering datasets with embedded images. + +The Cauldron datasets store images as a list of PIL objects in an ``images`` +column and conversations/questions in a ``texts`` column. This loader saves +the first image to disk (with caching) and flattens the QA text. +""" + +from __future__ import annotations + +import io +import os + +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + + +class CauldronVQALoader(HFLoader): + """Base loader for HuggingFaceM4/the_cauldron VQA subsets. + + Images (list of PIL) → saved as JPEG, first image path returned. + Texts (list of dicts with 'user'/'assistant' keys) → question + answer. + """ + + def _save_images(self, df: pd.DataFrame, split_name: str) -> pd.Series: + img_dir = os.path.join(self.processed_dataset_dir, "images", split_name) + os.makedirs(img_dir, exist_ok=True) + paths = [] + for idx, row in df.iterrows(): + img_path = os.path.join(img_dir, f"{idx}.jpg") + if not os.path.exists(img_path): + images = row.get("images", []) + if hasattr(images, "__iter__") and not isinstance(images, (str, dict)): + img_list = list(images) + if img_list: + img = img_list[0] + self._save_pil(img, img_path) + paths.append(img_path) + return pd.Series(paths, index=df.index) + + @staticmethod + def _save_pil(img, path: str) -> None: + if hasattr(img, "save"): + img.convert("RGB").save(path, format="JPEG") + elif isinstance(img, dict) and "bytes" in img: + from PIL import Image + + Image.open(io.BytesIO(img["bytes"])).convert("RGB").save(path, format="JPEG") + + @staticmethod + def _extract_qa(texts) -> tuple[str, str]: + if hasattr(texts, "__iter__") and not isinstance(texts, (str, dict)): + items = list(texts) + question, answer = "", "" + for item in items: + if isinstance(item, dict): + if "user" in item: + question = str(item["user"]) + if "assistant" in item: + answer = str(item["assistant"]) + return question, answer + return str(texts), "" + + def _transform(self, df: pd.DataFrame, split_name: str = "train") -> pd.DataFrame: + df = df.copy().reset_index(drop=True) + df["image_path"] = self._save_images(df, split_name) + qa = df["texts"].apply(self._extract_qa) + df["question"] = qa.apply(lambda x: x[0]) + df["answer"] = qa.apply(lambda x: x[1]) + keep = ["image_path", "question", "answer"] + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return ( + self._transform(train, "train"), + self._transform(val, "validation"), + self._transform(test, "test"), + ) + df = super().load(split=False) + parts = [] + for split_int, split_name in [(0, "train"), (1, "validation"), (2, "test")]: + part = df[df["split"] == split_int] + if not part.empty: + parts.append(self._transform(part, split_name)) + return pd.concat(parts).reset_index(drop=True) if parts else self._transform(df, "train") + + +class AI2DiagramsLoader(CauldronVQALoader): + pass + + +class TextVQALoader(CauldronVQALoader): + pass + + +class VQAv2Loader(CauldronVQALoader): + pass + + +class DocVQALoader(CauldronVQALoader): + pass + + +class MmmuLoader(HFLoader): + """MMMU — image(s) + question → answer (multiple choice).""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + # MMMU has image_1..image_7 columns; keep first non-null + for img_col in [f"image_{i}" for i in range(1, 8)]: + if img_col in df.columns: + df["image"] = df[img_col] + break + keep = ["question", "options", "answer"] + if "image" in df.columns: + keep = ["image"] + keep + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class MathVistaLoader(HFLoader): + """MathVista — image + question → answer.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + if "choices" in df.columns: + from ludwig.datasets.loaders.multiple_choice_loader import _linearize_choices + + df["choices_text"] = df["choices"].apply(_linearize_choices) + keep = ["image", "question", "answer"] + if "choices_text" in df.columns: + keep.insert(2, "choices_text") + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) + + +class ScienceQAImageLoader(HFLoader): + """ScienceQA — optional image + question + choices → answer letter.""" + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + from ludwig.datasets.loaders.multiple_choice_loader import _LABELS, _linearize_choices + + df["choices_text"] = df["choices"].apply(_linearize_choices) + + def _to_letter(ans): + try: + return _LABELS[int(ans)] + except (ValueError, IndexError, TypeError): + return "A" + + df["answer_label"] = df["answer"].apply(_to_letter) + keep = ["question", "choices_text", "answer_label"] + for col in ["hint", "lecture", "solution"]: + if col in df.columns: + keep.insert(1, col) + break + if "split" in df.columns: + keep.append("split") + return df[[c for c in keep if c in df.columns]] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + return self._transform(super().load(split=False)) diff --git a/ludwig/datasets/loaders/wikiann.py b/ludwig/datasets/loaders/wikiann.py new file mode 100644 index 0000000..9aca6be --- /dev/null +++ b/ludwig/datasets/loaders/wikiann.py @@ -0,0 +1,39 @@ +import pandas as pd + +from ludwig.datasets.loaders.hugging_face import HFLoader + +# WikiANN IOB2 tag vocabulary (integer index → string label). +_NER_LABELS = ["O", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC"] + + +class WikiANNLoader(HFLoader): + """WikiANN English NER dataset. + + The raw HuggingFace dataset stores tokens and ner_tags as Python lists. + Ludwig's text/sequence features expect space-separated strings, so this + loader joins the lists and maps integer NER tag ids to IOB2 label strings. + """ + + def _transform(self, df: pd.DataFrame) -> pd.DataFrame: + df = df.copy() + df["sentence"] = df["tokens"].apply( + lambda toks: ( + " ".join(str(t) for t in toks) if hasattr(toks, "__iter__") and not isinstance(toks, str) else str(toks) + ) + ) + df["ner_tags"] = df["ner_tags"].apply( + lambda tags: ( + " ".join(_NER_LABELS[int(t)] if int(t) < len(_NER_LABELS) else "O" for t in tags) + if hasattr(tags, "__iter__") and not isinstance(tags, str) + else str(tags) + ) + ) + keep = ["sentence", "ner_tags"] + (["split"] if "split" in df.columns else []) + return df[keep] + + def load(self, split: bool = False): + if split: + train, val, test = super().load(split=True) + return self._transform(train), self._transform(val), self._transform(test) + df = super().load(split=False) + return self._transform(df) diff --git a/ludwig/datasets/model_configs/__init__.py b/ludwig/datasets/model_configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/datasets/model_configs/adult_census_income_default.yaml b/ludwig/datasets/model_configs/adult_census_income_default.yaml new file mode 100644 index 0000000..cd15275 --- /dev/null +++ b/ludwig/datasets/model_configs/adult_census_income_default.yaml @@ -0,0 +1,42 @@ +output_features: + - name: income + type: category +input_features: + - name: age + type: number + - name: workclass + type: category + - name: fnlwgt + type: number + - name: education + type: category + - name: education-num + type: number + - name: marital-status + type: category + - name: occupation + type: category + - name: relationship + type: category + - name: race + type: category + - name: sex + type: category + - name: capital-gain + type: number + - name: capital-loss + type: number + - name: hours-per-week + type: number + - name: native-country + type: category +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 + steps_per_checkpoint: 1 diff --git a/ludwig/datasets/model_configs/allstate_claims_severity_default.yaml b/ludwig/datasets/model_configs/allstate_claims_severity_default.yaml new file mode 100644 index 0000000..7c30235 --- /dev/null +++ b/ludwig/datasets/model_configs/allstate_claims_severity_default.yaml @@ -0,0 +1,273 @@ +output_features: + - name: loss + type: number +input_features: + - name: cat1 + type: category + - name: cat2 + type: category + - name: cat3 + type: category + - name: cat4 + type: category + - name: cat5 + type: category + - name: cat6 + type: category + - name: cat7 + type: category + - name: cat8 + type: category + - name: cat9 + type: category + - name: cat10 + type: category + - name: cat11 + type: category + - name: cat12 + type: category + - name: cat13 + type: category + - name: cat14 + type: category + - name: cat15 + type: category + - name: cat16 + type: category + - name: cat17 + type: category + - name: cat18 + type: category + - name: cat19 + type: category + - name: cat20 + type: category + - name: cat21 + type: category + - name: cat22 + type: category + - name: cat23 + type: category + - name: cat24 + type: category + - name: cat25 + type: category + - name: cat26 + type: category + - name: cat27 + type: category + - name: cat28 + type: category + - name: cat29 + type: category + - name: cat30 + type: category + - name: cat31 + type: category + - name: cat32 + type: category + - name: cat33 + type: category + - name: cat34 + type: category + - name: cat35 + type: category + - name: cat36 + type: category + - name: cat37 + type: category + - name: cat38 + type: category + - name: cat39 + type: category + - name: cat40 + type: category + - name: cat41 + type: category + - name: cat42 + type: category + - name: cat43 + type: category + - name: cat44 + type: category + - name: cat45 + type: category + - name: cat46 + type: category + - name: cat47 + type: category + - name: cat48 + type: category + - name: cat49 + type: category + - name: cat50 + type: category + - name: cat51 + type: category + - name: cat52 + type: category + - name: cat53 + type: category + - name: cat54 + type: category + - name: cat55 + type: category + - name: cat56 + type: category + - name: cat57 + type: category + - name: cat58 + type: category + - name: cat59 + type: category + - name: cat60 + type: category + - name: cat61 + type: category + - name: cat62 + type: category + - name: cat63 + type: category + - name: cat64 + type: category + - name: cat65 + type: category + - name: cat66 + type: category + - name: cat67 + type: category + - name: cat68 + type: category + - name: cat69 + type: category + - name: cat70 + type: category + - name: cat71 + type: category + - name: cat72 + type: category + - name: cat73 + type: category + - name: cat74 + type: category + - name: cat75 + type: category + - name: cat76 + type: category + - name: cat77 + type: category + - name: cat78 + type: category + - name: cat79 + type: category + - name: cat80 + type: category + - name: cat81 + type: category + - name: cat82 + type: category + - name: cat83 + type: category + - name: cat84 + type: category + - name: cat85 + type: category + - name: cat86 + type: category + - name: cat87 + type: category + - name: cat88 + type: category + - name: cat89 + type: category + - name: cat90 + type: category + - name: cat91 + type: category + - name: cat92 + type: category + - name: cat93 + type: category + - name: cat94 + type: category + - name: cat95 + type: category + - name: cat96 + type: category + - name: cat97 + type: category + - name: cat98 + type: category + - name: cat99 + type: category + - name: cat100 + type: category + - name: cat101 + type: category + - name: cat102 + type: category + - name: cat103 + type: category + - name: cat104 + type: category + - name: cat105 + type: category + - name: cat106 + type: category + - name: cat107 + type: category + - name: cat108 + type: category + - name: cat109 + type: category + - name: cat110 + type: category + - name: cat111 + type: category + - name: cat112 + type: category + - name: cat113 + type: category + - name: cat114 + type: category + - name: cat115 + type: category + - name: cat116 + type: category + - name: cont1 + type: number + - name: cont2 + type: number + - name: cont3 + type: number + - name: cont4 + type: number + - name: cont5 + type: number + - name: cont6 + type: number + - name: cont7 + type: number + - name: cont8 + type: number + - name: cont9 + type: number + - name: cont10 + type: number + - name: cont11 + type: number + - name: cont12 + type: number + - name: cont13 + type: number + - name: cont14 + type: number +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/ames_housing_default.yaml b/ludwig/datasets/model_configs/ames_housing_default.yaml new file mode 100644 index 0000000..de50eb4 --- /dev/null +++ b/ludwig/datasets/model_configs/ames_housing_default.yaml @@ -0,0 +1,171 @@ +output_features: + - name: SalePrice + type: number +input_features: + - name: MSSubClass + type: category + - name: MSZoning + type: category + - name: LotFrontage + type: number + - name: LotArea + type: number + - name: Street + type: category + - name: Alley + type: category + - name: LotShape + type: category + - name: LandContour + type: category + - name: Utilities + type: category + - name: LotConfig + type: category + - name: LandSlope + type: category + - name: Neighborhood + type: category + - name: Condition1 + type: category + - name: Condition2 + type: category + - name: BldgType + type: category + - name: HouseStyle + type: category + - name: OverallQual + type: category + - name: OverallCond + type: category + - name: YearBuilt + type: number + - name: YearRemodAdd + type: number + - name: RoofStyle + type: category + - name: RoofMatl + type: category + - name: Exterior1st + type: category + - name: Exterior2nd + type: category + - name: MasVnrType + type: category + - name: MasVnrArea + type: number + - name: ExterQual + type: category + - name: ExterCond + type: category + - name: Foundation + type: category + - name: BsmtQual + type: category + - name: BsmtCond + type: category + - name: BsmtExposure + type: category + - name: BsmtFinType1 + type: category + - name: BsmtFinSF1 + type: number + - name: BsmtFinType2 + type: category + - name: BsmtFinSF2 + type: number + - name: BsmtUnfSF + type: number + - name: TotalBsmtSF + type: number + - name: Heating + type: category + - name: HeatingQC + type: category + - name: CentralAir + type: binary + - name: Electrical + type: category + - name: 1stFlrSF + type: number + - name: 2ndFlrSF + type: number + - name: LowQualFinSF + type: number + - name: GrLivArea + type: number + - name: BsmtFullBath + type: number + - name: BsmtHalfBath + type: number + - name: FullBath + type: number + - name: HalfBath + type: number + - name: BedroomAbvGr + type: number + - name: KitchenAbvGr + type: number + - name: KitchenQual + type: category + - name: TotRmsAbvGrd + type: number + - name: Functional + type: category + - name: Fireplaces + type: number + - name: FireplaceQu + type: category + - name: GarageType + type: category + - name: GarageYrBlt + type: number + - name: GarageFinish + type: category + - name: GarageCars + type: number + - name: GarageArea + type: number + - name: GarageQual + type: category + - name: GarageCond + type: category + - name: PavedDrive + type: category + - name: WoodDeckSF + type: number + - name: OpenPorchSF + type: number + - name: EnclosedPorch + type: number + - name: 3SsnPorch + type: number + - name: ScreenPorch + type: number + - name: PoolArea + type: number + - name: PoolQC + type: category + - name: Fence + type: category + - name: MiscFeature + type: category + - name: MiscVal + type: number + - name: MoSold + type: category + - name: YrSold + type: number + - name: SaleType + type: category + - name: SaleCondition + type: category +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/bnp_claims_management_default.yaml b/ludwig/datasets/model_configs/bnp_claims_management_default.yaml new file mode 100644 index 0000000..7e5de1e --- /dev/null +++ b/ludwig/datasets/model_configs/bnp_claims_management_default.yaml @@ -0,0 +1,275 @@ +output_features: + - name: target + type: binary +input_features: + - name: v1 + type: number + - name: v2 + type: number + - name: v3 + type: category + - name: v4 + type: number + - name: v5 + type: number + - name: v6 + type: number + - name: v7 + type: number + - name: v8 + type: number + - name: v9 + type: number + - name: v10 + type: number + - name: v11 + type: number + - name: v12 + type: number + - name: v13 + type: number + - name: v14 + type: number + - name: v15 + type: number + - name: v16 + type: number + - name: v17 + type: number + - name: v18 + type: number + - name: v19 + type: number + - name: v20 + type: number + - name: v21 + type: number + - name: v22 + type: category + - name: v23 + type: number + - name: v24 + type: category + - name: v25 + type: number + - name: v26 + type: number + - name: v27 + type: number + - name: v28 + type: number + - name: v29 + type: number + - name: v30 + type: category + - name: v31 + type: category + - name: v32 + type: number + - name: v33 + type: number + - name: v34 + type: number + - name: v35 + type: number + - name: v36 + type: number + - name: v37 + type: number + - name: v38 + type: number + - name: v39 + type: number + - name: v40 + type: number + - name: v41 + type: number + - name: v42 + type: number + - name: v43 + type: number + - name: v44 + type: number + - name: v45 + type: number + - name: v46 + type: number + - name: v47 + type: category + - name: v48 + type: number + - name: v49 + type: number + - name: v50 + type: number + - name: v51 + type: number + - name: v52 + type: category + - name: v53 + type: number + - name: v54 + type: number + - name: v55 + type: number + - name: v56 + type: category + - name: v57 + type: number + - name: v58 + type: number + - name: v59 + type: number + - name: v60 + type: number + - name: v61 + type: number + - name: v62 + type: number + - name: v63 + type: number + - name: v64 + type: number + - name: v65 + type: number + - name: v66 + type: category + - name: v67 + type: number + - name: v68 + type: number + - name: v69 + type: number + - name: v70 + type: number + - name: v71 + type: category + - name: v72 + type: number + - name: v73 + type: number + - name: v74 + type: category + - name: v75 + type: category + - name: v76 + type: number + - name: v77 + type: number + - name: v78 + type: number + - name: v79 + type: category + - name: v80 + type: number + - name: v81 + type: number + - name: v82 + type: number + - name: v83 + type: number + - name: v84 + type: number + - name: v85 + type: number + - name: v86 + type: number + - name: v87 + type: number + - name: v88 + type: number + - name: v89 + type: number + - name: v90 + type: number + - name: v91 + type: category + - name: v92 + type: number + - name: v93 + type: number + - name: v94 + type: number + - name: v95 + type: number + - name: v96 + type: number + - name: v97 + type: number + - name: v98 + type: number + - name: v99 + type: number + - name: v100 + type: number + - name: v101 + type: number + - name: v102 + type: number + - name: v103 + type: number + - name: v104 + type: number + - name: v105 + type: number + - name: v106 + type: number + - name: v107 + type: category + - name: v108 + type: number + - name: v109 + type: number + - name: v110 + type: category + - name: v111 + type: number + - name: v112 + type: category + - name: v113 + type: category + - name: v114 + type: number + - name: v115 + type: number + - name: v116 + type: number + - name: v117 + type: number + - name: v118 + type: number + - name: v119 + type: number + - name: v120 + type: number + - name: v121 + type: number + - name: v122 + type: number + - name: v123 + type: number + - name: v124 + type: number + - name: v125 + type: category + - name: v126 + type: number + - name: v127 + type: number + - name: v128 + type: number + - name: v129 + type: number + - name: v130 + type: number + - name: v131 + type: number +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/forest_cover_default.yaml b/ludwig/datasets/model_configs/forest_cover_default.yaml new file mode 100644 index 0000000..f0dc6ac --- /dev/null +++ b/ludwig/datasets/model_configs/forest_cover_default.yaml @@ -0,0 +1,37 @@ +output_features: + - name: Cover_Type + type: category +input_features: + - name: Elevation + type: number + - name: Aspect + type: number + - name: Slope + type: number + - name: Horizontal_Distance_To_Hydrology + type: number + - name: Vertical_Distance_To_Hydrology + type: number + - name: Horizontal_Distance_To_Roadways + type: number + - name: Hillshade_9am + type: number + - name: Hillshade_Noon + type: number + - name: Hillshade_3pm + type: number + - name: Horizontal_Distance_To_Fire_Points + type: number + - name: Wilderness_Area + type: category + - name: Soil_Type + type: category +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/higgs_best.yaml b/ludwig/datasets/model_configs/higgs_best.yaml new file mode 100644 index 0000000..223ff03 --- /dev/null +++ b/ludwig/datasets/model_configs/higgs_best.yaml @@ -0,0 +1,89 @@ +output_features: + - name: label + type: binary + weight_regularization: null +input_features: + - name: lepton_pT + type: number + - name: lepton_eta + type: number + - name: lepton_phi + type: number + - name: missing_energy_magnitude + type: number + - name: missing_energy_phi + type: number + - name: jet_1_pt + type: number + - name: jet_1_eta + type: number + - name: jet_1_phi + type: number + - name: jet_1_b-tag + type: number + - name: jet_2_pt + type: number + - name: jet_2_eta + type: number + - name: jet_2_phi + type: number + - name: jet_2_b-tag + type: number + - name: jet_3_pt + type: number + - name: jet_3_eta + type: number + - name: jet_3_phi + type: number + - name: jet_3_b-tag + type: number + - name: jet_4_pt + type: number + - name: jet_4_eta + type: number + - name: jet_4_phi + type: number + - name: jet_4_b-tag + type: number + - name: m_jj + type: number + - name: m_jjj + type: number + - name: m_lv + type: number + - name: m_jlv + type: number + - name: m_bb + type: number + - name: m_wbb + type: number + - name: m_wwbb + type: number +combiner: + type: tabnet + bn_momentum: 0.95 + bn_virtual_bs: 1024 + dropout: 0.05252744300130521 + fc_size: 128 + num_fc_layers: 3 + num_steps: 3 + output_size: 128 + relaxation_factor: 1.5 + size: 32 + sparsity: 0.0001 +training: + batch_size: 8192 + learning_rate: 0.01 + shuffle_buffer_size: 1000000 + should_shuffle: true + eval_batch_size: 500000 #4096 # 65536 131072 262144 524288 + epochs: 300 + early_stop: 30 + optimizer: + type: adam + learning_rate_scheduler: + decay: exponential + decay_rate: 0.8 + decay_steps: 20000 + regularization_lambda: 1 + validation_field: label diff --git a/ludwig/datasets/model_configs/higgs_default.yaml b/ludwig/datasets/model_configs/higgs_default.yaml new file mode 100644 index 0000000..8e6f7fb --- /dev/null +++ b/ludwig/datasets/model_configs/higgs_default.yaml @@ -0,0 +1,70 @@ +output_features: + - name: label + type: binary + weight_regularization: null +input_features: + - name: lepton_pT + type: number + - name: lepton_eta + type: number + - name: lepton_phi + type: number + - name: missing_energy_magnitude + type: number + - name: missing_energy_phi + type: number + - name: jet_1_pt + type: number + - name: jet_1_eta + type: number + - name: jet_1_phi + type: number + - name: jet_1_b-tag + type: number + - name: jet_2_pt + type: number + - name: jet_2_eta + type: number + - name: jet_2_phi + type: number + - name: jet_2_b-tag + type: number + - name: jet_3_pt + type: number + - name: jet_3_eta + type: number + - name: jet_3_phi + type: number + - name: jet_3_b-tag + type: number + - name: jet_4_pt + type: number + - name: jet_4_eta + type: number + - name: jet_4_phi + type: number + - name: jet_4_b-tag + type: number + - name: m_jj + type: number + - name: m_jjj + type: number + - name: m_lv + type: number + - name: m_jlv + type: number + - name: m_bb + type: number + - name: m_wbb + type: number + - name: m_wwbb + type: number +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/ieee_fraud_default.yaml b/ludwig/datasets/model_configs/ieee_fraud_default.yaml new file mode 100644 index 0000000..ec8d24b --- /dev/null +++ b/ludwig/datasets/model_configs/ieee_fraud_default.yaml @@ -0,0 +1,877 @@ +output_features: + - name: isFraud + type: binary +input_features: + - name: TransactionDT + type: number + - name: TransactionAmt + type: number + - name: ProductCD + type: category + - name: card1 + type: number + - name: card2 + type: number + - name: card3 + type: number + - name: card4 + type: category + - name: card5 + type: number + - name: card6 + type: category + - name: addr1 + type: number + - name: addr2 + type: number + - name: dist1 + type: number + - name: dist2 + type: number + - name: P_emaildomain + type: category + - name: R_emaildomain + type: number + - name: C1 + type: number + - name: C2 + type: number + - name: C3 + type: number + - name: C4 + type: number + - name: C5 + type: number + - name: C6 + type: number + - name: C7 + type: number + - name: C8 + type: number + - name: C9 + type: number + - name: C10 + type: number + - name: C11 + type: number + - name: C12 + type: number + - name: C13 + type: number + - name: C14 + type: number + - name: D1 + type: number + - name: D2 + type: number + - name: D3 + type: number + - name: D4 + type: number + - name: D5 + type: number + - name: D6 + type: number + - name: D7 + type: number + - name: D8 + type: number + - name: D9 + type: number + - name: D10 + type: number + - name: D11 + type: number + - name: D12 + type: number + - name: D13 + type: number + - name: D14 + type: number + - name: D15 + type: number + - name: M1 + type: category + - name: M2 + type: category + - name: M3 + type: category + - name: M4 + type: category + - name: M5 + type: category + - name: M6 + type: category + - name: M7 + type: category + - name: M8 + type: category + - name: M9 + type: category + - name: V1 + type: number + - name: V2 + type: number + - name: V3 + type: number + - name: V4 + type: number + - name: V5 + type: number + - name: V6 + type: number + - name: V7 + type: number + - name: V8 + type: number + - name: V9 + type: number + - name: V10 + type: number + - name: V11 + type: number + - name: V12 + type: number + - name: V13 + type: number + - name: V14 + type: number + - name: V15 + type: number + - name: V16 + type: number + - name: V17 + type: number + - name: V18 + type: number + - name: V19 + type: number + - name: V20 + type: number + - name: V21 + type: number + - name: V22 + type: number + - name: V23 + type: number + - name: V24 + type: number + - name: V25 + type: number + - name: V26 + type: number + - name: V27 + type: number + - name: V28 + type: number + - name: V29 + type: number + - name: V30 + type: number + - name: V31 + type: number + - name: V32 + type: number + - name: V33 + type: number + - name: V34 + type: number + - name: V35 + type: number + - name: V36 + type: number + - name: V37 + type: number + - name: V38 + type: number + - name: V39 + type: number + - name: V40 + type: number + - name: V41 + type: number + - name: V42 + type: number + - name: V43 + type: number + - name: V44 + type: number + - name: V45 + type: number + - name: V46 + type: number + - name: V47 + type: number + - name: V48 + type: number + - name: V49 + type: number + - name: V50 + type: number + - name: V51 + type: number + - name: V52 + type: number + - name: V53 + type: number + - name: V54 + type: number + - name: V55 + type: number + - name: V56 + type: number + - name: V57 + type: number + - name: V58 + type: number + - name: V59 + type: number + - name: V60 + type: number + - name: V61 + type: number + - name: V62 + type: number + - name: V63 + type: number + - name: V64 + type: number + - name: V65 + type: number + - name: V66 + type: number + - name: V67 + type: number + - name: V68 + type: number + - name: V69 + type: number + - name: V70 + type: number + - name: V71 + type: number + - name: V72 + type: number + - name: V73 + type: number + - name: V74 + type: number + - name: V75 + type: number + - name: V76 + type: number + - name: V77 + type: number + - name: V78 + type: number + - name: V79 + type: number + - name: V80 + type: number + - name: V81 + type: number + - name: V82 + type: number + - name: V83 + type: number + - name: V84 + type: number + - name: V85 + type: number + - name: V86 + type: number + - name: V87 + type: number + - name: V88 + type: number + - name: V89 + type: number + - name: V90 + type: number + - name: V91 + type: number + - name: V92 + type: number + - name: V93 + type: number + - name: V94 + type: number + - name: V95 + type: number + - name: V96 + type: number + - name: V97 + type: number + - name: V98 + type: number + - name: V99 + type: number + - name: V100 + type: number + - name: V101 + type: number + - name: V102 + type: number + - name: V103 + type: number + - name: V104 + type: number + - name: V105 + type: number + - name: V106 + type: number + - name: V107 + type: number + - name: V108 + type: number + - name: V109 + type: number + - name: V110 + type: number + - name: V111 + type: number + - name: V112 + type: number + - name: V113 + type: number + - name: V114 + type: number + - name: V115 + type: number + - name: V116 + type: number + - name: V117 + type: number + - name: V118 + type: number + - name: V119 + type: number + - name: V120 + type: number + - name: V121 + type: number + - name: V122 + type: number + - name: V123 + type: number + - name: V124 + type: number + - name: V125 + type: number + - name: V126 + type: number + - name: V127 + type: number + - name: V128 + type: number + - name: V129 + type: number + - name: V130 + type: number + - name: V131 + type: number + - name: V132 + type: number + - name: V133 + type: number + - name: V134 + type: number + - name: V135 + type: number + - name: V136 + type: number + - name: V137 + type: number + - name: V138 + type: number + - name: V139 + type: number + - name: V140 + type: number + - name: V141 + type: number + - name: V142 + type: number + - name: V143 + type: number + - name: V144 + type: number + - name: V145 + type: number + - name: V146 + type: number + - name: V147 + type: number + - name: V148 + type: number + - name: V149 + type: number + - name: V150 + type: number + - name: V151 + type: number + - name: V152 + type: number + - name: V153 + type: number + - name: V154 + type: number + - name: V155 + type: number + - name: V156 + type: number + - name: V157 + type: number + - name: V158 + type: number + - name: V159 + type: number + - name: V160 + type: number + - name: V161 + type: number + - name: V162 + type: number + - name: V163 + type: number + - name: V164 + type: number + - name: V165 + type: number + - name: V166 + type: number + - name: V167 + type: number + - name: V168 + type: number + - name: V169 + type: number + - name: V170 + type: number + - name: V171 + type: number + - name: V172 + type: number + - name: V173 + type: number + - name: V174 + type: number + - name: V175 + type: number + - name: V176 + type: number + - name: V177 + type: number + - name: V178 + type: number + - name: V179 + type: number + - name: V180 + type: number + - name: V181 + type: number + - name: V182 + type: number + - name: V183 + type: number + - name: V184 + type: number + - name: V185 + type: number + - name: V186 + type: number + - name: V187 + type: number + - name: V188 + type: number + - name: V189 + type: number + - name: V190 + type: number + - name: V191 + type: number + - name: V192 + type: number + - name: V193 + type: number + - name: V194 + type: number + - name: V195 + type: number + - name: V196 + type: number + - name: V197 + type: number + - name: V198 + type: number + - name: V199 + type: number + - name: V200 + type: number + - name: V201 + type: number + - name: V202 + type: number + - name: V203 + type: number + - name: V204 + type: number + - name: V205 + type: number + - name: V206 + type: number + - name: V207 + type: number + - name: V208 + type: number + - name: V209 + type: number + - name: V210 + type: number + - name: V211 + type: number + - name: V212 + type: number + - name: V213 + type: number + - name: V214 + type: number + - name: V215 + type: number + - name: V216 + type: number + - name: V217 + type: number + - name: V218 + type: number + - name: V219 + type: number + - name: V220 + type: number + - name: V221 + type: number + - name: V222 + type: number + - name: V223 + type: number + - name: V224 + type: number + - name: V225 + type: number + - name: V226 + type: number + - name: V227 + type: number + - name: V228 + type: number + - name: V229 + type: number + - name: V230 + type: number + - name: V231 + type: number + - name: V232 + type: number + - name: V233 + type: number + - name: V234 + type: number + - name: V235 + type: number + - name: V236 + type: number + - name: V237 + type: number + - name: V238 + type: number + - name: V239 + type: number + - name: V240 + type: number + - name: V241 + type: number + - name: V242 + type: number + - name: V243 + type: number + - name: V244 + type: number + - name: V245 + type: number + - name: V246 + type: number + - name: V247 + type: number + - name: V248 + type: number + - name: V249 + type: number + - name: V250 + type: number + - name: V251 + type: number + - name: V252 + type: number + - name: V253 + type: number + - name: V254 + type: number + - name: V255 + type: number + - name: V256 + type: number + - name: V257 + type: number + - name: V258 + type: number + - name: V259 + type: number + - name: V260 + type: number + - name: V261 + type: number + - name: V262 + type: number + - name: V263 + type: number + - name: V264 + type: number + - name: V265 + type: number + - name: V266 + type: number + - name: V267 + type: number + - name: V268 + type: number + - name: V269 + type: number + - name: V270 + type: number + - name: V271 + type: number + - name: V272 + type: number + - name: V273 + type: number + - name: V274 + type: number + - name: V275 + type: number + - name: V276 + type: number + - name: V277 + type: number + - name: V278 + type: number + - name: V279 + type: number + - name: V280 + type: number + - name: V281 + type: number + - name: V282 + type: number + - name: V283 + type: number + - name: V284 + type: number + - name: V285 + type: number + - name: V286 + type: number + - name: V287 + type: number + - name: V288 + type: number + - name: V289 + type: number + - name: V290 + type: number + - name: V291 + type: number + - name: V292 + type: number + - name: V293 + type: number + - name: V294 + type: number + - name: V295 + type: number + - name: V296 + type: number + - name: V297 + type: number + - name: V298 + type: number + - name: V299 + type: number + - name: V300 + type: number + - name: V301 + type: number + - name: V302 + type: number + - name: V303 + type: number + - name: V304 + type: number + - name: V305 + type: number + - name: V306 + type: number + - name: V307 + type: number + - name: V308 + type: number + - name: V309 + type: number + - name: V310 + type: number + - name: V311 + type: number + - name: V312 + type: number + - name: V313 + type: number + - name: V314 + type: number + - name: V315 + type: number + - name: V316 + type: number + - name: V317 + type: number + - name: V318 + type: number + - name: V319 + type: number + - name: V320 + type: number + - name: V321 + type: number + - name: V322 + type: number + - name: V323 + type: number + - name: V324 + type: number + - name: V325 + type: number + - name: V326 + type: number + - name: V327 + type: number + - name: V328 + type: number + - name: V329 + type: number + - name: V330 + type: number + - name: V331 + type: number + - name: V332 + type: number + - name: V333 + type: number + - name: V334 + type: number + - name: V335 + type: number + - name: V336 + type: number + - name: V337 + type: number + - name: V338 + type: number + - name: V339 + type: number + - name: id_01 + type: number + - name: id_02 + type: number + - name: id_03 + type: number + - name: id_04 + type: number + - name: id_05 + type: number + - name: id_06 + type: number + - name: id_07 + type: number + - name: id_08 + type: number + - name: id_09 + type: number + - name: id_10 + type: number + - name: id_11 + type: number + - name: id_12 + type: number + - name: id_13 + type: number + - name: id_14 + type: number + - name: id_15 + type: number + - name: id_16 + type: number + - name: id_17 + type: number + - name: id_18 + type: number + - name: id_19 + type: number + - name: id_20 + type: number + - name: id_21 + type: number + - name: id_22 + type: number + - name: id_23 + type: number + - name: id_24 + type: number + - name: id_25 + type: number + - name: id_26 + type: number + - name: id_27 + type: number + - name: id_28 + type: number + - name: id_29 + type: number + - name: id_30 + type: number + - name: id_31 + type: number + - name: id_32 + type: number + - name: id_33 + type: number + - name: id_34 + type: number + - name: id_35 + type: number + - name: id_36 + type: number + - name: id_37 + type: number + - name: id_38 + type: number + - name: DeviceType + type: number + - name: DeviceInfo + type: number +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/mercedes_benz_greener_default.yaml b/ludwig/datasets/model_configs/mercedes_benz_greener_default.yaml new file mode 100644 index 0000000..8e91367 --- /dev/null +++ b/ludwig/datasets/model_configs/mercedes_benz_greener_default.yaml @@ -0,0 +1,765 @@ +output_features: + - name: y + type: number +input_features: + - name: X0 + type: category + - name: X1 + type: category + - name: X2 + type: category + - name: X3 + type: category + - name: X4 + type: category + - name: X5 + type: category + - name: X6 + type: category + - name: X8 + type: category + - name: X10 + type: binary + - name: X11 + type: binary + - name: X12 + type: binary + - name: X13 + type: binary + - name: X14 + type: binary + - name: X15 + type: binary + - name: X16 + type: binary + - name: X17 + type: binary + - name: X18 + type: binary + - name: X19 + type: binary + - name: X20 + type: binary + - name: X21 + type: binary + - name: X22 + type: binary + - name: X23 + type: binary + - name: X24 + type: binary + - name: X26 + type: binary + - name: X27 + type: binary + - name: X28 + type: binary + - name: X29 + type: binary + - name: X30 + type: binary + - name: X31 + type: binary + - name: X32 + type: binary + - name: X33 + type: binary + - name: X34 + type: binary + - name: X35 + type: binary + - name: X36 + type: binary + - name: X37 + type: binary + - name: X38 + type: binary + - name: X39 + type: binary + - name: X40 + type: binary + - name: X41 + type: binary + - name: X42 + type: binary + - name: X43 + type: binary + - name: X44 + type: binary + - name: X45 + type: binary + - name: X46 + type: binary + - name: X47 + type: binary + - name: X48 + type: binary + - name: X49 + type: binary + - name: X50 + type: binary + - name: X51 + type: binary + - name: X52 + type: binary + - name: X53 + type: binary + - name: X54 + type: binary + - name: X55 + type: binary + - name: X56 + type: binary + - name: X57 + type: binary + - name: X58 + type: binary + - name: X59 + type: binary + - name: X60 + type: binary + - name: X61 + type: binary + - name: X62 + type: binary + - name: X63 + type: binary + - name: X64 + type: binary + - name: X65 + type: binary + - name: X66 + type: binary + - name: X67 + type: binary + - name: X68 + type: binary + - name: X69 + type: binary + - name: X70 + type: binary + - name: X71 + type: binary + - name: X73 + type: binary + - name: X74 + type: binary + - name: X75 + type: binary + - name: X76 + type: binary + - name: X77 + type: binary + - name: X78 + type: binary + - name: X79 + type: binary + - name: X80 + type: binary + - name: X81 + type: binary + - name: X82 + type: binary + - name: X83 + type: binary + - name: X84 + type: binary + - name: X85 + type: binary + - name: X86 + type: binary + - name: X87 + type: binary + - name: X88 + type: binary + - name: X89 + type: binary + - name: X90 + type: binary + - name: X91 + type: binary + - name: X92 + type: binary + - name: X93 + type: binary + - name: X94 + type: binary + - name: X95 + type: binary + - name: X96 + type: binary + - name: X97 + type: binary + - name: X98 + type: binary + - name: X99 + type: binary + - name: X100 + type: binary + - name: X101 + type: binary + - name: X102 + type: binary + - name: X103 + type: binary + - name: X104 + type: binary + - name: X105 + type: binary + - name: X106 + type: binary + - name: X107 + type: binary + - name: X108 + type: binary + - name: X109 + type: binary + - name: X110 + type: binary + - name: X111 + type: binary + - name: X112 + type: binary + - name: X113 + type: binary + - name: X114 + type: binary + - name: X115 + type: binary + - name: X116 + type: binary + - name: X117 + type: binary + - name: X118 + type: binary + - name: X119 + type: binary + - name: X120 + type: binary + - name: X122 + type: binary + - name: X123 + type: binary + - name: X124 + type: binary + - name: X125 + type: binary + - name: X126 + type: binary + - name: X127 + type: binary + - name: X128 + type: binary + - name: X129 + type: binary + - name: X130 + type: binary + - name: X131 + type: binary + - name: X132 + type: binary + - name: X133 + type: binary + - name: X134 + type: binary + - name: X135 + type: binary + - name: X136 + type: binary + - name: X137 + type: binary + - name: X138 + type: binary + - name: X139 + type: binary + - name: X140 + type: binary + - name: X141 + type: binary + - name: X142 + type: binary + - name: X143 + type: binary + - name: X144 + type: binary + - name: X145 + type: binary + - name: X146 + type: binary + - name: X147 + type: binary + - name: X148 + type: binary + - name: X150 + type: binary + - name: X151 + type: binary + - name: X152 + type: binary + - name: X153 + type: binary + - name: X154 + type: binary + - name: X155 + type: binary + - name: X156 + type: binary + - name: X157 + type: binary + - name: X158 + type: binary + - name: X159 + type: binary + - name: X160 + type: binary + - name: X161 + type: binary + - name: X162 + type: binary + - name: X163 + type: binary + - name: X164 + type: binary + - name: X165 + type: binary + - name: X166 + type: binary + - name: X167 + type: binary + - name: X168 + type: binary + - name: X169 + type: binary + - name: X170 + type: binary + - name: X171 + type: binary + - name: X172 + type: binary + - name: X173 + type: binary + - name: X174 + type: binary + - name: X175 + type: binary + - name: X176 + type: binary + - name: X177 + type: binary + - name: X178 + type: binary + - name: X179 + type: binary + - name: X180 + type: binary + - name: X181 + type: binary + - name: X182 + type: binary + - name: X183 + type: binary + - name: X184 + type: binary + - name: X185 + type: binary + - name: X186 + type: binary + - name: X187 + type: binary + - name: X189 + type: binary + - name: X190 + type: binary + - name: X191 + type: binary + - name: X192 + type: binary + - name: X194 + type: binary + - name: X195 + type: binary + - name: X196 + type: binary + - name: X197 + type: binary + - name: X198 + type: binary + - name: X199 + type: binary + - name: X200 + type: binary + - name: X201 + type: binary + - name: X202 + type: binary + - name: X203 + type: binary + - name: X204 + type: binary + - name: X205 + type: binary + - name: X206 + type: binary + - name: X207 + type: binary + - name: X208 + type: binary + - name: X209 + type: binary + - name: X210 + type: binary + - name: X211 + type: binary + - name: X212 + type: binary + - name: X213 + type: binary + - name: X214 + type: binary + - name: X215 + type: binary + - name: X216 + type: binary + - name: X217 + type: binary + - name: X218 + type: binary + - name: X219 + type: binary + - name: X220 + type: binary + - name: X221 + type: binary + - name: X222 + type: binary + - name: X223 + type: binary + - name: X224 + type: binary + - name: X225 + type: binary + - name: X226 + type: binary + - name: X227 + type: binary + - name: X228 + type: binary + - name: X229 + type: binary + - name: X230 + type: binary + - name: X231 + type: binary + - name: X232 + type: binary + - name: X233 + type: binary + - name: X234 + type: binary + - name: X235 + type: binary + - name: X236 + type: binary + - name: X237 + type: binary + - name: X238 + type: binary + - name: X239 + type: binary + - name: X240 + type: binary + - name: X241 + type: binary + - name: X242 + type: binary + - name: X243 + type: binary + - name: X244 + type: binary + - name: X245 + type: binary + - name: X246 + type: binary + - name: X247 + type: binary + - name: X248 + type: binary + - name: X249 + type: binary + - name: X250 + type: binary + - name: X251 + type: binary + - name: X252 + type: binary + - name: X253 + type: binary + - name: X254 + type: binary + - name: X255 + type: binary + - name: X256 + type: binary + - name: X257 + type: binary + - name: X258 + type: binary + - name: X259 + type: binary + - name: X260 + type: binary + - name: X261 + type: binary + - name: X262 + type: binary + - name: X263 + type: binary + - name: X264 + type: binary + - name: X265 + type: binary + - name: X266 + type: binary + - name: X267 + type: binary + - name: X268 + type: binary + - name: X269 + type: binary + - name: X270 + type: binary + - name: X271 + type: binary + - name: X272 + type: binary + - name: X273 + type: binary + - name: X274 + type: binary + - name: X275 + type: binary + - name: X276 + type: binary + - name: X277 + type: binary + - name: X278 + type: binary + - name: X279 + type: binary + - name: X280 + type: binary + - name: X281 + type: binary + - name: X282 + type: binary + - name: X283 + type: binary + - name: X284 + type: binary + - name: X285 + type: binary + - name: X286 + type: binary + - name: X287 + type: binary + - name: X288 + type: binary + - name: X289 + type: binary + - name: X290 + type: binary + - name: X291 + type: binary + - name: X292 + type: binary + - name: X293 + type: binary + - name: X294 + type: binary + - name: X295 + type: binary + - name: X296 + type: binary + - name: X297 + type: binary + - name: X298 + type: binary + - name: X299 + type: binary + - name: X300 + type: binary + - name: X301 + type: binary + - name: X302 + type: binary + - name: X304 + type: binary + - name: X305 + type: binary + - name: X306 + type: binary + - name: X307 + type: binary + - name: X308 + type: binary + - name: X309 + type: binary + - name: X310 + type: binary + - name: X311 + type: binary + - name: X312 + type: binary + - name: X313 + type: binary + - name: X314 + type: binary + - name: X315 + type: binary + - name: X316 + type: binary + - name: X317 + type: binary + - name: X318 + type: binary + - name: X319 + type: binary + - name: X320 + type: binary + - name: X321 + type: binary + - name: X322 + type: binary + - name: X323 + type: binary + - name: X324 + type: binary + - name: X325 + type: binary + - name: X326 + type: binary + - name: X327 + type: binary + - name: X328 + type: binary + - name: X329 + type: binary + - name: X330 + type: binary + - name: X331 + type: binary + - name: X332 + type: binary + - name: X333 + type: binary + - name: X334 + type: binary + - name: X335 + type: binary + - name: X336 + type: binary + - name: X337 + type: binary + - name: X338 + type: binary + - name: X339 + type: binary + - name: X340 + type: binary + - name: X341 + type: binary + - name: X342 + type: binary + - name: X343 + type: binary + - name: X344 + type: binary + - name: X345 + type: binary + - name: X346 + type: binary + - name: X347 + type: binary + - name: X348 + type: binary + - name: X349 + type: binary + - name: X350 + type: binary + - name: X351 + type: binary + - name: X352 + type: binary + - name: X353 + type: binary + - name: X354 + type: binary + - name: X355 + type: binary + - name: X356 + type: binary + - name: X357 + type: binary + - name: X358 + type: binary + - name: X359 + type: binary + - name: X360 + type: binary + - name: X361 + type: binary + - name: X362 + type: binary + - name: X363 + type: binary + - name: X364 + type: binary + - name: X365 + type: binary + - name: X366 + type: binary + - name: X367 + type: binary + - name: X368 + type: binary + - name: X369 + type: binary + - name: X370 + type: binary + - name: X371 + type: binary + - name: X372 + type: binary + - name: X373 + type: binary + - name: X374 + type: binary + - name: X375 + type: binary + - name: X376 + type: binary + - name: X377 + type: binary + - name: X378 + type: binary + - name: X379 + type: binary + - name: X380 + type: binary + - name: X382 + type: binary + - name: X383 + type: binary + - name: X384 + type: binary + - name: X385 + type: binary +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/mnist_default.yaml b/ludwig/datasets/model_configs/mnist_default.yaml new file mode 100644 index 0000000..9bec733 --- /dev/null +++ b/ludwig/datasets/model_configs/mnist_default.yaml @@ -0,0 +1,24 @@ +output_features: + - name: label + type: category +input_features: + - name: image_path + type: image + preprocessing: + num_processes: 4 + encoder: stacked_cnn + conv_layers: + - num_filters: 32 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + - num_filters: 64 + filter_size: 3 + pool_size: 2 + pool_stride: 2 + dropout: 0.4 + fc_layers: + - output_size: 128 + dropout: 0.4 +trainer: + epochs: 1 diff --git a/ludwig/datasets/model_configs/mushroom_edibility_default.yaml b/ludwig/datasets/model_configs/mushroom_edibility_default.yaml new file mode 100644 index 0000000..feaab93 --- /dev/null +++ b/ludwig/datasets/model_configs/mushroom_edibility_default.yaml @@ -0,0 +1,57 @@ +output_features: + - name: class + type: category +input_features: + - name: cap-shape + type: category + - name: cap-surface + type: category + - name: cap-color + type: category + - name: bruises? + type: category + - name: odor + type: category + - name: gill-attachment + type: category + - name: gill-spacing + type: category + - name: gill-size + type: category + - name: gill-color + type: category + - name: stalk-shape + type: category + - name: stalk-root + type: category + - name: stalk-surface-above-ring + type: category + - name: stalk-surface-below-ring + type: category + - name: stalk-color-above-ring + type: category + - name: stalk-color-below-ring + type: category + - name: veil-type + type: category + - name: veil-color + type: category + - name: ring-number + type: category + - name: ring-type + type: category + - name: spore-print-color + type: category + - name: population + type: category + - name: habitat + type: category +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/otto_group_product_default.yaml b/ludwig/datasets/model_configs/otto_group_product_default.yaml new file mode 100644 index 0000000..f5b7bf5 --- /dev/null +++ b/ludwig/datasets/model_configs/otto_group_product_default.yaml @@ -0,0 +1,199 @@ +output_features: + - name: target + type: category +input_features: + - name: feat_1 + type: number + - name: feat_2 + type: number + - name: feat_3 + type: number + - name: feat_4 + type: number + - name: feat_5 + type: number + - name: feat_6 + type: number + - name: feat_7 + type: number + - name: feat_8 + type: number + - name: feat_9 + type: number + - name: feat_10 + type: number + - name: feat_11 + type: number + - name: feat_12 + type: number + - name: feat_13 + type: number + - name: feat_14 + type: number + - name: feat_15 + type: number + - name: feat_16 + type: number + - name: feat_17 + type: number + - name: feat_18 + type: number + - name: feat_19 + type: number + - name: feat_20 + type: number + - name: feat_21 + type: number + - name: feat_22 + type: number + - name: feat_23 + type: number + - name: feat_24 + type: number + - name: feat_25 + type: number + - name: feat_26 + type: number + - name: feat_27 + type: number + - name: feat_28 + type: number + - name: feat_29 + type: number + - name: feat_30 + type: number + - name: feat_31 + type: number + - name: feat_32 + type: number + - name: feat_33 + type: number + - name: feat_34 + type: number + - name: feat_35 + type: number + - name: feat_36 + type: number + - name: feat_37 + type: number + - name: feat_38 + type: number + - name: feat_39 + type: number + - name: feat_40 + type: number + - name: feat_41 + type: number + - name: feat_42 + type: number + - name: feat_43 + type: number + - name: feat_44 + type: number + - name: feat_45 + type: number + - name: feat_46 + type: number + - name: feat_47 + type: number + - name: feat_48 + type: number + - name: feat_49 + type: number + - name: feat_50 + type: number + - name: feat_51 + type: number + - name: feat_52 + type: number + - name: feat_53 + type: number + - name: feat_54 + type: number + - name: feat_55 + type: number + - name: feat_56 + type: number + - name: feat_57 + type: number + - name: feat_58 + type: number + - name: feat_59 + type: number + - name: feat_60 + type: number + - name: feat_61 + type: number + - name: feat_62 + type: number + - name: feat_63 + type: number + - name: feat_64 + type: number + - name: feat_65 + type: number + - name: feat_66 + type: number + - name: feat_67 + type: number + - name: feat_68 + type: number + - name: feat_69 + type: number + - name: feat_70 + type: number + - name: feat_71 + type: number + - name: feat_72 + type: number + - name: feat_73 + type: number + - name: feat_74 + type: number + - name: feat_75 + type: number + - name: feat_76 + type: number + - name: feat_77 + type: number + - name: feat_78 + type: number + - name: feat_79 + type: number + - name: feat_80 + type: number + - name: feat_81 + type: number + - name: feat_82 + type: number + - name: feat_83 + type: number + - name: feat_84 + type: number + - name: feat_85 + type: number + - name: feat_86 + type: number + - name: feat_87 + type: number + - name: feat_88 + type: number + - name: feat_89 + type: number + - name: feat_90 + type: number + - name: feat_91 + type: number + - name: feat_92 + type: number + - name: feat_93 + type: number +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/poker_hand_default.yaml b/ludwig/datasets/model_configs/poker_hand_default.yaml new file mode 100644 index 0000000..edd260b --- /dev/null +++ b/ludwig/datasets/model_configs/poker_hand_default.yaml @@ -0,0 +1,33 @@ +output_features: + - name: hand + type: category +input_features: + - name: S1 + type: category + - name: C1 + type: category + - name: S2 + type: category + - name: C2 + type: category + - name: S3 + type: category + - name: C3 + type: category + - name: S4 + type: category + - name: C4 + type: category + - name: S5 + type: category + - name: C5 + type: category +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/porto_seguro_safe_driver_default.yaml b/ludwig/datasets/model_configs/porto_seguro_safe_driver_default.yaml new file mode 100644 index 0000000..65b0c86 --- /dev/null +++ b/ludwig/datasets/model_configs/porto_seguro_safe_driver_default.yaml @@ -0,0 +1,127 @@ +output_features: + - name: target + type: binary +input_features: + - name: ps_ind_01 + type: number + - name: ps_ind_02_cat + type: category + - name: ps_ind_03 + type: number + - name: ps_ind_04_cat + type: category + - name: ps_ind_05_cat + type: category + - name: ps_ind_06_bin + type: binary + - name: ps_ind_07_bin + type: binary + - name: ps_ind_08_bin + type: binary + - name: ps_ind_09_bin + type: binary + - name: ps_ind_10_bin + type: binary + - name: ps_ind_11_bin + type: binary + - name: ps_ind_12_bin + type: binary + - name: ps_ind_13_bin + type: binary + - name: ps_ind_14 + type: number + - name: ps_ind_15 + type: number + - name: ps_ind_16_bin + type: binary + - name: ps_ind_17_bin + type: binary + - name: ps_ind_18_bin + type: binary + - name: ps_reg_01 + type: number + - name: ps_reg_02 + type: number + - name: ps_reg_03 + type: number + - name: ps_car_01_cat + type: category + - name: ps_car_02_cat + type: category + - name: ps_car_03_cat + type: category + - name: ps_car_04_cat + type: category + - name: ps_car_05_cat + type: category + - name: ps_car_06_cat + type: category + - name: ps_car_07_cat + type: category + - name: ps_car_08_cat + type: category + - name: ps_car_09_cat + type: category + - name: ps_car_10_cat + type: category + - name: ps_car_11_cat + type: category + - name: ps_car_11 + type: number + - name: ps_car_12 + type: number + - name: ps_car_13 + type: number + - name: ps_car_14 + type: number + - name: ps_car_15 + type: number + - name: ps_calc_01 + type: number + - name: ps_calc_02 + type: number + - name: ps_calc_03 + type: number + - name: ps_calc_04 + type: number + - name: ps_calc_05 + type: number + - name: ps_calc_06 + type: number + - name: ps_calc_07 + type: number + - name: ps_calc_08 + type: number + - name: ps_calc_09 + type: number + - name: ps_calc_10 + type: number + - name: ps_calc_11 + type: number + - name: ps_calc_12 + type: number + - name: ps_calc_13 + type: number + - name: ps_calc_14 + type: number + - name: ps_calc_15_bin + type: binary + - name: ps_calc_16_bin + type: binary + - name: ps_calc_17_bin + type: binary + - name: ps_calc_18_bin + type: binary + - name: ps_calc_19_bin + type: binary + - name: ps_calc_20_bin + type: binary +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/synthetic_fraud_default.yaml b/ludwig/datasets/model_configs/synthetic_fraud_default.yaml new file mode 100644 index 0000000..6f8e0e7 --- /dev/null +++ b/ludwig/datasets/model_configs/synthetic_fraud_default.yaml @@ -0,0 +1,27 @@ +output_features: + - name: isFraud + type: binary +input_features: + - name: step + type: number + - name: type + type: category + - name: amount + type: number + - name: oldbalanceOrg + type: number + - name: newbalanceOrig + type: number + - name: oldbalanceDest + type: number + - name: newbalanceDest + type: number +combiner: + type: concat + num_fc_layers: 3 + fc_size: 128 + dropout: 0.1 +training: + batch_size: 256 + learning_rate: .001 + epochs: 1 diff --git a/ludwig/datasets/model_configs/titanic_default.yaml b/ludwig/datasets/model_configs/titanic_default.yaml new file mode 100644 index 0000000..9744ccf --- /dev/null +++ b/ludwig/datasets/model_configs/titanic_default.yaml @@ -0,0 +1,25 @@ +output_features: + - name: Survived + type: binary +input_features: + - name: Pclass + type: category + - name: Sex + type: category + - name: Age + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: SibSp + type: number + - name: Parch + type: number + - name: Fare + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: Embarked + type: category +training: + batch_size: 256 + epochs: 1 diff --git a/ludwig/datasets/utils.py b/ludwig/datasets/utils.py new file mode 100644 index 0000000..001d3f6 --- /dev/null +++ b/ludwig/datasets/utils.py @@ -0,0 +1,44 @@ +import os +from functools import lru_cache + +import yaml + +from ludwig.api_annotations import PublicAPI +from ludwig.datasets import model_configs + + +@PublicAPI +def model_configs_for_dataset(dataset_name: str) -> dict[str, dict]: + """Returns a dictionary of built-in model configs for the specified dataset. + + Maps config name to ludwig config dict. + """ + return _get_model_configs(dataset_name) + + +@lru_cache(maxsize=3) +def _get_model_configs(dataset_name: str) -> dict[str, dict]: + """Returns all model configs for the specified dataset. + + Model configs are named _.yaml + """ + import importlib.resources + + config_filenames = [ + f.name + for f in importlib.resources.files(model_configs).iterdir() + if f.name.endswith(".yaml") and f.name.startswith(dataset_name) + ] + configs = {} + for config_filename in config_filenames: + basename = os.path.splitext(config_filename)[0] + config_name = basename[len(dataset_name) + 1 :] + configs[config_name] = _load_model_config(config_filename) + return configs + + +def _load_model_config(model_config_filename: str): + """Loads a model config.""" + model_config_path = os.path.join(os.path.dirname(model_configs.__file__), model_config_filename) + with open(model_config_path) as f: + return yaml.safe_load(f) diff --git a/ludwig/decoders/__init__.py b/ludwig/decoders/__init__.py new file mode 100644 index 0000000..c726801 --- /dev/null +++ b/ludwig/decoders/__init__.py @@ -0,0 +1,6 @@ +# register all decoders +import ludwig.decoders.generic_decoders +import ludwig.decoders.image_decoders +import ludwig.decoders.llm_decoders +import ludwig.decoders.sequence_decoders +import ludwig.decoders.sequence_tagger # noqa diff --git a/ludwig/decoders/base.py b/ludwig/decoders/base.py new file mode 100644 index 0000000..89b1404 --- /dev/null +++ b/ludwig/decoders/base.py @@ -0,0 +1,31 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from abc import ABC, abstractmethod + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.torch_utils import LudwigModule + + +@DeveloperAPI +class Decoder(LudwigModule, ABC): + @abstractmethod + def forward(self, inputs, mask=None): + raise NotImplementedError + + @property + def name(self): + return self.__class__.__name__ diff --git a/ludwig/decoders/generic_decoders.py b/ludwig/decoders/generic_decoders.py new file mode 100644 index 0000000..e2dea31 --- /dev/null +++ b/ludwig/decoders/generic_decoders.py @@ -0,0 +1,439 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from functools import partial + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + ANOMALY, + BINARY, + CATEGORY, + CATEGORY_DISTRIBUTION, + LOSS, + NUMBER, + SET, + TIMESERIES, + TYPE, + VECTOR, +) +from ludwig.decoders.base import Decoder +from ludwig.decoders.registry import register_decoder +from ludwig.schema.decoders.base import ClassifierConfig, PassthroughDecoderConfig, ProjectorConfig, RegressorConfig +from ludwig.utils.torch_utils import Dense, get_activation + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +# TODO(Arnav): Re-enable once we add DotProduct Combiner: https://github.com/ludwig-ai/ludwig/issues/3150 +# @register_decoder("passthrough", [BINARY, CATEGORY, NUMBER, SET, VECTOR, SEQUENCE, TEXT]) +class PassthroughDecoder(Decoder): + def __init__(self, input_size: int = 1, num_classes: int | None = None, decoder_config=None, **kwargs): + super().__init__() + self.config = decoder_config + + logger.debug(f" {self.name}") + self.input_size = input_size + self.num_classes = num_classes + + def forward(self, inputs, **kwargs): + return inputs + + @staticmethod + def get_schema_cls(): + return PassthroughDecoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return self.input_shape + + +@DeveloperAPI +@register_decoder("regressor", [BINARY, NUMBER]) +class Regressor(Decoder): + def __init__( + self, + input_size, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + + logger.debug(f" {self.name}") + + logger.debug(" Dense") + + self.dense = Dense( + input_size=input_size, + output_size=1, + use_bias=use_bias, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + ) + + @staticmethod + def get_schema_cls(): + return RegressorConfig + + @property + def input_shape(self): + return self.dense.input_shape + + def forward(self, inputs, **kwargs): + return self.dense(inputs) + + +@DeveloperAPI +@register_decoder("projector", [VECTOR, TIMESERIES]) +class Projector(Decoder): + def __init__( + self, + input_size, + output_size, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + activation=None, + multiplier=1.0, + clip=None, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + + logger.debug(f" {self.name}") + + logger.debug(" Dense") + self.dense = Dense( + input_size=input_size, + output_size=output_size, + use_bias=use_bias, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + ) + + self.activation = get_activation(activation) + self.multiplier = multiplier + + if clip is not None: + if isinstance(clip, (list, tuple)) and len(clip) == 2: + self.clip = partial(torch.clip, min=clip[0], max=clip[1]) + else: + raise ValueError( + f"The clip parameter of {self.feature_name} is {self.clip}. " + "It must be a list or a tuple of length 2." + ) + else: + self.clip = None + + @staticmethod + def get_schema_cls(): + return ProjectorConfig + + @property + def input_shape(self): + return self.dense.input_shape + + def forward(self, inputs, **kwargs): + values = self.activation(self.dense(inputs)) * self.multiplier + if self.clip: + values = self.clip(values) + return values + + +@DeveloperAPI +@register_decoder("classifier", [CATEGORY, CATEGORY_DISTRIBUTION, SET]) +class Classifier(Decoder): + def __init__( + self, + input_size, + num_classes, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + + logger.debug(f" {self.name}") + + logger.debug(" Dense") + self.num_classes = num_classes + self.dense = Dense( + input_size=input_size, + output_size=num_classes, + use_bias=use_bias, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + ) + + self.sampled_loss = False + if LOSS in kwargs and TYPE in kwargs[LOSS] and kwargs[LOSS][TYPE] is not None: + self.sampled_loss = kwargs[LOSS][TYPE].startswith("sampled") + + @staticmethod + def get_schema_cls(): + return ClassifierConfig + + @property + def input_shape(self): + return self.dense.input_shape + + def forward(self, inputs, **kwargs): + return self.dense(inputs) + + +@DeveloperAPI +@register_decoder("anomaly", [ANOMALY]) +class AnomalyDecoder(Decoder): + """AnomalyDecoder: computes ||z - c||^2 as the anomaly score. + + The center ``c`` is the mean of all encoder outputs from the first training epoch, + computed by ``AnomalyOutputFeature.initialize_center()`` and stored as a non-trainable + ``register_buffer``. Until that call the center is the zero vector, so anomaly scores + are raw squared norms which is still a valid (if uncentered) distance metric. + + This implements the hard-boundary Deep SVDD objective from Ruff et al. (ICML 2018). + With the ECD combiner you get *free* multimodal anomaly detection: feed tabular, + image, text or any mix of input features and the combiner fuses them into a single + latent vector that the decoder compares against the center. + + Args: + input_size: Latent space dimensionality (set automatically from the FC stack). + decoder_config: AnomalyDecoderConfig instance. + """ + + def __init__(self, input_size: int | None = None, decoder_config=None, **kwargs): + super().__init__() + self.config = decoder_config + self.input_size = input_size or 1 + logger.debug(f" {self.name}") + self.register_buffer("center", torch.zeros(self.input_size)) + self._center_initialized = False + + def initialize_center(self, center: torch.Tensor) -> None: + """Set the hypersphere center from the mean of first-epoch encoder outputs. + + Args: + center: Tensor of shape [input_size]. + """ + if center.shape != self.center.shape: + raise ValueError(f"Center shape mismatch: expected {self.center.shape}, got {center.shape}.") + self.center.copy_(center) + self._center_initialized = True + + def forward(self, inputs: torch.Tensor, **kwargs) -> torch.Tensor: + """Compute anomaly scores as squared distance to hypersphere center. + + Args: + inputs: Encoder output, shape [batch, input_size]. + + Returns: + Anomaly scores, shape [batch] (higher = more anomalous). + """ + diff = inputs - self.center.unsqueeze(0) + return (diff * diff).sum(dim=-1) + + @staticmethod + def get_schema_cls(): + from ludwig.schema.decoders.base import AnomalyDecoderConfig + + return AnomalyDecoderConfig + + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([1]) + + +@DeveloperAPI +@register_decoder("mlp_classifier", [CATEGORY, BINARY]) +class MLPClassifier(Decoder): + """Multi-layer perceptron classifier decoder. + + Stacks ``num_fc_layers`` fully-connected hidden layers (each of size ``output_size``, with + configurable ``activation`` and ``dropout``) before a final linear projection to ``num_classes`` + logits. When ``num_fc_layers=0`` this is equivalent to the standard ``Classifier`` decoder. + Use this decoder when the combiner output benefits from additional non-linear transformation + before the classification head -- for example, when using a simple concatenation combiner with + heterogeneous input features. + + Supports two optional inference-time extensions: + + * **Temperature scaling** (``calibration='temperature_scaling'`` on the decoder config): + After training, a single scalar *T* is learned on the validation set so that + ``calibrated_logits = logits / T`` minimises negative log-likelihood. This reliably + reduces overconfidence without changing argmax predictions. + See: Guo et al., "On Calibration of Modern Neural Networks", ICML 2017. + + * **MC Dropout uncertainty** (``mc_dropout_samples > 0``): + At inference time the decoder is run ``mc_dropout_samples`` times with dropout layers kept + in training mode. The mean of the resulting probability distributions is returned as the + prediction, and the per-class variance is reported as an ``uncertainty`` tensor. + See: Gal & Ghahramani, "Dropout as a Bayesian Approximation: Representing Model + Uncertainty in Deep Learning", ICML 2016. + """ + + def __init__( + self, + input_size: int, + num_classes: int | None = None, + num_fc_layers: int = 1, + output_size: int = 256, + activation: str = "relu", + dropout: float = 0.0, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + mc_dropout_samples: int = 0, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + + logger.debug(f" {self.name}") + + # For binary features num_classes is not set by the caller; default to 1 (single logit). + effective_num_classes = num_classes if num_classes is not None else 1 + self.num_classes = effective_num_classes + self.mc_dropout_samples = mc_dropout_samples + self._input_size = input_size + + # Build hidden FC layers. FCLayer is imported here to avoid circular imports at + # module load time (fully_connected_modules imports from torch_utils which imports Decoder). + from ludwig.modules.fully_connected_modules import FCLayer + + self.fc_layers = torch.nn.ModuleList() + current_size = input_size + for i in range(num_fc_layers): + logger.debug(f" FCLayer {i}") + layer = FCLayer( + input_size=current_size, + output_size=output_size, + use_bias=use_bias, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + activation=activation, + dropout=dropout, + ) + self.fc_layers.append(layer) + current_size = output_size + + # Final linear projection to logits. + logger.debug(" Linear (classification head)") + self.output_layer = torch.nn.Linear(current_size, effective_num_classes, bias=use_bias) + if use_bias: + torch.nn.init.zeros_(self.output_layer.bias) + _init_fns = { + "xavier_uniform": torch.nn.init.xavier_uniform_, + "xavier_normal": torch.nn.init.xavier_normal_, + "zeros": torch.nn.init.zeros_, + } + _init_fns.get(weights_initializer, torch.nn.init.xavier_uniform_)(self.output_layer.weight) + + self._hidden_size = current_size + + @staticmethod + def get_schema_cls(): + from ludwig.schema.decoders.base import MLPClassifierConfig + + return MLPClassifierConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self._input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.num_classes]) + + def _single_forward(self, inputs: torch.Tensor) -> torch.Tensor: + """Run one forward pass through hidden layers + classification head.""" + hidden = inputs + for layer in self.fc_layers: + hidden = layer(hidden) + logits = self.output_layer(hidden) + # For binary (num_classes=1), squeeze to match the expected 1-D logit shape. + if self.num_classes == 1: + logits = logits.squeeze(-1) + return logits + + def forward(self, inputs: torch.Tensor, **kwargs) -> torch.Tensor: + """Run the forward pass and return raw logits. + + Returns logits of shape ``[batch, num_classes]`` for category features, + or ``[batch]`` for binary features. + MC Dropout inference is available via :meth:`mc_forward`. + """ + return self._single_forward(inputs) + + def mc_forward(self, inputs: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """MC Dropout inference: run ``mc_dropout_samples`` stochastic passes. + + Keeps dropout layers in training mode so each pass samples a different sub-network, + then averages the resulting probability distributions. + + For category features returns (mean_probs [batch, num_classes], variance [batch, num_classes]). + For binary features returns (mean_probs [batch, 2], variance [batch, 2]). + + Args: + inputs: Float tensor of shape ``[batch, input_size]``. + + Returns: + mean_probs: Averaged softmax/sigmoid probabilities over MC samples. + uncertainty: Per-class variance across MC samples. + + References: + Gal & Ghahramani, "Dropout as a Bayesian Approximation: Representing Model + Uncertainty in Deep Learning", ICML 2016. + """ + was_training = self.training + # Enable dropout (train mode) while keeping BN in eval mode if present. + self.train() + with torch.no_grad(): + probs_list = [] + for _ in range(self.mc_dropout_samples): + logits = self._single_forward(inputs) + if self.num_classes == 1: + # Binary: convert scalar logit to 2-class probability pair for consistent output. + pos_prob = torch.sigmoid(logits) + probs = torch.stack([1 - pos_prob, pos_prob], dim=-1) + else: + probs = torch.softmax(logits, dim=-1) + probs_list.append(probs) + if not was_training: + self.eval() + # Stack to [num_samples, batch, 2_or_num_classes] + probs_stack = torch.stack(probs_list, dim=0) + mean_probs = probs_stack.mean(dim=0) + uncertainty = probs_stack.var(dim=0) + return mean_probs, uncertainty diff --git a/ludwig/decoders/image_decoders.py b/ludwig/decoders/image_decoders.py new file mode 100644 index 0000000..7a392f3 --- /dev/null +++ b/ludwig/decoders/image_decoders.py @@ -0,0 +1,351 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Aizen Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT_STATE, HIDDEN, IMAGE, LOGITS, PREDICTIONS +from ludwig.decoders.base import Decoder +from ludwig.decoders.registry import register_decoder +from ludwig.modules.convolutional_modules import UNetUpStack +from ludwig.schema.decoders.image_decoders import ( + FPNDecoderConfig, + ImageDecoderConfig, + SegFormerDecoderConfig, + UNetDecoderConfig, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_decoder("unet", IMAGE) +class UNetDecoder(Decoder): + """U-Net decoder for dense pixel-level prediction (e.g. semantic segmentation). + + Implements the expansive upsampling path of U-Net, consisting of ``num_stages`` + transposed-convolution up-sampling blocks with skip connections from the encoder. + Each block doubles the spatial resolution and halves the channel count, ending + with a 1×1 convolution that maps to ``num_classes`` output channels. + + Choose this decoder when: + - Your encoder also follows a U-Net / encoder-decoder structure and provides skip + connections via ``ENCODER_OUTPUT_STATE``. + - You need high-resolution, pixel-accurate segmentation masks. + - You want a well-understood, battle-tested architecture. + + Reference: + Ronneberger et al., "U-Net: Convolutional Networks for Biomedical Image + Segmentation", MICCAI 2015. https://arxiv.org/abs/1505.04597 + """ + + def __init__( + self, + input_size: int, + height: int, + width: int, + num_channels: int = 1, + num_classes: int = 2, + conv_norm: str | None = None, + num_stages: int = 4, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + self.num_classes = num_classes + + logger.debug(f" {self.name}") + if num_classes < 2: + raise ValueError(f"Invalid `num_classes` {num_classes} for unet decoder") + + divisor = 2**num_stages + if height % divisor or width % divisor: + raise ValueError( + f"Invalid `height` {height} or `width` {width} for unet decoder with " + f"num_stages={num_stages}: dimensions must be divisible by {divisor}" + ) + + self.unet = UNetUpStack( + img_height=height, + img_width=width, + out_channels=num_classes, + norm=conv_norm, + stack_depth=num_stages, + ) + + self.input_reshape = list(self.unet.input_shape) + self.input_reshape.insert(0, -1) + self._output_shape = (height, width) + + def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor): + hidden = combiner_outputs[HIDDEN] + skips = combiner_outputs[ENCODER_OUTPUT_STATE] + + # unflatten combiner outputs + hidden = hidden.reshape(self.input_reshape) + + logits = self.unet(hidden, skips) + predictions = logits.argmax(dim=1).squeeze(1).byte() + + return {LOGITS: logits, PREDICTIONS: predictions} + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS} + + @staticmethod + def get_schema_cls() -> type[ImageDecoderConfig]: + return UNetDecoderConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return self.unet.input_shape + + +@DeveloperAPI +@register_decoder("segformer", IMAGE) +class SegFormerDecoder(Decoder): + """Lightweight all-MLP decoder head for semantic segmentation. + + Takes the flat feature vector produced by the combiner, reshapes it into a + spatial feature map (using the square-root of ``input_size // hidden_size`` + as the intermediate spatial extent), applies a two-layer MLP projection, then + bilinearly upsamples to the target ``(height, width)`` and produces per-pixel + class logits with a final 1×1 convolution. + + This design is inspired by the SegFormer decode head which intentionally omits + complex spatial attention so that the encoder (typically a hierarchical + transformer) carries the representational burden. + + Choose this decoder when: + - Your encoder is a transformer (ViT, Swin, DeiT, …) and produces a rich, + globally-aware feature vector. + - You want a fast, low-parameter decoder that does not bottleneck training. + - Memory is constrained — no skip connections or transposed convolutions. + + Reference: + Xie et al., "SegFormer: Simple and Efficient Design for Semantic + Segmentation with Transformers", NeurIPS 2021. + https://arxiv.org/abs/2105.15203 + """ + + def __init__( + self, + input_size: int, + height: int, + width: int, + num_channels: int = 1, + num_classes: int = 2, + hidden_size: int = 256, + dropout: float = 0.1, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + self.num_classes = num_classes + self.height = height + self.width = width + + logger.debug(f" {self.name}") + if num_classes < 2: + raise ValueError(f"Invalid `num_classes` {num_classes} for segformer decoder") + + # MLP projection: input_size → hidden_size → hidden_size + self.mlp = nn.Sequential( + nn.Linear(input_size, hidden_size), + nn.ReLU(inplace=True), + nn.Dropout(p=dropout), + nn.Linear(hidden_size, hidden_size), + nn.ReLU(inplace=True), + ) + + # Final 1×1 conv to produce per-pixel class scores. + # We treat the hidden_size channels as a (hidden_size, 1, 1) feature map + # and upsample to (height, width) before applying this conv. + self.classifier = nn.Conv2d(hidden_size, num_classes, kernel_size=1) + + self._input_shape = torch.Size([input_size]) + self._output_shape = torch.Size([height, width]) + + def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor): + hidden = combiner_outputs[HIDDEN] # (B, input_size) + + # MLP projection + features = self.mlp(hidden) # (B, hidden_size) + + # Reshape to (B, hidden_size, 1, 1) and upsample to target resolution + features = features.unsqueeze(-1).unsqueeze(-1) # (B, hidden_size, 1, 1) + features = F.interpolate(features, size=(self.height, self.width), mode="bilinear", align_corners=False) + + # Per-pixel classification + logits = self.classifier(features) # (B, num_classes, H, W) + predictions = logits.argmax(dim=1).squeeze(1).byte() # (B, H, W) + + return {LOGITS: logits, PREDICTIONS: predictions} + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS} + + @staticmethod + def get_schema_cls() -> type[ImageDecoderConfig]: + return SegFormerDecoderConfig + + @property + def output_shape(self) -> torch.Size: + return self._output_shape + + @property + def input_shape(self) -> torch.Size: + return self._input_shape + + +@DeveloperAPI +@register_decoder("fpn", IMAGE) +class FPNDecoder(Decoder): + """Feature Pyramid Network (FPN) decoder for multi-scale segmentation. + + Builds a feature pyramid using lateral 1×1 projections and a top-down + pathway. The flat feature vector from the combiner is reshaped into a + spatial map at the coarsest scale, then progressively upsampled and merged + across ``num_levels`` levels. Each level doubles the spatial resolution. + All levels are upsampled to the finest scale, concatenated, and projected + with a 3×3 convolution followed by a 1×1 classifier to produce per-pixel + logits. + + Choose this decoder when: + - Your task requires detecting / segmenting objects at multiple scales + simultaneously. + - You have a relatively powerful encoder (CNN or ViT) whose output you want + to leverage at different resolutions. + - You can afford slightly more compute than the SegFormer MLP head. + + Reference: + Lin et al., "Feature Pyramid Networks for Object Detection", + CVPR 2017. https://arxiv.org/abs/1612.03144 + """ + + def __init__( + self, + input_size: int, + height: int, + width: int, + num_classes: int = 2, + num_channels: int = 256, + num_levels: int = 4, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + self.num_classes = num_classes + self.num_channels = num_channels + self.num_levels = num_levels + self.height = height + self.width = width + + logger.debug(f" {self.name}") + if num_classes < 2: + raise ValueError(f"Invalid `num_classes` {num_classes} for fpn decoder") + if num_levels < 1: + raise ValueError(f"Invalid `num_levels` {num_levels} for fpn decoder; must be >= 1") + + # Determine the coarsest spatial size (level 0 of the pyramid). + # The coarsest height/width is height / 2^(num_levels-1), rounded up. + self.coarse_h = math.ceil(height / (2 ** (num_levels - 1))) + self.coarse_w = math.ceil(width / (2 ** (num_levels - 1))) + + # Project the flat combiner output into (num_channels, coarse_h, coarse_w) + coarse_spatial = self.coarse_h * self.coarse_w + self.input_proj = nn.Linear(input_size, num_channels * coarse_spatial) + + # Top-down lateral projections: one 1×1 conv per level (applied to + # the up-sampled feature from the previous level). Level 0 is the + # coarsest and requires no lateral merge — we start from level 1. + self.lateral_convs = nn.ModuleList( + [nn.Conv2d(num_channels, num_channels, kernel_size=1) for _ in range(num_levels - 1)] + ) + + # After merging all levels at the finest scale we apply a 3×3 conv + # over all concatenated levels to mix information. + self.merge_conv = nn.Conv2d(num_channels * num_levels, num_channels, kernel_size=3, padding=1) + + # Final 1×1 classifier + self.classifier = nn.Conv2d(num_channels, num_classes, kernel_size=1) + + self._input_shape = torch.Size([input_size]) + self._output_shape = torch.Size([height, width]) + + def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor): + hidden = combiner_outputs[HIDDEN] # (B, input_size) + batch_size = hidden.shape[0] + + # Project and reshape to coarsest spatial feature map + coarse = self.input_proj(hidden) # (B, num_channels * coarse_h * coarse_w) + coarse = coarse.view(batch_size, self.num_channels, self.coarse_h, self.coarse_w) + + # Build the top-down pyramid. + # pyramid[0] is the coarsest; pyramid[-1] will be the finest. + pyramid = [coarse] + current = coarse + for lateral_conv in self.lateral_convs: + # Double the spatial resolution + upsampled = F.interpolate(current, scale_factor=2, mode="nearest") + current = lateral_conv(upsampled) + pyramid.append(current) + + # Upsample all levels to the finest pyramid resolution, then to target size. + finest_h = pyramid[-1].shape[2] + finest_w = pyramid[-1].shape[3] + + merged = [] + for level_feat in pyramid: + if level_feat.shape[2] != finest_h or level_feat.shape[3] != finest_w: + level_feat = F.interpolate(level_feat, size=(finest_h, finest_w), mode="bilinear", align_corners=False) + level_feat = F.interpolate(level_feat, size=(self.height, self.width), mode="bilinear", align_corners=False) + merged.append(level_feat) + + # Concatenate across channel dimension and mix + fused = torch.cat(merged, dim=1) # (B, num_channels * num_levels, H, W) + fused = self.merge_conv(fused) # (B, num_channels, H, W) + + logits = self.classifier(fused) # (B, num_classes, H, W) + predictions = logits.argmax(dim=1).squeeze(1).byte() # (B, H, W) + + return {LOGITS: logits, PREDICTIONS: predictions} + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS} + + @staticmethod + def get_schema_cls() -> type[ImageDecoderConfig]: + return FPNDecoderConfig + + @property + def output_shape(self) -> torch.Size: + return self._output_shape + + @property + def input_shape(self) -> torch.Size: + return self._input_shape diff --git a/ludwig/decoders/llm_decoders.py b/ludwig/decoders/llm_decoders.py new file mode 100644 index 0000000..1e05779 --- /dev/null +++ b/ludwig/decoders/llm_decoders.py @@ -0,0 +1,522 @@ +import json +import logging +import math +import re +from typing import Any + +import torch +import torch.nn.functional as F +from transformers import LogitsProcessor, LogitsProcessorList + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import CATEGORY, LOGITS, PREDICTIONS, PROBABILITIES, TEXT +from ludwig.decoders.base import Decoder +from ludwig.decoders.registry import register_decoder +from ludwig.decoders.utils import extract_generated_tokens +from ludwig.schema.decoders.llm_decoders import CategoryExtractorDecoderConfig, TextExtractorDecoderConfig +from ludwig.utils.strings_utils import get_tokenizer + +logger = logging.getLogger(__name__) + + +class Matcher: + """Match a decoded LLM output string against a set of labelled patterns. + + Parameters + ---------- + match: + Dictionary mapping category label strings to pattern definitions. + Each definition is a dict with keys ``type`` (``"contains"``, + ``"regex"``, or ``"json_schema"``) and ``value`` (the substring or + regex pattern to match). + + ``__call__`` performs a greedy first-match scan and returns the first + label whose pattern matches. + """ + + def __init__(self, match: dict[str, dict[str, Any]]): + self.match = match + + def contains(self, decoded_input: str, value: str) -> bool: + """Return True if *value* is a substring of *decoded_input*.""" + return value in decoded_input + + def regex(self, decoded_input: str, regex_pattern: str) -> bool: + """Return True if *regex_pattern* matches anywhere in *decoded_input*. + + Parameters + ---------- + decoded_input: + The LLM-generated text to search. + regex_pattern: + A Python ``re``-compatible regular expression. + + Returns + ------- + bool + True when at least one match is found; False otherwise. + Compilation failures are logged as warnings and treated as no-match. + """ + matches = [] + try: + regex = re.compile(regex_pattern) + matches = regex.findall(decoded_input) + except re.error: + logger.warning(f"Regex pattern {regex_pattern} could not be compiled.") + return len(matches) > 0 + + def json_schema(self, decoded_input: str, expected_value: str) -> bool: + """Return True if the JSON-decoded output equals *expected_value*. + + The method attempts to parse *decoded_input* as JSON. If successful + it compares the (string-coerced) top-level value to *expected_value*. + Any parse error results in False so callers can fall back gracefully. + + Parameters + ---------- + decoded_input: + Raw text produced by the LLM (markdown code fences are stripped). + expected_value: + Category label string to compare against the parsed JSON value. + """ + text = decoded_input.strip() + if text.startswith("```"): + text = re.sub(r"^```[a-zA-Z]*\n?", "", text) + text = re.sub(r"\n?```$", "", text.strip()) + try: + parsed = json.loads(text) + if isinstance(parsed, dict): + parsed = ( + parsed.get("label") + or parsed.get("value") + or parsed.get("category") + or next(iter(parsed.values()), None) + ) + return str(parsed).lower() == expected_value.lower() + except (json.JSONDecodeError, TypeError, ValueError): + return False + + def __call__(self, decoded_input: str) -> str | None: + """Return the first label whose pattern matches *decoded_input*, or None.""" + for label, label_def in self.match.items(): + label_def_type = label_def["type"] + label_def_value = label_def["value"] + + if label_def_type == "contains": + is_match = self.contains(decoded_input, label_def_value) + elif label_def_type == "regex": + is_match = self.regex(decoded_input, label_def_value) + elif label_def_type == "json_schema": + is_match = self.json_schema(decoded_input, label_def_value) + else: + raise ValueError( + f"{label_def_type} is not a valid match `type`. Ludwig " + "currently supports `contains`, `regex`, and `json_schema` match types." + ) + + if is_match: + return label + return None + + +class CategoryVocabularyLogitsProcessor(LogitsProcessor): + """HuggingFace LogitsProcessor that restricts generation to valid category prefixes. + + At each decoding step the processor masks (sets to ``-inf``) every token + whose addition to the current partial sequence cannot be a prefix of any + known category label. This steers greedy / beam-search decoding towards + producing only recognisable label strings. + + Parameters + ---------- + category_labels: + The set of valid category label strings (lower-cased comparison). + tokenizer: + A HuggingFace tokenizer used to encode each label into token ids. + eos_token_id: + Token id of the end-of-sequence token; always kept unmasked. + """ + + def __init__(self, category_labels: list[str], tokenizer, eos_token_id: int): + self.eos_token_id = eos_token_id + self._label_token_ids: list[list[int]] = [] + for label in category_labels: + ids = tokenizer.encode(label.lower(), add_special_tokens=False) + self._label_token_ids.append(ids) + + self._prefix_cache: dict[tuple[int, ...], set[int]] = {} + self._build_prefix_cache() + + def _build_prefix_cache(self): + """Pre-populate the prefix cache for all label token sequences.""" + for ids in self._label_token_ids: + for depth in range(len(ids)): + prefix = tuple(ids[:depth]) + allowed = self._prefix_cache.setdefault(prefix, set()) + allowed.add(ids[depth]) + self._prefix_cache.setdefault((), set()) + + def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor: + """Mask tokens that cannot extend a valid category prefix. + + Parameters + ---------- + input_ids: + Shape ``(batch_size, sequence_length)``. + scores: + Shape ``(batch_size, vocab_size)``. + + Returns + ------- + torch.FloatTensor + Modified *scores* with invalid token positions set to ``-inf``. + """ + for batch_idx in range(input_ids.shape[0]): + generated_ids = input_ids[batch_idx] + allowed: set[int] | None = None + for tail_len in range(len(generated_ids) + 1): + tail = tuple(generated_ids[-tail_len:].tolist()) if tail_len > 0 else () + if tail in self._prefix_cache: + allowed = self._prefix_cache[tail] + break + + if allowed is not None: + mask = torch.full((scores.shape[1],), -math.inf, device=scores.device, dtype=scores.dtype) + for token_id in allowed: + if token_id < scores.shape[1]: + mask[token_id] = 0.0 + if self.eos_token_id is not None and self.eos_token_id < scores.shape[1]: + mask[self.eos_token_id] = 0.0 + scores[batch_idx] = scores[batch_idx] + mask + return scores + + +@DeveloperAPI +@register_decoder("text_extractor", [TEXT]) +class TextExtractorDecoder(Decoder): + """Decoder for free-form text generation from an LLM. + + Extracts the generated token sequences from the raw LLM output and + computes real token-level log-probabilities from the generation scores + returned by ``model.generate(..., output_scores=True)``. + + ``LOGITS`` contains per-step vocabulary logits stacked as a tensor of + shape ``(batch, max_new_tokens, vocab_size)``. ``PROBABILITIES`` is the + softmax of ``LOGITS`` over the vocabulary dimension. + + Parameters + ---------- + input_size: + Size of the input representation (vocabulary size of the LM head). + decoder_config: + An instance of ``TextExtractorDecoderConfig``. + """ + + def __init__( + self, + input_size: int, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + self.input_size = input_size + + self.tokenizer_type = self.config.tokenizer + self.pretrained_model_name_or_path = self.config.pretrained_model_name_or_path + self.vocab_file = self.config.vocab_file + + self.tokenizer = get_tokenizer(self.tokenizer_type, self.vocab_file, self.pretrained_model_name_or_path) + if hasattr(self.tokenizer, "tokenizer"): + self.tokenizer_vocab_size = self.tokenizer.tokenizer.vocab_size + else: + self.tokenizer_vocab_size = len(self.tokenizer.vocab) + + # TODO(geoffrey): figure out where self.max_sequence_length is used. If not used, consider removing it. + # It's confusing to have both this and `max_new_tokens` as a mandatory param in the `forward` function. + self.max_sequence_length = self.config.max_new_tokens + self.match_strategy = getattr(self.config, "match_strategy", "contains") + + @staticmethod + def get_schema_cls(): + return TextExtractorDecoderConfig + + @property + def input_shape(self): + return self.input_size + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS, PROBABILITIES} + + def forward( + self, + inputs: list[torch.Tensor], + input_lengths: list[int], + max_new_tokens: int, + generation_scores: list[tuple[torch.Tensor, ...]] | None = None, + ): + """Extract predictions and compute logits / probabilities. + + Parameters + ---------- + inputs: + List of raw generated sequence tensors (one per batch item), + each including the prompt tokens. + input_lengths: + Number of prompt tokens for each batch item. + max_new_tokens: + Maximum number of tokens that can be generated. + generation_scores: + Per-sample generation scores from + ``model.generate(..., output_scores=True)``. Each element is a + tuple of ``(vocab_size,)`` tensors—one per generated token. + When None, zero-filled tensors are returned for backward compat. + + Returns + ------- + dict + Keys: ``PREDICTIONS``, ``PROBABILITIES``, ``LOGITS``. + """ + generated_outputs = extract_generated_tokens( + raw_generated_output_sequences=inputs, + input_lengths=input_lengths, + max_new_tokens=max_new_tokens, + pad_sequence=True, + ) + for output in generated_outputs: + if output.shape[0] > max_new_tokens: + raise ValueError( + f"Output {output} is longer than the max_new_tokens {max_new_tokens} during decoding. " + f"This should never happen– please file an issue on GitHub." + ) + + generated_outputs = torch.stack(generated_outputs, dim=0) + outputs_device = generated_outputs.device + batch_size = generated_outputs.shape[0] + + if generation_scores is not None: + logits_list = [] + for sample_scores in generation_scores: + if len(sample_scores) == 0: + sample_logits = torch.zeros(max_new_tokens, self.tokenizer_vocab_size, device=outputs_device) + else: + stacked = torch.stack(list(sample_scores), dim=0).to(outputs_device) + num_generated = stacked.shape[0] + vocab_size = min(stacked.shape[1], self.tokenizer_vocab_size) + padded = torch.zeros(max_new_tokens, self.tokenizer_vocab_size, device=outputs_device) + padded[: min(num_generated, max_new_tokens), :vocab_size] = stacked[ + : min(num_generated, max_new_tokens), :vocab_size + ] + sample_logits = padded + logits_list.append(sample_logits) + + logits_tensor = torch.stack(logits_list, dim=0) + probabilities_tensor = F.softmax(logits_tensor, dim=-1) + else: + logits_tensor = torch.zeros(batch_size, max_new_tokens, self.tokenizer_vocab_size, device=outputs_device) + probabilities_tensor = torch.zeros( + batch_size, max_new_tokens, self.tokenizer_vocab_size, device=outputs_device + ) + + return { + PREDICTIONS: generated_outputs, + PROBABILITIES: probabilities_tensor, + LOGITS: logits_tensor, + } + + +@DeveloperAPI +@register_decoder("category_extractor", [CATEGORY]) +class CategoryExtractorDecoder(Decoder): + """Decoder that maps LLM-generated text to a discrete category label. + + Category prediction proceeds in three stages: + + 1. **Generation**: The parent LLM model generates token sequences. + 2. **Parsing**: Generated text is decoded and matched against label + patterns using :class:`Matcher`. + 3. **Probability estimation**: When ``generation_scores`` are available, + category probabilities are derived from token logits by mapping each + label's first token to its score and normalising via softmax. + + Constrained decoding is available when ``constrain_to_vocabulary=True``. + Call ``get_logits_processor()`` and pass the result to ``model.generate``. + + Parameters + ---------- + decoder_config: + An instance of ``CategoryExtractorDecoderConfig``. + """ + + def __init__( + self, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + + self.input_size = self.config.input_size + self.fallback_label = self.config.fallback_label + self.str2idx = self.config.str2idx + self.vocab_size = len(self.config.str2idx) + self.match_strategy = getattr(self.config, "match_strategy", "contains") + self.constrain_to_vocabulary = getattr(self.config, "constrain_to_vocabulary", False) + + self.matcher = Matcher(self.config.match) + + self.tokenizer_type = self.config.tokenizer + self.pretrained_model_name_or_path = self.config.pretrained_model_name_or_path + self.vocab_file = self.config.vocab_file + + self.tokenizer = get_tokenizer(self.tokenizer_type, self.vocab_file, self.pretrained_model_name_or_path) + + # Pre-compute first-token ids for logit-based probability estimation. + self._label_first_token_ids: dict[str, int] = {} + if hasattr(self.tokenizer, "tokenizer"): + hf_tok = self.tokenizer.tokenizer + for label in self.str2idx: + ids = hf_tok.encode(label.lower(), add_special_tokens=False) + if ids: + self._label_first_token_ids[label] = ids[0] + + @staticmethod + def get_schema_cls(): + return CategoryExtractorDecoderConfig + + @property + def input_shape(self): + return self.input_size + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS, PROBABILITIES} + + def get_logits_processor(self) -> LogitsProcessorList | None: + """Return a LogitsProcessorList for constrained decoding, or None. + + When ``constrain_to_vocabulary`` is True this returns a list containing + a :class:`CategoryVocabularyLogitsProcessor` that restricts generation + to valid category label prefixes. + + Returns + ------- + LogitsProcessorList or None + """ + if not self.constrain_to_vocabulary: + return None + if not hasattr(self.tokenizer, "tokenizer"): + logger.warning( + "constrain_to_vocabulary=True requires an HF tokenizer. Falling back to unconstrained generation." + ) + return None + + hf_tok = self.tokenizer.tokenizer + labels = list(self.str2idx.keys()) + processor = CategoryVocabularyLogitsProcessor(labels, hf_tok, hf_tok.eos_token_id) + return LogitsProcessorList([processor]) + + def _compute_category_logits_from_scores( + self, + generation_scores: tuple[torch.Tensor, ...], + device: torch.device, + ) -> torch.Tensor: + """Compute a ``(num_categories,)`` logit vector from token generation scores. + + For each known category label we look up the logit score of the + label's first sub-word token at the first generated position. + + Parameters + ---------- + generation_scores: + Tuple of per-step score tensors. Only index 0 is used. + device: + Target device for the output tensor. + + Returns + ------- + torch.Tensor + Shape ``(num_categories,)`` with logit scores; unknown labels + receive ``-inf``. + """ + cat_logits = torch.full((self.vocab_size,), -math.inf, device=device, dtype=torch.float32) + if not generation_scores or not self._label_first_token_ids: + return cat_logits + + first_step_scores = generation_scores[0].to(device) + for label, label_idx in self.str2idx.items(): + token_id = self._label_first_token_ids.get(label) + if token_id is not None and token_id < first_step_scores.shape[0]: + cat_logits[label_idx] = first_step_scores[token_id] + return cat_logits + + def forward( + self, + inputs: list[torch.Tensor], + input_lengths: list[int], + max_new_tokens: int, + generation_scores: list[tuple[torch.Tensor, ...]] | None = None, + ): + """Extract category predictions and compute probabilities. + + Parameters + ---------- + inputs: + List of raw generated sequence tensors (one per batch item). + input_lengths: + Number of prompt tokens for each batch item. + max_new_tokens: + Maximum number of tokens that may be generated. + generation_scores: + Per-sample generation scores from + ``model.generate(..., output_scores=True)``. Each element is a + tuple of ``(vocab_size,)`` tensors—one per generated token. + When None, one-hot probability vectors are used (original behaviour). + + Returns + ------- + dict + Keys: ``PREDICTIONS`` (int category indices), + ``PROBABILITIES`` (``(batch, num_classes)`` float), + ``LOGITS`` (same shape, raw scores before softmax). + """ + generated_outputs = extract_generated_tokens( + raw_generated_output_sequences=inputs, + input_lengths=input_lengths, + max_new_tokens=max_new_tokens, + pad_sequence=False, + ) + outputs_device = generated_outputs[0].device + + decoded_outputs = self.tokenizer.tokenizer.batch_decode(generated_outputs, skip_special_tokens=True) + + matched_labels = [] + probabilities = [] + logits = [] + + for sample_idx, output in enumerate(decoded_outputs): + output = output.lower() + + matched_label = self.matcher(output) + idx = self.str2idx[matched_label] if matched_label in self.str2idx else self.str2idx[self.fallback_label] + matched_labels.append(idx) + + if generation_scores is not None and sample_idx < len(generation_scores): + cat_logits = self._compute_category_logits_from_scores(generation_scores[sample_idx], outputs_device) + else: + # Fall back to one-hot hard assignment (original behaviour). + cat_logits = torch.full((self.vocab_size,), -math.inf, device=outputs_device, dtype=torch.float32) + cat_logits[idx] = 0.0 + + logits.append(cat_logits) + + # Replace -inf with a large negative number so softmax gives 0 prob. + finite_logits = cat_logits.clone() + finite_logits[finite_logits == -math.inf] = -1e9 + probs = F.softmax(finite_logits, dim=0) + probabilities.append(probs) + + return { + PREDICTIONS: torch.tensor(matched_labels, device=outputs_device), + PROBABILITIES: torch.stack(probabilities, dim=0), + LOGITS: torch.stack(logits, dim=0), + } diff --git a/ludwig/decoders/registry.py b/ludwig/decoders/registry.py new file mode 100644 index 0000000..05c1f6a --- /dev/null +++ b/ludwig/decoders/registry.py @@ -0,0 +1,35 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.decoders.base import Decoder +from ludwig.utils.registry import Registry + +_decoder_registry = Registry() + + +@DeveloperAPI +def get_decoder_registry() -> Registry: + return _decoder_registry + + +@DeveloperAPI +def register_decoder(name: str, features: str | list[str]): + if isinstance(features, str): + features = [features] + + def wrap(cls): + for feature in features: + feature_registry = get_decoder_registry().get(feature, {}) + feature_registry[name] = cls + get_decoder_registry()[feature] = feature_registry + return cls + + return wrap + + +@DeveloperAPI +def get_decoder_cls(feature: str, name: str) -> type[Decoder]: + return get_decoder_registry()[feature][name] + + +@DeveloperAPI +def get_decoder_classes(feature: str) -> dict[str, type[Decoder]]: + return get_decoder_registry()[feature] diff --git a/ludwig/decoders/sequence_decoder_utils.py b/ludwig/decoders/sequence_decoder_utils.py new file mode 100644 index 0000000..e6a4768 --- /dev/null +++ b/ludwig/decoders/sequence_decoder_utils.py @@ -0,0 +1,117 @@ +"""Utility functions related to sequence decoders.""" + +import torch + +from ludwig.constants import ENCODER_OUTPUT_STATE, HIDDEN +from ludwig.modules.reduction_modules import SequenceReducer + + +def repeat_2D_tensor(tensor, k): + """Repeats a 2D-tensor k times over the first dimension. + + For example: + Input: Tensor of [batch_size, state_size], k=2 + Output: Tensor of [k, batch_size, state_size] + """ + if len(tensor.size()) > 2: + raise ValueError("Cannot repeat a non-2D tensor with this method.") + return tensor.repeat(k, 1, 1) + + +def get_rnn_init_state( + combiner_outputs: dict[str, torch.Tensor], sequence_reducer: SequenceReducer, num_layers: int +) -> torch.Tensor: + """Computes the hidden state that the RNN decoder should start with. + + Args: + combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features. + sequence_reducer: SequenceReducer to reduce rank-3 to rank-2. + num_layers: Number of layers the decoder uses. + + Returns: + Tensor of [num_layers, batch_size, hidden_size]. + """ + if ENCODER_OUTPUT_STATE not in combiner_outputs: + # Use the combiner's hidden state. + encoder_output_state = combiner_outputs[HIDDEN] + else: + # Use the encoder's output state. + encoder_output_state = combiner_outputs[ENCODER_OUTPUT_STATE] + if isinstance(encoder_output_state, tuple): + if len(encoder_output_state) == 2: + # LSTM encoder. Use the hidden state and ignore the cell state. + encoder_output_state = encoder_output_state[0] + elif len(encoder_output_state) == 4: + # Bi-directional LSTM encoder. Use the average of hidden states and ignore cell state. + encoder_output_state = torch.mean([encoder_output_state[0], encoder_output_state[2]]) + else: + raise ValueError( + f"Invalid sequence decoder inputs with keys: {combiner_outputs.keys()} with extracted encoder " + + f"state: {encoder_output_state.size()} that was invalid. Please double check the compatibility " + + "of your encoder and decoder." + ) + + if len(encoder_output_state.size()) > 3: + raise ValueError("Init state for RNN decoders only works for 1d or 2d tensors (encoder_output).") + + if len(encoder_output_state.size()) == 3: + # Reduce to [batch_size, hidden_size]. + encoder_output_state = sequence_reducer(encoder_output_state) + + return repeat_2D_tensor(encoder_output_state, num_layers) + + +def get_lstm_init_state( + combiner_outputs: dict[str, torch.Tensor], sequence_reducer: SequenceReducer, num_layers: int +) -> tuple[torch.Tensor, torch.Tensor]: + """Returns the states that the LSTM decoder should start with. + + Args: + combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features. + sequence_reducer: SequenceReducer to reduce rank-3 to rank-2. + num_layers: Number of layers the decoder uses. + + Returns: + Tuple of 2 tensors (decoder hidden state, decoder cell state), each [num_layers, batch_size, hidden_size]. + """ + if ENCODER_OUTPUT_STATE not in combiner_outputs: + # Use the combiner's hidden state. + decoder_hidden_state = combiner_outputs[HIDDEN] + decoder_cell_state = torch.clone(decoder_hidden_state) + else: + # Use the encoder's output state. + encoder_output_state = combiner_outputs[ENCODER_OUTPUT_STATE] + if not isinstance(encoder_output_state, tuple): + decoder_hidden_state = encoder_output_state + decoder_cell_state = decoder_hidden_state + else: + if len(encoder_output_state) == 2: + # The encoder was probably an LSTM. + decoder_hidden_state, decoder_cell_state = encoder_output_state + elif len(encoder_output_state) == 4: + # The encoder was probably a bi-LSTM. + # Use the average of the encoder's hidden states for hidden state. + # Use the average of the encoder's cell states for cell state. + decoder_hidden_state = torch.mean([encoder_output_state[0], encoder_output_state[2]]) + decoder_cell_state = torch.mean([encoder_output_state[1], encoder_output_state[3]]) + else: + raise ValueError( + f"Invalid sequence decoder inputs with keys: {combiner_outputs.keys()} with extracted encoder " + + f"state: {encoder_output_state} that was invalid. Please double check the compatibility of your " + + "encoder and decoder." + ) + + # Check rank and reduce if necessary. + if len(decoder_hidden_state.size()) > 3 or len(decoder_cell_state.size()) > 3: + raise ValueError( + f"Invalid sequence decoder inputs with keys: {combiner_outputs.keys()} with extracted encoder " + + f"state: {decoder_hidden_state.size()} that was invalid. Please double check the compatibility " + + "of your encoder and decoder." + ) + if len(decoder_hidden_state.size()) == 3: + decoder_hidden_state = sequence_reducer(decoder_hidden_state) + if len(decoder_cell_state.size()) == 3: + decoder_cell_state = sequence_reducer(decoder_cell_state) + + # Repeat over the number of layers. + return repeat_2D_tensor(decoder_hidden_state, num_layers), repeat_2D_tensor(decoder_cell_state, num_layers) diff --git a/ludwig/decoders/sequence_decoders.py b/ludwig/decoders/sequence_decoders.py new file mode 100644 index 0000000..c5c2c32 --- /dev/null +++ b/ludwig/decoders/sequence_decoders.py @@ -0,0 +1,818 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import math + +import torch +import torch.nn as nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import LOGITS, PREDICTIONS, PROBABILITIES, SEQUENCE, TEXT +from ludwig.decoders.base import Decoder +from ludwig.decoders.registry import register_decoder +from ludwig.decoders.sequence_decoder_utils import get_lstm_init_state, get_rnn_init_state +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.schema.decoders.sequence_decoders import SequenceGeneratorDecoderConfig, TransformerDecoderConfig +from ludwig.utils import strings_utils + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +class RNNDecoder(nn.Module): + """GRU or RNN-based decoder.""" + + def __init__(self, hidden_size: int, vocab_size: int, cell_type: str, num_layers: int = 1): + super().__init__() + self.hidden_size = hidden_size + self.vocab_size = vocab_size + self.embedding = nn.Embedding(vocab_size, hidden_size) + if cell_type == "gru": + self.rnn = nn.GRU(hidden_size, hidden_size, num_layers=num_layers, batch_first=True) + else: + self.rnn = nn.RNN(hidden_size, hidden_size, num_layers=num_layers, batch_first=True) + self.out = nn.Linear(hidden_size, vocab_size) + + # Have the embedding and projection share weights. + # This is a trick used by the Transformer, and seems to attain better loss. + # See section 3.4 of https://arxiv.org/pdf/1706.03762.pdf. + self.out.weight = self.embedding.weight + + def forward(self, input: torch.Tensor, hidden: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Runs a single decoding time step. + + Modeled off of https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html. + + Args: + input: [batch_size] tensor with the previous step's predicted symbol. + hidden: [batch_size, hidden_size] tensor with the previous step's hidden state. + + Returns: + Tuple of two tensors: + - output: [batch_size, 1, vocab_size] tensor with the logits. + - hidden: [num_layers, batch_size, hidden_size] tensor with the hidden state for the next time step. + """ + # Unsqueeze predicted tokens. + input = input.unsqueeze(1).to(torch.int) + output = self.embedding(input) + output, hidden = self.rnn(output, hidden) + output_logits = self.out(output) + return output_logits, hidden + + +@DeveloperAPI +class LSTMDecoder(nn.Module): + """LSTM-based decoder.""" + + def __init__(self, hidden_size: int, vocab_size: int, num_layers: int = 1): + super().__init__() + self.hidden_size = hidden_size + self.vocab_size = vocab_size + self.embedding = nn.Embedding(vocab_size, hidden_size) + self.lstm = nn.LSTM(hidden_size, hidden_size, batch_first=True, num_layers=num_layers) + self.out = nn.Linear(hidden_size, vocab_size) + + # Have the embedding and projection share weights. + # This is a trick used by the Transformer, and seems to attain better loss. + # See section 3.4 of https://arxiv.org/pdf/1706.03762.pdf. + self.out.weight = self.embedding.weight + + def forward( + self, input: torch.Tensor, hidden_state: torch.Tensor, cell_state: torch.Tensor + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Runs a single decoding time step. + + Args: + input: [batch_size] tensor with the previous step's predicted symbol. + hidden_state: [num_layers, batch_size, hidden_size] tensor. + cell_state: [num_layers, batch_size, hidden_size] tensor. + + Returns: + Tuple of 3 tensors: + - output: [batch_size, vocab_size] tensor with the logits. + - hidden_state: [num_layers, batch_size, hidden_size] tensor. + - cell_state: [num_layers, batch_size, hidden_size] tensor. + """ + input = input.unsqueeze(1).to(torch.int) + output = self.embedding(input) + output, (hidden_state, cell_state) = self.lstm(output, (hidden_state, cell_state)) + output_logits = self.out(output) + return output_logits, hidden_state, cell_state + + +def _teacher_forcing_prob(step: int, decay: str, decay_rate: float) -> float: + """Computes the teacher-forcing probability for a given decoding step. + + Implements scheduled sampling schedules from Bengio et al., NeurIPS 2015: + "Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks". + + Args: + step: Current decoding step index (0-based). + decay: One of 'none', 'linear', or 'exponential'. + decay_rate: Per-step decay rate. + + Returns: + Probability in [0, 1] of using the ground-truth token as next input. + """ + if decay == "none": + return 1.0 + if decay == "linear": + return max(0.0, 1.0 - decay_rate * step) + if decay == "exponential": + return math.exp(-decay_rate * step) + raise ValueError(f"Unknown teacher_forcing_decay: {decay!r}. Choose 'none', 'linear', or 'exponential'.") + + +@DeveloperAPI +class SequenceRNNDecoder(nn.Module): + """RNN-based decoder over multiple time steps. + + Supports scheduled sampling (Bengio et al., NeurIPS 2015) to smoothly + interpolate between teacher forcing during early training and fully + autoregressive decoding at inference. + + References: + Bengio, S., Vinyals, O., Jaitly, N., & Shazeer, N. (2015). + Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks. + NeurIPS 2015. https://arxiv.org/abs/1506.03099 + + When to use: + - Sequence/text generation with a simple RNN backbone. + - Use teacher_forcing_decay='linear' or 'exponential' when the model + overfits to teacher forcing and performs poorly at inference. + """ + + def __init__( + self, + hidden_size: int, + vocab_size: int, + max_sequence_length: int, + cell_type: str, + num_layers: int = 1, + reduce_input="sum", + teacher_forcing_decay: str = "none", + teacher_forcing_decay_rate: float = 0.01, + ): + super().__init__() + self.hidden_size = hidden_size + self.vocab_size = vocab_size + self.rnn_decoder = RNNDecoder(hidden_size, vocab_size, cell_type, num_layers=num_layers) + self.max_sequence_length = max_sequence_length + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_input) + self.num_layers = num_layers + self.teacher_forcing_decay = teacher_forcing_decay + self.teacher_forcing_decay_rate = teacher_forcing_decay_rate + + self.register_buffer("logits", torch.zeros([max_sequence_length, vocab_size])) + self.register_buffer("decoder_input", torch.Tensor([strings_utils.SpecialSymbol.START.value])) + + def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor): + """Runs max_sequence_length RNN decoding time steps. + + Args: + combiner_outputs: Dict of tensors from the combiner and other output features. + target: [batch_size, max_sequence_length] target symbols, or None at inference. + + Returns: + Tensor of logits [batch_size, max_sequence_length, vocab_size]. + """ + decoder_hidden = get_rnn_init_state(combiner_outputs, self.reduce_sequence, self.num_layers) + batch_size = decoder_hidden.size()[1] + logits = self.logits.unsqueeze(0).repeat(batch_size, 1, 1) + decoder_input = self.decoder_input.repeat(batch_size) + is_training = self.training and target is not None + + for di in range(self.max_sequence_length): + decoder_output, decoder_hidden = self.rnn_decoder(decoder_input, decoder_hidden) + logits[:, di, :] = decoder_output.squeeze(1) + + if target is None: + _, topi = decoder_output.topk(1) + decoder_input = topi.squeeze(1).squeeze(1).detach() + elif is_training and self.teacher_forcing_decay != "none": + tf_prob = _teacher_forcing_prob(di, self.teacher_forcing_decay, self.teacher_forcing_decay_rate) + if torch.rand(1).item() < tf_prob: + decoder_input = target[:, di] + else: + _, topi = decoder_output.topk(1) + decoder_input = topi.squeeze(1).squeeze(1).detach() + else: + decoder_input = target[:, di] + + return logits + + +@DeveloperAPI +class SequenceLSTMDecoder(nn.Module): + """LSTM-based decoder over multiple time steps. + + Supports scheduled sampling (Bengio et al., NeurIPS 2015) to smoothly + interpolate between teacher forcing during early training and fully + autoregressive decoding at inference. + + References: + Bengio, S., Vinyals, O., Jaitly, N., & Shazeer, N. (2015). + Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks. + NeurIPS 2015. https://arxiv.org/abs/1506.03099 + + When to use: + - Sequence/text generation tasks where long-range dependencies matter. + - Use teacher_forcing_decay='linear' or 'exponential' when the model + overfits to teacher forcing and performs poorly at inference. + """ + + def __init__( + self, + hidden_size: int, + vocab_size: int, + max_sequence_length: int, + reduce_input: str = "sum", + num_layers: int = 1, + teacher_forcing_decay: str = "none", + teacher_forcing_decay_rate: float = 0.01, + ): + super().__init__() + self.hidden_size = hidden_size + self.vocab_size = vocab_size + self.lstm_decoder = LSTMDecoder(hidden_size, vocab_size, num_layers) + self.max_sequence_length = max_sequence_length + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_input) + self.num_layers = num_layers + self.teacher_forcing_decay = teacher_forcing_decay + self.teacher_forcing_decay_rate = teacher_forcing_decay_rate + + self.register_buffer("logits", torch.zeros([max_sequence_length, vocab_size])) + self.register_buffer("decoder_input", torch.Tensor([strings_utils.SpecialSymbol.START.value])) + + def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor) -> torch.Tensor: + """Runs max_sequence_length LSTM decoding time steps. + + Args: + combiner_outputs: Dict of tensors from the combiner and other output features. + target: [batch_size, max_sequence_length] target symbols, or None at inference. + + Returns: + Tensor of logits [batch_size, max_sequence_length, vocab_size]. + """ + decoder_hidden, decoder_cell_state = get_lstm_init_state( + combiner_outputs, self.reduce_sequence, self.num_layers + ) + batch_size = decoder_hidden.size()[1] + decoder_input = self.decoder_input.repeat(batch_size) + logits = self.logits.unsqueeze(0).repeat(batch_size, 1, 1) + is_training = self.training and target is not None + + for di in range(self.max_sequence_length): + decoder_output, decoder_hidden, decoder_cell_state = self.lstm_decoder( + decoder_input, decoder_hidden, decoder_cell_state + ) + logits[:, di, :] = decoder_output.squeeze(1) + + if target is None: + _, topi = decoder_output.topk(1) + decoder_input = topi.squeeze(1).squeeze(1).detach() + elif is_training and self.teacher_forcing_decay != "none": + tf_prob = _teacher_forcing_prob(di, self.teacher_forcing_decay, self.teacher_forcing_decay_rate) + if torch.rand(1).item() < tf_prob: + decoder_input = target[:, di] + else: + _, topi = decoder_output.topk(1) + decoder_input = topi.squeeze(1).squeeze(1).detach() + else: + decoder_input = target[:, di] + + return logits + + +def _beam_search_stateful( + step_fn, + init_state, + state_index_fn, + start_token: int, + max_sequence_length: int, + vocab_size: int, + beam_width: int, + length_penalty: float, + device: torch.device, +): + """Beam search with an arbitrary stateful step function. + + Args: + step_fn: (tokens [beam_width], state) -> (log_probs [beam_width, vocab], new_state). + init_state: Initial state sized for beam_width beams. + state_index_fn: (state, beam_indices) -> reordered state. + start_token: Vocabulary index of the start token. + max_sequence_length: Maximum number of tokens to generate. + vocab_size: Vocabulary size. + beam_width: Number of beams. + length_penalty: Exponent for length normalisation. + device: Torch device. + + Returns: + sequences: [beam_width, max_sequence_length] best sequences (highest score first). + scores: [beam_width] length-normalised log-probability scores. + """ + sequences = torch.full((beam_width, 1), start_token, dtype=torch.long, device=device) + cum_log_probs = torch.zeros(beam_width, device=device) + state = init_state + + for _step in range(max_sequence_length): + last_tokens = sequences[:, -1] + log_probs, state = step_fn(last_tokens, state) + + total_log_probs = cum_log_probs.unsqueeze(1) + log_probs + flat = total_log_probs.view(-1) + + top_log_probs, top_idx = flat.topk(beam_width) + beam_idx = top_idx // vocab_size + token_idx = top_idx % vocab_size + + sequences = torch.cat([sequences[beam_idx], token_idx.unsqueeze(1)], dim=1) + cum_log_probs = top_log_probs + state = state_index_fn(state, beam_idx) + + lengths = torch.full((beam_width,), max_sequence_length, dtype=torch.float, device=device) + scores = cum_log_probs / lengths.pow(length_penalty) + order = scores.argsort(descending=True) + return sequences[order, 1:], scores[order] + + +@DeveloperAPI +@register_decoder("generator", [SEQUENCE, TEXT]) +class SequenceGeneratorDecoder(Decoder): + """Dispatcher for different sequence generator decoders. + + Supports three RNN cell types (gru, lstm, rnn) plus: + + - Scheduled sampling (Bengio et al., NeurIPS 2015): set teacher_forcing_decay + to 'linear' or 'exponential' to gradually shift from teacher forcing to + model predictions during training. + + - Beam search: set beam_width > 1 to enable beam search at inference time. + The beam_length_penalty controls length normalisation of beam scores. + + References: + Bengio, S., Vinyals, O., Jaitly, N., & Shazeer, N. (2015). + Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks. + NeurIPS 2015. https://arxiv.org/abs/1506.03099 + + When to use: + - Classic sequence generation with RNN backbones. + - Use cell_type='gru' (default) for most tasks. + - Use cell_type='lstm' when long-range dependencies are important. + - Use beam_width > 1 for higher-quality outputs at inference time. + """ + + def __init__( + self, + vocab_size: int, + max_sequence_length: int, + cell_type: str = "gru", + input_size: int = 256, + reduce_input: str = "sum", + num_layers: int = 1, + teacher_forcing_decay: str = "none", + teacher_forcing_decay_rate: float = 0.01, + beam_width: int = 1, + beam_length_penalty: float = 1.0, + decoder_config=None, + **kwargs, + ): + """ + Args: + vocab_size: Vocab size. + max_sequence_length: Maximum sequence length. + cell_type: Type of RNN cell to use. 'rnn', 'gru', or 'lstm'. + input_size: Size of incoming combiner output. + reduce_input: Mode with which to reduce incoming combiner output, if needed. + num_layers: Number of layers for the RNN decoders. + teacher_forcing_decay: Schedule for teacher forcing probability. + teacher_forcing_decay_rate: Per-step decay rate for teacher forcing probability. + beam_width: Beam width for inference (1 = greedy). + beam_length_penalty: Length penalty exponent for beam search scoring. + """ + super().__init__() + self.config = decoder_config + + self.vocab_size = vocab_size + self.input_size = input_size + self.max_sequence_length = max_sequence_length + self.beam_width = beam_width + self.beam_length_penalty = beam_length_penalty + + if cell_type == "lstm": + self.rnn_decoder = SequenceLSTMDecoder( + hidden_size=input_size, + vocab_size=vocab_size, + max_sequence_length=max_sequence_length, + reduce_input=reduce_input, + num_layers=num_layers, + teacher_forcing_decay=teacher_forcing_decay, + teacher_forcing_decay_rate=teacher_forcing_decay_rate, + ) + else: + self.rnn_decoder = SequenceRNNDecoder( + hidden_size=input_size, + vocab_size=vocab_size, + max_sequence_length=max_sequence_length, + cell_type=cell_type, + reduce_input=reduce_input, + num_layers=num_layers, + teacher_forcing_decay=teacher_forcing_decay, + teacher_forcing_decay_rate=teacher_forcing_decay_rate, + ) + + def _greedy_decode(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor) -> torch.Tensor: + return self.rnn_decoder(combiner_outputs, target) + + def _beam_decode(self, combiner_outputs: dict[str, torch.Tensor]) -> torch.Tensor: + """Beam search decoding. Returns logits [batch_size, max_sequence_length, vocab_size]. + + Since beam search selects hard token indices, the returned tensor has 1.0 at the chosen token and 0.0 elsewhere + so downstream argmax produces correct predictions. + """ + inner = self.rnn_decoder + is_lstm = isinstance(inner, SequenceLSTMDecoder) + device = next(self.parameters()).device + + if is_lstm: + h, c = get_lstm_init_state(combiner_outputs, inner.reduce_sequence, inner.num_layers) + batch_size = h.size(1) + else: + h = get_rnn_init_state(combiner_outputs, inner.reduce_sequence, inner.num_layers) + batch_size = h.size(1) + + start_token = int(strings_utils.SpecialSymbol.START.value) + all_logits = [] + + for b in range(batch_size): + if is_lstm: + h_b = h[:, b : b + 1, :].expand(-1, self.beam_width, -1).contiguous() + c_b = c[:, b : b + 1, :].expand(-1, self.beam_width, -1).contiguous() + + def step_fn_lstm(tokens, state): + h_s, c_s = state + logits_out, h_new, c_new = inner.lstm_decoder(tokens, h_s, c_s) + return torch.log_softmax(logits_out.squeeze(1), dim=-1), (h_new, c_new) + + def state_index_lstm(state, idx): + h_s, c_s = state + return h_s[:, idx, :].unsqueeze(1), c_s[:, idx, :].unsqueeze(1) + + seqs, _ = _beam_search_stateful( + step_fn_lstm, + (h_b, c_b), + state_index_lstm, + start_token, + self.max_sequence_length, + self.vocab_size, + self.beam_width, + self.beam_length_penalty, + device, + ) + else: + h_b = h[:, b : b + 1, :].expand(-1, self.beam_width, -1).contiguous() + + def step_fn_rnn(tokens, state): + logits_out, h_new = inner.rnn_decoder(tokens, state) + return torch.log_softmax(logits_out.squeeze(1), dim=-1), h_new + + def state_index_rnn(state, idx): + return state[:, idx, :].unsqueeze(1) + + seqs, _ = _beam_search_stateful( + step_fn_rnn, + h_b, + state_index_rnn, + start_token, + self.max_sequence_length, + self.vocab_size, + self.beam_width, + self.beam_length_penalty, + device, + ) + + best_seq = seqs[0] + item_logits = torch.zeros(self.max_sequence_length, self.vocab_size, device=device) + for t in range(self.max_sequence_length): + item_logits[t, best_seq[t]] = 1.0 + all_logits.append(item_logits) + + return torch.stack(all_logits, dim=0) + + def forward( + self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor = None + ) -> dict[str, torch.Tensor]: + """Decodes combiner_outputs into a sequence. + + During training (target provided): uses teacher forcing with optional scheduled sampling. + During inference (target=None): greedy decoding when beam_width=1, beam search otherwise. + + Args: + combiner_outputs: Dict of tensors from the combiner and other output features. + target: [batch_size, max_sequence_length] target symbols. + + Returns: + Dict with LOGITS -> [batch_size, max_sequence_length, vocab_size]. + """ + if target is None and self.beam_width > 1: + logits = self._beam_decode(combiner_outputs) + else: + logits = self._greedy_decode(combiner_outputs, target) + return {LOGITS: logits} + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS, PROBABILITIES} + + @staticmethod + def get_schema_cls(): + return SequenceGeneratorDecoderConfig + + @property + def input_shape(self): + return torch.Size([1]) + + @property + def output_shape(self): + return torch.Size([self.max_sequence_length, self.vocab_size]) + + +class _PositionalEncoding(nn.Module): + """Standard sinusoidal positional encoding (Vaswani et al., NeurIPS 2017).""" + + def __init__(self, d_model: int, max_len: int = 512, dropout: float = 0.1): + super().__init__() + self.dropout = nn.Dropout(p=dropout) + + position = torch.arange(max_len).unsqueeze(1) + div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)) + pe = torch.zeros(max_len, d_model) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + self.register_buffer("pe", pe) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x + self.pe[: x.size(0)].unsqueeze(1) + return self.dropout(x) + + +@DeveloperAPI +@register_decoder("transformer_generator", [SEQUENCE, TEXT]) +class SequenceTransformerDecoder(Decoder): + """Transformer-based autoregressive sequence/text decoder. + + Uses nn.TransformerDecoder with cross-attention over the encoder memory. + Training uses parallel teacher forcing for efficiency; inference is + autoregressive (greedy or beam search). + + Architecture: + encoder output -> [optional linear projection] -> cross-attention memory + target tokens -> embedding + positional encoding + -> N x TransformerDecoderLayer (self-attn, cross-attn, FFN) + -> Linear (d_model -> vocab_size) [weight-tied to embedding] + + References: + Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., + Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). + Attention Is All You Need. NeurIPS 2017. + https://arxiv.org/abs/1706.03762 + + When to use: + - Prefer over RNN decoders when the encoder produces rich contextual + representations (e.g. BERT-style) or for sequences longer than ~30 tokens. + - Set beam_width > 1 to improve output quality at inference time. + - Increase num_layers / num_heads for more expressive capacity. + """ + + def __init__( + self, + vocab_size: int, + max_sequence_length: int, + input_size: int = 256, + d_model: int = 256, + num_layers: int = 2, + num_heads: int = 8, + ffn_size: int = 1024, + dropout: float = 0.1, + reduce_input: str = "sum", + beam_width: int = 1, + beam_length_penalty: float = 1.0, + decoder_config=None, + **kwargs, + ): + """ + Args: + vocab_size: Target vocabulary size. + max_sequence_length: Maximum number of tokens to generate. + input_size: Dimension of the encoder output (memory). + d_model: Internal transformer dimension. A projection is inserted if input_size != d_model. + num_layers: Number of TransformerDecoderLayer stacks. + num_heads: Number of attention heads. d_model % num_heads must equal 0. + ffn_size: Feed-forward network hidden size. + dropout: Dropout probability. + reduce_input: How to reduce a 3-D encoder output to 2-D when needed. + beam_width: Beam width for inference (1 = greedy). + beam_length_penalty: Length penalty exponent for beam search scoring. + """ + super().__init__() + self.config = decoder_config + self.vocab_size = vocab_size + self.max_sequence_length = max_sequence_length + self.d_model = d_model + self.beam_width = beam_width + self.beam_length_penalty = beam_length_penalty + + self.memory_projection = nn.Linear(input_size, d_model) if input_size != d_model else None + self.embedding = nn.Embedding(vocab_size, d_model) + self.pos_encoder = _PositionalEncoding(d_model, max_len=max_sequence_length + 1, dropout=dropout) + + decoder_layer = nn.TransformerDecoderLayer( + d_model=d_model, + nhead=num_heads, + dim_feedforward=ffn_size, + dropout=dropout, + batch_first=True, + ) + self.transformer_decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers) + + self.out = nn.Linear(d_model, vocab_size) + self.out.weight = self.embedding.weight # weight tying + + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_input) + self.register_buffer("start_token", torch.tensor([strings_utils.SpecialSymbol.START.value], dtype=torch.long)) + + def _get_memory(self, combiner_outputs: dict[str, torch.Tensor]) -> torch.Tensor: + """Returns encoder memory [batch_size, src_len, d_model] for cross-attention.""" + from ludwig.constants import HIDDEN + + hidden = combiner_outputs.get("encoder_output", combiner_outputs.get(HIDDEN)) + if hidden is None: + raise ValueError("SequenceTransformerDecoder requires 'encoder_output' or 'hidden' in combiner_outputs.") + + if hidden.dim() == 2: + memory = hidden.unsqueeze(1) + elif hidden.dim() == 3: + memory = hidden + else: + raise ValueError(f"Unexpected encoder output shape: {hidden.shape}") + + if self.memory_projection is not None: + memory = self.memory_projection(memory) + return memory + + def _causal_mask(self, size: int, device: torch.device) -> torch.Tensor: + return torch.triu(torch.ones(size, size, device=device, dtype=torch.bool), diagonal=1) + + def _teacher_forced_forward(self, memory: torch.Tensor, target: torch.Tensor) -> torch.Tensor: + """Parallel teacher-forced forward used during training. + + Args: + memory: [batch_size, src_len, d_model] + target: [batch_size, tgt_len] ground-truth token ids. + + Returns: + logits: [batch_size, tgt_len, vocab_size] + """ + batch_size, tgt_len = target.shape + device = target.device + + start = self.start_token.expand(batch_size, 1) + decoder_input = torch.cat([start, target[:, :-1]], dim=1) + + tgt_emb = self.embedding(decoder_input) * math.sqrt(self.d_model) + tgt_emb = tgt_emb + self.pos_encoder.pe[:tgt_len].unsqueeze(0) + + causal_mask = self._causal_mask(tgt_len, device) + out = self.transformer_decoder(tgt_emb, memory, tgt_mask=causal_mask) + return self.out(out) + + def _autoregressive_forward(self, memory: torch.Tensor) -> torch.Tensor: + """Autoregressive greedy decoding used at inference. + + Args: + memory: [batch_size, src_len, d_model] + + Returns: + logits: [batch_size, max_sequence_length, vocab_size] + """ + batch_size = memory.size(0) + device = memory.device + generated = self.start_token.expand(batch_size, 1).clone() + all_logits = [] + + for _t in range(self.max_sequence_length): + tgt_len = generated.size(1) + tgt_emb = self.embedding(generated) * math.sqrt(self.d_model) + tgt_emb = tgt_emb + self.pos_encoder.pe[:tgt_len].unsqueeze(0) + causal_mask = self._causal_mask(tgt_len, device) + out = self.transformer_decoder(tgt_emb, memory, tgt_mask=causal_mask) + step_logits = self.out(out[:, -1, :]) + all_logits.append(step_logits) + next_token = step_logits.argmax(dim=-1, keepdim=True) + generated = torch.cat([generated, next_token], dim=1) + + return torch.stack(all_logits, dim=1) + + def _beam_forward(self, memory: torch.Tensor) -> torch.Tensor: + """Beam search decoding at inference when beam_width > 1. + + Args: + memory: [batch_size, src_len, d_model] + + Returns: + logits: [batch_size, max_sequence_length, vocab_size] one-hot at selected tokens. + """ + batch_size = memory.size(0) + device = memory.device + start_token = int(self.start_token.item()) + all_logits = [] + + for b in range(batch_size): + mem_b = memory[b : b + 1, :, :].expand(self.beam_width, -1, -1) + + def _step(tokens: torch.Tensor, gen: torch.Tensor): + new_gen = torch.cat([gen, tokens.unsqueeze(1)], dim=1) + tgt_len = new_gen.size(1) + tgt_emb = self.embedding(new_gen) * math.sqrt(self.d_model) + tgt_emb = tgt_emb + self.pos_encoder.pe[:tgt_len].unsqueeze(0) + causal_mask = self._causal_mask(tgt_len, device) + out = self.transformer_decoder(tgt_emb, mem_b, tgt_mask=causal_mask) + step_logits = self.out(out[:, -1, :]) + return torch.log_softmax(step_logits, dim=-1), new_gen + + def _state_index(gen: torch.Tensor, idx: torch.Tensor): + return gen[idx] + + init_gen = torch.full((self.beam_width, 1), start_token, dtype=torch.long, device=device) + seqs, _ = _beam_search_stateful( + _step, + init_gen, + _state_index, + start_token, + self.max_sequence_length, + self.vocab_size, + self.beam_width, + self.beam_length_penalty, + device, + ) + + best_seq = seqs[0] + item_logits = torch.zeros(self.max_sequence_length, self.vocab_size, device=device) + for t in range(self.max_sequence_length): + item_logits[t, best_seq[t]] = 1.0 + all_logits.append(item_logits) + + return torch.stack(all_logits, dim=0) + + def forward( + self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor = None + ) -> dict[str, torch.Tensor]: + """Decode combiner outputs into a token sequence. + + Training (target provided): parallel teacher-forced forward pass. + Inference (target=None): greedy decoding or beam search when beam_width > 1. + + Args: + combiner_outputs: Dict containing 'hidden' or 'encoder_output'. + target: [batch_size, max_sequence_length] ground-truth token ids (training only). + + Returns: + Dict with LOGITS -> [batch_size, max_sequence_length, vocab_size]. + """ + memory = self._get_memory(combiner_outputs) + + if target is not None: + logits = self._teacher_forced_forward(memory, target) + elif self.beam_width > 1: + logits = self._beam_forward(memory) + else: + logits = self._autoregressive_forward(memory) + + return {LOGITS: logits} + + def get_prediction_set(self): + return {LOGITS, PREDICTIONS, PROBABILITIES} + + @staticmethod + def get_schema_cls(): + return TransformerDecoderConfig + + @property + def input_shape(self): + return torch.Size([1]) + + @property + def output_shape(self): + return torch.Size([self.max_sequence_length, self.vocab_size]) diff --git a/ludwig/decoders/sequence_tagger.py b/ludwig/decoders/sequence_tagger.py new file mode 100644 index 0000000..cd1788d --- /dev/null +++ b/ludwig/decoders/sequence_tagger.py @@ -0,0 +1,92 @@ +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import HIDDEN, LOGITS, PREDICTIONS, PROBABILITIES, SEQUENCE, TEXT +from ludwig.decoders.base import Decoder +from ludwig.decoders.registry import register_decoder +from ludwig.modules.attention_modules import MultiHeadSelfAttention +from ludwig.schema.decoders.sequence_decoders import SequenceTaggerDecoderConfig +from ludwig.utils.torch_utils import Dense + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_decoder("tagger", [SEQUENCE, TEXT]) +class SequenceTaggerDecoder(Decoder): + def __init__( + self, + input_size: int, + vocab_size: int, + max_sequence_length: int, + use_attention: bool = False, + use_bias: bool = True, + attention_embedding_size: int = 256, + attention_num_heads: int = 8, + decoder_config=None, + **kwargs, + ): + super().__init__() + self.config = decoder_config + + self.vocab_size = vocab_size + self.max_sequence_length = max_sequence_length + self.input_size = input_size + self.use_attention = use_attention + if use_attention: + logger.debug(" MultiHeadSelfAttention") + self.self_attention = MultiHeadSelfAttention( + input_size=input_size, hidden_size=attention_embedding_size, num_heads=attention_num_heads + ) + # Adjust the input size to the final projection layer. + input_size = self.self_attention.output_shape[0] + self.projection_layer = Dense(input_size=input_size, output_size=vocab_size, use_bias=use_bias) + + def forward(self, inputs: dict[str, torch.Tensor], target: torch.Tensor = None) -> dict[str, torch.Tensor]: + """Decodes the inputs into a sequence. + + Args: + inputs: Dictionary of tensors from the outputs of the combiner and other output features. + target: Tensor [batch_size, max_sequence_length] with predictions. + + Returns: + Dictionary of tensors with logits [batch_size, max_sequence_length, vocab_size]. + """ + hidden = inputs[HIDDEN] + if len(hidden.size()) != 3: + raise ValueError( + f"Decoder inputs rank is {len(hidden.size())}, but should be 3: " + + "[batch_size x max_sequence_length x hidden_size] in when using a tagger sequential decoder. " + + "Consider setting reduce_output to None if a sequential encoder / combiner is used." + ) + if list(hidden.shape[1:]) != [self.max_sequence_length, self.input_size]: + raise ValueError( + "Sequence tagger decoder inputs (hidden) should be [batch_size, self.max_sequence_length, " + + f"input_size], or [batch_size, {self.max_sequence_length}, {self.input_size}]. However, the " + + f"inputs (hidden) was instead: {list(hidden.size())}. " + + "The encoder is not length preserving. Please check its configuration." + ) + + if self.use_attention: + hidden = self.self_attention(hidden) + + logits = self.projection_layer(hidden) + return {LOGITS: logits} + + def get_prediction_set(self): + return {LOGITS, PROBABILITIES, PREDICTIONS} + + @staticmethod + def get_schema_cls(): + return SequenceTaggerDecoderConfig + + @property + def input_shape(self): + # Dummy implementation. + return torch.Size([1]) + + @property + def output_shape(self): + return torch.Size([self.max_sequence_length, self.vocab_size]) diff --git a/ludwig/decoders/utils.py b/ludwig/decoders/utils.py new file mode 100644 index 0000000..189c9b0 --- /dev/null +++ b/ludwig/decoders/utils.py @@ -0,0 +1,41 @@ +import torch +from torch import Tensor + + +def extract_generated_tokens( + raw_generated_output_sequences: list[Tensor], + input_lengths: list[int], + max_new_tokens: int, + pad_sequence: bool, +) -> list[Tensor]: + """Extracts the generated tokens from the raw output sequences of the language model. + + Args: + raw_generated_output_sequences: The raw output sequences of the language model. + Represented as a list to handle variable length sequences. + input_lengths: The length of the inputs to the language model. + max_new_tokens: The maximum number of new tokens that were generated. Used to + pad the generated sequences to the max_new_tokens. + pad_sequence: Whether to pad the generated sequences to the max_new_tokens. + + Returns: + The generated tokens. + """ + if len(raw_generated_output_sequences) != len(input_lengths): + raise ValueError( + f"The number of raw_generated_output_sequences ({len(raw_generated_output_sequences)}) " + f"must be the same as the number of input_lengths ({len(input_lengths)})." + ) + + generated_outputs = [] + for idx, input_length in enumerate(input_lengths): + # Remove the input sequence from the generated sequence + generated_sequence = raw_generated_output_sequences[idx][input_length:] + + # Pad the sequence if it is shorter than the max_new_tokens for downstream metric computation + if pad_sequence and generated_sequence.size()[0] < max_new_tokens: + generated_sequence = torch.nn.functional.pad( + generated_sequence, (0, max_new_tokens - generated_sequence.size()[0]), "constant", 0 + ) + generated_outputs.append(generated_sequence) + return generated_outputs diff --git a/ludwig/distributed/__init__.py b/ludwig/distributed/__init__.py new file mode 100644 index 0000000..4ac3ae4 --- /dev/null +++ b/ludwig/distributed/__init__.py @@ -0,0 +1,54 @@ +from typing import Any + +from ludwig.distributed.base import DistributedStrategy, LocalStrategy + + +def load_local(): + return LocalStrategy + + +def load_accelerate(): + from ludwig.distributed.accelerate import AccelerateStrategy + + return AccelerateStrategy + + +STRATEGIES = { + "accelerate": load_accelerate, + "local": load_local, + # Legacy aliases for backward compatibility + "ddp": load_accelerate, + "fsdp": load_accelerate, + "deepspeed": load_accelerate, +} + + +_current_strategy: DistributedStrategy = None + + +def init_dist_strategy(strategy: str | dict[str, Any], **kwargs) -> DistributedStrategy: + global _current_strategy + if isinstance(strategy, dict): + dtype = strategy.pop("type", None) + obj = get_dist_strategy(dtype)(**strategy) + else: + obj = get_dist_strategy(strategy)(**kwargs) + _current_strategy = obj + return obj + + +def get_current_dist_strategy() -> DistributedStrategy: + if _current_strategy is None: + raise RuntimeError("Distributed strategy not initialized") + return _current_strategy + + +def get_dist_strategy(strategy: str | dict[str, Any]) -> type[DistributedStrategy]: + name = strategy + if isinstance(strategy, dict): + name = strategy["type"] + return STRATEGIES[name]() + + +def get_default_strategy_name() -> str: + return "accelerate" diff --git a/ludwig/distributed/accelerate.py b/ludwig/distributed/accelerate.py new file mode 100644 index 0000000..f041416 --- /dev/null +++ b/ludwig/distributed/accelerate.py @@ -0,0 +1,247 @@ +"""HuggingFace Accelerate distributed training strategy. + +Provides a single abstraction for DDP, FSDP, and DeepSpeed via the Accelerate library. +This replaces the need for separate DDPStrategy, FSDPStrategy, and DeepSpeedStrategy +classes with one unified implementation. + +Usage: + # In config or backend: + strategy: accelerate + + # With FSDP: + strategy: + type: accelerate + mixed_precision: bf16 + fsdp_config: + fsdp_sharding_strategy: FULL_SHARD + + # With DeepSpeed: + strategy: + type: accelerate + deepspeed_config: path/to/ds_config.json +""" + +import contextlib +import logging +import os +from typing import Any + +import torch +import torch.distributed as dist +from torch.nn import Module +from torch.optim import Optimizer + +from ludwig.distributed.base import DistributedStrategy +from ludwig.modules.optimization_modules import create_optimizer +from ludwig.utils.checkpoint_utils import Checkpoint, MultiNodeCheckpoint + +logger = logging.getLogger(__name__) + + +class AccelerateStrategy(DistributedStrategy): + """Distributed training via HuggingFace Accelerate. + + Accelerate provides a single prepare() call that auto-detects and configures DDP, FSDP, or DeepSpeed based on the + environment and configuration. + """ + + def __init__( + self, + mixed_precision: str | None = None, + gradient_accumulation_steps: int = 1, + fsdp_config: dict | None = None, + deepspeed_config: str | dict | None = None, + **kwargs, + ): + from accelerate import Accelerator + + accelerator_kwargs = {} + if mixed_precision: + accelerator_kwargs["mixed_precision"] = mixed_precision + if gradient_accumulation_steps > 1: + accelerator_kwargs["gradient_accumulation_steps"] = gradient_accumulation_steps + if fsdp_config: + # Accelerate FSDP config is passed via fsdp_plugin + from accelerate import FullyShardedDataParallelPlugin + + accelerator_kwargs["fsdp_plugin"] = FullyShardedDataParallelPlugin(**fsdp_config) + if deepspeed_config: + from accelerate import DeepSpeedPlugin + + if isinstance(deepspeed_config, str): + accelerator_kwargs["deepspeed_plugin"] = DeepSpeedPlugin(hf_ds_config=deepspeed_config) + else: + accelerator_kwargs["deepspeed_plugin"] = DeepSpeedPlugin(**deepspeed_config) + + self.accelerator = Accelerator(**accelerator_kwargs) + self._prepared_model = None + self._prepared_optimizer = None + + logger.info( + f"AccelerateStrategy initialized: distributed_type={self.accelerator.distributed_type}, " + f"num_processes={self.accelerator.num_processes}, device={self.accelerator.device}" + ) + + def prepare( + self, + model: Module, + trainer_config, + base_learning_rate: float, + ) -> tuple[Module, Optimizer]: + optimizer = create_optimizer(model, trainer_config.optimizer, base_learning_rate) + self._prepared_model, self._prepared_optimizer = self.accelerator.prepare(model, optimizer) + return self._prepared_model, self._prepared_optimizer + + def to_device(self, model, device=None): + # Accelerate handles device placement via prepare() + if device is not None: + return model.to_device(device) + return model.to_device(self.accelerator.device) + + def backward(self, loss: torch.Tensor, model: Module): + self.accelerator.backward(loss) + + def step(self, optimizer: Optimizer, *args, **kwargs): + optimizer.step(*args, **kwargs) + + def zero_grad(self, optimizer: Optimizer): + optimizer.zero_grad() + + def size(self) -> int: + return self.accelerator.num_processes + + def rank(self) -> int: + return self.accelerator.process_index + + def local_size(self) -> int: + # Accelerate doesn't expose local_size directly. Compute from environment. + return int(os.environ.get("LOCAL_SIZE", os.environ.get("LOCAL_WORLD_SIZE", 1))) + + def local_rank(self) -> int: + return self.accelerator.local_process_index + + def barrier(self): + self.accelerator.wait_for_everyone() + + def allreduce(self, t: torch.Tensor) -> torch.Tensor: + return self.accelerator.reduce(t, reduction="sum") + + def broadcast(self, t: torch.Tensor) -> torch.Tensor: + if dist.is_available() and dist.is_initialized(): + dist.broadcast(t, src=0) + return t + + def sync_model(self, model: Module): + # Accelerate handles model sync through prepare() + pass + + def sync_optimizer(self, optimizer: Optimizer): + # Accelerate handles optimizer sync through prepare() + pass + + def broadcast_object(self, v: Any, name: str | None = None) -> Any: + if not dist.is_available() or not dist.is_initialized(): + return v + obj_list = [v] + dist.broadcast_object_list(obj_list, src=0) + return obj_list[0] + + def wait_optimizer_synced(self, optimizer: Optimizer): + # Accelerate handles gradient sync automatically + pass + + @contextlib.contextmanager + def prepare_model_update(self, model: Module, should_step: bool): + # Emulate DDP's no_sync() behavior for gradient accumulation. + # When should_step is False, we skip gradient synchronization. + if not should_step and hasattr(model, "no_sync"): + with model.no_sync(): + yield + else: + yield + + @contextlib.contextmanager + def prepare_optimizer_update(self, optimizer: Optimizer): + yield + + @classmethod + def is_available(cls) -> bool: + try: + import accelerate # noqa: F401 + + return True + except ImportError: + return False + + @classmethod + def gather_all_tensors_fn(cls): + try: + from torchmetrics.utilities.distributed import gather_all_tensors + + return gather_all_tensors + except ImportError: + return None + + @classmethod + def get_ray_trainer_backend(cls, **kwargs): + # Accelerate can work with Ray but requires specific setup. + # Return TorchConfig for basic compatibility. + try: + from ray.train.torch import TorchConfig + + return TorchConfig() + except ImportError: + return None + + @classmethod + def get_trainer_cls(cls, backend_config): + try: + from ray.train.torch import TorchTrainer + + return TorchTrainer, {"torch_config": backend_config} + except ImportError: + raise ImportError("Ray is required for distributed training with AccelerateStrategy via Ray backend.") + + def shutdown(self): + pass + + def prepare_for_inference(self, model: Module) -> Module: + return self.accelerator.unwrap_model(model) + + def allow_gradient_accumulation(self) -> bool: + return True + + def allow_mixed_precision(self) -> bool: + # Let Accelerate handle mixed precision if configured + if self.accelerator.mixed_precision != "no": + return False + return True + + def allow_clip_gradients(self) -> bool: + return True + + def prepare_before_load(self) -> bool: + return False + + @classmethod + def is_model_parallel(cls) -> bool: + return False + + def create_checkpoint_handle(self, dist_model, model, optimizer, scheduler) -> Checkpoint: + return MultiNodeCheckpoint(self, model, optimizer, scheduler) + + @classmethod + def extract_model_for_serialization(cls, model): + try: + from accelerate import Accelerator + + accelerator = Accelerator() + return accelerator.unwrap_model(model) + except ImportError: + return model + + @classmethod + def replace_model_from_serialization(cls, state): + if not isinstance(state, Module): + raise TypeError(f"replace_model_from_serialization expected an nn.Module, got {type(state).__name__}.") + return state diff --git a/ludwig/distributed/base.py b/ludwig/distributed/base.py new file mode 100644 index 0000000..cc91610 --- /dev/null +++ b/ludwig/distributed/base.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +import contextlib +from abc import ABC, abstractmethod +from collections.abc import Callable +from typing import Any, TYPE_CHECKING + +import torch +from torch import nn +from torch.optim import Optimizer + +from ludwig.modules.optimization_modules import create_optimizer +from ludwig.utils.torch_utils import get_torch_device + +if TYPE_CHECKING: + from ray.train.backend import BackendConfig + from ray.train.data_parallel_trainer import DataParallelTrainer + + from ludwig.models.base import BaseModel + from ludwig.modules.lr_scheduler import LRScheduler + from ludwig.schema.trainer import ECDTrainerConfig + from ludwig.utils.checkpoint_utils import Checkpoint + + +class DistributedStrategy(ABC): + """Interface that wraps a distributed training framework. + + Distributed strategies modify the model and/or optimizer to coordinate gradient updates among multiple workers + running in parallel. The primary implementation is AccelerateStrategy, which uses HuggingFace Accelerate to provide + a unified abstraction for DDP, FSDP, and DeepSpeed. + """ + + @abstractmethod + def prepare( + self, + model: nn.Module, + trainer_config: ECDTrainerConfig, + base_learning_rate: float, + ) -> tuple[nn.Module, Optimizer]: + """Modifies the model to support distributed training and creates the optimizer. + + Args: + model: The model to wrap for distributed training. + trainer_config: The trainer configuration, which includes optimizer params. + base_learning_rate: The base learning rate to init the optimizer, which may be scaled by the strategy. + + Returns: + A tuple of the wrapped model and the optimizer. + """ + + def prepare_for_inference(self, model: nn.Module) -> nn.Module: + return model + + def to_device(self, model: BaseModel, device: torch.device | None = None) -> nn.Module: + return model.to_device(device if device is not None else get_torch_device()) + + def backward(self, loss: torch.Tensor, model: nn.Module): + loss.backward() + + def step(self, optimizer: Optimizer, *args, **kwargs): + optimizer.step(*args, **kwargs) + + def zero_grad(self, optimizer: Optimizer): + optimizer.zero_grad() + + def set_batch_size(self, model: nn.Module, batch_size: int): + pass + + @abstractmethod + def size(self) -> int: + pass + + @abstractmethod + def rank(self) -> int: + pass + + @abstractmethod + def local_size(self) -> int: + pass + + @abstractmethod + def local_rank(self) -> int: + pass + + def is_coordinator(self) -> bool: + return self.rank() == 0 + + @abstractmethod + def barrier(self): + pass + + @abstractmethod + def allreduce(self, t: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def broadcast(self, t: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def sync_model(self, model: nn.Module): + pass + + @abstractmethod + def sync_optimizer(self, optimizer: Optimizer): + pass + + @abstractmethod + def broadcast_object(self, v: Any, name: str | None = None) -> Any: + pass + + @abstractmethod + def wait_optimizer_synced(self, optimizer: Optimizer): + pass + + @abstractmethod + @contextlib.contextmanager + def prepare_model_update(self, model: nn.Module, should_step: bool): + pass + + @abstractmethod + @contextlib.contextmanager + def prepare_optimizer_update(self, optimizer: Optimizer): + pass + + @classmethod + @abstractmethod + def is_available(cls) -> bool: + pass + + @classmethod + @abstractmethod + def gather_all_tensors_fn(cls) -> Callable | None: + pass + + @classmethod + @abstractmethod + def get_ray_trainer_backend(cls, **kwargs) -> Any | None: + pass + + @classmethod + @abstractmethod + def get_trainer_cls(cls, backend_config: BackendConfig) -> tuple[type[DataParallelTrainer], dict[str, Any]]: + pass + + @abstractmethod + def shutdown(self): + pass + + def return_first(self, fn: Callable) -> Callable: + """Wraps function so results are only returned by the first (coordinator) rank. + + The purpose of this function is to reduce network overhead. + """ + + def wrapped(*args, **kwargs): + res = fn(*args, **kwargs) + return res if self.rank() == 0 else None + + return wrapped + + def allow_gradient_accumulation(self) -> bool: + return True + + def allow_mixed_precision(self) -> bool: + return True + + def allow_clip_gradients(self) -> bool: + return True + + def prepare_before_load(self) -> bool: + """True if we need to call `prepare` again before loading a checkpoint.""" + return False + + @classmethod + def is_model_parallel(cls) -> bool: + return False + + def create_checkpoint_handle( + self, + dist_model: nn.Module, + model: nn.Module, + optimizer: Optimizer | None = None, + scheduler: LRScheduler | None = None, + ) -> Checkpoint: + from ludwig.utils.checkpoint_utils import MultiNodeCheckpoint + + return MultiNodeCheckpoint(self, model, optimizer, scheduler) + + @classmethod + def extract_model_for_serialization(cls, model: nn.Module) -> nn.Module | tuple[nn.Module, list[dict]]: + return model + + @classmethod + def replace_model_from_serialization(cls, state: nn.Module | tuple[nn.Module, list[dict]]) -> nn.Module: + if not isinstance(state, nn.Module): + raise TypeError(f"replace_model_from_serialization expected an nn.Module, got {type(state).__name__}.") + return state + + +class LocalStrategy(DistributedStrategy): + def prepare( + self, + model: nn.Module, + trainer_config: ECDTrainerConfig, + base_learning_rate: float, + ) -> tuple[nn.Module, Optimizer]: + return model, create_optimizer(model, trainer_config.optimizer, base_learning_rate) + + def size(self) -> int: + return 1 + + def rank(self) -> int: + return 0 + + def local_size(self) -> int: + return 0 + + def local_rank(self) -> int: + return 0 + + def barrier(self): + pass + + def allreduce(self, t: torch.Tensor) -> torch.Tensor: + return t + + def broadcast(self, t: torch.Tensor) -> torch.Tensor: + return t + + def sync_model(self, model: nn.Module): + pass + + def sync_optimizer(self, optimizer: Optimizer): + pass + + def broadcast_object(self, v: Any, name: str | None = None) -> Any: + return v + + def wait_optimizer_synced(self, optimizer: Optimizer): + pass + + @contextlib.contextmanager + def prepare_model_update(self, model: nn.Module, should_step: bool): + yield + + @contextlib.contextmanager + def prepare_optimizer_update(self, optimizer: Optimizer): + yield + + @classmethod + def is_available(cls) -> bool: + # While this strategy is always an option, it is not "distributed" which is the meaning of availability + # in this context. + return False + + @classmethod + def gather_all_tensors_fn(cls) -> Callable | None: + return None + + @classmethod + def get_ray_trainer_backend(cls, **kwargs) -> Any | None: + return None + + @classmethod + def get_trainer_cls(cls, backend_config: BackendConfig) -> tuple[type[DataParallelTrainer], dict[str, Any]]: + raise ValueError("Cannot construct a trainer from a local strategy.") + + def shutdown(self): + pass diff --git a/ludwig/encoders/__init__.py b/ludwig/encoders/__init__.py new file mode 100644 index 0000000..e921e18 --- /dev/null +++ b/ludwig/encoders/__init__.py @@ -0,0 +1,14 @@ +# register all encoders +import ludwig.encoders.audio_encoders +import ludwig.encoders.bag_encoders +import ludwig.encoders.category_encoders +import ludwig.encoders.date_encoders +import ludwig.encoders.generic_encoders +import ludwig.encoders.h3_encoders +import ludwig.encoders.image +import ludwig.encoders.mamba_hybrid +import ludwig.encoders.number_encoders +import ludwig.encoders.sequence_encoders +import ludwig.encoders.set_encoders +import ludwig.encoders.text_encoders +import ludwig.encoders.timeseries_encoders # noqa diff --git a/ludwig/encoders/audio_encoders.py b/ludwig/encoders/audio_encoders.py new file mode 100644 index 0000000..56670d8 --- /dev/null +++ b/ludwig/encoders/audio_encoders.py @@ -0,0 +1,265 @@ +"""Pretrained audio encoders using HuggingFace transformers. + +These encoders accept raw audio waveforms or preprocessed features and produce fixed-size representations using +pretrained foundation models. +""" + +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, ENCODER_OUTPUT +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("wav2vec2", [AUDIO]) +class Wav2Vec2Encoder(Encoder): + """Wav2Vec2 audio encoder (Baevski et al., NeurIPS 2020). + + Self-supervised speech representation learning using contrastive learning + over masked latent representations. + + Use when: speech recognition, audio classification, speaker identification. + Best for: English speech tasks with the base model, multilingual with XLSR variants. + Expects raw waveform input (16kHz sample rate). + """ + + def __init__( + self, + pretrained_model_name_or_path: str = "facebook/wav2vec2-base", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "mean", + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + try: + from transformers import Wav2Vec2Model + except ImportError: + raise RuntimeError("transformers is required for Wav2Vec2Encoder. pip install transformers") + + if use_pretrained and not saved_weights_in_checkpoint: + self.model = Wav2Vec2Model.from_pretrained(pretrained_model_name_or_path) + else: + from transformers import Wav2Vec2Config + + self.model = Wav2Vec2Model(Wav2Vec2Config()) + + self._output_dim = self.model.config.hidden_size + self._reduce_output = reduce_output + + if not trainable: + for p in self.model.parameters(): + p.requires_grad_(False) + + def forward(self, inputs: torch.Tensor, mask=None) -> EncoderOutputDict: + # inputs shape: [batch_size, sequence_length] (raw waveform) + if inputs.dim() == 3: + inputs = inputs.squeeze(1) # Remove channel dim if present + outputs = self.model(inputs) + hidden = outputs.last_hidden_state # [batch, seq_len, hidden_size] + + if self._reduce_output == "mean": + hidden = hidden.mean(dim=1) + elif self._reduce_output == "last": + hidden = hidden[:, -1, :] + elif self._reduce_output == "cls_pooled": + hidden = hidden[:, 0, :] + # else: return full sequence + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls(): + from ludwig.schema.encoders.audio_encoders import Wav2Vec2EncoderConfig + + return Wav2Vec2EncoderConfig + + @property + def output_shape(self) -> torch.Size: + if self._reduce_output is None: + return torch.Size([None, self._output_dim]) # variable length + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) # raw waveform, variable length + + +@DeveloperAPI +@register_encoder("whisper", [AUDIO]) +class WhisperEncoder(Encoder): + """Whisper audio encoder (Radford et al., ICML 2023). + + Robust Speech Recognition via Large-Scale Weak Supervision. Uses the + encoder portion of the Whisper model to produce audio representations + from log-mel spectrogram input (80 mel bins). + + Use when: multilingual/noisy audio, automatic speech recognition tasks. + Best for: robust transcription across languages, accents, and noise conditions. + Expects log-mel spectrogram input (80 mel bins, 3000 time frames for 30s audio). + """ + + def __init__( + self, + pretrained_model_name_or_path: str = "openai/whisper-base", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "mean", + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + try: + from transformers import WhisperModel + except ImportError: + raise RuntimeError("transformers is required for WhisperEncoder. pip install transformers") + + if use_pretrained and not saved_weights_in_checkpoint: + self.model = WhisperModel.from_pretrained(pretrained_model_name_or_path) + else: + from transformers import WhisperConfig + + self.model = WhisperModel(WhisperConfig()) + + # Use only the encoder portion + self.encoder = self.model.encoder + self._output_dim = self.model.config.d_model + self._reduce_output = reduce_output + + if not trainable: + for p in self.encoder.parameters(): + p.requires_grad_(False) + + def forward(self, inputs: torch.Tensor, mask=None) -> EncoderOutputDict: + # inputs shape: [batch_size, n_mels, seq_len] (log-mel spectrogram) + # Whisper encoder expects [batch, n_mels, seq_len] + if inputs.dim() == 2: + # If [batch, seq_len], assume single mel bin - unlikely but handle gracefully + inputs = inputs.unsqueeze(1) + if inputs.dim() == 3 and inputs.shape[1] != 80: + # If shape is [batch, seq_len, n_mels], transpose to [batch, n_mels, seq_len] + if inputs.shape[2] == 80: + inputs = inputs.transpose(1, 2) + + outputs = self.encoder(inputs) + hidden = outputs.last_hidden_state # [batch, seq_len, d_model] + + if self._reduce_output == "mean": + hidden = hidden.mean(dim=1) + elif self._reduce_output == "last": + hidden = hidden[:, -1, :] + elif self._reduce_output == "cls_pooled": + hidden = hidden[:, 0, :] + # else: return full sequence + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls(): + from ludwig.schema.encoders.audio_encoders import WhisperEncoderConfig + + return WhisperEncoderConfig + + @property + def output_shape(self) -> torch.Size: + if self._reduce_output is None: + return torch.Size([None, self._output_dim]) # variable length + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([80]) # 80 mel bins + + +@DeveloperAPI +@register_encoder("hubert", [AUDIO]) +class HuBERTEncoder(Encoder): + """HuBERT audio encoder (Hsu et al., IEEE/ACM TASLP 2021). + + Self-Supervised Speech Representation Learning by Masked Prediction of + Hidden Units. Uses an offline clustering step to provide aligned target + labels for a BERT-like prediction loss. + + Use when: speaker verification, emotion recognition, audio classification. + Best for: tasks requiring robust speech representations without labeled data. + Expects raw waveform input (16kHz sample rate). + """ + + def __init__( + self, + pretrained_model_name_or_path: str = "facebook/hubert-base-ls960", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "mean", + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + try: + from transformers import HubertModel + except ImportError: + raise RuntimeError("transformers is required for HuBERTEncoder. pip install transformers") + + if use_pretrained and not saved_weights_in_checkpoint: + self.model = HubertModel.from_pretrained(pretrained_model_name_or_path) + else: + from transformers import HubertConfig + + self.model = HubertModel(HubertConfig()) + + self._output_dim = self.model.config.hidden_size + self._reduce_output = reduce_output + + if not trainable: + for p in self.model.parameters(): + p.requires_grad_(False) + + def forward(self, inputs: torch.Tensor, mask=None) -> EncoderOutputDict: + # inputs shape: [batch_size, sequence_length] (raw waveform) + if inputs.dim() == 3: + inputs = inputs.squeeze(1) # Remove channel dim if present + outputs = self.model(inputs) + hidden = outputs.last_hidden_state # [batch, seq_len, hidden_size] + + if self._reduce_output == "mean": + hidden = hidden.mean(dim=1) + elif self._reduce_output == "last": + hidden = hidden[:, -1, :] + elif self._reduce_output == "cls_pooled": + hidden = hidden[:, 0, :] + # else: return full sequence + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls(): + from ludwig.schema.encoders.audio_encoders import HuBERTEncoderConfig + + return HuBERTEncoderConfig + + @property + def output_shape(self) -> torch.Size: + if self._reduce_output is None: + return torch.Size([None, self._output_dim]) # variable length + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) # raw waveform, variable length diff --git a/ludwig/encoders/bag_encoders.py b/ludwig/encoders/bag_encoders.py new file mode 100644 index 0000000..e93a042 --- /dev/null +++ b/ludwig/encoders/bag_encoders.py @@ -0,0 +1,115 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from typing import Any + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BAG, ENCODER_OUTPUT +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.embedding_modules import EmbedWeighted +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.schema.encoders.bag_encoders import BagEmbedWeightedConfig +from ludwig.schema.encoders.base import BaseEncoderConfig + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("embed", BAG) +class BagEmbedWeightedEncoder(Encoder): + def __init__( + self, + vocab: list[str], + embedding_size: int = 50, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embeddings_on_cpu: bool = False, + fc_layers=None, + num_fc_layers: int = 0, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict[str, Any] | None = None, + activation: str = "relu", + dropout: float = 0.0, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + logger.debug(" EmbedWeighted") + self.embed_weighted = EmbedWeighted( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.embed_weighted.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return BagEmbedWeightedConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([len(self.vocab)]) + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """Forward pass through the encoder. + + Args: + inputs: The inputs fed into the encoder. Shape: [batch x vocab size]. + + Returns: + Embeddings of shape [batch x embed size]. + """ + hidden = self.embed_weighted(inputs) + hidden = self.fc_stack(hidden) + + return {ENCODER_OUTPUT: hidden} diff --git a/ludwig/encoders/base.py b/ludwig/encoders/base.py new file mode 100644 index 0000000..5d28127 --- /dev/null +++ b/ludwig/encoders/base.py @@ -0,0 +1,41 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from abc import ABC, abstractmethod + +from torch import nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.torch_utils import LudwigModule + + +@DeveloperAPI +class Encoder(LudwigModule, ABC): + @abstractmethod + def forward(self, inputs, training=None, mask=None): + raise NotImplementedError + + def get_embedding_layer(self) -> nn.Module: + """Returns layer that embeds inputs, used for computing explanations. + + Captum adds an evaluation hook to this module returned by this function. The hook copies the module's return + with .clone(). The module returned by this function must return a tensor, not a dictionary of tensors. + """ + return next(self.children()) + + @property + def name(self) -> str: + return self.__class__.__name__ diff --git a/ludwig/encoders/category_encoders.py b/ludwig/encoders/category_encoders.py new file mode 100644 index 0000000..5e7b773 --- /dev/null +++ b/ludwig/encoders/category_encoders.py @@ -0,0 +1,319 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch +from torch import nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import CATEGORY, ENCODER_OUTPUT +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.embedding_modules import Embed +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.category_encoders import ( + CategoricalEmbedConfig, + CategoricalHashEncoderConfig, + CategoricalOneHotEncoderConfig, + CategoricalPassthroughEncoderConfig, + CategoricalSparseConfig, + CategoricalTargetEncoderConfig, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("passthrough", [CATEGORY]) +class CategoricalPassthroughEncoder(Encoder): + def __init__(self, input_size=1, encoder_config=None, **kwargs): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.input_size = input_size + self.identity = nn.Identity() + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """ + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1] + """ + return {"encoder_output": self.identity(inputs.float())} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CategoricalPassthroughEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return self.input_shape + + def get_embedding_layer(self) -> nn.Module: + return self.identity + + +@DeveloperAPI +@register_encoder("dense", CATEGORY) +class CategoricalEmbedEncoder(Encoder): + def __init__( + self, + vocab: list[str], + embedding_size: int = 50, + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + embeddings_on_cpu: bool = False, + dropout: float = 0.0, + embedding_initializer: str | dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + logger.debug(" Embed") + self.embed = Embed( + vocab=vocab, + embedding_size=embedding_size, + representation="dense", + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=embedding_initializer, + ) + self.embedding_size = self.embed.embedding_size + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """ + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1], type torch.int32. + + Returns: + embeddings of shape [batch x embed size], type torch.float32. + """ + embedded = self.embed(inputs) + return {ENCODER_OUTPUT: embedded} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CategoricalEmbedConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.embedding_size]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + +@DeveloperAPI +@register_encoder("sparse", CATEGORY) +class CategoricalSparseEncoder(Encoder): + def __init__( + self, + vocab: list[str], + embeddings_trainable: bool = False, + pretrained_embeddings: str | None = None, + embeddings_on_cpu: bool = False, + dropout: float = 0.0, + embedding_initializer: str | dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + logger.debug(" Embed") + self.embed = Embed( + vocab=vocab, + embedding_size=len(vocab), + representation="sparse", + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=embedding_initializer, + ) + self.embedding_size = self.embed.embedding_size + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """ + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1], type torch.int32. + + Returns: + embeddings of shape [batch x embed size], type torch.float32. + """ + embedded = self.embed(inputs) + return {ENCODER_OUTPUT: embedded} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CategoricalSparseConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.embedding_size]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + +@DeveloperAPI +@register_encoder("onehot", [CATEGORY]) +class CategoricalOneHotEncoder(Encoder): + def __init__( + self, + vocab: list[str], + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.vocab_size = len(vocab) + self.identity = nn.Identity() + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """ + Args: + inputs: The inputs fed into the encoder. Shape: [batch, 1] or [batch] + """ + t = inputs.reshape(-1).long() + # the output of this must be a float so that it can be concatenated with other + # encoder outputs and passed to dense layers in the combiner, decoder, etc. + outputs = self.identity(torch.nn.functional.one_hot(t, num_classes=self.vocab_size).float()) + return {"encoder_output": outputs} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CategoricalOneHotEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.vocab_size]) + + def get_embedding_layer(self) -> nn.Module: + return self.identity + + +@DeveloperAPI +@register_encoder("target", [CATEGORY]) +class CategoricalTargetEncoder(Encoder): + """Target encoding: encode categories by smoothed mean target value. + + Cite: Micci-Barreca, "A Preprocessing Scheme for High-Cardinality Categorical + Attributes in Classification and Prediction Problems", ACM SIGKDD 2001. + + Use when: high-cardinality categorical features (postal codes, user IDs) with + sufficient training data. Not suitable for small datasets (overfitting risk). + """ + + def __init__(self, vocab: list[str], output_size: int = 1, encoder_config=None, **kwargs): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.vocab_size = len(vocab) + # Learnable target encoding per category (initialized to 0, set during training via metadata) + self.target_values = nn.Embedding(self.vocab_size, output_size) + nn.init.zeros_(self.target_values.weight) + self._output_size = output_size + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """ + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1], type torch.int32. + """ + return {ENCODER_OUTPUT: self.target_values(inputs.long().squeeze(-1))} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CategoricalTargetEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_size]) + + +@DeveloperAPI +@register_encoder("hash", [CATEGORY]) +class CategoricalHashEncoder(Encoder): + """Feature hashing encoder for ultra-high-cardinality categoricals. + + Cite: Weinberger et al., "Feature Hashing for Large Scale Multitask Learning", ICML 2009. + + Use when: extremely large vocabularies where embedding tables are too expensive, + or streaming settings where vocabulary is not known ahead of time. + """ + + def __init__( + self, + vocab: list[str], + num_hash_buckets: int = 1024, + embedding_size: int = 50, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.num_hash_buckets = num_hash_buckets + self.embedding = nn.Embedding(num_hash_buckets, embedding_size) + self._output_size = embedding_size + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """ + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1], type torch.int32. + """ + # Hash input indices to bucket range + hashed = inputs.long().squeeze(-1) % self.num_hash_buckets + return {ENCODER_OUTPUT: self.embedding(hashed)} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CategoricalHashEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_size]) diff --git a/ludwig/encoders/date_encoders.py b/ludwig/encoders/date_encoders.py new file mode 100644 index 0000000..96e9791 --- /dev/null +++ b/ludwig/encoders/date_encoders.py @@ -0,0 +1,340 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from abc import abstractmethod + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DATE, DATE_VECTOR_LENGTH, ENCODER_OUTPUT +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.embedding_modules import Embed +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.date_encoders import DateEmbedConfig, DateWaveConfig +from ludwig.utils import torch_utils + +logger = logging.getLogger(__name__) + +# Date components and their properties. +# (name, vocabulary_size, periodic_period, index, needs_offset) +# vocabulary_size is used by DateEmbed for categorical embeddings. +# periodic_period is used by DateWave for sinusoidal encoding. +# needs_offset: whether to subtract 1 from the input (1-indexed -> 0-indexed). +DATE_COMPONENTS = [ + # name, vocab, period, col, offset + ("month", 12, 12, 1, True), + ("day", 31, 31, 2, True), + ("weekday", 7, 7, 3, False), + ("yearday", 366, 366, 4, True), + ("hour", 24, 24, 5, False), + ("minute", 60, 60, 6, False), + ("second", 60, 60, 7, False), +] + +# second_of_day is always encoded with periodic encoding in both DateEmbed and DateWave. +SECOND_OF_DAY_PERIOD = 86400 + + +@DeveloperAPI +class DateEncoderBase(Encoder): + """Base class for date encoders providing shared infrastructure. + + Date features are preprocessed into a fixed-size vector of 9 integer components: + [year, month, day, weekday, yearday, hour, minute, second, second_of_day]. + + Subclasses must implement ``encode_components()`` to define how individual + date components are encoded (e.g., categorical embeddings vs. sinusoidal + periodic encoding). The base class handles year scaling via a learned FC layer, + second_of_day periodic encoding, concatenation, and the final FC stack. + """ + + def __init__( + self, + fc_layers: list[dict] | None = None, + num_fc_layers: int = 0, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict | None = None, + activation: str = "relu", + dropout: float = 0, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + # Year is always encoded with a learned linear projection (continuous value). + logger.debug(" year FCStack") + self.year_fc = FCStack( + first_layer_input_size=1, + num_layers=1, + default_output_size=1, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=None, + default_norm_params=None, + default_activation=None, + default_dropout=dropout, + ) + + # Store FC stack params for use after subclass sets up component encoders. + self._fc_stack_params = { + "fc_layers": fc_layers, + "num_fc_layers": num_fc_layers, + "output_size": output_size, + "use_bias": use_bias, + "weights_initializer": weights_initializer, + "bias_initializer": bias_initializer, + "norm": norm, + "norm_params": norm_params, + "activation": activation, + "dropout": dropout, + } + + def _build_fc_stack(self, component_output_size: int): + """Build the final FC stack given the total size of encoded components. + + Args: + component_output_size: Sum of output sizes from all component encoders + (excluding year_fc and second_of_day periodic, which are added here). + """ + # year_fc output (1) + component encodings + second_of_day periodic (1) + fc_layer_input_size = self.year_fc.output_shape[0] + component_output_size + 1 + + p = self._fc_stack_params + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=fc_layer_input_size, + layers=p["fc_layers"], + num_layers=p["num_fc_layers"], + default_output_size=p["output_size"], + default_use_bias=p["use_bias"], + default_weights_initializer=p["weights_initializer"], + default_bias_initializer=p["bias_initializer"], + default_norm=p["norm"], + default_norm_params=p["norm_params"], + default_activation=p["activation"], + default_dropout=p["dropout"], + ) + # Clean up stored params after use. + del self._fc_stack_params + + @abstractmethod + def encode_components(self, input_vector: torch.Tensor) -> list[torch.Tensor]: + """Encode the date components (month through second) into a list of tensors. + + Args: + input_vector: Integer tensor of shape [batch, DATE_VECTOR_LENGTH]. + + Returns: + List of tensors, one per component, each of shape [batch, encoding_dim]. + """ + ... + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """Encode a date feature vector. + + Args: + inputs: Tensor of shape [batch, DATE_VECTOR_LENGTH] with dtype int or float, + containing [year, month, day, weekday, yearday, hour, minute, second, second_of_day]. + + Returns: + Dictionary with ENCODER_OUTPUT key mapping to tensor of shape [batch, output_size]. + """ + input_vector = inputs.to(torch.int) + + # Year: continuous, passed through a learned linear layer. + scaled_year = self.year_fc(input_vector[:, 0:1].to(torch.float)) + + # Components (month..second): encoded by subclass. + encoded_components = self.encode_components(input_vector) + + # second_of_day: always periodic (shared by both embed and wave encoders). + periodic_second_of_day = torch_utils.periodic(input_vector[:, 8:9].to(torch.float), SECOND_OF_DAY_PERIOD) + + hidden = torch.cat([scaled_year] + encoded_components + [periodic_second_of_day], dim=1) + + hidden = self.fc_stack(hidden) + return {ENCODER_OUTPUT: hidden} + + @property + def input_shape(self) -> torch.Size: + return torch.Size([DATE_VECTOR_LENGTH]) + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape + + +@DeveloperAPI +@register_encoder("embed", DATE) +class DateEmbed(DateEncoderBase): + """Encodes date components using learned categorical embeddings. + + Each cyclic date component (month, day, weekday, yearday, hour, minute, second) + is mapped to a dense embedding vector via a lookup table, similar to how categorical + features are encoded. The year is projected through a linear layer, and second_of_day + uses a periodic (cosine) encoding. All representations are concatenated and passed + through an optional FC stack. + + Use this encoder when you want the model to learn arbitrary (non-sinusoidal) + representations for each date component. This is the default date encoder and works + well in most scenarios. For datasets where the cyclic nature of time components is + important (e.g., hourly patterns wrapping around midnight), consider ``DateWave``. + """ + + def __init__( + self, + embedding_size: int = 10, + embeddings_on_cpu: bool = False, + fc_layers: list[dict] | None = None, + num_fc_layers: int = 0, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict | None = None, + activation: str = "relu", + dropout: float = 0, + encoder_config=None, + **kwargs, + ): + super().__init__( + fc_layers=fc_layers, + num_fc_layers=num_fc_layers, + output_size=output_size, + use_bias=use_bias, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + norm=norm, + norm_params=norm_params, + activation=activation, + dropout=dropout, + encoder_config=encoder_config, + **kwargs, + ) + + self.embedding_size = embedding_size + + # Build one embedding module per date component. + self.embed_modules = torch.nn.ModuleDict() + total_embed_size = 0 + for name, vocab_size, _period, _col, _offset in DATE_COMPONENTS: + logger.debug(f" {name} Embed") + embed = Embed( + [str(i) for i in range(vocab_size)], + embedding_size, + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + self.embed_modules[name] = embed + total_embed_size += embed.output_shape[0] + + self._build_fc_stack(total_embed_size) + + def encode_components(self, input_vector: torch.Tensor) -> list[torch.Tensor]: + """Encode date components using categorical embeddings.""" + encoded = [] + for name, _vocab, _period, col, offset in DATE_COMPONENTS: + val = input_vector[:, col : col + 1] + if offset: + val = val - 1 + encoded.append(self.embed_modules[name](val)) + return encoded + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return DateEmbedConfig + + +@DeveloperAPI +@register_encoder("wave", DATE) +class DateWave(DateEncoderBase): + """Encodes date components using periodic sinusoidal (cosine) functions. + + Each cyclic date component is encoded as cos(2*pi*x/period), which naturally + captures the cyclic nature of time -- e.g., hour 23 is close to hour 0, and + December is close to January. The year is projected through a linear layer. + All representations are concatenated and passed through an FC stack. + + This encoding is parameter-free for the components (no learned embeddings), + making it more compact than ``DateEmbed``. It is inspired by the positional + encoding approach from Vaswani et al., "Attention Is All You Need" (2017). + + Use this encoder when cyclic continuity matters and you want a lightweight + encoding. For richer learned representations, use ``DateEmbed``. + """ + + def __init__( + self, + fc_layers: list[FCStack] | None = None, + num_fc_layers: int = 1, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict | None = None, + activation: str = "relu", + dropout: float = 0, + encoder_config=None, + **kwargs, + ): + super().__init__( + fc_layers=fc_layers, + num_fc_layers=num_fc_layers, + output_size=output_size, + use_bias=use_bias, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + norm=norm, + norm_params=norm_params, + activation=activation, + dropout=dropout, + encoder_config=encoder_config, + **kwargs, + ) + + # Each periodic component produces 1 output (cosine value). + # 7 components (month..second), each producing 1 value. + total_component_size = len(DATE_COMPONENTS) + self._build_fc_stack(total_component_size) + + def encode_components(self, input_vector: torch.Tensor) -> list[torch.Tensor]: + """Encode date components using periodic cosine functions.""" + input_float = input_vector.to(torch.float) + encoded = [] + for _name, _vocab, period, col, _offset in DATE_COMPONENTS: + encoded.append(torch_utils.periodic(input_float[:, col : col + 1], period)) + return encoded + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return DateWaveConfig diff --git a/ludwig/encoders/generic_encoders.py b/ludwig/encoders/generic_encoders.py new file mode 100644 index 0000000..0b886e4 --- /dev/null +++ b/ludwig/encoders/generic_encoders.py @@ -0,0 +1,120 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, ENCODER_OUTPUT, NUMBER, TEXT, TIMESERIES, VECTOR +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.schema.encoders.base import BaseEncoderConfig, DenseEncoderConfig, PassthroughEncoderConfig + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("passthrough", [BINARY, NUMBER, TEXT, VECTOR]) +class PassthroughEncoder(Encoder): + def __init__(self, input_size=1, encoder_config=None, **kwargs): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.input_size = input_size + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Forward pass through the encoder. + + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1]. + """ + return {ENCODER_OUTPUT: inputs} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return PassthroughEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return self.input_shape + + +@DeveloperAPI +@register_encoder("dense", [BINARY, NUMBER, VECTOR, TIMESERIES]) +class DenseEncoder(Encoder): + def __init__( + self, + input_size, + fc_layers=None, + num_layers=1, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + activation="relu", + dropout=0, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.input_size = input_size + + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=input_size, + layers=fc_layers, + num_layers=num_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Forward pass through the encoder. + + Args: + inputs: The inputs fed into the encoder. Shape: [batch x 1]. + """ + return {ENCODER_OUTPUT: self.fc_stack(inputs)} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return DenseEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.fc_stack.layers[-1]["output_size"]]) diff --git a/ludwig/encoders/h3_encoders.py b/ludwig/encoders/h3_encoders.py new file mode 100644 index 0000000..981ca57 --- /dev/null +++ b/ludwig/encoders/h3_encoders.py @@ -0,0 +1,412 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, H3, H3_VECTOR_LENGTH, MAX_H3_RESOLUTION +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.embedding_modules import Embed, EmbedSequence +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.modules.initializer_modules import get_initializer +from ludwig.modules.recurrent_modules import RecurrentStack +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.h3_encoders import H3EmbedConfig, H3RNNConfig, H3WeightedSumConfig +from ludwig.utils import torch_utils + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("embed", H3) +class H3Embed(Encoder): + """Encodes H3 geospatial indices using learned categorical embeddings. + + H3 is a hierarchical hexagonal geospatial indexing system (Uber, 2018). Each H3 + index is decomposed into components: mode, edge, resolution, base cell, and up to + 15 resolution cells. Each component is embedded via a learned lookup table, the + resolution cells are masked to the actual resolution, and the sequence is reduced + (default: sum) and passed through an optional FC stack. + + Use this encoder for geospatial features encoded as H3 indices. It captures the + hierarchical structure of H3 at multiple resolutions. + + Reference: https://h3geo.org/ + """ + + def __init__( + self, + embedding_size: int = 10, + embeddings_on_cpu: bool = False, + fc_layers: list | None = None, + num_fc_layers: int = 0, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict | None = None, + activation: str = "relu", + dropout: float = 0, + reduce_output: str = "sum", + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.embedding_size = embedding_size + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + + logger.debug(" mode Embed") + self.embed_mode = Embed( + [str(i) for i in range(3)], + embedding_size, + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + force_embedding_size=True, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" edge Embed") + self.embed_edge = Embed( + [str(i) for i in range(7)], + embedding_size, + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + force_embedding_size=True, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" resolution Embed") + self.embed_resolution = Embed( + [str(i) for i in range(16)], + embedding_size, + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + force_embedding_size=True, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" base cell Embed") + self.embed_base_cell = Embed( + [str(i) for i in range(122)], + embedding_size, + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + force_embedding_size=True, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" cells Embed") + self.embed_cells = EmbedSequence( + [str(i) for i in range(8)], + embedding_size, + max_sequence_length=(H3_VECTOR_LENGTH - 4), + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + force_embedding_size=True, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=embedding_size, + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """Encode an H3 feature vector. + + Args: + inputs: Tensor of shape [batch, H3_VECTOR_LENGTH] with dtype int, + containing [mode, edge, resolution, base_cell, cell_0, ..., cell_14]. + + Returns: + Dictionary with ENCODER_OUTPUT key mapping to tensor of shape [batch, output_size]. + """ + input_vector = inputs.int() + + # ================ Embeddings ================ + embedded_mode = self.embed_mode(input_vector[:, 0:1]).unsqueeze(1) + embedded_edge = self.embed_edge(input_vector[:, 1:2]).unsqueeze(1) + embedded_resolution = self.embed_resolution(input_vector[:, 2:3]).unsqueeze(1) + embedded_base_cell = self.embed_base_cell(input_vector[:, 3:4]).unsqueeze(1) + embedded_cells = self.embed_cells(input_vector[:, 4:]) + + # ================ Masking ================ + # Mask out cells beyond the resolution of interest. + resolution = input_vector[:, 2] + mask = torch.unsqueeze(torch_utils.sequence_mask(resolution, MAX_H3_RESOLUTION), dim=-1).float() + # Batch size X 15(max resolution) X embedding size + masked_embedded_cells = embedded_cells * mask + + # ================ Reduce ================ + # Batch size X H3_VECTOR_LENGTH X embedding size + concatenated = torch.cat( + [embedded_mode, embedded_edge, embedded_resolution, embedded_base_cell, masked_embedded_cells], dim=1 + ) + + hidden = self.reduce_sequence(concatenated) + + # ================ FC Stack ================ + hidden = self.fc_stack(hidden) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return H3EmbedConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([H3_VECTOR_LENGTH]) + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape + + +@DeveloperAPI +@register_encoder("weighted_sum", H3) +class H3WeightedSum(Encoder): + """Encodes H3 indices using a learned weighted sum over component embeddings. + + This encoder first embeds all H3 components using ``H3Embed`` (with no reduction), + then computes a weighted sum across the component dimension using learned (or + optionally softmax-normalized) weights. The result is passed through an FC stack. + + Compared to ``H3Embed`` with sum reduction, this encoder learns per-component + importance weights, allowing the model to attend more to certain hierarchy levels + (e.g., base cell vs. fine-grained resolution cells). + """ + + def __init__( + self, + embedding_size: int = 10, + embeddings_on_cpu: bool = False, + should_softmax: bool = False, + fc_layers: list | None = None, + num_fc_layers: int = 0, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict | None = None, + activation: str = "relu", + dropout: float = 0, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.should_softmax = should_softmax + self.sum_sequence_reducer = SequenceReducer(reduce_mode="sum") + + self.h3_embed = H3Embed( + embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + reduce_output="None", + ) + + self.register_buffer( + "aggregation_weights", torch.Tensor(get_initializer(weights_initializer)([H3_VECTOR_LENGTH, 1])) + ) + + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=self.h3_embed.output_shape[0], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """Encode an H3 feature vector using a learned weighted sum. + + Args: + inputs: Tensor of shape [batch, H3_VECTOR_LENGTH] with dtype int. + + Returns: + Dictionary with ENCODER_OUTPUT key mapping to tensor of shape [batch, output_size]. + """ + # ================ Embeddings ================ + input_vector = inputs + embedded_h3 = self.h3_embed(input_vector) + + # ================ Weighted Sum ================ + if self.should_softmax: + weights = torch.softmax(self.aggregation_weights, dim=None) + else: + weights = self.aggregation_weights + + hidden = self.sum_sequence_reducer(embedded_h3[ENCODER_OUTPUT] * weights) + + # ================ FC Stack ================ + hidden = self.fc_stack(hidden) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return H3WeightedSumConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([H3_VECTOR_LENGTH]) + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape + + +@DeveloperAPI +@register_encoder("rnn", H3) +class H3RNN(Encoder): + """Encodes H3 indices by treating the component sequence as a time series for an RNN. + + This encoder first embeds all H3 components using ``H3Embed`` (with no reduction), + then feeds the resulting sequence of embeddings through a recurrent neural network + (RNN, LSTM, or GRU). This allows the model to capture sequential dependencies + across the H3 hierarchy levels (mode -> edge -> resolution -> base cell -> cells). + + Use this encoder when the sequential/hierarchical structure of H3 indices is + important for the task. For simpler pooling-based approaches, use ``H3Embed`` + or ``H3WeightedSum``. + """ + + def __init__( + self, + embedding_size: int = 10, + embeddings_on_cpu: bool = False, + num_layers: int = 1, + hidden_size: int = 10, + cell_type: str = "rnn", + bidirectional: bool = False, + activation: str = "tanh", + recurrent_activation: str = "sigmoid", + use_bias: bool = True, + unit_forget_bias: bool = True, + weights_initializer: str = "xavier_uniform", + recurrent_initializer: str = "orthogonal", + bias_initializer: str = "zeros", + dropout: float = 0.0, + recurrent_dropout: float = 0.0, + reduce_output: str = "last", + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.embedding_size = embedding_size + + self.h3_embed = H3Embed( + embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + weights_initializer=weights_initializer, + bias_initializer=bias_initializer, + reduce_output="None", + ) + + logger.debug(" RecurrentStack") + self.recurrent_stack = RecurrentStack( + input_size=self.h3_embed.output_shape[0], + max_sequence_length=H3_VECTOR_LENGTH, + hidden_size=hidden_size, + cell_type=cell_type, + num_layers=num_layers, + bidirectional=bidirectional, + use_bias=use_bias, + dropout=recurrent_dropout, + ) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """Encode an H3 feature vector through an RNN. + + Args: + inputs: Tensor of shape [batch, H3_VECTOR_LENGTH] with dtype int. + + Returns: + Dictionary with ENCODER_OUTPUT and ENCODER_OUTPUT_STATE keys. + """ + # ================ Embeddings ================ + embedded_h3 = self.h3_embed(inputs) + + # ================ RNN ================ + hidden, final_state = self.recurrent_stack(embedded_h3[ENCODER_OUTPUT]) + + return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: final_state} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return H3RNNConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([H3_VECTOR_LENGTH]) + + @property + def output_shape(self) -> torch.Size: + return self.recurrent_stack.output_shape diff --git a/ludwig/encoders/image/__init__.py b/ludwig/encoders/image/__init__.py new file mode 100644 index 0000000..366b0f3 --- /dev/null +++ b/ludwig/encoders/image/__init__.py @@ -0,0 +1,4 @@ +import ludwig.encoders.image.base +import ludwig.encoders.image.pretrained +import ludwig.encoders.image.timm +import ludwig.encoders.image.torchvision # noqa diff --git a/ludwig/encoders/image/base.py b/ludwig/encoders/image/base.py new file mode 100644 index 0000000..ca6956e --- /dev/null +++ b/ludwig/encoders/image/base.py @@ -0,0 +1,275 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from typing import Any + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, IMAGE +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.convolutional_modules import Conv2DStack, UNetDownStack +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.modules.mlp_mixer_modules import MLPMixer +from ludwig.schema.encoders.image.base import ( + ImageEncoderConfig, + MLPMixerConfig, + Stacked2DCNNConfig, + UNetEncoderConfig, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +class ImageEncoder(Encoder): + pass + + +@DeveloperAPI +@register_encoder("stacked_cnn", IMAGE) +class Stacked2DCNN(ImageEncoder): + def __init__( + self, + height: int, + width: int, + conv_layers: list[dict] | None = None, + num_conv_layers: int | None = None, + num_channels: int | None = None, + out_channels: int = 32, + kernel_size: int | tuple[int] = 3, + stride: int | tuple[int] = 1, + padding: int | tuple[int] | str = "valid", + dilation: int | tuple[int] = 1, + conv_use_bias: bool = True, + padding_mode: str = "zeros", + conv_norm: str | None = None, + conv_norm_params: dict[str, Any] | None = None, + conv_activation: str = "relu", + conv_dropout: int = 0, + pool_function: str = "max", + pool_kernel_size: int | tuple[int] = 2, + pool_stride: int | tuple[int] | None = None, + pool_padding: int | tuple[int] = 0, + pool_dilation: int | tuple[int] = 1, + groups: int = 1, + fc_layers: list[dict] | None = None, + num_fc_layers: int | None = 1, + output_size: int = 128, + fc_use_bias: bool = True, + fc_weights_initializer: str = "xavier_uniform", + fc_bias_initializer: str = "zeros", + fc_norm: str | None = None, + fc_norm_params: dict[str, Any] | None = None, + fc_activation: str = "relu", + fc_dropout: float = 0, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + # map parameter input feature config names to internal names + img_height = height + img_width = width + first_in_channels = num_channels + + self._input_shape = (first_in_channels, img_height, img_width) + + if first_in_channels is None: + raise ValueError("first_in_channels must not be None.") + + logger.debug(" Conv2DStack") + self.conv_stack_2d = Conv2DStack( + img_height=img_height, + img_width=img_width, + layers=conv_layers, + num_layers=num_conv_layers, + first_in_channels=first_in_channels, + default_out_channels=out_channels, + default_kernel_size=kernel_size, + default_stride=stride, + default_padding=padding, + default_dilation=dilation, + default_groups=groups, + default_use_bias=conv_use_bias, + default_padding_mode=padding_mode, + default_norm=conv_norm, + default_norm_params=conv_norm_params, + default_activation=conv_activation, + default_dropout=conv_dropout, + default_pool_function=pool_function, + default_pool_kernel_size=pool_kernel_size, + default_pool_stride=pool_stride, + default_pool_padding=pool_padding, + default_pool_dilation=pool_dilation, + ) + out_channels, img_height, img_width = self.conv_stack_2d.output_shape + first_fc_layer_input_size = out_channels * img_height * img_width + + self.flatten = torch.nn.Flatten() + + logger.debug(" FCStack") + self.fc_stack = FCStack( + first_layer_input_size=first_fc_layer_input_size, + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=fc_use_bias, + default_weights_initializer=fc_weights_initializer, + default_bias_initializer=fc_bias_initializer, + default_norm=fc_norm, + default_norm_params=fc_norm_params, + default_activation=fc_activation, + default_dropout=fc_dropout, + ) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """Forward pass through the encoder. + + Args: + inputs: The inputs fed into the encoder. Shape: [batch x channels x height x width]. + """ + + hidden = self.conv_stack_2d(inputs) + hidden = self.flatten(hidden) + outputs = self.fc_stack(hidden) + + return {ENCODER_OUTPUT: outputs} + + @staticmethod + def get_schema_cls() -> type[ImageEncoderConfig]: + return Stacked2DCNNConfig + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +@DeveloperAPI +@register_encoder("mlp_mixer", IMAGE) +class MLPMixerEncoder(ImageEncoder): + def __init__( + self, + height: int, + width: int, + num_channels: int | None = None, + patch_size: int = 16, + embed_size: int = 512, + token_size: int = 2048, + channel_dim: int = 256, + num_layers: int = 8, + dropout: float = 0.0, + avg_pool: bool = True, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + # map parameter input feature config names to internal names + img_height = height + img_width = width + in_channels = num_channels + + if num_channels is None: + raise RuntimeError("num_channels must not be None") + + self._input_shape = (in_channels, img_height, img_width) + + logger.debug(" MLPMixer") + self.mlp_mixer = MLPMixer( + img_height=img_height, + img_width=img_width, + in_channels=in_channels, + patch_size=patch_size, + embed_size=embed_size, + token_size=token_size, + channel_dim=channel_dim, + num_layers=num_layers, + dropout=dropout, + avg_pool=avg_pool, + ) + + self._output_shape = self.mlp_mixer.output_shape + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + hidden = self.mlp_mixer(inputs) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[ImageEncoderConfig]: + return MLPMixerConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + @property + def output_shape(self) -> torch.Size: + return self._output_shape + + +@DeveloperAPI +@register_encoder("unet", IMAGE) +class UNetEncoder(ImageEncoder): + def __init__( + self, + height: int, + width: int, + num_channels: int = 3, + conv_norm: str | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + if height % 16 or width % 16: + raise ValueError(f"Invalid `height` {height} or `width` {width} for unet encoder") + + self.unet = UNetDownStack( + img_height=height, + img_width=width, + in_channels=num_channels, + norm=conv_norm, + ) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + hidden, skips = self.unet(inputs) + return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: skips} + + @staticmethod + def get_schema_cls() -> type[ImageEncoderConfig]: + return UNetEncoderConfig + + @property + def output_shape(self) -> torch.Size: + return self.unet.output_shape + + @property + def input_shape(self) -> torch.Size: + return self.unet.input_shape diff --git a/ludwig/encoders/image/pretrained.py b/ludwig/encoders/image/pretrained.py new file mode 100644 index 0000000..3a35e16 --- /dev/null +++ b/ludwig/encoders/image/pretrained.py @@ -0,0 +1,185 @@ +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, IMAGE +from ludwig.encoders.image.base import ImageEncoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.image.pretrained import ( + CLIPImageEncoderConfig, + DINOv2ImageEncoderConfig, + SigLIPImageEncoderConfig, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("clip", IMAGE) +class CLIPImageEncoder(ImageEncoder): + """CLIP image encoder (Radford et al., ICML 2021). + + Encodes images using CLIP's vision transformer. Produces embeddings aligned with + text in a shared latent space, enabling zero-shot classification and multimodal tasks. + + Use when: zero-shot image classification, image-text retrieval, or multimodal + fusion where visual-semantic alignment matters. + """ + + def __init__( + self, + pretrained_model_name_or_path: str = "openai/clip-vit-base-patch32", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + from transformers import CLIPVisionConfig, CLIPVisionModel + + if use_pretrained and not saved_weights_in_checkpoint: + logger.info(f"Loading pretrained CLIP vision model: {pretrained_model_name_or_path}") + self.model = CLIPVisionModel.from_pretrained(pretrained_model_name_or_path) + else: + logger.info("Instantiating CLIP vision model without pretrained weights.") + self.model = CLIPVisionModel(CLIPVisionConfig()) + + self._output_dim = self.model.config.hidden_size + + for p in self.model.parameters(): + p.requires_grad_(trainable) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + outputs = self.model(pixel_values=inputs) + return {ENCODER_OUTPUT: outputs.pooler_output} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CLIPImageEncoderConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + size = self.model.config.image_size + return torch.Size([3, size, size]) + + +@DeveloperAPI +@register_encoder("dinov2", IMAGE) +class DINOv2ImageEncoder(ImageEncoder): + """DINOv2 image encoder (Oquab et al., TMLR 2024). + + Self-supervised visual features that work well as frozen backbones for dense prediction + and linear probing. No text alignment needed. + + Use when: image classification/segmentation without labels for pretraining, + dense prediction tasks, or as a general-purpose frozen feature extractor. + """ + + def __init__( + self, + pretrained_model_name_or_path: str = "facebook/dinov2-base", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + from transformers import Dinov2Config, Dinov2Model + + if use_pretrained and not saved_weights_in_checkpoint: + logger.info(f"Loading pretrained DINOv2 model: {pretrained_model_name_or_path}") + self.model = Dinov2Model.from_pretrained(pretrained_model_name_or_path) + else: + logger.info("Instantiating DINOv2 model without pretrained weights.") + self.model = Dinov2Model(Dinov2Config()) + + self._output_dim = self.model.config.hidden_size + + for p in self.model.parameters(): + p.requires_grad_(trainable) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + outputs = self.model(pixel_values=inputs) + return {ENCODER_OUTPUT: outputs.pooler_output} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return DINOv2ImageEncoderConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + size = self.model.config.image_size + return torch.Size([3, size, size]) + + +@DeveloperAPI +@register_encoder("siglip", IMAGE) +class SigLIPImageEncoder(ImageEncoder): + """SigLIP image encoder (Zhai et al., ICCV 2023). + + Uses sigmoid loss instead of softmax for image-text pre-training, enabling + better scaling and more efficient batch processing than CLIP. + + Use when: similar to CLIP but with better scaling properties, + or when using SigLIP-specific pretrained models. + """ + + def __init__( + self, + pretrained_model_name_or_path: str = "google/siglip-base-patch16-224", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + from transformers import SiglipVisionConfig, SiglipVisionModel + + if use_pretrained and not saved_weights_in_checkpoint: + logger.info(f"Loading pretrained SigLIP vision model: {pretrained_model_name_or_path}") + self.model = SiglipVisionModel.from_pretrained(pretrained_model_name_or_path) + else: + logger.info("Instantiating SigLIP vision model without pretrained weights.") + self.model = SiglipVisionModel(SiglipVisionConfig()) + + self._output_dim = self.model.config.hidden_size + + for p in self.model.parameters(): + p.requires_grad_(trainable) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + outputs = self.model(pixel_values=inputs) + return {ENCODER_OUTPUT: outputs.pooler_output} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return SigLIPImageEncoderConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + size = self.model.config.image_size + return torch.Size([3, size, size]) diff --git a/ludwig/encoders/image/timm.py b/ludwig/encoders/image/timm.py new file mode 100644 index 0000000..9934c96 --- /dev/null +++ b/ludwig/encoders/image/timm.py @@ -0,0 +1,163 @@ +import logging + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, IMAGE +from ludwig.encoders.image.base import ImageEncoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.image.timm import ( + TimmCAFormerEncoderConfig, + TimmConvFormerEncoderConfig, + TimmConvNeXtV2EncoderConfig, + TimmEncoderConfig, + TimmPoolFormerEncoderConfig, +) + +logger = logging.getLogger(__name__) + + +def _get_timm(): + try: + import timm + except ImportError: + raise ImportError("timm is required for this encoder. Install it with: pip install timm") + return timm + + +@DeveloperAPI +@register_encoder("timm", IMAGE) +class TimmEncoder(ImageEncoder): + """Wraps any model from the timm (pytorch-image-models) library as a Ludwig image encoder. + + This provides access to hundreds of pretrained vision models including MetaFormer variants + (CAFormer, ConvFormer, PoolFormer), ConvNeXt V2, EfficientFormer, and many more. + + Usage in Ludwig config: + encoder: + type: timm + model_name: caformer_s18.sail_in22k_ft_in1k + use_pretrained: true + trainable: true + """ + + def __init__( + self, + model_name: str = "caformer_s18", + use_pretrained: bool = True, + trainable: bool = True, + saved_weights_in_checkpoint: bool = False, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + timm = _get_timm() + + pretrained = use_pretrained and not saved_weights_in_checkpoint + if pretrained: + logger.info(f"Instantiating timm image encoder '{model_name}' with pretrained weights.") + else: + logger.info(f"Instantiating timm image encoder '{model_name}' without pretrained weights.") + + # num_classes=0 removes the classification head, returning pooled features + self.model = timm.create_model(model_name, pretrained=pretrained, num_classes=0) + + # Get the model's expected input config for input_shape + data_config = timm.data.resolve_model_data_config(self.model) + self._input_size = data_config["input_size"] # (C, H, W) + + # Compute output dim by running a dummy forward + with torch.no_grad(): + dummy = torch.zeros(1, *self._input_size) + out = self.model(dummy) + self._output_dim = out.shape[-1] + + for p in self.model.parameters(): + p.requires_grad_(trainable) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + return {ENCODER_OUTPUT: self.model(inputs)} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TimmEncoderConfig + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_dim]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_size) + + +@DeveloperAPI +@register_encoder("caformer", IMAGE) +class TimmCAFormerEncoder(TimmEncoder): + """CAFormer encoder — hybrid Conv+Attention MetaFormer achieving SOTA accuracy on ImageNet. + + Variants: s18 (26M, 83.6%), s36 (39M, 84.5%), m36 (56M, 85.2%), b36 (99M, 85.5%). + """ + + def __init__(self, model_name: str = "caformer_s18", **kwargs): + super().__init__(model_name=model_name, **kwargs) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TimmCAFormerEncoderConfig + + +@DeveloperAPI +@register_encoder("convformer", IMAGE) +class TimmConvFormerEncoder(TimmEncoder): + """ConvFormer encoder — pure CNN MetaFormer that outperforms ConvNeXt.""" + + def __init__(self, model_name: str = "convformer_s18", **kwargs): + super().__init__(model_name=model_name, **kwargs) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TimmConvFormerEncoderConfig + + +@DeveloperAPI +@register_encoder("poolformer", IMAGE) +class TimmPoolFormerEncoder(TimmEncoder): + """PoolFormer encoder — MetaFormer using simple average pooling as token mixer.""" + + def __init__(self, model_name: str = "poolformerv2_s12", **kwargs): + super().__init__(model_name=model_name, **kwargs) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TimmPoolFormerEncoderConfig + + +@DeveloperAPI +@register_encoder("convnextv2", IMAGE) +class TimmConvNeXtV2Encoder(TimmEncoder): + """ConvNeXt V2 image encoder (Woo et al., CVPR 2023). + + Improved ConvNeXt with Global Response Normalization (GRN) layer and + fully convolutional masked autoencoder (FCMAE) pre-training. Matches + or exceeds Swin Transformer V2 on ImageNet. + + Use when: image classification/feature extraction where you want a + pure-CNN architecture competitive with vision transformers. Better + than ConvNeXt V1 across all model sizes. + + Default model: convnextv2_base (via TIMM). + """ + + def __init__(self, model_name: str = "convnextv2_base", **kwargs): + super().__init__(model_name=model_name, **kwargs) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TimmConvNeXtV2EncoderConfig diff --git a/ludwig/encoders/image/torchvision.py b/ludwig/encoders/image/torchvision.py new file mode 100644 index 0000000..39a3dfa --- /dev/null +++ b/ludwig/encoders/image/torchvision.py @@ -0,0 +1,540 @@ +import logging +import os + +import torch +import torchvision.models as tvm + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, IMAGE +from ludwig.encoders.image.base import ImageEncoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.image.torchvision import ( + TVAlexNetEncoderConfig, + TVConvNeXtEncoderConfig, + TVDenseNetEncoderConfig, + TVEfficientNetEncoderConfig, + TVGoogLeNetEncoderConfig, + TVInceptionV3EncoderConfig, + TVMaxVitEncoderConfig, + TVMNASNetEncoderConfig, + TVMobileNetV2EncoderConfig, + TVMobileNetV3EncoderConfig, + TVRegNetEncoderConfig, + TVResNetEncoderConfig, + TVResNeXtEncoderConfig, + TVShuffleNetV2EncoderConfig, + TVSqueezeNetEncoderConfig, + TVSwinTransformerEncoderConfig, + TVVGGEncoderConfig, + TVViTEncoderConfig, + TVWideResNetEncoderConfig, +) +from ludwig.utils.image_utils import register_torchvision_model_variants, torchvision_model_registry, TVModelVariant + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +class TVBaseEncoder(ImageEncoder): + # Softmax removal strategy. Override in subclasses. + # "classifier_last" -> model.classifier[-1] = Identity() + # "classifier" -> model.classifier = Identity() + # "fc" -> model.fc = Identity() + # "head" -> model.head = Identity() + # "heads_last" -> model.heads[-1] = Identity() + # "none" -> no-op + _softmax_removal: str = "classifier_last" + + def __init__( + self, + model_variant: str | int | None = None, + use_pretrained: bool = True, + saved_weights_in_checkpoint: bool = False, + model_cache_dir: str | None = None, + trainable: bool = True, + **kwargs, + ): + super().__init__() + + logger.debug(f" {self.name}") + # map parameter input feature config names to internal names + self.model_variant = model_variant + self.use_pretrained = use_pretrained + self.model_cache_dir = model_cache_dir + + # remove any Ludwig specific keyword parameters + kwargs.pop("encoder_config", None) + kwargs.pop("type", None) + kwargs.pop("skip", None) + + # cache pre-trained models if requested + # based on https://github.com/pytorch/vision/issues/616#issuecomment-428637564 + if self.model_cache_dir is not None: + os.environ["TORCH_HOME"] = self.model_cache_dir + + # retrieve function to create requested model + self.create_model = torchvision_model_registry[self.torchvision_model_type][ + self.model_variant + ].create_model_function + + # get weight specification + if use_pretrained and not saved_weights_in_checkpoint: + weights_specification = torchvision_model_registry[self.torchvision_model_type][ + self.model_variant + ].model_weights.DEFAULT + logger.info( + f"Instantiating torchvision image encoder '{self.torchvision_model_type}' with pretrained weights: " + f"{weights_specification}." + ) + else: + weights_specification = None + if saved_weights_in_checkpoint: + logger.info( + f"Instantiating torchvision image encoder: '{self.torchvision_model_type}' " + "with weights saved in the checkpoint." + ) + else: + logger.info( + f"Instantiating torchvision image encoder: '{self.torchvision_model_type}' " + "with no pretrained weights." + ) + + # get torchvision transforms object + transforms_obj = torchvision_model_registry[self.torchvision_model_type][ + self.model_variant + ].model_weights.DEFAULT.transforms() + + # capture key attributes from torchvision transform for later use + self.num_channels = len(transforms_obj.mean) + self.normalize_mean = transforms_obj.mean + self.normalize_std = transforms_obj.std + self.crop_size = transforms_obj.crop_size + + logger.debug(f" {self.torchvision_model_type}") + # create pretrained model with pretrained weights or None for untrained model + self.model = self.create_model(weights=weights_specification, **kwargs) + + # remove final classification layer + self._remove_softmax_layer() + + # freeze parameters if requested + for p in self.model.parameters(): + p.requires_grad_(trainable) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + return {ENCODER_OUTPUT: self.model(inputs)} + + def _remove_softmax_layer(self): + """Remove the final classification layer, replacing it with Identity() so the encoder outputs features + instead of class logits.""" + strategy = self._softmax_removal + if strategy == "classifier_last": + self.model.classifier[-1] = torch.nn.Identity() + elif strategy == "classifier": + self.model.classifier = torch.nn.Identity() + elif strategy == "fc": + self.model.fc = torch.nn.Identity() + elif strategy == "head": + self.model.head = torch.nn.Identity() + elif strategy == "heads_last": + self.model.heads[-1] = torch.nn.Identity() + # else: "none" — no-op (e.g. squeezenet) + + @property + def output_shape(self) -> torch.Size: + # create synthetic image and run through forward method + inputs = torch.randn([1, *self.input_shape]) + output = self.model(inputs) + return torch.Size(output.shape[1:]) + + @property + def input_shape(self) -> torch.Size: + # expected shape after all pre-processing + # len(transforms_obj.mean) determines the number of channels + # transforms_obj.crop_size determines the height and width of image + # [num_channels, height, width] + return torch.Size([self.num_channels, *(2 * self.crop_size)]) + + +# --------------------------------------------------------------------------- +# TorchVision encoder subclasses +# +# Each subclass only needs to set: +# - torchvision_model_type (str) +# - _softmax_removal (str) if different from "classifier_last" +# - get_schema_cls() +# - optional __init__ overrides for special cases (googlenet, inceptionv3, vit) +# --------------------------------------------------------------------------- + + +@DeveloperAPI +@register_torchvision_model_variants( + [TVModelVariant(variant_id="base", create_model_function=tvm.alexnet, model_weights=tvm.AlexNet_Weights)] +) +@register_encoder("alexnet", IMAGE) +class TVAlexNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "alexnet" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVAlexNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("tiny", tvm.convnext_tiny, tvm.ConvNeXt_Tiny_Weights), + TVModelVariant("small", tvm.convnext_small, tvm.ConvNeXt_Small_Weights), + TVModelVariant("base", tvm.convnext_base, tvm.ConvNeXt_Base_Weights), + TVModelVariant("large", tvm.convnext_large, tvm.ConvNeXt_Large_Weights), + ] +) +@register_encoder("convnext", IMAGE) +class TVConvNeXtEncoder(TVBaseEncoder): + torchvision_model_type: str = "convnext" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVConvNeXtEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant(121, tvm.densenet121, tvm.DenseNet121_Weights), + TVModelVariant(161, tvm.densenet161, tvm.DenseNet161_Weights), + TVModelVariant(169, tvm.densenet169, tvm.DenseNet169_Weights), + TVModelVariant(201, tvm.densenet201, tvm.DenseNet201_Weights), + ] +) +@register_encoder("densenet", IMAGE) +class TVDenseNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "densenet" + _softmax_removal: str = "classifier" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVDenseNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("b0", tvm.efficientnet_b0, tvm.EfficientNet_B0_Weights), + TVModelVariant("b1", tvm.efficientnet_b1, tvm.EfficientNet_B1_Weights), + TVModelVariant("b2", tvm.efficientnet_b2, tvm.EfficientNet_B2_Weights), + TVModelVariant("b3", tvm.efficientnet_b3, tvm.EfficientNet_B3_Weights), + TVModelVariant("b4", tvm.efficientnet_b4, tvm.EfficientNet_B4_Weights), + TVModelVariant("b5", tvm.efficientnet_b5, tvm.EfficientNet_B5_Weights), + TVModelVariant("b6", tvm.efficientnet_b6, tvm.EfficientNet_B6_Weights), + TVModelVariant("b7", tvm.efficientnet_b7, tvm.EfficientNet_B7_Weights), + TVModelVariant("v2_s", tvm.efficientnet_v2_s, tvm.EfficientNet_V2_S_Weights), + TVModelVariant("v2_m", tvm.efficientnet_v2_m, tvm.EfficientNet_V2_M_Weights), + TVModelVariant("v2_l", tvm.efficientnet_v2_l, tvm.EfficientNet_V2_L_Weights), + ] +) +@register_encoder("efficientnet", IMAGE) +class TVEfficientNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "efficientnet" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVEfficientNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants([TVModelVariant("base", tvm.googlenet, tvm.GoogLeNet_Weights)]) +@register_encoder("googlenet", IMAGE) +class TVGoogLeNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "googlenet" + _softmax_removal: str = "fc" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # Eliminate auxiliary network to resolve issues when loading a saved model + # that does not contain the auxiliary network + if self.model.aux_logits: + self.model.aux_logits = False + self.model.aux1 = None + self.model.aux2 = None + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVGoogLeNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants([TVModelVariant("base", tvm.inception_v3, tvm.Inception_V3_Weights)]) +@register_encoder("inceptionv3", IMAGE) +class TVInceptionV3Encoder(TVBaseEncoder): + torchvision_model_type: str = "inceptionv3" + _softmax_removal: str = "fc" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # Eliminate auxiliary network to resolve issues when loading a saved model + # that does not contain the auxiliary network + if self.model.aux_logits: + self.model.aux_logits = False + self.model.AuxLogits = None + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVInceptionV3EncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants([TVModelVariant("t", tvm.maxvit_t, tvm.MaxVit_T_Weights)]) +@register_encoder("maxvit", IMAGE) +class TVMaxVitEncoder(TVBaseEncoder): + torchvision_model_type: str = "maxvit" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVMaxVitEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("0_5", tvm.mnasnet0_5, tvm.mnasnet.MNASNet0_5_Weights), + TVModelVariant("0_75", tvm.mnasnet0_75, tvm.mnasnet.MNASNet0_75_Weights), + TVModelVariant("1_0", tvm.mnasnet1_0, tvm.mnasnet.MNASNet1_0_Weights), + TVModelVariant("1_3", tvm.mnasnet1_3, tvm.mnasnet.MNASNet1_3_Weights), + ] +) +@register_encoder("mnasnet", IMAGE) +class TVMNASNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "mnasnet" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVMNASNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants([TVModelVariant("base", tvm.mobilenet_v2, tvm.MobileNet_V2_Weights)]) +@register_encoder("mobilenetv2", IMAGE) +class TVMobileNetV2Encoder(TVBaseEncoder): + torchvision_model_type: str = "mobilenetv2" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVMobileNetV2EncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("small", tvm.mobilenet_v3_small, tvm.MobileNet_V3_Small_Weights), + TVModelVariant("large", tvm.mobilenet_v3_large, tvm.MobileNet_V3_Large_Weights), + ] +) +@register_encoder("mobilenetv3", IMAGE) +class TVMobileNetV3Encoder(TVBaseEncoder): + torchvision_model_type: str = "mobilenetv3" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVMobileNetV3EncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("x_16gf", tvm.regnet_x_16gf, tvm.RegNet_X_16GF_Weights), + TVModelVariant("x_1_6gf", tvm.regnet_x_1_6gf, tvm.RegNet_X_1_6GF_Weights), + TVModelVariant("x_32gf", tvm.regnet_x_32gf, tvm.RegNet_X_32GF_Weights), + TVModelVariant("x_3_2gf", tvm.regnet_x_3_2gf, tvm.RegNet_X_3_2GF_Weights), + TVModelVariant("x_400mf", tvm.regnet_x_400mf, tvm.RegNet_X_400MF_Weights), + TVModelVariant("x_800mf", tvm.regnet_x_800mf, tvm.RegNet_X_800MF_Weights), + TVModelVariant("x_8gf", tvm.regnet_x_8gf, tvm.RegNet_X_8GF_Weights), + TVModelVariant("y_128gf", tvm.regnet_y_128gf, tvm.RegNet_Y_128GF_Weights), + TVModelVariant("y_16gf", tvm.regnet_y_16gf, tvm.RegNet_Y_16GF_Weights), + TVModelVariant("y_1_6gf", tvm.regnet_y_1_6gf, tvm.RegNet_Y_1_6GF_Weights), + TVModelVariant("y_32gf", tvm.regnet_y_32gf, tvm.RegNet_Y_32GF_Weights), + TVModelVariant("y_3_2gf", tvm.regnet_y_3_2gf, tvm.RegNet_Y_3_2GF_Weights), + TVModelVariant("y_400mf", tvm.regnet_y_400mf, tvm.RegNet_Y_400MF_Weights), + TVModelVariant("y_800mf", tvm.regnet_y_800mf, tvm.RegNet_Y_800MF_Weights), + TVModelVariant("y_8gf", tvm.regnet_y_8gf, tvm.RegNet_Y_8GF_Weights), + ] +) +@register_encoder("regnet", IMAGE) +class TVRegNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "regnet" + _softmax_removal: str = "fc" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVRegNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant(18, tvm.resnet18, tvm.ResNet18_Weights), + TVModelVariant(34, tvm.resnet34, tvm.ResNet34_Weights), + TVModelVariant(50, tvm.resnet50, tvm.ResNet50_Weights), + TVModelVariant(101, tvm.resnet101, tvm.ResNet101_Weights), + TVModelVariant(152, tvm.resnet152, tvm.ResNet152_Weights), + ] +) +@register_encoder("resnet", IMAGE) +class TVResNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "resnet" + _softmax_removal: str = "fc" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVResNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("50_32x4d", tvm.resnext50_32x4d, tvm.ResNeXt50_32X4D_Weights), + TVModelVariant("101_328xd", tvm.resnext101_32x8d, tvm.ResNeXt101_32X8D_Weights), + TVModelVariant("101_64x4d", tvm.resnext101_64x4d, tvm.ResNeXt101_64X4D_Weights), + ] +) +@register_encoder("resnext", IMAGE) +class TVResNeXtEncoder(TVBaseEncoder): + torchvision_model_type: str = "resnext" + _softmax_removal: str = "fc" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVResNeXtEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("x0_5", tvm.shufflenet_v2_x0_5, tvm.ShuffleNet_V2_X0_5_Weights), + TVModelVariant("x1_0", tvm.shufflenet_v2_x1_0, tvm.ShuffleNet_V2_X1_0_Weights), + TVModelVariant("x1_5", tvm.shufflenet_v2_x1_5, tvm.ShuffleNet_V2_X1_5_Weights), + TVModelVariant("x2_0", tvm.shufflenet_v2_x2_0, tvm.ShuffleNet_V2_X2_0_Weights), + ] +) +@register_encoder("shufflenet_v2", IMAGE) +class TVShuffleNetV2Encoder(TVBaseEncoder): + torchvision_model_type: str = "shufflenet_v2" + _softmax_removal: str = "fc" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVShuffleNetV2EncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("1_0", tvm.squeezenet1_0, tvm.SqueezeNet1_0_Weights), + TVModelVariant("1_1", tvm.squeezenet1_1, tvm.SqueezeNet1_1_Weights), + ] +) +@register_encoder("squeezenet", IMAGE) +class TVSqueezeNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "squeezenet" + _softmax_removal: str = "none" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVSqueezeNetEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("t", tvm.swin_t, tvm.Swin_T_Weights), + TVModelVariant("s", tvm.swin_s, tvm.Swin_S_Weights), + TVModelVariant("b", tvm.swin_b, tvm.Swin_B_Weights), + ] +) +@register_encoder("swin_transformer", IMAGE) +class TVSwinTransformerEncoder(TVBaseEncoder): + torchvision_model_type: str = "swin_transformer" + _softmax_removal: str = "head" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVSwinTransformerEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant(11, tvm.vgg11, tvm.VGG11_Weights), + TVModelVariant("11_bn", tvm.vgg11_bn, tvm.VGG11_BN_Weights), + TVModelVariant(13, tvm.vgg13, tvm.VGG13_Weights), + TVModelVariant("13_bn", tvm.vgg13_bn, tvm.VGG13_BN_Weights), + TVModelVariant(16, tvm.vgg16, tvm.VGG16_Weights), + TVModelVariant("16_bn", tvm.vgg16_bn, tvm.VGG16_BN_Weights), + TVModelVariant(19, tvm.vgg19, tvm.VGG19_Weights), + TVModelVariant("19_bn", tvm.vgg19_bn, tvm.VGG19_BN_Weights), + ] +) +@register_encoder("vgg", IMAGE) +class TVVGGEncoder(TVBaseEncoder): + torchvision_model_type: str = "vgg" + _softmax_removal: str = "classifier_last" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVVGGEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("b_16", tvm.vit_b_16, tvm.ViT_B_16_Weights), + TVModelVariant("b_32", tvm.vit_b_32, tvm.ViT_B_32_Weights), + TVModelVariant("l_16", tvm.vit_l_16, tvm.ViT_L_16_Weights), + TVModelVariant("l_32", tvm.vit_l_32, tvm.ViT_L_32_Weights), + TVModelVariant("h_14", tvm.vit_h_14, tvm.ViT_H_14_Weights), + ] +) +@register_encoder("vit", IMAGE) +class TVViTEncoder(TVBaseEncoder): + torchvision_model_type: str = "vit" + _softmax_removal: str = "heads_last" + + def __init__(self, **kwargs): + # For unpretrained ViT, determine the expected image size at runtime + # and pass it as a kwarg. Pretrained weights set the correct image size automatically. + if not kwargs["use_pretrained"]: + weights_specification = torchvision_model_registry[self.torchvision_model_type][ + kwargs["model_variant"] + ].model_weights.DEFAULT + kwargs["image_size"] = weights_specification.transforms.keywords["crop_size"] + super().__init__(**kwargs) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVViTEncoderConfig + + +@DeveloperAPI +@register_torchvision_model_variants( + [ + TVModelVariant("50_2", tvm.wide_resnet50_2, tvm.Wide_ResNet50_2_Weights), + TVModelVariant("101_2", tvm.wide_resnet101_2, tvm.Wide_ResNet101_2_Weights), + ] +) +@register_encoder("wide_resnet", IMAGE) +class TVWideResNetEncoder(TVBaseEncoder): + torchvision_model_type: str = "wide_resnet" + _softmax_removal: str = "fc" + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TVWideResNetEncoderConfig diff --git a/ludwig/encoders/mamba_hybrid.py b/ludwig/encoders/mamba_hybrid.py new file mode 100644 index 0000000..25603dc --- /dev/null +++ b/ludwig/encoders/mamba_hybrid.py @@ -0,0 +1,344 @@ +"""Mamba-2 and Jamba-style hybrid sequence encoders (Phase 6.6.2). + +``Mamba2Encoder`` + Successor to Mamba-1. The key architectural differences: + * multi-head parametrisation with a per-head scalar decay (SSD simplification — + Dao & Gu, "State Space Duality", 2024); + * wider inner expansion with group-wise output gating; + * slightly cheaper per-step compute because the state update is scalar per head + instead of the full rank-1 Mamba-1 update. + Like ``MambaEncoder`` in this repo, this is a pure-PyTorch approximation that + keeps the architectural pattern without needing the CUDA ``mamba_ssm`` kernel. + +``JambaEncoder`` + Hybrid encoder that interleaves Mamba-style SSM blocks with attention blocks. + Following the Jamba architecture (Lieber et al., 2024), every + ``attention_every_k``-th layer is a TransformerEncoderLayer (attention + MLP) and + the remaining layers are Mamba-2 SSM blocks. Default is ``attention_every_k=4`` + to match the 1:3 attention:SSM ratio reported in the Jamba paper. +""" + +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, SEQUENCE, TEXT, TIMESERIES +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.schema.encoders.mamba_hybrid import JambaEncoderConfig, Mamba2EncoderConfig +from ludwig.utils.torch_utils import initializer_registry + + +class _Mamba2Block(nn.Module): + """Single Mamba-2 SSM block. + + Input: ``(batch, seq_len, d_model)``. + Output: ``(batch, seq_len, d_model)``. + + Multi-head SSD approximation: split ``d_model`` into ``num_heads`` channels, give + each head a learnable scalar decay, mix along the sequence with a depthwise 1D + convolution, then gate + project back. + """ + + def __init__( + self, + d_model: int, + num_heads: int = 8, + d_conv: int = 4, + expand_factor: int = 2, + dropout: float = 0.1, + ) -> None: + super().__init__() + if d_model % num_heads != 0: + raise ValueError(f"d_model={d_model} must be divisible by num_heads={num_heads}") + + d_inner = d_model * expand_factor + head_dim = d_inner // num_heads + + self.norm = nn.LayerNorm(d_model) + self.in_proj = nn.Linear(d_model, d_inner * 2, bias=False) + self.conv1d = nn.Conv1d(d_inner, d_inner, kernel_size=d_conv, padding=d_conv - 1, groups=d_inner, bias=True) + self.out_proj = nn.Linear(d_inner, d_model, bias=False) + self.dropout = nn.Dropout(dropout) + + # Per-head scalar decay, parametrised in log space for positivity. + self.log_alpha = nn.Parameter(torch.empty(num_heads).uniform_(math.log(0.1), math.log(0.99))) + self.num_heads = num_heads + self.head_dim = head_dim + self.d_inner = d_inner + + def forward(self, x: torch.Tensor) -> torch.Tensor: + residual = x + x = self.norm(x) + + xz = self.in_proj(x) # (batch, seq_len, 2 * d_inner) + x_path, gate = xz.chunk(2, dim=-1) # each (batch, seq_len, d_inner) + + # Local depthwise convolution (trim trailing padding to preserve causal length). + batch, seq_len, _ = x_path.shape + xt = x_path.transpose(1, 2) # (batch, d_inner, seq_len) + xt = self.conv1d(xt)[:, :, :seq_len] + x_path = xt.transpose(1, 2) # (batch, seq_len, d_inner) + + # Per-head scalar decay mixing (SSD-style): y_t = alpha_h * y_{t-1} + x_t. + x_path = x_path.view(batch, seq_len, self.num_heads, self.head_dim) + # (1, num_heads, 1) — hoisted outside the loop; shape unchanged per step. + alpha = torch.sigmoid(self.log_alpha).view(self.num_heads, 1) + outputs = torch.empty_like(x_path) + y = torch.zeros(batch, self.num_heads, self.head_dim, device=x.device, dtype=x.dtype) + for t in range(seq_len): + y = alpha * y + x_path[:, t] + outputs[:, t] = y + x_path = outputs.view(batch, seq_len, self.d_inner) + + # Gated SiLU output — shared across heads. + x_path = F.silu(x_path) * gate + + out = self.out_proj(x_path) + out = self.dropout(out) + return out + residual + + +class _Mamba2Stack(nn.Module): + def __init__( + self, + d_model: int, + n_layers: int, + num_heads: int, + d_conv: int, + expand_factor: int, + dropout: float, + ) -> None: + super().__init__() + self.layers = nn.ModuleList( + _Mamba2Block(d_model, num_heads=num_heads, d_conv=d_conv, expand_factor=expand_factor, dropout=dropout) + for _ in range(n_layers) + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + for layer in self.layers: + x = layer(x) + return x + + +@DeveloperAPI +@register_encoder("mamba2", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class Mamba2Encoder(Encoder): + """Mamba-2 SSM encoder (Dao & Gu, 2024). + + Multi-head selective SSM with per-head scalar decay. Linear-time in sequence length like Mamba-1 but with a more + expressive hidden state and slightly cheaper per-step compute thanks to the state-space duality (SSD) + simplification. + """ + + def __init__( + self, + max_sequence_length: int = 256, + should_embed: bool = True, + vocab=None, + embedding_size: int = 256, + d_model: int = 256, + n_layers: int = 4, + num_heads: int = 8, + d_conv: int = 4, + expand_factor: int = 2, + dropout: float = 0.1, + output_size: int = 256, + reduce_output: str = "mean", + encoder_config=None, + **kwargs, + ) -> None: + super().__init__() + self.config = encoder_config + self.should_embed = should_embed + self.reduce_output = reduce_output + self.max_sequence_length = max_sequence_length + + if should_embed: + vocab_size = len(vocab) if vocab is not None else 1 + self.embedding = nn.Embedding(vocab_size, embedding_size) + embed_dim = embedding_size + if embed_dim != d_model: + self.embed_proj = nn.Linear(embed_dim, d_model) + else: + self.embed_proj = nn.Identity() + else: + self.embedding = None + self.embed_proj = nn.Identity() + + self.stack = _Mamba2Stack(d_model, n_layers, num_heads, d_conv, expand_factor, dropout) + self.final_norm = nn.LayerNorm(d_model) + self.output_proj = nn.Linear(d_model, output_size) + + self._output_size = output_size + + @property + def input_dtype(self): + return torch.int32 if self.should_embed else torch.float32 + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output in (None, "none"): + return torch.Size([self.max_sequence_length, self._output_size]) + return torch.Size([self._output_size]) + + def forward(self, inputs: torch.Tensor, mask=None) -> dict[str, torch.Tensor]: + if self.should_embed: + x = self.embedding(inputs.long()) + else: + x = inputs + x = self.embed_proj(x) + x = self.stack(x) + x = self.final_norm(x) + if self.reduce_output in (None, "none"): + pass + elif self.reduce_output == "mean": + x = x.mean(dim=1) + elif self.reduce_output == "sum": + x = x.sum(dim=1) + elif self.reduce_output == "max": + x = x.max(dim=1).values + elif self.reduce_output == "last": + x = x[:, -1] + else: + raise ValueError( + f"Unknown reduce_output={self.reduce_output!r}. " + "Valid options: None, 'none', 'mean', 'sum', 'max', 'last'." + ) + x = self.output_proj(x) + return {"encoder_output": x} + + @staticmethod + def get_schema_cls(): + return Mamba2EncoderConfig + + +@DeveloperAPI +@register_encoder("jamba", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class JambaEncoder(Encoder): + """Jamba-style hybrid encoder (Lieber et al., 2024). + + Interleaves Mamba-2 SSM blocks with TransformerEncoderLayer attention blocks. + Every ``attention_every_k``-th layer is an attention block; the rest are SSM + blocks. With ``attention_every_k=4`` (default) and ``n_layers=8`` the pattern is + ``[S S S A S S S A]`` — 1:3 attention:SSM as reported in the Jamba paper. + + The attention block is a standard pre-norm Transformer encoder layer so the + hybrid drops straight in as a Ludwig sequence encoder; the SSM block is the same + ``_Mamba2Block`` used above. + """ + + def __init__( + self, + max_sequence_length: int = 256, + should_embed: bool = True, + vocab=None, + embedding_size: int = 256, + d_model: int = 256, + n_layers: int = 8, + attention_every_k: int = 4, + num_heads: int = 8, + ffn_size: int = 1024, + d_conv: int = 4, + expand_factor: int = 2, + dropout: float = 0.1, + output_size: int = 256, + reduce_output: str = "mean", + encoder_config=None, + **kwargs, + ) -> None: + super().__init__() + self.config = encoder_config + self.should_embed = should_embed + self.reduce_output = reduce_output + self.max_sequence_length = max_sequence_length + + if should_embed: + vocab_size = len(vocab) if vocab is not None else 1 + self.embedding = nn.Embedding(vocab_size, embedding_size) + if embedding_size != d_model: + self.embed_proj = nn.Linear(embedding_size, d_model) + else: + self.embed_proj = nn.Identity() + else: + self.embedding = None + self.embed_proj = nn.Identity() + + layers: list[nn.Module] = [] + for i in range(n_layers): + if (i + 1) % attention_every_k == 0: + layers.append( + nn.TransformerEncoderLayer( + d_model=d_model, + nhead=num_heads, + dim_feedforward=ffn_size, + dropout=dropout, + batch_first=True, + norm_first=True, + ) + ) + else: + layers.append( + _Mamba2Block( + d_model, num_heads=num_heads, d_conv=d_conv, expand_factor=expand_factor, dropout=dropout + ) + ) + self.layers = nn.ModuleList(layers) + self.final_norm = nn.LayerNorm(d_model) + self.output_proj = nn.Linear(d_model, output_size) + + self._output_size = output_size + + @property + def input_dtype(self): + return torch.int32 if self.should_embed else torch.float32 + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output in (None, "none"): + return torch.Size([self.max_sequence_length, self._output_size]) + return torch.Size([self._output_size]) + + def forward(self, inputs: torch.Tensor, mask=None) -> dict[str, torch.Tensor]: + if self.should_embed: + x = self.embedding(inputs.long()) + else: + x = inputs + x = self.embed_proj(x) + for layer in self.layers: + if isinstance(layer, nn.TransformerEncoderLayer): + x = layer(x, src_key_padding_mask=mask) + else: + x = layer(x) + x = self.final_norm(x) + if self.reduce_output in (None, "none"): + pass + elif self.reduce_output == "mean": + x = x.mean(dim=1) + elif self.reduce_output == "sum": + x = x.sum(dim=1) + elif self.reduce_output == "max": + x = x.max(dim=1).values + elif self.reduce_output == "last": + x = x[:, -1] + else: + raise ValueError( + f"Unknown reduce_output={self.reduce_output!r}. " + "Valid options: None, 'none', 'mean', 'sum', 'max', 'last'." + ) + x = self.output_proj(x) + return {"encoder_output": x} + + @staticmethod + def get_schema_cls(): + return JambaEncoderConfig + + +# Silence unused-import warning — initializer_registry is imported so subclasses can use +# Ludwig's standard weight init if ever extended. +_ = initializer_registry diff --git a/ludwig/encoders/number_encoders.py b/ludwig/encoders/number_encoders.py new file mode 100644 index 0000000..9ee9b6e --- /dev/null +++ b/ludwig/encoders/number_encoders.py @@ -0,0 +1,181 @@ +"""Number-specific encoders: PLE (Piecewise Linear Encoding) and Periodic. + +Based on: +- PLE: "On Embeddings for Numerical Features in Tabular Deep Learning" (Gorishniy et al., NeurIPS 2022) +- Periodic: Same paper, Section 3.2 +""" + +import logging +import math + +import torch +import torch.nn as nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, NUMBER +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("ple", [NUMBER]) +class PLEEncoder(Encoder): + """Piecewise Linear Encoding for numerical features. + + Computes quantile-based bin edges from training data, then for each input value produces a num_bins-dimensional + vector where each element is a piecewise-linear interpolation within that bin. A learned linear projection maps this + to the output embedding space. + + This encoding makes even simple MLPs competitive with attention-based tabular models. + """ + + def __init__(self, input_size=1, num_bins=64, output_size=256, encoder_config=None, **kwargs): + super().__init__() + self.config = encoder_config + self.num_bins = num_bins + self._output_size = output_size + + # Bin edges are set from training data metadata via update_config_with_metadata + self.register_buffer("bin_edges", torch.linspace(0, 1, num_bins + 1)) + self.projection = nn.Linear(num_bins, output_size) + + def set_bin_edges(self, bin_edges: list[float]): + """Set bin edges from training data statistics.""" + edges = torch.tensor(bin_edges, dtype=torch.float32) + # Ensure edges are strictly increasing by adding small epsilon to duplicates + for i in range(1, len(edges)): + if edges[i] <= edges[i - 1]: + edges[i] = edges[i - 1] + 1e-8 + self.bin_edges.copy_(edges) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + # inputs: [batch, 1] + x = inputs.squeeze(-1) if inputs.dim() > 1 else inputs # [batch] + + edges = self.bin_edges # [num_bins + 1] + # Compute piecewise linear encoding: for each bin, a value in [0, 1] + # representing how far through the bin the input value is + left_edges = edges[:-1] # [num_bins] + right_edges = edges[1:] # [num_bins] + widths = right_edges - left_edges # [num_bins] + + # [batch, num_bins]: linear interpolation within each bin, clamped to [0, 1] + ple = torch.clamp((x.unsqueeze(-1) - left_edges) / (widths + 1e-8), 0.0, 1.0) + + # Learned projection to output size + output = self.projection(ple) # [batch, output_size] + return {ENCODER_OUTPUT: output} + + @staticmethod + def get_schema_cls(): + from ludwig.schema.encoders.number_encoders import PLEEncoderConfig + + return PLEEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_size]) + + +@DeveloperAPI +@register_encoder("periodic", [NUMBER]) +class PeriodicEncoder(Encoder): + """Periodic encoding for numerical features. + + Uses learned sinusoidal features: sin(2*pi*f*x + phi) where f and phi are learnable per-frequency parameters. + A linear projection maps the periodic features to the output embedding space. + """ + + def __init__(self, input_size=1, num_frequencies=64, output_size=256, sigma=1.0, encoder_config=None, **kwargs): + super().__init__() + self.config = encoder_config + self.num_frequencies = num_frequencies + self._output_size = output_size + + # Learnable frequencies and phases + self.frequencies = nn.Parameter(torch.randn(num_frequencies) * sigma) + self.phases = nn.Parameter(torch.zeros(num_frequencies)) + self.projection = nn.Linear(num_frequencies, output_size) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + # inputs: [batch, 1] or [batch] + x = inputs.squeeze(-1) if inputs.dim() > 1 else inputs # [batch] + + # Compute periodic features: sin(2*pi*f*x + phi) + periodic = torch.sin(2 * math.pi * x.unsqueeze(-1) * self.frequencies + self.phases) # [batch, num_freq] + + output = self.projection(periodic) # [batch, output_size] + return {ENCODER_OUTPUT: output} + + @staticmethod + def get_schema_cls(): + from ludwig.schema.encoders.number_encoders import PeriodicEncoderConfig + + return PeriodicEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_size]) + + +@DeveloperAPI +@register_encoder("bins", [NUMBER]) +class BinsEncoder(Encoder): + """Binning encoder: discretize numbers into equal-width or equal-frequency bins. + + Simpler alternative to PLE. Good for small/medium datasets where PLE may overfit. + Each bin gets a learned embedding. + """ + + def __init__(self, input_size: int = 1, num_bins: int = 32, output_size: int = 256, encoder_config=None, **kwargs): + super().__init__() + self.config = encoder_config + self.num_bins = num_bins + self._input_size = input_size + self._output_size = output_size + # Bin edges will be set from training data metadata, default: uniform [0,1] + self.register_buffer("bin_edges", torch.linspace(0, 1, num_bins + 1)) + self.bin_embeddings = nn.Embedding(num_bins, output_size) + + def set_bin_edges(self, bin_edges: list[float]): + """Set bin edges from training data statistics.""" + edges = torch.tensor(bin_edges, dtype=torch.float32) + # Ensure edges are strictly increasing by adding small epsilon to duplicates + for i in range(1, len(edges)): + if edges[i] <= edges[i - 1]: + edges[i] = edges[i - 1] + 1e-8 + self.bin_edges.copy_(edges) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + # inputs: [batch, 1] or [batch] + x = inputs.float() + if x.dim() == 1: + x = x.unsqueeze(-1) + # Digitize: find bin index for each input value using searchsorted, clamp to valid range + bin_idx = torch.searchsorted(self.bin_edges[1:-1], x.squeeze(-1)).clamp(0, self.num_bins - 1) + return {ENCODER_OUTPUT: self.bin_embeddings(bin_idx)} + + @staticmethod + def get_schema_cls(): + from ludwig.schema.encoders.number_encoders import BinsEncoderConfig + + return BinsEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_size]) diff --git a/ludwig/encoders/registry.py b/ludwig/encoders/registry.py new file mode 100644 index 0000000..d2eadd5 --- /dev/null +++ b/ludwig/encoders/registry.py @@ -0,0 +1,49 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.encoders.base import Encoder +from ludwig.utils.registry import Registry + +_encoder_registry = Registry() +_sequence_encoder_registry = Registry() + + +@DeveloperAPI +def get_encoder_registry() -> Registry: + return _encoder_registry + + +@DeveloperAPI +def get_sequence_encoder_registry() -> Registry: + return _sequence_encoder_registry + + +def register_sequence_encoder(name: str): + def wrap(cls): + get_sequence_encoder_registry()[name] = cls + return cls + + return wrap + + +def register_encoder(name: str, features: str | list[str]): + if isinstance(features, str): + features = [features] + + def update_registry(registry_getter_fn, cls, feature): + feature_registry = registry_getter_fn().get(feature, {}) + feature_registry[name] = cls + registry_getter_fn()[feature] = feature_registry + + def wrap(cls): + for feature in features: + update_registry(get_encoder_registry, cls, feature) + return cls + + return wrap + + +def get_encoder_cls(feature: str, name: str) -> type[Encoder]: + return get_encoder_registry()[feature][name] + + +def get_encoder_classes(feature: str) -> dict[str, type[Encoder]]: + return get_encoder_registry()[feature] diff --git a/ludwig/encoders/sequence_encoders.py b/ludwig/encoders/sequence_encoders.py new file mode 100644 index 0000000..11ffd9d --- /dev/null +++ b/ludwig/encoders/sequence_encoders.py @@ -0,0 +1,1943 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch +from torch import nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, SEQUENCE, TEXT, TIMESERIES +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder, register_sequence_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.attention_modules import TransformerStack +from ludwig.modules.convolutional_modules import Conv1DStack, ParallelConv1D, ParallelConv1DStack +from ludwig.modules.embedding_modules import EmbedSequence, TokenAndPositionEmbedding +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.modules.recurrent_modules import RecurrentStack +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.schema.encoders.sequence_encoders import ( + MambaEncoderConfig, + ParallelCNNConfig, + SequenceEmbedConfig, + SequenceEncoderConfig, + SequencePassthroughConfig, + StackedCNNConfig, + StackedCNNRNNConfig, + StackedParallelCNNConfig, + StackedRNNConfig, + StackedTransformerConfig, +) + +logger = logging.getLogger(__name__) + + +class SequenceEncoder(Encoder): + """Base class for all sequence encoders in Ludwig. + + Sequence encoders take a sequence of tokens (integers) or a sequence of vectors (floats) and produce a fixed-size or + variable-length hidden representation. + """ + + pass + + +@DeveloperAPI +@register_encoder("passthrough", [SEQUENCE, TEXT, TIMESERIES]) +class SequencePassthroughEncoder(SequenceEncoder): + """Identity encoder that passes through the input sequence without transformation. + + The input sequence is cast to float32 and optionally reduced along the sequence + dimension. This is useful when the input features are already pre-computed embeddings + or continuous-valued sequences (e.g., timeseries) that do not need learned encoding. + + Use when: input features are already meaningful representations (pre-computed embeddings, + raw timeseries values) and no learned transformation is needed. + + Alternatives: SequenceEmbedEncoder (if inputs are token IDs needing embeddings), + any other sequence encoder (if learned feature extraction is desired). + + Performance: O(1) computation (no learnable parameters beyond optional reduction). + """ + + def __init__( + self, + reduce_output: str | None = None, + max_sequence_length: int = 256, + encoding_size: int | None = None, + encoder_config=None, + **kwargs, + ): + """Initializes SequencePassthroughEncoder. + + Args: + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension if the rank of the tensor is greater than 2. Available values are: `sum`, + `mean` or `avg`, `max`, `concat` (concatenates along the first dimension), `last` + (returns the last vector of the first dimension) and `None` or `null` (which does + not reduce and returns the full tensor). + max_sequence_length: The maximum sequence length. + encoding_size: The size of the encoding vector, or None if sequence elements are scalars. + """ + super().__init__() + self.config = encoder_config + self.max_sequence_length = max_sequence_length + + logger.debug(f" {self.name}") + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, max_sequence_length=max_sequence_length, encoding_size=encoding_size + ) + if self.reduce_output is None: + self.supports_masking = True + + def forward(self, input_sequence: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence by casting to float and optionally reducing. + + Args: + input_sequence: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32 or + [batch x sequence length x encoding size], type torch.float32. + mask: Sequence mask (not yet implemented). Shape: [batch x sequence length]. + """ + input_sequence = input_sequence.type(torch.float32) + while len(input_sequence.shape) < 3: + input_sequence = input_sequence.unsqueeze(-1) + hidden = self.reduce_sequence(input_sequence) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return SequencePassthroughConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return self.input_shape + + +@DeveloperAPI +@register_encoder("embed", [SEQUENCE, TEXT]) +class SequenceEmbedEncoder(SequenceEncoder): + """Embedding-only encoder that maps token sequences to embeddings and reduces them. + + Maps each token in the input sequence to a dense embedding vector, then reduces the + sequence of embeddings to a single vector using the specified reduce_output strategy + (default: sum). No convolutional, recurrent, or attention layers are applied. + + Use when: a simple bag-of-words style representation is sufficient. Works well for + short sequences or when the order of tokens is not important. Very fast and memory-efficient. + + Alternatives: ParallelCNN or StackedCNN (to capture local n-gram patterns), + StackedRNN (to capture sequential dependencies), StackedTransformer (to capture + long-range dependencies with attention). + + Performance: O(n) time and memory where n is sequence length. No sequential dependencies + computed, making it the fastest learned sequence encoder. + """ + + def __init__( + self, + vocab, + max_sequence_length, + representation="dense", + embedding_size=256, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + weights_initializer=None, + dropout=0, + reduce_output="sum", + encoder_config=None, + **kwargs, + ): + """Initializes SequenceEmbedEncoder. + + Args: + vocab: Vocabulary of the input feature to encode. + max_sequence_length: The maximum sequence length. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + Options include: `constant`, `identity`, `zeros`, `ones`, `orthogonal`, `normal`, + `uniform`, `truncated_normal`, `variance_scaling`, `xavier_normal`, + `xavier_uniform`, `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`. + Can also be a dict with a `type` key and initializer parameters. + dropout: The dropout probability. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension if the rank of the tensor is greater than 2. Available values are: + `sum`, `mean` or `avg`, `max`, `concat` (concatenates along the first dimension), + `last` (returns the last vector of the first dimension) and `None` or `null` + (which does not reduce and returns the full tensor). + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + self.embedding_size = embedding_size + self.max_sequence_length = max_sequence_length + + self.reduce_output = reduce_output + if self.reduce_output is None: + self.supports_masking = True + + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=max_sequence_length, + encoding_size=self.embed_sequence.output_shape[-1], + ) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence by embedding tokens and reducing the sequence. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented in EmbedSequence). + """ + embedded_sequence = self.embed_sequence(inputs, mask=mask) + hidden = self.reduce_sequence(embedded_sequence) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return SequenceEmbedConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return self.reduce_sequence.output_shape + + +@DeveloperAPI +@register_sequence_encoder("parallel_cnn") +@register_encoder("parallel_cnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class ParallelCNN(SequenceEncoder): + """Parallel 1D CNN encoder with multiple filter sizes applied simultaneously. + + Embeds the input sequence, then passes it through multiple parallel 1D convolutional + layers with different filter sizes (default: 2, 3, 4, 5). The outputs are pooled and + concatenated, then passed through fully connected layers. This architecture captures + n-gram patterns of different lengths simultaneously, similar to the approach in + "Convolutional Neural Networks for Sentence Classification" (Kim, 2014). + + Reference: Kim, Y. (2014). Convolutional Neural Networks for Sentence Classification. + https://arxiv.org/abs/1408.5882 + + Use when: capturing local n-gram patterns of varying lengths is important (e.g., text + classification, sentiment analysis). Good default choice for sequence/text features. + + Alternatives: StackedCNN (deeper sequential convolutions for hierarchical features), + StackedRNN (when sequential order matters more than local patterns), + StackedTransformer (when long-range dependencies are important). + + Performance: O(n * k * f) where n is sequence length, k is max filter size, f is + num_filters. Parallelizable across filters. Memory-efficient compared to RNNs/Transformers. + """ + + def __init__( + self, + should_embed=True, + vocab=None, + representation="dense", + embedding_size=256, + max_sequence_length=None, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + conv_layers=None, + num_conv_layers=None, + filter_size=3, + num_filters=256, + pool_function="max", + pool_size=None, + fc_layers=None, + num_fc_layers=None, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + activation="relu", + dropout=0, + reduce_output="max", + encoder_config=None, + **kwargs, + ): + """Initializes ParallelCNN. + + Args: + should_embed: If True, the input sequence is expected to be made of integers and will + be mapped into embeddings. + vocab: Vocabulary of the input feature to encode. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + conv_layers: A list of dicts containing the parameters of the parallel convolutional + layers. Keys per dict: `filter_size`, `num_filters`, `pool`, `norm`, `activation`. + Missing values fall back to encoder-level defaults. If both `conv_layers` and + `num_conv_layers` are `None`, defaults to + `[{filter_size: 2}, {filter_size: 3}, {filter_size: 4}, {filter_size: 5}]`. + num_conv_layers: If `conv_layers` is `None`, the number of parallel convolutional layers. + filter_size: Default filter size for convolutional layers (width of 1D filter). + num_filters: Default number of filters (output channels) for convolutional layers. + pool_size: Default pool size for max pooling along the sequence dimension after convolution. + fc_layers: A list of dicts containing the parameters of the fully connected layers. + Keys per dict: `output_size`, `norm`, `activation`. Missing values fall back to + encoder-level defaults. Defaults to `[{output_size: 512}, {output_size: 256}]` + when `reduce_output` is not `None`. + num_fc_layers: If `fc_layers` is `None`, the number of stacked fully connected layers. + output_size: Default output size for fully connected layers. + norm: Default norm to use for convolutional and fully connected layers. + activation: Default activation function to use. + dropout: Dropout probability. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + bias_initializer: The bias initializer to use. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension. Available values: `sum`, `mean` or `avg`, `max`, `concat`, `last`, + `None` or `null`. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.max_sequence_length = max_sequence_length + + if conv_layers is not None and num_conv_layers is None: + # use custom-defined layers + self.conv_layers = conv_layers + self.num_conv_layers = len(conv_layers) + elif conv_layers is None and num_conv_layers is not None: + # generate num_conv_layers with default parameters + self.conv_layers = None + self.num_conv_layers = num_conv_layers + elif conv_layers is None and num_conv_layers is None: + # use default layers with varying filter sizes + self.conv_layers = [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}] + self.num_conv_layers = 4 + else: + raise ValueError("Invalid layer parametrization, use either conv_layers or num_conv_layers") + + # The user is expected to provide fc_layers or num_fc_layers + # The following logic handles the case where the user either provides + # both or neither. + if fc_layers is None and num_fc_layers is None: + # use default layers with varying filter sizes + fc_layers = [{"output_size": 512}, {"output_size": 256}] + num_fc_layers = 2 + elif fc_layers is not None and num_fc_layers is not None: + raise ValueError("Invalid layer parametrization, use either fc_layers or num_fc_layers only. Not both.") + + self.should_embed = should_embed + self.embed_sequence = None + + if self.should_embed: + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" ParallelConv1D") + in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size + self.parallel_conv1d = ParallelConv1D( + in_channels=in_channels, + max_sequence_length=self.max_sequence_length, + layers=self.conv_layers, + default_num_filters=num_filters, + default_filter_size=filter_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + default_pool_function=pool_function, + default_pool_size=pool_size, + default_pool_padding="same", + ) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=max_sequence_length, + encoding_size=self.parallel_conv1d.output_shape[-1], + ) + if self.reduce_output is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through parallel convolutions and optional FC layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + embedded_sequence = embedded_sequence.to(dtype=torch.float) + + # shape=(?, sequence_length, embedding_size) + hidden = embedded_sequence + + # ================ Conv Layers ================ + hidden = self.parallel_conv1d(hidden, mask=mask) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return ParallelCNNConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is not None: + return self.fc_stack.output_shape + return self.parallel_conv1d.output_shape + + +@DeveloperAPI +@register_sequence_encoder("stacked_cnn") +@register_encoder("stacked_cnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedCNN(SequenceEncoder): + """Stacked 1D CNN encoder with sequential convolutional layers. + + Embeds the input sequence, then passes it through a stack of 1D convolutional layers + applied sequentially (each layer's output feeds into the next). Optionally includes + pooling between layers. The deeper architecture allows learning hierarchical features: + lower layers capture local patterns while higher layers capture broader patterns. + Default architecture uses 6 convolutional layers inspired by the VDCNN model. + + Reference: Conneau, A., et al. (2017). Very Deep Convolutional Networks for Text + Classification. https://arxiv.org/abs/1606.01781 + + Use when: hierarchical feature extraction is needed, where lower-level n-gram patterns + should be composed into higher-level representations. Good for longer sequences where + progressive pooling can reduce computation. + + Alternatives: ParallelCNN (simpler, captures multiple filter sizes simultaneously), + StackedParallelCNN (combines both parallel and stacked convolutions), + StackedRNN (when order/sequential dependencies are crucial). + + Performance: O(n * L * k * f) where n is sequence length, L is num layers, k is filter + size, f is num_filters. Fully parallelizable. Pooling reduces sequence length progressively. + """ + + def __init__( + self, + should_embed=True, + vocab=None, + representation="dense", + embedding_size=256, + max_sequence_length=None, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + conv_layers=None, + num_conv_layers=None, + num_filters=256, + filter_size=5, + strides=1, + # todo: assess how to specify padding for equivalent to 'same' + padding="same", + dilation_rate=1, + pool_function="max", + pool_size=None, + pool_strides=None, + # todo: determine how to pool_padding equivalent of 'same' + pool_padding="same", + fc_layers=None, + num_fc_layers=None, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + activation="relu", + dropout=0, + reduce_output="max", + encoder_config=None, + **kwargs, + ): + """Initializes StackedCNN. + + Args: + should_embed: If True, the input sequence is expected to be made of integers and will + be mapped into embeddings. + vocab: Vocabulary of the input feature to encode. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + conv_layers: A list of dicts containing the parameters of the stacked convolutional + layers. Keys per dict: `filter_size`, `num_filters`, `pool`, `norm`, `activation`. + Missing values fall back to encoder-level defaults. If both `conv_layers` and + `num_conv_layers` are `None`, a default 6-layer architecture is used. + num_conv_layers: If `conv_layers` is `None`, the number of stacked convolutional layers. + filter_size: Default filter size for convolutional layers (width of 1D filter). + num_filters: Default number of filters (output channels) for convolutional layers. + pool_size: Default pool size for max pooling along the sequence dimension after convolution. + fc_layers: A list of dicts containing the parameters of the fully connected layers. + Keys per dict: `output_size`, `norm`, `activation`. Missing values fall back to + encoder-level defaults. Defaults to `[{output_size: 512}, {output_size: 256}]` + when `reduce_output` is not `None`. + num_fc_layers: If `fc_layers` is `None`, the number of stacked fully connected layers. + output_size: Default output size for fully connected layers. + norm: Default norm to use for convolutional and fully connected layers. + activation: Default activation function to use. + dropout: Dropout probability. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + bias_initializer: The bias initializer to use. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension. Available values: `sum`, `mean` or `avg`, `max`, `concat`, `last`, + `None` or `null`. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + if conv_layers is not None and num_conv_layers is None: + # use custom-defined layers + self.conv_layers = conv_layers + self.num_conv_layers = len(conv_layers) + elif conv_layers is None and num_conv_layers is not None: + # generate num_conv_layers with default parameters + self.conv_layers = None + self.num_conv_layers = num_conv_layers + elif conv_layers is None and num_conv_layers is None: + # use default layers with varying filter sizes + self.conv_layers = [ + { + "filter_size": 7, + "pool_size": 3, + }, + { + "filter_size": 7, + "pool_size": 3, + }, + { + "filter_size": 3, + "pool_size": None, + }, + { + "filter_size": 3, + "pool_size": None, + }, + { + "filter_size": 3, + "pool_size": None, + }, + { + "filter_size": 3, + "pool_size": 3, + }, + ] + self.num_conv_layers = 6 + else: + raise ValueError("Invalid layer parametrization, use either conv_layers or num_conv_layers") + + # The user is expected to provide fc_layers or num_fc_layers + # The following logic handles the case where the user either provides + # both or neither. + if fc_layers is None and num_fc_layers is None: + # use default layers with varying filter sizes + fc_layers = [{"output_size": 512}, {"output_size": 256}] + num_fc_layers = 2 + elif fc_layers is not None and num_fc_layers is not None: + raise ValueError("Invalid layer parametrization, use either fc_layers or num_fc_layers only. Not both.") + + self.max_sequence_length = max_sequence_length + self.num_filters = num_filters + self.should_embed = should_embed + self.embed_sequence = None + + if self.should_embed: + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=self.max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" Conv1DStack") + in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size + self.conv1d_stack = Conv1DStack( + in_channels=in_channels, + max_sequence_length=max_sequence_length, + layers=self.conv_layers, + default_num_filters=num_filters, + default_filter_size=filter_size, + default_strides=strides, + default_padding=padding, + default_dilation_rate=dilation_rate, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + default_pool_function=pool_function, + default_pool_size=pool_size, + default_pool_strides=pool_strides, + default_pool_padding=pool_padding, + ) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=self.conv1d_stack.output_shape[-2], + encoding_size=self.conv1d_stack.output_shape[-1], + ) + if self.reduce_output is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return StackedCNNConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return self.conv1d_stack.output_shape + return self.fc_stack.output_shape + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through stacked convolutions and optional FC layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + + # shape=(?, sequence_length, embedding_size) + hidden = embedded_sequence + + # ================ Conv Layers ================ + hidden = self.conv1d_stack(hidden, mask=mask) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + # no reduction: hidden [batch_size, seq_size, num_filters] + # with reduction: hidden [batch_size, output_size] + return {ENCODER_OUTPUT: hidden} + + +@DeveloperAPI +@register_sequence_encoder("stacked_parallel_cnn") +@register_encoder("stacked_parallel_cnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedParallelCNN(SequenceEncoder): + """Stacked Parallel CNN encoder combining both parallel and stacked convolutions. + + Embeds the input sequence, then passes it through a stack of parallel convolutional + layers. At each level in the stack, multiple parallel 1D convolutions with different + filter sizes are applied and their outputs concatenated (like ParallelCNN). These + stacked parallel blocks are applied sequentially (like StackedCNN), combining the + benefits of both approaches: multi-scale pattern detection at each level with + hierarchical composition across levels. + + Use when: both multi-scale n-gram detection AND hierarchical feature composition are + needed. This is the most expressive CNN-based sequence encoder but also the most + computationally expensive. + + Alternatives: ParallelCNN (simpler, single level of parallel convolutions), + StackedCNN (simpler, single filter size per level but deeper), + StackedTransformer (when long-range dependencies matter more than local patterns). + + Performance: O(n * S * P * k * f) where n is sequence length, S is num stacked layers, + P is num parallel filters per layer, k is filter size, f is num_filters. Higher memory + usage than ParallelCNN or StackedCNN alone. + """ + + def __init__( + self, + should_embed=True, + vocab=None, + representation="dense", + embedding_size=256, + max_sequence_length=None, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + stacked_layers=None, + num_stacked_layers=None, + filter_size=3, + num_filters=256, + pool_function="max", + pool_size=None, + fc_layers=None, + num_fc_layers=None, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + activation="relu", + dropout=0, + reduce_output="max", + encoder_config=None, + **kwargs, + ): + """Initializes StackedParallelCNN. + + Args: + should_embed: If True, the input sequence is expected to be made of integers and will + be mapped into embeddings. + vocab: Vocabulary of the input feature to encode. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + stacked_layers: A list of lists of dicts specifying the stacked parallel convolutional + layers. The outer list length is the number of stacked levels; each inner list + specifies the parallel conv layers at that level. Keys per dict: `filter_size`, + `num_filters`, `pool_size`, `norm`, `activation`. If both `stacked_layers` and + `num_stacked_layers` are `None`, defaults to 3 stacked levels each with filter + sizes [2, 3, 4, 5]. + num_stacked_layers: If `stacked_layers` is `None`, the number of stacked parallel + convolutional levels. + filter_size: Default filter size for convolutional layers (width of 1D filter). + num_filters: Default number of filters (output channels) for convolutional layers. + pool_size: Default pool size for max pooling along the sequence dimension after convolution. + fc_layers: A list of dicts containing the parameters of the fully connected layers. + Keys per dict: `output_size`, `norm`, `activation`. Defaults to + `[{output_size: 512}, {output_size: 256}]` when `reduce_output` is not `None`. + num_fc_layers: If `fc_layers` is `None`, the number of stacked fully connected layers. + output_size: Default output size for fully connected layers. + norm: Default norm to use for convolutional and fully connected layers. + activation: Default activation function to use. + dropout: Dropout probability. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + bias_initializer: The bias initializer to use. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension. Available values: `sum`, `mean` or `avg`, `max`, `concat`, `last`, + `None` or `null`. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.max_sequence_length = max_sequence_length + self.embedding_size = embedding_size + + if stacked_layers is not None and num_stacked_layers is None: + # use custom-defined layers + self.stacked_layers = stacked_layers + self.num_stacked_layers = len(stacked_layers) + elif stacked_layers is None and num_stacked_layers is not None: + # generate num_conv_layers with default parameters + self.stacked_layers = None + self.num_stacked_layers = num_stacked_layers + elif stacked_layers is None and num_stacked_layers is None: + # use default layers with varying filter sizes + self.stacked_layers = [ + [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}], + [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}], + [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}], + ] + self.num_stacked_layers = 6 + else: + raise ValueError("Invalid layer parametrization, use either stacked_layers or num_stacked_layers") + + # The user is expected to provide fc_layers or num_fc_layers + # The following logic handles the case where the user either provides + # both or neither. + if fc_layers is None and num_fc_layers is None: + # use default layers with varying filter sizes + fc_layers = [{"output_size": 512}, {"output_size": 256}] + num_fc_layers = 2 + elif fc_layers is not None and num_fc_layers is not None: + raise ValueError("Invalid layer parametrization, use either fc_layers or num_fc_layers only. Not both.") + + self.should_embed = should_embed + self.embed_sequence = None + + if self.should_embed: + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=self.max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size + logger.debug(" ParallelConv1DStack") + self.parallel_conv1d_stack = ParallelConv1DStack( + in_channels=in_channels, + stacked_layers=self.stacked_layers, + max_sequence_length=max_sequence_length, + default_num_filters=num_filters, + default_filter_size=filter_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + default_pool_function=pool_function, + default_pool_size=pool_size, + ) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=self.parallel_conv1d_stack.output_shape[-2], + encoding_size=self.parallel_conv1d_stack.output_shape[-1], + ) + if self.reduce_output is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return StackedParallelCNNConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is not None: + return self.fc_stack.output_shape + return self.parallel_conv1d_stack.output_shape + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through stacked parallel convolutions and optional FC layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + + # shape=(?, sequence_length, embedding_size) + hidden = embedded_sequence + + # ================ Conv Layers ================ + hidden = self.parallel_conv1d_stack(hidden, mask=mask) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + # no reduction: hidden [batch_size, seq_size, num_filter] + # with reduction: hidden [batch_size, output_size] + return {ENCODER_OUTPUT: hidden} + + +@DeveloperAPI +@register_sequence_encoder("rnn") +@register_encoder("rnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedRNN(SequenceEncoder): + """Stacked recurrent neural network encoder (RNN/LSTM/GRU). + + Embeds the input sequence, then passes it through one or more stacked recurrent layers. + Supports RNN, LSTM, and GRU cell types, with optional bidirectionality. The recurrent + layers process the sequence step by step, maintaining a hidden state that captures + sequential dependencies. Returns both the sequence of hidden states and the final state. + + Use when: sequential order and long-range dependencies are important. LSTM and GRU cells + handle vanishing gradients better than vanilla RNN. Bidirectional mode captures both + forward and backward context. Good for tasks where the meaning depends on token order + (e.g., named entity recognition, sequence labeling). + + Alternatives: ParallelCNN/StackedCNN (faster, but only capture local patterns), + StackedTransformer (captures long-range dependencies without sequential bottleneck), + StackedCNNRNN (CNN for local features + RNN for sequential modeling). + + Performance: O(n * L * h) where n is sequence length, L is num layers, h is state size. + Sequential computation (cannot parallelize across time steps). LSTM/GRU use ~4x/3x the + parameters of vanilla RNN respectively. Bidirectional doubles parameters and compute. + """ + + def __init__( + self, + should_embed=True, + vocab=None, + representation="dense", + embedding_size=256, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + num_layers=1, + max_sequence_length=None, + state_size=256, + cell_type="rnn", + bidirectional=False, + activation="tanh", + recurrent_activation="sigmoid", + unit_forget_bias=True, + recurrent_initializer="orthogonal", + dropout=0.0, + recurrent_dropout=0.0, + fc_layers=None, + num_fc_layers=0, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + fc_activation="relu", + fc_dropout=0, + reduce_output="last", + encoder_config=None, + **kwargs, + ): + """Initializes StackedRNN. + + Args: + should_embed: If True, the input sequence is expected to be made of integers and will + be mapped into embeddings. + vocab: Vocabulary of the input feature to encode. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + num_layers: The number of stacked recurrent layers. + cell_type: The type of recurrent cell to use. Available values: `rnn`, `lstm`, `gru`. + state_size: The size of the recurrent state. + bidirectional: If `True`, two recurrent networks encode in forward and backward + directions and their outputs are concatenated. + dropout: Dropout probability. + recurrent_dropout: Dropout rate for the recurrent stack. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension. Available values: `sum`, `mean` or `avg`, `max`, `concat`, `last`, + `None` or `null`. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.max_sequence_length = max_sequence_length + self.hidden_size = state_size + self.embedding_size = embedding_size + + self.should_embed = should_embed + self.embed_sequence = None + + if self.should_embed: + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=self.max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" RecurrentStack") + input_size = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size + self.recurrent_stack = RecurrentStack( + input_size=input_size, + hidden_size=state_size, + cell_type=cell_type, + max_sequence_length=max_sequence_length, + num_layers=num_layers, + bidirectional=bidirectional, + activation=activation, + recurrent_activation=recurrent_activation, + use_bias=use_bias, + unit_forget_bias=unit_forget_bias, + weights_initializer=weights_initializer, + recurrent_initializer=recurrent_initializer, + bias_initializer=bias_initializer, + dropout=recurrent_dropout, + ) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=self.recurrent_stack.output_shape[-2], + encoding_size=self.recurrent_stack.output_shape[-1], # state_size + ) + if self.reduce_output is None: + self.supports_masking = True + else: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=fc_activation, + default_dropout=fc_dropout, + ) + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return StackedRNNConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is not None: + return self.fc_stack.output_shape + return self.recurrent_stack.output_shape + + def input_dtype(self): + return torch.int32 + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through stacked recurrent layers and optional FC layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + + # shape=(?, sequence_length, embedding_size) + hidden = embedded_sequence + + # ================ Recurrent Layers ================ + hidden, final_state = self.recurrent_stack(hidden, mask=mask) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: final_state} + + +@DeveloperAPI +@register_sequence_encoder("cnnrnn") +@register_encoder("cnnrnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedCNNRNN(SequenceEncoder): + """Hybrid CNN-RNN encoder: convolutional layers followed by recurrent layers. + + Embeds the input sequence, passes it through a stack of 1D convolutional layers + (with optional pooling) to extract local features and reduce sequence length, then + feeds the result through stacked recurrent layers (RNN/LSTM/GRU) to capture sequential + dependencies. This two-stage approach combines the strengths of both architectures: + CNNs efficiently extract local n-gram features while RNNs model long-range sequential + dependencies over the CNN-reduced sequence. + + Use when: both local pattern extraction AND sequential dependencies are important. + The CNN layers reduce the sequence length before the RNN, making the RNN more efficient + on long sequences. Good for tasks like document classification where local features + need to be composed sequentially. + + Alternatives: StackedRNN (skip CNN, use RNN directly on embeddings), + StackedCNN (skip RNN, use only convolutions), + StackedTransformer (attention-based alternative to RNN for long-range dependencies). + + Performance: O(n * k * f) for CNN + O(n' * L * h) for RNN, where n' is the reduced + sequence length after CNN pooling. The CNN pooling makes the RNN portion faster than + using RNN alone on the full sequence. + """ + + def __init__( + self, + should_embed=True, + vocab=None, + max_sequence_length=None, + representation="dense", + embedding_size=256, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + conv_layers=None, + num_conv_layers=None, + num_filters=256, + filter_size=5, + strides=1, + padding="same", + dilation_rate=1, + conv_activation="relu", + conv_dropout=0.0, + pool_function="max", + pool_size=2, + pool_strides=None, + pool_padding="same", + num_rec_layers=1, + state_size=256, + cell_type="rnn", + bidirectional=False, + activation="tanh", + recurrent_activation="sigmoid", + unit_forget_bias=True, + recurrent_initializer="orthogonal", + dropout=0.0, + recurrent_dropout=0.0, + fc_layers=None, + num_fc_layers=0, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + fc_activation="relu", + fc_dropout=0, + reduce_output="last", + encoder_config=None, + **kwargs, + ): + """Initializes StackedCNNRNN. + + Args: + should_embed: If True, the input sequence is expected to be made of integers and will + be mapped into embeddings. + vocab: Vocabulary of the input feature to encode. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + num_rec_layers: The number of stacked recurrent layers. + cell_type: The type of recurrent cell to use. Available values: `rnn`, `lstm`, `gru`. + state_size: The size of the recurrent state. + bidirectional: If `True`, two recurrent networks encode in forward and backward + directions and their outputs are concatenated. + dropout: Dropout probability. + recurrent_dropout: Dropout rate for the recurrent stack. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension. Available values: `sum`, `mean` or `avg`, `max`, `concat`, `last`, + `None` or `null`. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + if conv_layers is not None and num_conv_layers is None: + # use custom-defined layers + self.conv_layers = conv_layers + self.num_conv_layers = len(conv_layers) + elif conv_layers is None and num_conv_layers is not None: + # generate num_conv_layers with default parameters + self.conv_layers = None + self.num_conv_layers = num_conv_layers + elif conv_layers is None and num_conv_layers is None: + # use default layers with varying filter sizes + self.conv_layers = [{"pool_size": 3}, {"pool_size": None}] + self.num_conv_layers = 2 + else: + raise ValueError("Invalid layer parametrization, use either conv_layers or num_conv_layers") + + self.max_sequence_length = max_sequence_length + self.should_embed = should_embed + self.embed_sequence = None + + if self.should_embed: + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=self.max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" Conv1DStack") + in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size + self.conv1d_stack = Conv1DStack( + in_channels=in_channels, + max_sequence_length=max_sequence_length, + layers=self.conv_layers, + default_num_filters=num_filters, + default_filter_size=filter_size, + default_strides=strides, + default_padding=padding, + default_dilation_rate=dilation_rate, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=conv_activation, + default_dropout=conv_dropout, + default_pool_function=pool_function, + default_pool_size=pool_size, + default_pool_strides=pool_strides, + default_pool_padding=pool_padding, + ) + + logger.debug(" RecurrentStack") + self.recurrent_stack = RecurrentStack( + input_size=self.conv1d_stack.output_shape[1], + hidden_size=state_size, + max_sequence_length=self.conv1d_stack.output_shape[0], + cell_type=cell_type, + num_layers=num_rec_layers, + bidirectional=bidirectional, + activation=activation, + recurrent_activation=recurrent_activation, + use_bias=use_bias, + unit_forget_bias=unit_forget_bias, + weights_initializer=weights_initializer, + recurrent_initializer=recurrent_initializer, + bias_initializer=bias_initializer, + dropout=recurrent_dropout, + ) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=self.recurrent_stack.output_shape[-2], + encoding_size=self.recurrent_stack.output_shape[-1], # State size + ) + if self.reduce_output is not None: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=fc_activation, + default_dropout=fc_dropout, + ) + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return StackedCNNRNNConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is not None: + return self.fc_stack.output_shape + return self.recurrent_stack.output_shape + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through CNN layers then recurrent layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + + # shape=(?, sequence_length, embedding_size) + hidden = embedded_sequence + + # ================ Conv Layers ================ + hidden = self.conv1d_stack(hidden, mask=mask) + + # ================ Recurrent Layers ================ + hidden, final_state = self.recurrent_stack(hidden) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + # no reduction: hidden [batch_size, seq_size, state_size] + # with reduction: hidden [batch_size, seq_size, output_size] + # final_state: if rnn/gru [batch_size, state_size] + # lstm ([batch_size, state_size], [batch_size, state_size]) + return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: final_state} + + +@DeveloperAPI +@register_sequence_encoder("transformer") +@register_encoder("transformer", [SEQUENCE, TEXT, TIMESERIES]) +class StackedTransformer(SequenceEncoder): + """Stacked Transformer encoder using multi-head self-attention. + + Embeds the input sequence with token and positional embeddings, then passes it through + a stack of Transformer blocks. Each block consists of multi-head self-attention followed + by a position-wise feedforward network, with residual connections and layer normalization. + This is the encoder architecture from "Attention Is All You Need" (Vaswani et al., 2017). + + Reference: Vaswani, A., et al. (2017). Attention Is All You Need. + https://arxiv.org/abs/1706.03762 + + Use when: long-range dependencies between tokens are important and the sequence is not + excessively long. Self-attention allows every position to attend to every other position, + capturing global context. Best for medium-length sequences (up to ~512-1024 tokens). + + Alternatives: StackedRNN (lower memory for very long sequences, sequential processing), + ParallelCNN/StackedCNN (faster for local pattern extraction only), + MambaEncoder (linear-time alternative for very long sequences). + + Performance: O(n^2 * d) time and O(n^2) memory for self-attention, where n is sequence + length and d is hidden size. Fully parallelizable across positions (unlike RNNs). + Memory can be a bottleneck for long sequences due to quadratic attention. + """ + + def __init__( + self, + max_sequence_length, + should_embed=True, + vocab=None, + representation="dense", + embedding_size=256, + embeddings_trainable=True, + pretrained_embeddings=None, + embeddings_on_cpu=False, + num_layers=1, + hidden_size=256, + num_heads=8, + transformer_output_size=256, + dropout=0.1, + fc_layers=None, + num_fc_layers=0, + output_size=256, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + fc_activation="relu", + fc_dropout=0, + use_rope=False, + reduce_output="last", + encoder_config=None, + **kwargs, + ): + """Initializes StackedTransformer. + + Args: + max_sequence_length: Maximum sequence length. + should_embed: If True, the input sequence is expected to be made of integers and will + be mapped into embeddings. + vocab: Vocabulary of the input feature to encode. + representation: The possible values are `dense` and `sparse`. `dense` means the + embeddings are initialized randomly, `sparse` means they are initialized to be + one-hot encodings. + embedding_size: The maximum embedding size; the actual size will be + `min(vocabulary_size, embedding_size)` for `dense` representations and exactly + `vocabulary_size` for the `sparse` encoding. + embeddings_trainable: If `True` embeddings are trained during the training process, + if `False` embeddings are fixed. Only applies when `representation` is `dense`. + pretrained_embeddings: Path to a file containing embeddings in the GloVe format. + Only applies when `representation` is `dense`. + embeddings_on_cpu: If True, forces the embedding matrix to be stored in CPU memory + rather than GPU memory, slightly slowing down access. + num_layers: Number of Transformer blocks to stack. + hidden_size: Hidden dimension of the Transformer. + num_heads: Number of self-attention heads. + transformer_output_size: Output size of the feedforward layer inside each Transformer block. + dropout: Dropout probability. + use_rope: If True, use Rotary Position Embeddings (RoPE) instead of learned positional + embeddings. + weights_initializer: The initializer to use. If `None`, uses `xavier_uniform`. + reduce_output: Defines how to reduce the output tensor along the `s` sequence length + dimension. Available values: `sum`, `mean` or `avg`, `max`, `concat`, `last`, + `None` or `null`. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.max_sequence_length = max_sequence_length + + self.should_embed = should_embed + self.should_project = False + self.embed_sequence = None + self.use_rope = use_rope + + if self.should_embed: + if use_rope: + logger.debug(" EmbedSequence (token-only, RoPE handles positions)") + self.embed_sequence = EmbedSequence( + vocab=vocab, + embedding_size=embedding_size, + max_sequence_length=max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + else: + logger.debug(" EmbedSequence") + self.embed_sequence = TokenAndPositionEmbedding( + max_sequence_length=max_sequence_length, + vocab=vocab, + embedding_size=embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + # If vocab size is smaller than embedding size, embedding layer will use len(vocab) as embedding_size. + used_embedding_size = self.embed_sequence.output_shape[-1] + if used_embedding_size != hidden_size: + logger.debug(" project_to_embed_size") + self.project_to_hidden_size = nn.Linear(self.embed_sequence.output_shape[-1], hidden_size) + self.should_project = True + else: + logger.debug(" project_to_embed_size") + self.project_to_hidden_size = nn.Linear(embedding_size, hidden_size) + self.should_project = True + + logger.debug(" TransformerStack") + self.transformer_stack = TransformerStack( + input_size=hidden_size, + max_sequence_length=max_sequence_length, + hidden_size=hidden_size, + num_heads=num_heads, + output_size=transformer_output_size, + num_layers=num_layers, + dropout=dropout, + use_rope=use_rope, + ) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=self.transformer_stack.output_shape[-2], + encoding_size=self.transformer_stack.output_shape[-1], # hidden_size + ) + if self.reduce_output is None: + self.supports_masking = True + else: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=fc_activation, + default_dropout=fc_dropout, + ) + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return StackedTransformerConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is not None: + return self.fc_stack.output_shape + return self.transformer_stack.output_shape + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through Transformer blocks and optional FC layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused, not yet implemented). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + + # shape=(?, sequence_length, embedding_size) + if self.should_project: + hidden = self.project_to_hidden_size(embedded_sequence) + else: + hidden = embedded_sequence + # shape=(?, sequence_length, hidden) + + # ================ Transformer Layers ================ + hidden = self.transformer_stack(hidden, mask=mask) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + return {ENCODER_OUTPUT: hidden} + + +class GatedConvSSMLayer(nn.Module): + """A single SSM-inspired layer using gated depthwise convolution. + + Approximates the selective state space model mechanism using: + 1. Linear projection to an expanded inner dimension + 2. Depthwise 1D convolution for local context mixing + 3. SiLU activation gating (like Mamba's selective mechanism) + 4. Linear projection back to model dimension + + This captures the key insight of Mamba (local convolution + gating) without + requiring the mamba_ssm CUDA kernels. + """ + + def __init__(self, d_model: int, d_conv: int = 4, expand_factor: int = 2, dropout: float = 0.1): + super().__init__() + d_inner = d_model * expand_factor + + # Input projection (expand) + self.in_proj = nn.Linear(d_model, d_inner * 2, bias=False) + # Depthwise conv for local context + self.conv1d = nn.Conv1d(d_inner, d_inner, kernel_size=d_conv, padding=d_conv - 1, groups=d_inner, bias=True) + # Output projection (contract) + self.out_proj = nn.Linear(d_inner, d_model, bias=False) + self.norm = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(dropout) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # x: [batch, seq_len, d_model] + residual = x + x = self.norm(x) + + # Project and split into two paths (gate and value) + xz = self.in_proj(x) # [batch, seq_len, 2 * d_inner] + x_path, z = xz.chunk(2, dim=-1) # each [batch, seq_len, d_inner] + + # Depthwise convolution on x path + x_path = x_path.transpose(1, 2) # [batch, d_inner, seq_len] + x_path = self.conv1d(x_path)[:, :, : x.shape[1]] # Causal: trim to original length + x_path = x_path.transpose(1, 2) # [batch, seq_len, d_inner] + + # Gated activation (SiLU gate, like Mamba) + x_path = torch.nn.functional.silu(x_path) * z + + # Project back + out = self.out_proj(x_path) + out = self.dropout(out) + return out + residual + + +@DeveloperAPI +@register_encoder("mamba", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class MambaEncoder(SequenceEncoder): + """State space model (SSM) encoder inspired by Mamba (Gu & Dao, 2024). + + Uses a stack of gated convolutional SSM layers that combine depthwise 1D convolution + with SiLU-gated projections, capturing the key mechanism of Mamba's selective state + space model. Each layer uses local convolution for context mixing and gating for + selective information flow, without requiring the mamba_ssm CUDA kernels. + + For full Mamba support with CUDA-optimized selective scan, install the mamba_ssm + package (pip install mamba-ssm). This encoder provides a pure-PyTorch approximation + that captures the architectural pattern. + + Reference: Gu, A. & Dao, T. (2024). Mamba: Linear-Time Sequence Modeling with + Selective State Spaces. https://arxiv.org/abs/2312.00752 + + Use when: processing very long sequences where Transformer attention is too expensive + (memory grows linearly instead of quadratically), or when linear-time inference is + required. Also effective for audio and timeseries data. + + Alternatives: StackedTransformer (better for short/medium sequences where quadratic + attention is affordable), StackedRNN (simpler recurrent alternative), + StackedCNN (when only local patterns matter). + + Performance: O(n * d * e) time and O(n * d) memory where n is sequence length, + d is d_model, e is expand_factor. Linear in sequence length (vs. quadratic for + Transformers). The gated convolution is fully parallelizable. + """ + + def __init__( + self, + max_sequence_length: int = 256, + should_embed: bool = True, + vocab=None, + representation: str = "dense", + embedding_size: int = 256, + embeddings_trainable: bool = True, + pretrained_embeddings=None, + embeddings_on_cpu: bool = False, + d_model: int = 256, + n_layers: int = 4, + d_state: int = 16, + d_conv: int = 4, + expand_factor: int = 2, + dropout: float = 0.1, + fc_layers=None, + num_fc_layers: int = 0, + output_size: int = 256, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm=None, + norm_params=None, + fc_activation: str = "relu", + fc_dropout: float = 0, + reduce_output: str = "mean", + encoder_config=None, + **kwargs, + ): + """Initializes MambaEncoder. + + Args: + max_sequence_length: Maximum sequence length. + should_embed: If True, input tokens (integers) are mapped to embeddings. + vocab: Vocabulary of the input feature. + representation: 'dense' for learned embeddings, 'sparse' for one-hot. + embedding_size: Size of token embeddings. + d_model: Hidden dimension of the SSM layers. + n_layers: Number of stacked SSM layers. + d_state: State dimension (unused in gated-conv fallback, reserved for full Mamba). + d_conv: Kernel size of the depthwise convolution in each SSM layer. + expand_factor: Expansion factor for the inner dimension of SSM layers. + dropout: Dropout rate. + reduce_output: How to reduce the sequence dimension. Default 'mean'. + """ + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.max_sequence_length = max_sequence_length + self.d_model = d_model + self.should_embed = should_embed + self.should_project = False + self.embed_sequence = None + + if self.should_embed: + logger.debug(" EmbedSequence") + self.embed_sequence = EmbedSequence( + vocab, + embedding_size, + max_sequence_length=max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + used_embedding_size = self.embed_sequence.output_shape[-1] + if used_embedding_size != d_model: + logger.debug(" project_to_d_model") + self.project_to_d_model = nn.Linear(used_embedding_size, d_model) + self.should_project = True + else: + if embedding_size != d_model: + logger.debug(" project_to_d_model") + self.project_to_d_model = nn.Linear(embedding_size, d_model) + self.should_project = True + + logger.debug(" SSM Layers") + self.ssm_layers = nn.ModuleList( + [ + GatedConvSSMLayer( + d_model=d_model, + d_conv=d_conv, + expand_factor=expand_factor, + dropout=dropout, + ) + for _ in range(n_layers) + ] + ) + self.final_norm = nn.LayerNorm(d_model) + + self.reduce_output = reduce_output + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, + max_sequence_length=max_sequence_length, + encoding_size=d_model, + ) + if self.reduce_output is None: + self.supports_masking = True + else: + logger.debug(" FCStack") + self.fc_stack = FCStack( + self.reduce_sequence.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=fc_activation, + default_dropout=fc_dropout, + ) + + @staticmethod + def get_schema_cls() -> type[SequenceEncoderConfig]: + return MambaEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is not None: + return self.fc_stack.output_shape + return torch.Size([self.max_sequence_length, self.d_model]) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + """Encodes the input sequence through SSM layers and optional FC layers. + + Args: + inputs: The input sequence fed into the encoder. + Shape: [batch x sequence length], type torch.int32. + mask: Input mask (unused in current SSM implementation). + """ + # ================ Embeddings ================ + if self.should_embed: + embedded_sequence = self.embed_sequence(inputs, mask=mask) + else: + embedded_sequence = inputs + while len(embedded_sequence.shape) < 3: + embedded_sequence = embedded_sequence.unsqueeze(-1) + + # shape=(?, sequence_length, embedding_size) + if self.should_project: + hidden = self.project_to_d_model(embedded_sequence) + else: + hidden = embedded_sequence + + # ================ SSM Layers ================ + for layer in self.ssm_layers: + hidden = layer(hidden) + hidden = self.final_norm(hidden) + + # ================ Sequence Reduction ================ + if self.reduce_output is not None: + hidden = self.reduce_sequence(hidden) + + # ================ FC Layers ================ + hidden = self.fc_stack(hidden, mask=mask) + + return {ENCODER_OUTPUT: hidden} diff --git a/ludwig/encoders/set_encoders.py b/ludwig/encoders/set_encoders.py new file mode 100644 index 0000000..9395ee1 --- /dev/null +++ b/ludwig/encoders/set_encoders.py @@ -0,0 +1,117 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from typing import Any + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, SET +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.embedding_modules import EmbedSet +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.set_encoders import SetSparseEncoderConfig + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +@register_encoder("embed", SET) +class SetSparseEncoder(Encoder): + def __init__( + self, + vocab: list[str], + representation: str = "dense", + embedding_size: int = 50, + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + embeddings_on_cpu: bool = False, + fc_layers=None, + num_fc_layers: int = 0, + output_size: int = 10, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict[str, Any] | None = None, + activation: str = "relu", + dropout: float = 0.0, + encoder_config=None, + **kwargs, + ): + super().__init__() + self.config = encoder_config + + logger.debug(f" {self.name}") + + self.vocab_size = len(vocab) + + logger.debug(" Embed") + self.embed = EmbedSet( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=weights_initializer, + ) + + logger.debug(" FCStack") + # TODO(shreya): Make sure this is updated when FCStack is updated + self.fc_stack = FCStack( + first_layer_input_size=self.embed.output_shape[-1], + layers=fc_layers, + num_layers=num_fc_layers, + default_output_size=output_size, + default_use_bias=use_bias, + default_weights_initializer=weights_initializer, + default_bias_initializer=bias_initializer, + default_norm=norm, + default_norm_params=norm_params, + default_activation=activation, + default_dropout=dropout, + ) + + def forward(self, inputs: torch.Tensor) -> EncoderOutputDict: + """ + Params: + inputs: The inputs fed into the encoder. + Shape: [batch x vocab_size], type tf.int32. + + Returns: + Embeddings of shape [batch x vocab_size x embed size], type float32. + """ + hidden = self.embed(inputs) + hidden = self.fc_stack(hidden) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return SetSparseEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.vocab_size]) + + @property + def output_shape(self) -> torch.Size: + return self.fc_stack.output_shape diff --git a/ludwig/encoders/text_encoders.py b/ludwig/encoders/text_encoders.py new file mode 100644 index 0000000..2ec53af --- /dev/null +++ b/ludwig/encoders/text_encoders.py @@ -0,0 +1,2683 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import inspect +import logging +from collections.abc import Callable +from typing import Any, TYPE_CHECKING, TypeVar + +import numpy as np +import torch +from torch import nn +from transformers import AutoConfig + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, TEXT +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.sequence_encoders import SequenceEncoderConfig +from ludwig.schema.encoders.text_encoders import ( + ALBERTConfig, + AutoTransformerConfig, + BERTConfig, + CamemBERTConfig, + DebertaV2Config, + DistilBERTConfig, + ELECTRAConfig, + GPT2Config, + GPTConfig, + LLMEncoderConfig, + LongformerConfig, + ModernBERTConfig, + MT5Config, + RoBERTaConfig, + T5Config, + TfIdfEncoderConfig, + XLMConfig, + XLMRoBERTaConfig, + XLNetConfig, +) +from ludwig.schema.llms.peft import adapter_registry, BaseAdapterConfig +from ludwig.utils.data_utils import clear_data_cache +from ludwig.utils.hf_utils import load_pretrained_hf_model_with_hub_fallback +from ludwig.utils.llm_utils import get_context_len, initialize_adapter, load_pretrained_from_config +from ludwig.utils.tokenizers import HFTokenizer +from ludwig.utils.torch_utils import FreezeModule + +if TYPE_CHECKING: + from transformers import PretrainedConfig, PreTrainedModel + + from ludwig.schema.encoders.text_encoders import HFEncoderConfig + +logger = logging.getLogger(__name__) + + +def _cls_pooled_error_message(encoder: str): + # TODO(Arnav): Remove this once we have reduce_output options set for + # each encoder type in the schema + raise ValueError(f"reduce_output cannot be cls_pooled for {encoder}") + + +class HFTextEncoder(Encoder): + def _init_config(self, transformer, schema_keys: list[str], encoder_config: SequenceEncoderConfig): + """Creates a config object for the encoder using the transformer model and the passed-in encoder config. + + The transformer's config is only known after it is instantiated, so we must update the + encoder config with the values from the transformer config. + + Args: + transformer: The transformer model. + schema_keys: The keys in the encoder config schema. We only want to update the encoder config + with the values from the transformer config that are in the schema. + encoder_config: The existing encoder config containing defaults and user-specified values. + If the values in this config differ from the transformer's config, the transformer's config + values will override this config's values. + Returns: + A new encoder config object with the updated values from the transformer config. + """ + transformer_config = transformer.config.to_dict() + final_hf_config_params = {k: v for k, v in transformer_config.items() if k in schema_keys} + encoder_config_dict = encoder_config.to_dict() + encoder_config_dict.update(final_hf_config_params) + return self.get_schema_cls().from_dict(encoder_config_dict) + + def _init_transformer_from_scratch( + self, hf_model_cls: type, hf_config_cls: type, hf_config_params: dict[str, Any], vocab_size: int + ): + """Initializes the transformer model from scratch. This is in contrast to loading a pre-trained model. + + Args: + hf_model_cls: The HuggingFace model class. + hf_config_cls: The HuggingFace config class. + hf_config_params: The HuggingFace config parameters exposed through the Ludwig schema. + vocab_size: The vocab size of the dataset. Because we are training from scratch, we can resize the + token embeddings table freely. + Returns: + The transformer model. + """ + config = hf_config_cls(**hf_config_params) + transformer = hf_model_cls(config) + self._maybe_resize_token_embeddings(transformer, vocab_size) + return transformer + + def _maybe_resize_token_embeddings(self, transformer, vocab_size: int) -> None: + """Resizes the token embeddings if the vocab size is different from the transformer's vocab size. + + This should only happen if we are instantiating a model from scratch (i.e. not loading from a pretrained model + or checkpoint). Pretrained models update the vocab size stored in the config. This means if we are loading a + pretrained model from a checkpoint, the config vocab size should match the model's vocab size. + + It is important that pretrained models update the vocab size stored in the config because sometimes the + pretrained models will have an embeddings table that is a different size than the vocab size. Examples: + + CamemBERT: https://github.com/huggingface/tokenizers/issues/900#issue-1122256698 + T5: https://github.com/huggingface/transformers/issues/4875#issue-635471552 + + Args: + transformer: The transformer model. + vocab_size: The vocab size of the dataset. + """ + if vocab_size != transformer.config.vocab_size: + transformer.resize_token_embeddings(vocab_size) + + def _wrap_transformer( + self, transformer: nn.Module, adapter: BaseAdapterConfig | dict | None, trainable: bool + ) -> nn.Module: + if adapter is not None: + from peft import get_peft_model + + if isinstance(adapter, dict): + adapter_cls = adapter_registry[adapter["type"]] + adapter = adapter_cls.model_validate(adapter) + peft_config = adapter.to_config() + transformer = get_peft_model(transformer, peft_config) + + logger.info("==================================================") + logger.info("Trainable Parameter Summary For Fine-Tuning:") + transformer.print_trainable_parameters() + logger.info("==================================================") + return FreezeModule(transformer, frozen=not trainable) + + def get_embedding_layer(self) -> nn.Module: + return next(self.transformer.module.children()) + + +HFModelT = TypeVar("HFModelT", bound="PreTrainedModel") +HFConfigT = TypeVar("HFConfigT", bound="PretrainedConfig") +ConfigT = TypeVar("ConfigT", bound="HFEncoderConfig") + + +class HFTextEncoderImpl(HFTextEncoder): + def __init__( + self, + model_cls: type[HFModelT], + config_cls: type[HFConfigT], + schema_cls: type[ConfigT], + max_sequence_length: int, + use_pretrained: bool, + pretrained_model_name_or_path: str, + saved_weights_in_checkpoint: bool, + reduce_output: str, + trainable: bool, + adapter: BaseAdapterConfig | None, + pretrained_kwargs: dict, + encoder_config: ConfigT | None, + **kwargs, + ): + super().__init__() + + vocab_size = kwargs["vocab_size"] + hf_config_params = {k: v for k, v in kwargs.items() if k in schema_cls.get_hf_config_param_names()} + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + model_cls, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(model_cls, config_cls, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs["pooler_output"] + else: + hidden = transformer_outputs["last_hidden_state"][:, 1:-1, :] # bos + [sent] + sep + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return torch.Size([self.max_sequence_length - 2, self.transformer.module.config.hidden_size]) + if self.reduce_output == "concat": + return torch.Size( + [ + (self.max_sequence_length - 2) * self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("albert", TEXT) +class ALBERTEncoder(HFTextEncoder): + """ALBERT encoder (Lan et al., ICLR 2020). + + A Lite BERT architecture that uses parameter-sharing across layers and factorized embedding + parameterization to drastically reduce model size while maintaining competitive performance. + + Use when: you need a smaller, faster model with comparable quality to BERT. Good for + resource-constrained environments. + + Alternatives: DistilBERT (different compression approach), BERT (full-size baseline), + AutoTransformer (flexible, any HF model). + + Default model: ``albert-base-v2`` + """ + + DEFAULT_MODEL_NAME = "albert-base-v2" + + def __init__( + self, + max_sequence_length, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + reduce_output: str = "cls_pooled", + vocab_size: int = 30000, + embedding_size: int = 128, + hidden_size: int = 4096, + num_hidden_layers: int = 12, + num_hidden_groups: int = 1, + num_attention_heads: int = 64, + intermediate_size: int = 16384, + inner_group_num: int = 1, + hidden_act: str = "gelu_new", + hidden_dropout_prob: float = 0, + attention_probs_dropout_prob: float = 0, + max_position_embeddings: int = 512, + type_vocab_size: int = 2, + initializer_range: float = 0.02, + layer_norm_eps: float = 1e-12, + classifier_dropout_prob: float = 0.1, + position_embedding_type: str = "absolute", + pad_token_id: int = 0, + bos_token_id: int = 2, + eos_token_id: int = 3, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import AlbertConfig, AlbertModel + + hf_config_params = { + "vocab_size": vocab_size, + "embedding_size": embedding_size, + "hidden_size": hidden_size, + "num_hidden_layers": num_hidden_layers, + "num_hidden_groups": num_hidden_groups, + "num_attention_heads": num_attention_heads, + "intermediate_size": intermediate_size, + "inner_group_num": inner_group_num, + "hidden_act": hidden_act, + "hidden_dropout_prob": hidden_dropout_prob, + "attention_probs_dropout_prob": attention_probs_dropout_prob, + "max_position_embeddings": max_position_embeddings, + "type_vocab_size": type_vocab_size, + "initializer_range": initializer_range, + "layer_norm_eps": layer_norm_eps, + "classifier_dropout_prob": classifier_dropout_prob, + "position_embedding_type": position_embedding_type, + "pad_token_id": pad_token_id, + "bos_token_id": bos_token_id, + "eos_token_id": eos_token_id, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + AlbertModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(AlbertModel, AlbertConfig, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs[1] + else: + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return ALBERTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("mt5", TEXT) +class MT5Encoder(HFTextEncoder): + """MT5 encoder (Xue et al., NAACL 2021). + + Multilingual variant of T5 pre-trained on the mC4 corpus covering 101 languages. + Uses the encoder portion of the T5 architecture with relative position embeddings. + + Use when: multilingual text understanding tasks. Supports 101 languages out of the box. + + Alternatives: XLM-RoBERTa (multilingual, BERT-style), T5 (English only, same architecture), + AutoTransformer (flexible, any HF model). + + Default model: ``google/mt5-base`` + """ + + DEFAULT_MODEL_NAME = "google/mt5-base" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + reduce_output: str = "sum", + vocab_size: int = 250112, + d_model: int = 512, + d_kv: int = 64, + d_ff: int = 1024, + num_layers: int = 8, + num_decoder_layers: int | None = None, + num_heads: int = 6, + relative_attention_num_buckets: int = 32, + dropout_rate: float = 0.1, + layer_norm_epsilon: float = 1e-06, + initializer_factor: float = 1.0, + feed_forward_proj: str = "gated-gelu", + is_encoder_decoder: bool = True, + use_cache: bool = True, + tokenizer_class: str = "T5Tokenizer", + tie_word_embeddings: bool = False, + pad_token_id: int = 0, + eos_token_id: int = 1, + decoder_start_token_id: int = 0, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import MT5Config, MT5EncoderModel + + hf_config_params = { + "vocab_size": vocab_size, + "d_model": d_model, + "d_kv": d_kv, + "d_ff": d_ff, + "num_layers": num_layers, + "num_decoder_layers": num_decoder_layers, + "num_heads": num_heads, + "relative_attention_num_buckets": relative_attention_num_buckets, + "dropout_rate": dropout_rate, + "layer_norm_epsilon": layer_norm_epsilon, + "initializer_factor": initializer_factor, + "feed_forward_proj": feed_forward_proj, + "is_encoder_decoder": is_encoder_decoder, + "use_cache": use_cache, + "tokenizer_class": tokenizer_class, + "tie_word_embeddings": tie_word_embeddings, + "pad_token_id": pad_token_id, + "eos_token_id": eos_token_id, + "decoder_start_token_id": decoder_start_token_id, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + MT5EncoderModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(MT5EncoderModel, MT5Config, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + ) + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return MT5Config + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by MT5 tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("xlmroberta", TEXT) +class XLMRoBERTaEncoder(HFTextEncoder): + """XLM-RoBERTa encoder (Conneau et al., ACL 2020). + + Cross-lingual language model pre-trained on 2.5TB of filtered CommonCrawl data in 100 + languages using masked language modeling. Based on the RoBERTa architecture. + + Use when: cross-lingual transfer learning or multilingual text classification. Strong + zero-shot cross-lingual performance. + + Alternatives: mT5 (multilingual seq2seq), CamemBERT (French-specific), BERT (English only), + AutoTransformer (flexible, any HF model). + + Default model: ``xlm-roberta-base`` + """ + + DEFAULT_MODEL_NAME = "xlm-roberta-base" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "cls_pooled", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int | None = None, + pad_token_id: int = 1, + bos_token_id: int = 0, + eos_token_id: int = 2, + max_position_embeddings: int = 514, + type_vocab_size: int = 1, + add_pooling_layer: bool = True, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import XLMRobertaConfig, XLMRobertaModel + + hf_config_params = { + "pad_token_id": pad_token_id, + "bos_token_id": bos_token_id, + "eos_token_id": eos_token_id, + "max_position_embeddings": max_position_embeddings, + "type_vocab_size": type_vocab_size, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + XLMRobertaModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch( + XLMRobertaModel, XLMRobertaConfig, hf_config_params, vocab_size + ) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs[1] + else: + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return XLMRoBERTaConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by XLMRoberta tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("bert", TEXT) +class BERTEncoder(HFTextEncoder): + """BERT encoder (Devlin et al., NAACL 2019). + + Bidirectional transformer pre-trained on masked language modeling and next sentence + prediction. Produces contextual word embeddings. + + Use when: general NLU tasks (classification, NER, QA). Good default for English text + up to 512 tokens. For longer text, consider Longformer. + + Alternatives: DistilBERT (6x faster, 97% of BERT quality), RoBERTa (better pretraining), + DeBERTa (SOTA on many benchmarks), AutoTransformer (flexible, any HF model). + + Default model: ``bert-base-uncased`` + """ + + DEFAULT_MODEL_NAME = "bert-base-uncased" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + reduce_output: str = "cls_pooled", + vocab_size: int = 30522, + hidden_size: int = 768, + num_hidden_layers: int = 12, + num_attention_heads: int = 12, + intermediate_size: int = 3072, + hidden_act: str | Callable = "gelu", + hidden_dropout_prob: float = 0.1, + attention_probs_dropout_prob: float = 0.1, + max_position_embeddings: int = 512, + type_vocab_size: int = 2, + initializer_range: float = 0.02, + layer_norm_eps: float = 1e-12, + pad_token_id: int = 0, + gradient_checkpointing: bool = False, + position_embedding_type: str = "absolute", + classifier_dropout: float | None = None, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import BertConfig, BertModel + + hf_config_params = { + "vocab_size": vocab_size, + "hidden_size": hidden_size, + "num_hidden_layers": num_hidden_layers, + "num_attention_heads": num_attention_heads, + "intermediate_size": intermediate_size, + "hidden_act": hidden_act, + "hidden_dropout_prob": hidden_dropout_prob, + "attention_probs_dropout_prob": attention_probs_dropout_prob, + "max_position_embeddings": max_position_embeddings, + "type_vocab_size": type_vocab_size, + "initializer_range": initializer_range, + "layer_norm_eps": layer_norm_eps, + "pad_token_id": pad_token_id, + "gradient_checkpointing": gradient_checkpointing, + "position_embedding_type": position_embedding_type, + "classifier_dropout": classifier_dropout, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + BertModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(BertModel, BertConfig, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs[1] + else: + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return BERTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + # TODO(shreya): Confirm that this is it + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("xlm", TEXT) +class XLMEncoder(HFTextEncoder): + """XLM encoder (Lample & Conneau, NeurIPS 2019). + + Cross-lingual language model pre-trained with masked language modeling and translation + language modeling objectives. Supports multiple languages. + + Use when: legacy multilingual tasks. Largely superseded by XLM-RoBERTa. + + Alternatives: XLM-RoBERTa (stronger cross-lingual transfer), mT5 (multilingual seq2seq), + AutoTransformer (flexible, any HF model). + + Default model: ``xlm-mlm-en-2048`` + """ + + DEFAULT_MODEL_NAME = "xlm-mlm-en-2048" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + reduce_output: str = "sum", + vocab_size: int = 30145, + emb_dim: int = 2048, + n_layers: int = 12, + n_heads: int = 16, + dropout: float = 0.1, + attention_dropout: float = 0.1, + gelu_activation: bool = True, + sinusoidal_embeddings: bool = False, + causal: bool = False, + asm: bool = False, + n_langs: int = 1, + use_lang_emb: bool = True, + max_position_embeddings: int = 512, + embed_init_std: float = 2048**-0.5, + layer_norm_eps: float = 1e-12, + init_std: float = 0.02, + bos_index: int = 0, + eos_index: int = 1, + pad_index: int = 2, + unk_index: int = 3, + mask_index: int = 5, + is_encoder: bool = True, + start_n_top: int = 5, + end_n_top: int = 5, + mask_token_id: int = 0, + lang_id: int = 0, + pad_token_id: int = 2, + bos_token_id: int = 0, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import XLMConfig, XLMModel + + hf_config_params = { + "vocab_size": vocab_size, + "emb_dim": emb_dim, + "n_layers": n_layers, + "n_heads": n_heads, + "dropout": dropout, + "attention_dropout": attention_dropout, + "gelu_activation": gelu_activation, + "sinusoidal_embeddings": sinusoidal_embeddings, + "causal": causal, + "asm": asm, + "n_langs": n_langs, + "use_lang_emb": use_lang_emb, + "max_position_embeddings": max_position_embeddings, + "embed_init_std": embed_init_std, + "layer_norm_eps": layer_norm_eps, + "init_std": init_std, + "bos_index": bos_index, + "eos_index": eos_index, + "pad_index": pad_index, + "unk_index": unk_index, + "mask_index": mask_index, + "is_encoder": is_encoder, + "start_n_top": start_n_top, + "end_n_top": end_n_top, + "mask_token_id": mask_token_id, + "lang_id": lang_id, + "pad_token_id": pad_token_id, + "bos_token_id": bos_token_id, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + XLMModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(XLMModel, XLMConfig, hf_config_params, vocab_size) + + self.config = self._init_config(transformer, hf_config_params, encoder_config) + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + hidden = transformer_outputs[0] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return XLMConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + # TODO(shreya): Confirm that this is it + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("gpt", TEXT) +class GPTEncoder(HFTextEncoder): + """GPT encoder (Radford et al., 2018). + + Original unidirectional (left-to-right) transformer language model from OpenAI. + Pre-trained on BookCorpus with causal language modeling. + + Use when: legacy compatibility. Largely superseded by GPT-2 and later models. + + Alternatives: GPT2 (larger, better quality), LLMEncoder (modern LLMs with adapter support), + AutoTransformer (flexible, any HF model). + + Default model: ``openai-gpt`` + """ + + DEFAULT_MODEL_NAME = "openai-gpt" + + def __init__( + self, + max_sequence_length: int, + reduce_output: str = "sum", + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 30522, + n_positions: int = 40478, + n_ctx: int = 512, + n_embd: int = 768, + n_layer: int = 12, + n_head: int = 12, + afn: str = "gelu", + resid_pdrop: float = 0.1, + embd_pdrop: float = 0.1, + attn_pdrop: float = 0.1, + layer_norm_epsilon: float = 1e-5, + initializer_range: float = 0.02, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import OpenAIGPTConfig, OpenAIGPTModel + + hf_config_params = { + "vocab_size": vocab_size, + "n_positions": n_positions, + "n_ctx": n_ctx, + "n_embd": n_embd, + "n_layer": n_layer, + "n_head": n_head, + "afn": afn, + "resid_pdrop": resid_pdrop, + "embd_pdrop": embd_pdrop, + "attn_pdrop": attn_pdrop, + "layer_norm_epsilon": layer_norm_epsilon, + "initializer_range": initializer_range, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + OpenAIGPTModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch( + OpenAIGPTModel, OpenAIGPTConfig, hf_config_params, vocab_size + ) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + hidden = transformer_outputs[0] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return GPTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return torch.Size([self.max_sequence_length, self.transformer.module.config.hidden_size]) + elif self.reduce_output == "concat": + return torch.Size([self.transformer.module.config.hidden_size * self.max_sequence_length]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("gpt2", TEXT) +class GPT2Encoder(HFTextEncoder): + """GPT-2 encoder (Radford et al., 2019). + + Unidirectional (left-to-right) transformer language model. Produces contextual embeddings + using causal attention. Supports sequences up to 1024 tokens. + + Use when: you need causal (left-to-right) text representations, or when the downstream task + benefits from autoregressive pre-training (e.g., text generation features). + + Alternatives: LLMEncoder (modern LLMs with adapter/quantization support), BERT (bidirectional), + AutoTransformer (flexible, any HF model). + + Default model: ``gpt2`` + """ + + DEFAULT_MODEL_NAME = "gpt2" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + reduce_output: str = "sum", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 50257, + n_positions: int = 1024, + n_ctx: int = 1024, + n_embd: int = 768, + n_layer: int = 12, + n_head: int = 12, + n_inner: int | None = None, + activation_function: str = "gelu", + resid_pdrop: float = 0.1, + embd_pdrop: float = 0.1, + attn_pdrop: float = 0.1, + layer_norm_epsilon: float = 1e-5, + initializer_range: float = 0.02, + scale_attn_weights: bool = True, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import GPT2Config, GPT2Model + + hf_config_params = { + "vocab_size": vocab_size, + "n_positions": n_positions, + "n_ctx": n_ctx, + "n_embd": n_embd, + "n_layer": n_layer, + "n_head": n_head, + "n_inner": n_inner, + "activation_function": activation_function, + "resid_pdrop": resid_pdrop, + "embd_pdrop": embd_pdrop, + "attn_pdrop": attn_pdrop, + "layer_norm_epsilon": layer_norm_epsilon, + "initializer_range": initializer_range, + "scale_attn_weights": scale_attn_weights, + } + + if use_pretrained: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + GPT2Model, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(GPT2Model, GPT2Config, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + hidden = transformer_outputs[0] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return GPT2Config + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return torch.Size([self.max_sequence_length, self.transformer.module.config.hidden_size]) + elif self.reduce_output == "concat": + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("deberta", TEXT) +class DeBERTaEncoder(HFTextEncoderImpl): + """DeBERTa v2/v3 encoder (He et al., ICLR 2021). + + Disentangled attention mechanism with enhanced mask decoder. DeBERTa v3 uses ELECTRA-style + pre-training for improved efficiency. Achieves state-of-the-art on many NLU benchmarks. + + Use when: you want the best quality on NLU tasks and can afford slightly higher compute + than BERT. Excellent for classification, NER, and QA. + + Alternatives: BERT (simpler, faster), RoBERTa (strong baseline), ELECTRA (efficient pre-training), + AutoTransformer (flexible, any HF model). + + Default model: ``microsoft/deberta-v3-base`` + """ + + def __init__(self, *args, **kwargs): + from transformers import DebertaV2Config as _DebertaV2Config + from transformers import DebertaV2Model + + super().__init__(DebertaV2Model, _DebertaV2Config, DebertaV2Config, *args, **kwargs) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return DebertaV2Config + + +@DeveloperAPI +@register_encoder("roberta", TEXT) +class RoBERTaEncoder(HFTextEncoder): + """RoBERTa encoder (Liu et al., 2019). + + Robustly optimized BERT with dynamic masking, larger batches, and no next sentence + prediction objective. Consistently outperforms BERT on downstream tasks. + + Use when: you want better-than-BERT quality with the same architecture. Strong general-purpose + encoder for English text. + + Alternatives: DeBERTa (even better quality), BERT (simpler baseline), DistilBERT (faster), + AutoTransformer (flexible, any HF model). + + Default model: ``roberta-base`` + """ + + DEFAULT_MODEL_NAME = "roberta-base" + + def __init__( + self, + max_sequence_length, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "cls_pooled", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int | None = None, + pad_token_id: int = 1, + bos_token_id: int = 0, + eos_token_id: int = 2, + max_position_embeddings: int = 514, + type_vocab_size: int = 1, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import RobertaConfig, RobertaModel + + hf_config_params = { + "pad_token_id": pad_token_id, + "bos_token_id": bos_token_id, + "eos_token_id": eos_token_id, + "max_position_embeddings": max_position_embeddings, + "type_vocab_size": type_vocab_size, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + RobertaModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(RobertaModel, RobertaConfig, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs[1] + else: + hidden = transformer_outputs[0][:, 1:-1, :] # bos + [sent] + sep + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return RoBERTaConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return torch.Size([self.max_sequence_length - 2, self.transformer.module.config.hidden_size]) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("xlnet", TEXT) +class XLNetEncoder(HFTextEncoder): + """XLNet encoder (Yang et al., NeurIPS 2019). + + Generalized autoregressive pre-training with permutation language modeling. Captures + bidirectional context without the [MASK] token discrepancy between pre-training and + fine-tuning. Uses Transformer-XL's segment recurrence for longer context. + + Use when: tasks requiring long-range dependencies or when permutation-based pre-training + is beneficial. + + Alternatives: BERT (simpler bidirectional), RoBERTa (often comparable quality), + Longformer (explicit long-context support), AutoTransformer (flexible, any HF model). + + Default model: ``xlnet-base-cased`` + """ + + DEFAULT_MODEL_NAME = "xlnet-base-cased" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "sum", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 32000, + d_model: int = 1024, + n_layer: int = 24, + n_head: int = 16, + d_inner: int = 4096, + ff_activation: str = "gelu", + untie_r: bool = True, + attn_type: str = "bi", + initializer_range: float = 0.02, + layer_norm_eps: float = 1e-12, + dropout: float = 0.1, + mem_len: int | None = 512, + reuse_len: int | None = None, + use_mems_eval: bool = True, + use_mems_train: bool = False, + bi_data: bool = False, + clamp_len: int = -1, + same_length: bool = False, + summary_type: str = "last", + summary_use_proj: bool = True, + summary_activation: str = "tanh", + summary_last_dropout: float = 0.1, + start_n_top: int = 5, + end_n_top: int = 5, + pad_token_id: int = 5, + bos_token_id: int = 1, + eos_token_id: int = 2, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import XLNetConfig, XLNetModel + + hf_config_params = { + "vocab_size": vocab_size, + "d_model": d_model, + "n_layer": n_layer, + "n_head": n_head, + "d_inner": d_inner, + "ff_activation": ff_activation, + "untie_r": untie_r, + "attn_type": attn_type, + "initializer_range": initializer_range, + "layer_norm_eps": layer_norm_eps, + "dropout": dropout, + "mem_len": mem_len, + "reuse_len": reuse_len, + "use_mems_eval": use_mems_eval, + "use_mems_train": use_mems_train, + "bi_data": bi_data, + "clamp_len": clamp_len, + "same_length": same_length, + "summary_type": summary_type, + "summary_use_proj": summary_use_proj, + "summary_activation": summary_activation, + "summary_last_dropout": summary_last_dropout, + "start_n_top": start_n_top, + "end_n_top": end_n_top, + "pad_token_id": pad_token_id, + "bos_token_id": bos_token_id, + "eos_token_id": eos_token_id, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + XLNetModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(XLNetModel, XLNetConfig, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.max_sequence_length = max_sequence_length + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + hidden = transformer_outputs[0] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return XLNetConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return torch.Size([self.max_sequence_length, self.transformer.module.config.d_model]) + elif self.reduce_output == "concat": + return torch.Size([self.transformer.module.config.d_model * self.max_sequence_length]) + return torch.Size([self.transformer.module.config.d_model]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("distilbert", TEXT) +class DistilBERTEncoder(HFTextEncoder): + """DistilBERT encoder (Sanh et al., NeurIPS 2019 Workshop). + + Knowledge-distilled version of BERT that is 60% faster and 40% smaller while retaining + 97% of BERT's language understanding. Uses 6 transformer layers instead of 12. + + Use when: you need fast inference or have limited compute/memory. Excellent trade-off + between speed and quality. + + Alternatives: BERT (full quality), ALBERT (parameter-efficient), RoBERTa (better quality), + AutoTransformer (flexible, any HF model). + + Default model: ``distilbert-base-uncased`` + """ + + DEFAULT_MODEL_NAME = "distilbert-base-uncased" + + def __init__( + self, + max_sequence_length: int, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "sum", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + use_pretrained: bool = True, + vocab_size: int = 30522, + max_position_embeddings: int = 512, + sinusoidal_pos_embds: bool = False, + n_layers: int = 6, + n_heads: int = 12, + dim: int = 768, + hidden_dim: int = 3072, + dropout: float = 0.1, + attention_dropout: float = 0.1, + activation: str | Callable = "gelu", + initializer_range: float = 0.02, + qa_dropout: float = 0.1, + seq_classif_dropout: float = 0.2, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import DistilBertConfig, DistilBertModel + + hf_config_params = { + "vocab_size": vocab_size, + "max_position_embeddings": max_position_embeddings, + "sinusoidal_pos_embds": sinusoidal_pos_embds, + "n_layers": n_layers, + "n_heads": n_heads, + "dim": dim, + "hidden_dim": hidden_dim, + "dropout": dropout, + "attention_dropout": attention_dropout, + "activation": activation, + "initializer_range": initializer_range, + "qa_dropout": qa_dropout, + "seq_classif_dropout": seq_classif_dropout, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + DistilBertModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch( + DistilBertModel, DistilBertConfig, hf_config_params, vocab_size + ) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.max_sequence_length = max_sequence_length + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.last_inputs = None + self.last_hidden = None + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + ) + hidden = transformer_outputs[0][:, 1:-1, :] + self.last_inputs = inputs + self.last_hidden = hidden + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return DistilBERTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer. + return torch.Size([self.max_sequence_length - 2, self.transformer.module.config.dim]) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.dim * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.dim]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("camembert", TEXT) +class CamemBERTEncoder(HFTextEncoder): + """CamemBERT encoder (Martin et al., ACL 2020). + + French language model based on the RoBERTa architecture, pre-trained on 138GB of French + text from the OSCAR corpus. State-of-the-art for French NLP tasks. + + Use when: French text understanding tasks. Preferred over multilingual models for + French-specific applications. + + Alternatives: XLM-RoBERTa (multilingual including French), BERT (English), + AutoTransformer (flexible, any HF model). + + Default model: ``camembert-base`` + """ + + DEFAULT_MODEL_NAME = "camembert-base" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "cls-pooled", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 30522, + hidden_size: int = 768, + num_hidden_layers: int = 12, + num_attention_heads: int = 12, + intermediate_size: int = 3072, + hidden_act: str | Callable = "gelu", + hidden_dropout_prob: float = 0.1, + attention_probs_dropout_prob: float = 0.1, + max_position_embeddings: int = 512, + type_vocab_size: int = 2, + initializer_range: float = 0.02, + layer_norm_eps: float = 1e-12, + pad_token_id: int = 0, + gradient_checkpointing: bool = False, + position_embedding_type: str = "absolute", + classifier_dropout: float | None = None, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import CamembertConfig, CamembertModel + + hf_config_params = { + "vocab_size": vocab_size, + "hidden_size": hidden_size, + "num_hidden_layers": num_hidden_layers, + "num_attention_heads": num_attention_heads, + "intermediate_size": intermediate_size, + "hidden_act": hidden_act, + "hidden_dropout_prob": hidden_dropout_prob, + "attention_probs_dropout_prob": attention_probs_dropout_prob, + "max_position_embeddings": max_position_embeddings, + "type_vocab_size": type_vocab_size, + "initializer_range": initializer_range, + "layer_norm_eps": layer_norm_eps, + "pad_token_id": pad_token_id, + "gradient_checkpointing": gradient_checkpointing, + "position_embedding_type": position_embedding_type, + "classifier_dropout": classifier_dropout, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + CamembertModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch( + CamembertModel, CamembertConfig, hf_config_params, vocab_size + ) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs[1] + else: + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return CamemBERTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("t5", TEXT) +class T5Encoder(HFTextEncoder): + """T5 encoder (Raffel et al., JMLR 2020). + + Text-to-Text Transfer Transformer pre-trained on the C4 corpus. Uses the encoder portion + of the encoder-decoder architecture with relative position embeddings. Treats every NLP + task as a text-to-text problem. + + Use when: tasks that benefit from the T5 pre-training approach, or when you need the + encoder half of a T5 model for representation learning. + + Alternatives: mT5 (multilingual variant), BERT (classification-oriented), LLMEncoder + (full decoder-based LLMs), AutoTransformer (flexible, any HF model). + + Default model: ``t5-small`` + """ + + DEFAULT_MODEL_NAME = "t5-small" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "sum", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 32128, + d_model: int = 512, + d_kv: int = 64, + d_ff: int = 2048, + num_layers: int = 6, + num_decoder_layers: int | None = None, + num_heads: int = 8, + relative_attention_num_buckets: int = 32, + dropout_rate: float = 0.1, + layer_norm_eps: float = 1e-6, + initializer_factor: float = 1, + feed_forward_proj: str = "relu", + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import T5Config, T5Model + + hf_config_params = { + "vocab_size": vocab_size, + "d_model": d_model, + "d_kv": d_kv, + "d_ff": d_ff, + "num_layers": num_layers, + "num_decoder_layers": num_decoder_layers, + "num_heads": num_heads, + "relative_attention_num_buckets": relative_attention_num_buckets, + "dropout_rate": dropout_rate, + "layer_norm_eps": layer_norm_eps, + "initializer_factor": initializer_factor, + "feed_forward_proj": feed_forward_proj, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + T5Model, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(T5Model, T5Config, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.max_sequence_length = max_sequence_length + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + inputs, + decoder_input_ids=inputs, + attention_mask=mask, + ) + hidden = transformer_outputs[0][:, 0:-1, :] # [eos token] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return T5Config + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 1 to remove EOS token added by T5 tokenizer. + return torch.Size( + [ + self.max_sequence_length - 1, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -1 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 1)]) + return torch.Size([self.transformer.module.config.d_model]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("electra", TEXT) +class ELECTRAEncoder(HFTextEncoder): + """ELECTRA encoder (Clark et al., ICLR 2020). + + Uses a replaced token detection pre-training objective instead of masked language modeling. + A small generator produces plausible replacements and the discriminator (this encoder) + learns to detect them, making pre-training much more sample-efficient than BERT. + + Use when: you want BERT-level quality with significantly less pre-training compute, or + when using smaller model sizes (ELECTRA-small outperforms similarly-sized BERT variants). + + Alternatives: DeBERTa (highest quality), BERT (simpler), ALBERT (parameter-efficient), + AutoTransformer (flexible, any HF model). + + Default model: ``google/electra-small-discriminator`` + """ + + DEFAULT_MODEL_NAME = "google/electra-small-discriminator" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str = "sum", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 30522, + embedding_size: int = 128, + hidden_size: int = 256, + num_hidden_layers: int = 12, + num_attention_heads: int = 4, + intermediate_size: int = 1024, + hidden_act: str | Callable = "gelu", + hidden_dropout_prob: float = 0.1, + attention_probs_dropout_prob: float = 0.1, + max_position_embeddings: int = 512, + type_vocab_size: int = 2, + initializer_range: float = 0.02, + layer_norm_eps: float = 1e-12, + position_embedding_type: str = "absolute", + classifier_dropout: float | None = None, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import ElectraConfig, ElectraModel + + hf_config_params = { + "vocab_size": vocab_size, + "embedding_size": embedding_size, + "hidden_size": hidden_size, + "num_hidden_layers": num_hidden_layers, + "num_attention_heads": num_attention_heads, + "intermediate_size": intermediate_size, + "hidden_act": hidden_act, + "hidden_dropout_prob": hidden_dropout_prob, + "attention_probs_dropout_prob": attention_probs_dropout_prob, + "max_position_embeddings": max_position_embeddings, + "type_vocab_size": type_vocab_size, + "initializer_range": initializer_range, + "layer_norm_eps": layer_norm_eps, + "position_embedding_type": position_embedding_type, + "classifier_dropout": classifier_dropout, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + ElectraModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch(ElectraModel, ElectraConfig, hf_config_params, vocab_size) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.max_sequence_length = max_sequence_length + self.reduce_output = reduce_output + if self.reduce_output == "cls_pooled": + _cls_pooled_error_message(self.__class__.__name__) + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return ELECTRAConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("longformer", TEXT) +class LongformerEncoder(HFTextEncoder): + """Longformer encoder (Beltagy et al., 2020). + + Transformer with a sliding-window local attention pattern combined with task-motivated + global attention, enabling efficient processing of documents up to 4096 tokens. + + Use when: text exceeds BERT's 512-token limit. Ideal for document classification, + long-form QA, and summarization features. + + Alternatives: BERT (shorter text, up to 512 tokens), RoBERTa (shorter text), + AutoTransformer (flexible, any HF model). + + Default model: ``allenai/longformer-base-4096`` + """ + + DEFAULT_MODEL_NAME = "allenai/longformer-base-4096" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + attention_window: list[int] | int = 512, + sep_token_id: int = 2, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + reduce_output: str | None = "cls_pooled", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int = 50265, + num_tokens: int | None = None, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import LongformerConfig, LongformerModel + + hf_config_params = dict( + attention_window=attention_window, + sep_token_id=sep_token_id, + vocab_size=vocab_size, + **kwargs, + ) + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + LongformerModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch( + LongformerModel, LongformerConfig, hf_config_params, vocab_size + ) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + token_type_ids=torch.zeros_like(inputs), + ) + if self.reduce_output == "cls_pooled": + hidden = transformer_outputs[1] + else: + hidden = transformer_outputs[0][:, 1:-1, :] # bos + [sent] + sep + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return LongformerConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # Subtract 2 to remove CLS and PAD tokens added by Longformer (== Roberta) tokenizer. + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("modernbert", TEXT) +class ModernBERTEncoder(HFTextEncoder): + """ModernBERT encoder (Warner et al., December 2024). + + First major architectural upgrade to BERT, incorporating modern transformer + improvements: Flash Attention 2, Rotary Position Embeddings (RoPE), GeGLU + activations, unpadding for efficiency, and alternating local/global attention. + Supports up to 8192 tokens (vs BERT's 512). + + Use when: general NLU tasks where you would use BERT or RoBERTa. Faster + and more accurate than both, especially on longer texts. Best for + classification, NER, and embedding tasks. + + Alternatives: BERT (legacy baseline), RoBERTa (established alternative), + DeBERTa (strong on benchmarks but slower), AutoTransformer (for any HF model). + + Default model: ``answerdotai/ModernBERT-base`` + """ + + DEFAULT_MODEL_NAME = "answerdotai/ModernBERT-base" + + def __init__( + self, + max_sequence_length: int, + use_pretrained: bool = True, + pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME, + saved_weights_in_checkpoint: bool = False, + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + reduce_output: str = "cls_pooled", + vocab_size: int = 50368, + hidden_size: int = 768, + num_hidden_layers: int = 22, + num_attention_heads: int = 12, + intermediate_size: int = 1152, + hidden_act: str | Callable = "gelu", + hidden_dropout_prob: float = 0.0, + max_position_embeddings: int = 8192, + initializer_range: float = 0.02, + layer_norm_eps: float = 1e-5, + pad_token_id: int = 50283, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import ModernBertConfig, ModernBertModel + + hf_config_params = { + "vocab_size": vocab_size, + "hidden_size": hidden_size, + "num_hidden_layers": num_hidden_layers, + "num_attention_heads": num_attention_heads, + "intermediate_size": intermediate_size, + "hidden_act": hidden_act, + "hidden_dropout_prob": hidden_dropout_prob, + "max_position_embeddings": max_position_embeddings, + "initializer_range": initializer_range, + "layer_norm_eps": layer_norm_eps, + "pad_token_id": pad_token_id, + } + + if use_pretrained and not saved_weights_in_checkpoint: + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + ModernBertModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + else: + transformer = self._init_transformer_from_scratch( + ModernBertModel, ModernBertConfig, hf_config_params, vocab_size + ) + + if encoder_config is not None: + self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config) + else: + self.config = None + + self.reduce_output = reduce_output + if not self.reduce_output == "cls_pooled": + self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output) + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + + self.max_sequence_length = max_sequence_length + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + transformer_outputs = self.transformer.module( + input_ids=inputs, + attention_mask=mask, + ) + if self.reduce_output == "cls_pooled": + # ModernBERT does not have a pooler layer, so use CLS token hidden state + hidden = transformer_outputs[0][:, 0, :] + else: + hidden = transformer_outputs[0][:, 1:-1, :] + hidden = self.reduce_sequence(hidden, self.reduce_output) + + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return ModernBERTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + return torch.Size( + [ + self.max_sequence_length - 2, + self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("auto_transformer", TEXT) +class AutoTransformerEncoder(HFTextEncoder): + """Generic HuggingFace AutoModel encoder. + + Loads any HuggingFace model using ``AutoModel.from_pretrained()``. This is the most + flexible text encoder -- it works with any model on the HuggingFace Hub without + requiring a dedicated Ludwig encoder class. + + Use when: you want to use a model not covered by the named encoders (e.g., ModernBERT, + Mistral, custom fine-tuned models), or when you want a single configuration entry point + for any HuggingFace model. + + Note: ``pretrained_model_name_or_path`` is required (no default model). The encoder + auto-detects the model's forward signature to avoid passing unsupported arguments. + + Alternatives: Named encoders (BERT, RoBERTa, etc.) for model-specific parameter control, + LLMEncoder (for decoder-based LLMs with adapter/quantization support). + """ + + DEFAULT_MODEL_NAME = None + + def __init__( + self, + pretrained_model_name_or_path: str, + max_sequence_length: int, + reduce_output: str = "sum", + trainable: bool = False, + adapter: BaseAdapterConfig | None = None, + vocab_size: int | None = None, + pretrained_kwargs: dict | None = None, + encoder_config=None, + **kwargs, + ): + super().__init__() + + from transformers import AutoModel + + pretrained_kwargs = pretrained_kwargs or {} + transformer, _ = load_pretrained_hf_model_with_hub_fallback( + AutoModel, pretrained_model_name_or_path, **pretrained_kwargs + ) + self._maybe_resize_token_embeddings(transformer, vocab_size) + + self.config = self._init_config(transformer, [], encoder_config) + + # Precompute the set of params that are included in the forward signature of the AutoModel implementation so + # we can filter out unused params during the `forward` call. + self.forward_kwargs = set(inspect.signature(transformer.forward).parameters.keys()) + + self.transformer = self._wrap_transformer(transformer, adapter, trainable) + self.reduce_output = reduce_output + if self.reduce_output != "cls_pooled": + self.reduce_sequence = SequenceReducer( + reduce_mode=reduce_output, encoding_size=self.transformer.module.config.hidden_size + ) + self.max_sequence_length = max_sequence_length + + def _maybe_resize_token_embeddings(self, transformer, vocab_size: int | None = None): + """Overridden because AutoModel should use its own vocab size unless vocab size is explicitly specified.""" + if vocab_size is not None: + transformer.resize_token_embeddings(vocab_size) + self.vocab_size = vocab_size + else: + self.vocab_size = transformer.config.vocab_size + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + if mask is not None: + mask = mask.to(torch.int32) + + # The forward signature of AutoModel is not consistent across implementations, so we need to make sure we're + # only passing in params included in the forward signature. + kwargs = { + "input_ids": inputs, + "attention_mask": mask, + "token_type_ids": torch.zeros_like(inputs), + } + kwargs = {k: v for k, v in kwargs.items() if k in self.forward_kwargs} + + transformer_outputs = self.transformer.module(**kwargs) + if self.reduce_output == "cls_pooled": + # this works only if the user know that the specific model + # they want to use has the same outputs of + # the BERT base class call() function + hidden = transformer_outputs["pooler_output"] + else: + hidden = transformer_outputs["last_hidden_state"] + hidden = self.reduce_sequence(hidden, self.reduce_output) + return {ENCODER_OUTPUT: hidden} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return AutoTransformerConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + if self.reduce_output is None: + # TODO(justin): This may need to be conditioned on which AutoModel gets chosen. + return torch.Size([self.max_sequence_length, self.transformer.module.config.hidden_size]) + if self.reduce_output == "concat": + return torch.Size( + [ + self.max_sequence_length * self.transformer.module.config.hidden_size, + ] + ) + elif self.reduce_output == "concat": + # add the -2 to account of start and end tokens. + return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)]) + return torch.Size([self.transformer.module.config.hidden_size]) + + @property + def input_dtype(self) -> torch.dtype: + return torch.int32 + + +@DeveloperAPI +@register_encoder("tf_idf", [TEXT]) +class TfIdfEncoder(Encoder): + """TF-IDF (Term Frequency - Inverse Document Frequency) encoder. + + Classical sparse text representation that weights each token by its frequency in the + document (TF) multiplied by its rarity across the corpus (IDF). Produces a fixed-size + dense vector of size ``vocab_size``. + + Supports n-gram ranges via ``ngram_range`` (e.g., (1,2) for unigrams+bigrams) and + document-frequency pruning via ``max_df`` / ``min_df``. These parameters are applied + during preprocessing to build the vocabulary; at encoding time, this module uses the + pre-computed IDF weights over the final vocabulary. + + Use when: you want a simple, non-neural baseline, or when training data is very small + and pretrained transformers are overkill. No GPU required. + + Alternatives: Any HF encoder (BERT, DistilBERT, etc.) for contextual embeddings, + AutoTransformer for pretrained representations. + """ + + def __init__( + self, + max_sequence_length: int, + encoder_config=None, + str2idf=None, + vocab=None, + vocab_size: int | None = None, + ngram_range: tuple[int, int] = (1, 1), + max_df: float = 1.0, + min_df: int = 1, + **kwargs, + ): + super().__init__() + self.config = encoder_config + self.max_sequence_length = max_sequence_length + self.vocab_size = vocab_size + self.ngram_range = tuple(ngram_range) if ngram_range is not None else (1, 1) + self.max_df = max_df + self.min_df = min_df + + logger.debug(f" {self.name}") + + # Convert mapping of token -> frequency to a dense array + idf = np.zeros(vocab_size) + for i, s in enumerate(vocab): + idf[i] = str2idf[s] + self.register_buffer("idf", torch.from_numpy(idf).float().unsqueeze(0)) + + def forward(self, t: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + # Compute the term frequency within each row + tf = torch.stack([t_i.bincount(minlength=self.vocab_size) for t_i in torch.unbind(t.long())]) + + # Normalize the term frequency by the number of tokens in each row + tf = tf / tf.sum(dim=1).unsqueeze(-1) + + # Multiply the term frequency by the inverse document frequency + tfidf = tf * self.idf + + return {ENCODER_OUTPUT: tfidf} + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return TfIdfEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.vocab_size]) + + def get_embedding_layer(self) -> nn.Module: + return self + + +@DeveloperAPI +@register_encoder("llm", [TEXT]) +class LLMEncoder(Encoder): + """Large Language Model encoder for ECD (Encoder-Combiner-Decoder) architectures. + + Loads a causal language model (e.g., Llama, Mistral, Phi) and uses its last hidden state + as text representations. Supports PEFT adapters (LoRA, IA3, AdaLoRA) for parameter-efficient + fine-tuning and quantization (4-bit, 8-bit) for reduced memory usage. + + Use when: you want to use a modern decoder-only LLM as a text encoder within Ludwig's ECD + framework, especially with adapter-based fine-tuning. + + Alternatives: AutoTransformer (encoder-only HF models), named encoders (BERT, RoBERTa, etc.) + for smaller, bidirectional models. + """ + + # Per-adapter type prefixes for parameter names in the state dict, taken from + # https://github.com/huggingface/peft/blob/0f1e9091cc975eb5458cc163bf1843a34fb42b76/src/peft/utils/save_and_load.py#L173C9-L180 + ADAPTER_PARAM_NAME_PREFIX = { + "adalora": "lora_", + "ia3": "ia3_", + "lora": "lora_", + } + + def __init__(self, encoder_config: LLMEncoderConfig = None, **kwargs): + super().__init__() + self.register_load_state_dict_post_hook(self.remove_missing_non_adapter_keys) + + self.config = encoder_config + + self.adapter_is_initialized = False + + self.model_name = self.config.base_model + self.model_config = AutoConfig.from_pretrained(self.config.base_model) + + self.model = load_pretrained_from_config(self.config, model_config=self.model_config) + self.curr_device = next(self.model.parameters()).device + logger.info("Done.") + + self.context_len = get_context_len(self.model_config) + + # When merging input IDs and target IDs for LLM fine-tuning, we want to make sure that the merged tensor is + # not longer than the global maximum sequence length. This is provided in the preprocessing config. We never + # want to exceed the maximum possible context length so we also check for that. + # + # If the model uses RoPE scaling (e.g., YaRN, Linear, Dynamic NTK), adjust the effective context length + # by the scaling factor so we can use the extended context window. + effective_context_len = self.context_len + rope_scaling = getattr(self.model_config, "rope_scaling", None) + if rope_scaling and isinstance(rope_scaling, dict): + scaling_factor = rope_scaling.get("factor", 1.0) + effective_context_len = int(effective_context_len * scaling_factor) + logger.info( + f"Adjusted effective context length to {effective_context_len} " + f"using RoPE scaling factor {scaling_factor} (base context_len={self.context_len})" + ) + + if self.config.max_sequence_length: + max_sequence_length = self.config.max_sequence_length + self.max_sequence_length = ( + max_sequence_length if max_sequence_length <= effective_context_len else effective_context_len + ) + else: + self.max_sequence_length = effective_context_len + + # Initialize tokenizer + self.tokenizer = HFTokenizer(self.config.base_model).tokenizer + + self.attention_masks = None + + clear_data_cache() + + # Because we use the last hidden state as encoder output rather than the logits, the final module of the model + # has input pass through but no gradient update in the backward pass. This can lead to a distributed training + # error. Freezing the module prevents this. This is done at initialization to prevent "unused parameters" errors + # from happening when the encoder is used before `prepare_for_training` is called, for example during batch + # size tuning. + out_module = list(self.model.modules())[-1] + out_module.requires_grad_(requires_grad=False) + + @staticmethod + def get_schema_cls() -> type[BaseEncoderConfig]: + return LLMEncoderConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length, self.model_config.hidden_size]) + + def get_embedding_layer(self) -> nn.Module: + return self + + def initialize_adapter(self): + """If an adapter config is provided, we want to wrap the model with a PEFT model for fine-tuning.""" + if self.config.adapter: + self.model = initialize_adapter(self.model, self.config) + + logger.info("==================================================") + logger.info("Trainable Parameter Summary For LLM Encoder Fine-Tuning") + logger.info(f"Fine-tuning with adapter: {self.config.adapter.type}") + self.model.print_trainable_parameters() + logger.info("==================================================") + + self.adapter_is_initialized = True + + def prepare_for_training(self): + if self.config.quantization: + self.prepare_for_quantized_training() + self.initialize_adapter() + + def resume_from_checkpoint(self, checkpoint_path: str): + """Resume training from a checkpoint, handling quantized + adapter models correctly. + + When resuming a quantized model with adapters, we cannot simply load the full state dict + because quantization changes parameter dtypes/shapes. Instead, the correct flow is: + 1. The base model is already loaded with quantization (done in __init__). + 2. Prepare for quantized training (cast non-quantized layers to float32, freeze base). + 3. Re-initialize the PEFT adapter on top of the quantized base. + 4. Load only the adapter weights from the checkpoint. + + When resuming a quantized model without an adapter, the quantized base weights cannot be + restored from a standard state dict (bitsandbytes quantization is applied at load time). + The model is already correctly quantized from __init__; we only need to prepare it for + training. No weights need loading from the checkpoint because the base model weights are + always re-quantized from the pretrained HuggingFace checkpoint. + + For non-quantized models, standard state_dict loading works via Ludwig's default mechanism. + """ + import os + + if self.config.quantization and self.config.adapter: + logger.info("Resuming quantized adapter model from checkpoint.") + + # Step 1: Prepare quantized base model for training (freeze + cast). + # prepare_for_quantized_training is idempotent, so safe to call on every resume. + if not self.adapter_is_initialized: + self.prepare_for_quantized_training() + + # Step 2: Initialize adapter on quantized base if not already done + if not self.adapter_is_initialized: + self.initialize_adapter() + + # Step 3: Load adapter weights from checkpoint + adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX.get(self.config.adapter.type, "") + checkpoint_file = os.path.join(checkpoint_path, "model", "model_weights") + if os.path.exists(checkpoint_file): + checkpoint_sd = torch.load(checkpoint_file, map_location="cpu", weights_only=True) + adapter_sd = {k: v for k, v in checkpoint_sd.items() if adapter_type_prefix in k} + if adapter_sd: + from peft.utils.save_and_load import set_peft_model_state_dict + + set_peft_model_state_dict(self.model, adapter_sd) + logger.info(f"Loaded {len(adapter_sd)} adapter weight tensors from checkpoint.") + else: + logger.warning( + "No adapter weights found in checkpoint. The model will start with freshly initialized " + "adapter weights on top of the quantized base model." + ) + else: + logger.warning(f"Checkpoint file not found at {checkpoint_file}. Starting with fresh adapter weights.") + elif self.config.quantization and not self.config.adapter: + # Quantized model without adapter: bitsandbytes quantization is applied at load time + # in __init__, so the base weights cannot be restored from a standard state dict. + # The model is already correctly quantized; we only need to prepare it for training. + logger.info( + "Resuming quantized model (no adapter) from checkpoint. " + "Base model weights are always loaded fresh from the pretrained HuggingFace checkpoint; " + "no weight restoration from Ludwig checkpoint is needed." + ) + self.prepare_for_quantized_training() + else: + # Non-quantized case: standard state_dict loading is handled by Ludwig's trainer + logger.info("Resuming from checkpoint (non-quantized path).") + if not self.adapter_is_initialized: + self.prepare_for_training() + + def prepare_for_quantized_training(self): + from peft import prepare_model_for_kbit_training + + self.model = prepare_model_for_kbit_training(self.model, use_gradient_checkpointing=False) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None): + # Get the hidden state of the last layer and return it as the text encoding + model_outputs = self.model(input_ids=inputs, output_hidden_states=True).hidden_states[-1] + + return {ENCODER_OUTPUT: model_outputs.type(torch.float32)} + + def _save_to_state_dict(self, destination: dict, prefix: str, keep_vars: bool): + # This is called by `torch.nn.Module.state_dict()` under the hood. `state_dict()` does additional work to + # prep the dictionary, get submodule state, and run hooks. Overriding this method only impacts the + # contents of the state_dict. + # The three args to this method are supplied by Module.state_dict + # https://github.com/pytorch/pytorch/blob/8739d1e3f9b08f4282fe79fc8dacd781d16913ff/torch/nn/modules/module.py#L1824 + if self.config.adapter and self.adapter_is_initialized: + # get_peft_model_state_dict geneates a state dict that only contains the adapter weights + from peft.utils.save_and_load import get_peft_model_state_dict + + sd = get_peft_model_state_dict(self.model) + destination.update(sd) + + else: + super()._save_to_state_dict(destination, prefix=prefix, keep_vars=keep_vars) + + def state_dict(self, *args, destination=None, prefix="", keep_vars=False): + destination = super().state_dict(destination=destination, prefix=prefix, keep_vars=keep_vars) + + if self.config.adapter and self.adapter_is_initialized: + adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX[self.config.adapter.type] + exclude_model_keys = [k for k in destination if adapter_type_prefix not in k] + + for k in exclude_model_keys: + del destination[k] + + return destination + + def _load_from_state_dict( + self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs + ): + # Call this first to make sure torch can do its usual load. In the adapter case, this should essentially be a + # no-op, but the adapter weights will be collected in `unexpected_keys` because PEFT changes the parameter + # names under the hood. + + super()._load_from_state_dict( + state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs + ) + + if self.config.adapter and self.adapter_is_initialized: + # When using an adapter, only the adapter weights are saved, and so we only want to load those weights. + # Under the hood, PEFT alters the names of the parameters, which leads to an "unexpected keys" error when + # using strict mode. This block uses PEFT's version of `load_state_dict` to handle loading in weights. + from peft.utils.save_and_load import set_peft_model_state_dict + + adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX[self.config.adapter.type] + peft_model_state_dict = {k: v for k, v in state_dict.items() if adapter_type_prefix in k} + set_peft_model_state_dict(self.model, peft_model_state_dict) + + def remove_missing_non_adapter_keys(self, module, incompatible_keys): + """Update the missing and unexpected keys lists to reflect custom adapter state load logic. + + This method should never return anything unless the underlying torch hook logic is updated. Any changes to the + lists in `incompatible_keys` must be made in-place. + + Args: + module: The torch module with newly loaded state + incompatible_keys: A tuple with the lists of missing and unexpected keys that were recorded while loading + """ + # If no adapter was used, `LLMEncoder.load_state_dict` should use the default `torch.Module.load_state_dict` + # code path to load weights and no modification should be necessary. + if self.config.adapter and self.adapter_is_initialized: + adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX[self.config.adapter.type] + missing_keys, unexpected_keys = incompatible_keys + + # The state dict uses fully qualified parameter names, but this function does not have access to the + # fully qualified names or a prefix to recreate them. Iterate over the missing keys and greedily select the + # first non-adapter key that shares a suffix with a model parameter name. + sample_missing_key = "" + sample_model_key = "" + for k in missing_keys: + # Exclude any adapter weight--those should not be missing. Let torch handle that downstream. + if adapter_type_prefix not in k: + sample_model_keys = [p for p, _ in self.named_parameters() if p in k] + if sample_model_keys: + sample_model_key = sample_model_keys[0] + sample_missing_key = k + break + sd_prefix = sample_missing_key.replace(sample_model_key, "") + + # When loading the adapter weights in strict mode, torch will register the base model weights as missing + # from the state dict and raise an exception. The base model weights are intended to be excluded, so the + # missing_keys list is updated post-load to avoid the error. + for k, _ in self.named_parameters(): + full_name = f"{sd_prefix}{k}" + if full_name in missing_keys and adapter_type_prefix not in full_name: + missing_keys.remove(full_name) + + # peft changes the adapter parameter names under the hood to include the adapter name. When retreiving the + # adapter state dict, however, the name is not included. This causes the adpater weights to be recorded as + # unexpected parameters. `LLMEncoder._load_from_state_dict` loads the adapter parameters using a peft + # utility that accounts for the updated names, so here we remove any adapter parameters from the unexpected + # keys list to avoid errors. + from peft.utils.save_and_load import get_peft_model_state_dict + + sd = get_peft_model_state_dict(self.model) + for k in sd: + if k in unexpected_keys: + unexpected_keys.remove(k) diff --git a/ludwig/encoders/timeseries_encoders.py b/ludwig/encoders/timeseries_encoders.py new file mode 100644 index 0000000..e87faba --- /dev/null +++ b/ludwig/encoders/timeseries_encoders.py @@ -0,0 +1,216 @@ +import torch +import torch.nn as nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, TIMESERIES +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.encoders.types import EncoderOutputDict +from ludwig.schema.encoders.timeseries_encoders import NBEATSConfig, PatchTSTConfig + + +@DeveloperAPI +@register_encoder("patchtst", [TIMESERIES]) +class PatchTSTEncoder(Encoder): + """PatchTST encoder. + + Splits the input time series into fixed-length patches and encodes them with a Transformer. + Channel-independent: each feature dimension is processed independently. + Reference: "A Time Series is Worth 64 Words" (Nie et al., 2023). https://arxiv.org/abs/2211.14730 + """ + + def __init__( + self, + max_sequence_length: int, + patch_size: int = 16, + patch_stride: int = 8, + d_model: int = 128, + num_heads: int = 8, + num_layers: int = 3, + ffn_dim: int = 256, + dropout: float = 0.1, + output_size: int = 256, + reduce_output: str = "mean", + encoder_config: PatchTSTConfig | None = None, + **kwargs, + ): + super().__init__() + self.patch_size = patch_size + self.patch_stride = patch_stride + self.reduce_output = reduce_output + self.output_size = output_size + self.max_sequence_length = max_sequence_length + + # Patch projection + self.patch_proj = nn.Linear(patch_size, d_model) + + # Positional encoding (learnable) + num_patches = max(1, (max_sequence_length - patch_size) // patch_stride + 1) + self.pos_embed = nn.Parameter(torch.zeros(1, num_patches, d_model)) + nn.init.trunc_normal_(self.pos_embed, std=0.02) + + encoder_layer = nn.TransformerEncoderLayer( + d_model=d_model, + nhead=num_heads, + dim_feedforward=ffn_dim, + dropout=dropout, + batch_first=True, + norm_first=True, + ) + self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) + self.norm = nn.LayerNorm(d_model) + self.head = nn.Linear(d_model, output_size) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + # inputs: [batch, seq_len] or [batch, seq_len, channels] + if inputs.dim() == 2: + x = inputs.unsqueeze(-1) # [B, T, 1] + else: + x = inputs # [B, T, C] + + B, T, C = x.shape + + # Process each channel independently by merging into batch dimension + x = x.permute(0, 2, 1) # [B, C, T] + x = x.reshape(B * C, T) # [B*C, T] + + # Extract patches via unfold: [B*C, num_patches, patch_size] + x_patched = x.unfold(dimension=1, size=self.patch_size, step=self.patch_stride) + + # Project patches to d_model + x_patched = self.patch_proj(x_patched) # [B*C, num_patches, d_model] + + # Add positional embedding (clip to actual num_patches) + num_patches = x_patched.shape[1] + x_patched = x_patched + self.pos_embed[:, :num_patches, :] + + # Transformer encoder + x_enc = self.transformer(x_patched) # [B*C, num_patches, d_model] + x_enc = self.norm(x_enc) + + # Reduce across patches + if self.reduce_output == "mean": + x_rep = x_enc.mean(dim=1) # [B*C, d_model] + elif self.reduce_output == "last": + x_rep = x_enc[:, -1, :] + else: # first + x_rep = x_enc[:, 0, :] + + # Average over channels to get per-sample representation + x_rep = x_rep.reshape(B, C, -1).mean(dim=1) # [B, d_model] + out = self.head(x_rep) # [B, output_size] + + return {ENCODER_OUTPUT: out} + + @staticmethod + def get_schema_cls(): + return PatchTSTConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.output_size]) + + +class NBEATSBlock(nn.Module): + """A single N-BEATS block with backcast and forecast branches.""" + + def __init__(self, input_size: int, theta_size: int, layer_size: int, num_layers: int, dropout: float): + super().__init__() + layers = [] + in_size = input_size + for _ in range(num_layers): + layers.extend([nn.Linear(in_size, layer_size), nn.ReLU()]) + if dropout > 0: + layers.append(nn.Dropout(dropout)) + in_size = layer_size + self.fc = nn.Sequential(*layers) + self.backcast_head = nn.Linear(layer_size, input_size) + self.forecast_head = nn.Linear(layer_size, theta_size) + + def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + h = self.fc(x) + backcast = self.backcast_head(h) + forecast = self.forecast_head(h) + return backcast, forecast + + +@DeveloperAPI +@register_encoder("nbeats", [TIMESERIES]) +class NBEATSEncoder(Encoder): + """N-BEATS encoder. + + Pure MLP architecture with doubly residual stacking. Each block produces a backcast (reconstruction) + and a forecast contribution. Residuals are passed between blocks. The encoder returns a fixed-size + learned representation from the aggregated forecast vectors. + Reference: "N-BEATS" (Oreshkin et al., 2020). https://arxiv.org/abs/1905.10437 + """ + + def __init__( + self, + max_sequence_length: int, + num_stacks: int = 2, + num_blocks: int = 3, + num_layers: int = 4, + layer_size: int = 256, + output_size: int = 256, + dropout: float = 0.0, + encoder_config: NBEATSConfig | None = None, + **kwargs, + ): + super().__init__() + self.output_size = output_size + self.max_sequence_length = max_sequence_length + input_size = max_sequence_length + theta_size = output_size + + self.stacks = nn.ModuleList() + for _ in range(num_stacks): + stack = nn.ModuleList() + for _ in range(num_blocks): + block = NBEATSBlock(input_size, theta_size, layer_size, num_layers, dropout) + stack.append(block) + self.stacks.append(stack) + + self.output_proj = nn.Linear(theta_size, output_size) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict: + # inputs: [batch, seq_len] or [batch, seq_len, C] + if inputs.dim() == 3: + B, T, C = inputs.shape + x = inputs.permute(0, 2, 1).reshape(B * C, T) + else: + B, T = inputs.shape + C = 1 + x = inputs # [B, T] + + residual = x + forecast = torch.zeros(x.shape[0], self.output_size, device=x.device, dtype=x.dtype) + + for stack in self.stacks: + for block in stack: + backcast, block_forecast = block(residual) + residual = residual - backcast + forecast = forecast + block_forecast + + # Average over channels if multi-channel input + if C > 1: + forecast = forecast.reshape(B, C, -1).mean(dim=1) # [B, output_size] + + out = self.output_proj(forecast) # [B, output_size] + return {ENCODER_OUTPUT: out} + + @staticmethod + def get_schema_cls(): + return NBEATSConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.output_size]) diff --git a/ludwig/encoders/types.py b/ludwig/encoders/types.py new file mode 100644 index 0000000..5dc0732 --- /dev/null +++ b/ludwig/encoders/types.py @@ -0,0 +1,9 @@ +from typing import TypedDict + +import torch + + +class EncoderOutputDict(TypedDict, total=False): + encoder_output: torch.Tensor + encoder_output_state: torch.Tensor # only used by sequence and h3 encoders + attentions: torch.Tensor # only used by the vit legacy encoder diff --git a/ludwig/error.py b/ludwig/error.py new file mode 100644 index 0000000..d470718 --- /dev/null +++ b/ludwig/error.py @@ -0,0 +1,72 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from ludwig.api_annotations import PublicAPI + + +@PublicAPI +class LudwigError(Exception): + """Base class for all custom exceptions raised by the Ludwig framework.""" + + def __reduce__(self): + """Docs: https://docs.python.org/3/library/pickle.html#object.__reduce__.""" + raise NotImplementedError( + "Implement __reduce__ for all subclasses of LudwigError as it's necessary for " + "serialization by Ray. See https://github.com/ludwig-ai/ludwig/pull/2695." + ) + + +@PublicAPI +class InputDataError(LudwigError, ValueError): + """Exception raised for errors in the input data. + + Appropriate for data which is not convertible to the input feature type, columns with all missing values, + categorical columns with only one category, etc... + + Attributes: + column - The name of the input column which caused the error + feature_type - The Ludwig feature type which caused the error (number, binary, category...). + message - An error message describing the situation. + """ + + def __init__(self, column_name: str, feature_type: str, message: str): + self.column_name = column_name + self.feature_type = feature_type + self.message = message + super().__init__(message) + + def __str__(self): + return f'Column "{self.column_name}" as {self.feature_type} feature: {self.message}' + + def __reduce__(self): + return type(self), (self.column_name, self.feature_type, self.message) + + +@PublicAPI +class ConfigValidationError(LudwigError, ValueError): + """Exception raised for errors in the Ludwig configuration. + + Appropriate for bad configuration values, missing required configuration values, etc... + + Attributes: + message - An error message describing the situation. + """ + + def __init__(self, message: str): + self.message = message + super().__init__(message) + + def __reduce__(self): + return type(self), (self.message,) diff --git a/ludwig/evaluate.py b/ludwig/evaluate.py new file mode 100644 index 0000000..b780bc5 --- /dev/null +++ b/ludwig/evaluate.py @@ -0,0 +1,242 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import sys + +import pandas as pd + +from ludwig.api import LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.constants import FULL, TEST, TRAINING, VALIDATION +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + + +def evaluate_cli( + model_path: str, + dataset: str | dict | pd.DataFrame = None, + data_format: str | None = None, + split: str = FULL, + batch_size: int = 128, + skip_save_unprocessed_output: bool = False, + skip_save_predictions: bool = False, + skip_save_eval_stats: bool = False, + skip_collect_predictions: bool = False, + skip_collect_overall_stats: bool = False, + output_directory: str = "results", + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + logging_level: int = logging.INFO, + **kwargs, +) -> None: + """Loads pre-trained model and evaluates its performance by comparing the predictions against ground truth. + + Args: + model_path: Filepath to pre-trained model. + dataset: Source containing the entire dataset to be used in the evaluation. + data_format: Format to interpret data sources. Will be inferred automatically if not specified. + Valid formats are 'auto', 'csv', 'excel', 'feather', 'fwf', 'hdf5' (cache file produced + during previous training), 'html' (file containing a single HTML table), 'json', 'jsonl', + 'parquet', 'pickle' (pickled Pandas DataFrame), 'sas', 'spss', 'stata', 'tsv'. + split: Split on which to perform predictions. Valid values are 'training', 'validation', + 'test' and 'full'. + batch_size: Size of batches for processing. + skip_save_unprocessed_output: By default predictions and their probabilities are saved in both + raw unprocessed numpy files containing tensors and as postprocessed CSV files (one for each + output feature). If True, only the CSV ones are saved and the numpy ones are skipped. + skip_save_predictions: Skips saving test predictions CSV files. + skip_save_eval_stats: Skips saving test statistics JSON file. + skip_collect_predictions: Skips collecting post-processed predictions during eval. + skip_collect_overall_stats: Skips collecting overall stats during eval. + output_directory: The directory that will contain the training statistics, TensorBoard logs, + the saved model and the training progress files. + gpus: List of GPUs that are available for training. + gpu_memory_limit: Maximum memory fraction [0, 1] allowed to allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading parallelism to improve performance + at the cost of determinism. + callbacks: A list of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline. + backend: Backend or string name of backend to use to execute preprocessing / training steps. + logging_level: Log level that will be sent to stderr. + """ + model = LudwigModel.load( + model_path, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + model.evaluate( + dataset=dataset, + data_format=data_format, + batch_size=batch_size, + split=split, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + collect_predictions=not skip_collect_predictions, + collect_overall_stats=not skip_collect_overall_stats, + output_directory=output_directory, + return_type="dict", + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script loads a pretrained model " + "and evaluates its performance by comparing" + "its predictions with ground truth.", + prog="ludwig evaluate", + usage="%(prog)s [options]", + ) + + # --------------- + # Data parameters + # --------------- + parser.add_argument("--dataset", help="input data file path", required=True) + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "htmltables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + parser.add_argument( + "-s", "--split", default=FULL, choices=[TRAINING, VALIDATION, TEST, FULL], help="the split to test the model on" + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=True) + + # ------------------------- + # Output results parameters + # ------------------------- + parser.add_argument( + "-od", "--output_directory", type=str, default="results", help="directory that contains the results" + ) + parser.add_argument( + "-ssuo", + "--skip_save_unprocessed_output", + help="skips saving intermediate NPY output files", + action="store_true", + default=False, + ) + parser.add_argument( + "-sses", + "--skip_save_eval_stats", + help="skips saving intermediate JSON eval statistics", + action="store_true", + default=False, + ) + parser.add_argument( + "-scp", "--skip_collect_predictions", help="skips collecting predictions", action="store_true", default=False + ) + parser.add_argument( + "-scos", + "--skip_collect_overall_stats", + help="skips collecting overall stats", + action="store_true", + default=False, + ) + + # ------------------ + # Generic parameters + # ------------------ + parser.add_argument("-bs", "--batch_size", type=int, default=128, help="size of batches") + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument("-g", "--gpus", type=int, default=0, help="list of gpu to use") + parser.add_argument( + "-gml", + "--gpu_memory_limit", + type=float, + default=None, + help="maximum memory fraction [0, 1] allowed to allocate per GPU device", + ) + parser.add_argument( + "-dpt", + "--disable_parallel_threads", + action="store_false", + dest="allow_parallel_threads", + help="disable PyTorch from using multithreading for reproducibility", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + args.evaluate_performance = True + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("evaluate", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.test_performance") + + backend = initialize_backend(args.backend) + if backend.is_coordinator(): + print_ludwig("Evaluate", LUDWIG_VERSION) + logger.info(f"Dataset path: {args.dataset}") + logger.info(f"Model path: {args.model_path}") + logger.info("") + + evaluate_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/experiment.py b/ludwig/experiment.py new file mode 100644 index 0000000..5894d0e --- /dev/null +++ b/ludwig/experiment.py @@ -0,0 +1,472 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import os +import sys + +import pandas as pd + +from ludwig.api import kfold_cross_validate, LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.constants import CONTINUE_PROMPT, FULL, HYPEROPT, HYPEROPT_WARNING, TEST, TRAINING, VALIDATION +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.data_utils import load_config_from_str, load_yaml, save_json +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig, query_yes_no + +logger = logging.getLogger(__name__) + + +def experiment_cli( + config: str | dict, + dataset: str | dict | pd.DataFrame = None, + training_set: str | dict | pd.DataFrame = None, + validation_set: str | dict | pd.DataFrame = None, + test_set: str | dict | pd.DataFrame = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + experiment_name: str = "experiment", + model_name: str = "run", + model_load_path: str | None = None, + model_resume_path: str | None = None, + eval_split: str = TEST, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = False, + skip_save_unprocessed_output: bool = False, + skip_save_predictions: bool = False, + skip_save_eval_stats: bool = False, + skip_collect_predictions: bool = False, + skip_collect_overall_stats: bool = False, + output_directory: str = "results", + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + random_seed: int = default_random_seed, + logging_level: int = logging.INFO, + **kwargs, +): + """Train a model and evaluate it on a test split, saving both model and statistics. + + Args: + config: In-memory config dict or path to a YAML config file. + dataset: Source containing the entire dataset. If it has a split + column, it will be used for splitting (0: train, 1: validation, + 2: test); otherwise the dataset will be randomly split. + training_set: Source containing training data. + validation_set: Source containing validation data. + test_set: Source containing test data. + training_set_metadata: Metadata JSON file or loaded metadata dict. + data_format: Format to interpret data sources. Inferred automatically + if not specified. Valid values: ``'auto'``, ``'csv'``, + ``'excel'``, ``'feather'``, ``'fwf'``, ``'hdf5'``, + ``'html'``, ``'json'``, ``'jsonl'``, ``'parquet'``, + ``'pickle'``, ``'sas'``, ``'spss'``, ``'stata'``, ``'tsv'``. + experiment_name: Name for the experiment. + model_name: Name of the model being used. + model_load_path: If specified, load this pre-trained model as + initialization (useful for transfer learning). + model_resume_path: Resume training from this checkpoint directory. + eval_split: Split to evaluate on. Valid values: ``'training'``, + ``'validation'``, ``'test'``. + skip_save_training_description: Disable saving the description JSON + file. + skip_save_training_statistics: Disable saving training statistics + JSON file. + skip_save_model: Disable saving model weights after each epoch the + validation metric improves. + skip_save_progress: Disable saving weights and stats after each epoch. + skip_save_log: Disable saving TensorBoard logs. + skip_save_processed_input: Disable caching preprocessed input. + skip_save_unprocessed_output: If ``True``, skip saving raw numpy + output files; only postprocessed CSV files are saved. + skip_save_predictions: Disable saving test prediction CSV files. + skip_save_eval_stats: Disable saving test statistics JSON file. + skip_collect_predictions: Skip collecting postprocessed predictions + during evaluation. + skip_collect_overall_stats: Skip collecting overall stats during + evaluation. + output_directory: Directory that will contain all results. + gpus: List of GPUs available for training. + gpu_memory_limit: Maximum memory fraction ``[0, 1]`` allowed to + allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading + parallelism. + callbacks: List of ``Callback`` objects providing hooks into the + Ludwig pipeline. + backend: Backend or string name of the backend to use. + random_seed: Random seed for weights initialization, splits, and + shuffling. + logging_level: Log level sent to stderr. + + Returns: + Tuple of ``(model, eval_stats, train_stats, preprocessed_data, + output_directory)`` where ``model`` is the trained ``LudwigModel``, + ``eval_stats`` are per-split evaluation metrics, ``train_stats`` + are per-epoch training metrics, ``preprocessed_data`` is a tuple of + ``(training_set, validation_set, test_set)``, and + ``output_directory`` is the path where results were saved. + """ + if HYPEROPT in config: + if not query_yes_no(HYPEROPT_WARNING + CONTINUE_PROMPT): + exit(1) + + if isinstance(config, str): + config = load_yaml(config) + backend = initialize_backend(backend or config.get("backend")) + + if model_load_path: + model = LudwigModel.load( + model_load_path, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + else: + model = LudwigModel( + config=config, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + eval_stats, train_stats, preprocessed_data, output_directory = model.experiment( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + model_resume_path=model_resume_path, + eval_split=eval_split, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + skip_collect_predictions=skip_collect_predictions, + skip_collect_overall_stats=skip_collect_overall_stats, + output_directory=output_directory, + random_seed=random_seed, + ) + + return model, eval_stats, train_stats, preprocessed_data, output_directory + + +def kfold_cross_validate_cli( + k_fold, + config=None, + dataset=None, + data_format=None, + output_directory="results", + random_seed=default_random_seed, + skip_save_k_fold_split_indices=False, + **kwargs, +): + """Run k-fold cross validation and save results to ``output_directory``. + + Args: + k_fold: Number of folds to create for cross-validation. + config: Config dict or path to a YAML config file. + dataset: Dataset source. + data_format: Format to interpret the dataset. + output_directory: Directory into which to write results. + random_seed: Random seed used for k-fold splits. + skip_save_k_fold_split_indices: If ``True``, skip saving the per-fold + split index arrays. + """ + + kfold_cv_stats, kfold_split_indices = kfold_cross_validate( + k_fold, + config=config, + dataset=dataset, + data_format=data_format, + output_directory=output_directory, + random_seed=random_seed, + ) + + # save k-fold cv statistics + save_json(os.path.join(output_directory, "kfold_training_statistics.json"), kfold_cv_stats) + + # save k-fold split indices + if not skip_save_k_fold_split_indices: + save_json(os.path.join(output_directory, "kfold_split_indices.json"), kfold_split_indices) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script trains and evaluates a model", prog="ludwig experiment", usage="%(prog)s [options]" + ) + + # ---------------------------- + # Experiment naming parameters + # ---------------------------- + parser.add_argument("--output_directory", type=str, default="results", help="directory that contains the results") + parser.add_argument("--experiment_name", type=str, default="experiment", help="experiment name") + parser.add_argument("--model_name", type=str, default="run", help="name for the model") + + # --------------- + # Data parameters + # --------------- + parser.add_argument( + "--dataset", + help="input data file path. " + "If it has a split column, it will be used for splitting " + "(0: train, 1: validation, 2: test), " + "otherwise the dataset will be randomly split", + ) + parser.add_argument("--training_set", help="input train data file path") + parser.add_argument("--validation_set", help="input validation data file path") + parser.add_argument("--test_set", help="input test data file path") + + parser.add_argument( + "--training_set_metadata", + help="input metadata JSON file path. An intermediate preprocessed file " + "containing the mappings of the input file created " + "the first time a file is used, in the same directory " + "with the same name and a .json extension", + ) + + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "htmltables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + + parser.add_argument( + "-es", + "--eval_split", + default=TEST, + choices=[TRAINING, VALIDATION, TEST, FULL], + help="the split to evaluate the model on", + ) + + parser.add_argument( + "-sspi", + "--skip_save_processed_input", + help="skips saving intermediate HDF5 and JSON files", + action="store_true", + default=False, + ) + parser.add_argument( + "-ssuo", + "--skip_save_unprocessed_output", + help="skips saving intermediate NPY output files", + action="store_true", + default=False, + ) + + # ----------------- + # K-fold parameters + # ----------------- + parser.add_argument( + "-kf", "--k_fold", type=int, default=None, help="number of folds for a k-fold cross validation run " + ) + parser.add_argument( + "-skfsi", + "--skip_save_k_fold_split_indices", + action="store_true", + default=False, + help="disables saving indices generated to split training data set " + "for the k-fold cross validation run, but if it is not needed " + "turning it off can slightly increase the overall speed", + ) + + # ---------------- + # Model parameters + # ---------------- + config = parser.add_mutually_exclusive_group(required=True) + config.add_argument( + "-c", + "--config", + type=load_yaml, + help="Path to the YAML file containing the model configuration", + ) + config.add_argument( + "-cs", + "--config_str", + dest="config", + type=load_config_from_str, + help="JSON or YAML serialized string of the model configuration", + ) + + parser.add_argument("-mlp", "--model_load_path", help="path of a pretrained model to load as initialization") + parser.add_argument("-mrp", "--model_resume_path", help="path of the model directory to resume training of") + parser.add_argument( + "-sstd", + "--skip_save_training_description", + action="store_true", + default=False, + help="disables saving the description JSON file", + ) + parser.add_argument( + "-ssts", + "--skip_save_training_statistics", + action="store_true", + default=False, + help="disables saving training statistics JSON file", + ) + parser.add_argument( + "-sstp", + "--skip_save_predictions", + help="skips saving test predictions CSV files", + action="store_true", + default=False, + ) + parser.add_argument( + "-sstes", + "--skip_save_eval_stats", + help="skips saving eval statistics JSON file", + action="store_true", + default=False, + ) + parser.add_argument( + "-ssm", + "--skip_save_model", + action="store_true", + default=False, + help="disables saving model weights and hyperparameters each time " + "the model improves. " + "By default Ludwig saves model weights after each epoch " + "the validation metric improves, but if the model is really big " + "that can be time consuming. If you do not want to keep " + "the weights and just find out what performance a model can get " + "with a set of hyperparameters, use this parameter to skip it," + "but the model will not be loadable later on", + ) + parser.add_argument( + "-ssp", + "--skip_save_progress", + action="store_true", + default=False, + help="disables saving progress each epoch. By default Ludwig saves " + "weights and stats after each epoch for enabling resuming " + "of training, but if the model is really big that can be " + "time consuming and will uses twice as much space, use " + "this parameter to skip it, but training cannot be resumed " + "later on", + ) + parser.add_argument( + "-ssl", + "--skip_save_log", + action="store_true", + default=False, + help="disables saving TensorBoard logs. By default Ludwig saves " + "logs for the TensorBoard, but if it is not needed turning it off " + "can slightly increase the overall speed", + ) + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument( + "-rs", + "--random_seed", + type=int, + default=42, + help="a random seed that is going to be used anywhere there is a call " + "to a random number generator: data splitting, parameter " + "initialization and training set shuffling", + ) + parser.add_argument("-g", "--gpus", nargs="+", type=int, default=None, help="list of GPUs to use") + parser.add_argument( + "-gml", + "--gpu_memory_limit", + type=float, + default=None, + help="maximum memory fraction [0, 1] allowed to allocate per GPU device", + ) + parser.add_argument( + "-dpt", + "--disable_parallel_threads", + action="store_false", + dest="allow_parallel_threads", + help="disable PyTorch from using multithreading for reproducibility", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("experiment", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.experiment") + + args.backend = initialize_backend(args.backend or args.config.get("backend")) + if args.backend.is_coordinator(): + print_ludwig("Experiment", LUDWIG_VERSION) + + if args.k_fold is None: + experiment_cli(**vars(args)) + else: + kfold_cross_validate_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/experiment_utils.py b/ludwig/experiment_utils.py new file mode 100644 index 0000000..ae615c8 --- /dev/null +++ b/ludwig/experiment_utils.py @@ -0,0 +1,105 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Utilities for generating experiment metadata and compute descriptions. + +These utilities are used by ``LudwigModel`` and by the experiment CLI, and +are kept here to avoid coupling the CLI (``experiment.py``) to the full +``api.py`` module. +""" + +import sys +from collections import OrderedDict + +import pandas as pd +import torch + +from ludwig.api_annotations import PublicAPI +from ludwig.backend import Backend +from ludwig.globals import LUDWIG_VERSION +from ludwig.types import ModelConfigDict, TrainingSetMetadataDict +from ludwig.utils.data_utils import figure_data_format +from ludwig.utils.misc_utils import get_commit_hash + + +def _get_compute_description(backend: Backend) -> dict: + """Returns the compute description for the backend.""" + compute_description = {"num_nodes": backend.num_nodes} + + if torch.cuda.is_available(): + # Assumption: All nodes are of the same instance type (not yet verified across Ray workers). + compute_description.update( + { + "gpus_per_node": torch.cuda.device_count(), + "arch_list": torch.cuda.get_arch_list(), + "gencode_flags": torch.cuda.get_gencode_flags(), + "devices": {}, + } + ) + for i in range(torch.cuda.device_count()): + compute_description["devices"][i] = { + "gpu_type": torch.cuda.get_device_name(i), + "device_capability": torch.cuda.get_device_capability(i), + "device_properties": str(torch.cuda.get_device_properties(i)), + } + + return compute_description + + +@PublicAPI +def get_experiment_description( + config: ModelConfigDict, + dataset: str | dict | pd.DataFrame | None = None, + training_set: str | dict | pd.DataFrame | None = None, + validation_set: str | dict | pd.DataFrame | None = None, + test_set: str | dict | pd.DataFrame | None = None, + training_set_metadata: TrainingSetMetadataDict | None = None, + data_format: str | None = None, + backend: Backend | None = None, + random_seed: int | None = None, +) -> dict: + description = OrderedDict() + description["ludwig_version"] = LUDWIG_VERSION + description["command"] = " ".join(sys.argv) + + commit_hash = get_commit_hash() + if commit_hash is not None: + description["commit_hash"] = commit_hash[:12] + + if random_seed is not None: + description["random_seed"] = random_seed + + if isinstance(dataset, str): + description["dataset"] = dataset + if isinstance(training_set, str): + description["training_set"] = training_set + if isinstance(validation_set, str): + description["validation_set"] = validation_set + if isinstance(test_set, str): + description["test_set"] = test_set + if training_set_metadata is not None: + description["training_set_metadata"] = training_set_metadata + + # determine data format if not provided or auto + if not data_format or data_format == "auto": + data_format = figure_data_format(dataset, training_set, validation_set, test_set) + + if data_format: + description["data_format"] = str(data_format) + + description["config"] = config + description["torch_version"] = torch.__version__ + description["compute"] = _get_compute_description(backend) + + return description diff --git a/ludwig/explain/__init__.py b/ludwig/explain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/explain/captum.py b/ludwig/explain/captum.py new file mode 100644 index 0000000..690240f --- /dev/null +++ b/ludwig/explain/captum.py @@ -0,0 +1,540 @@ +import copy +import gc +import logging +from collections import defaultdict +from dataclasses import dataclass + +import numpy as np +import numpy.typing as npt +import pandas as pd +import torch +from captum.attr import LayerIntegratedGradients, TokenReferenceBase +from captum.attr._utils.input_layer_wrapper import InputIdentity +from torch.autograd import Variable +from tqdm import tqdm + +from ludwig.api import LudwigModel +from ludwig.api_annotations import PublicAPI +from ludwig.constants import ( + BINARY, + CATEGORY, + DATE, + IMAGE, + INPUT_FEATURES, + MINIMUM_BATCH_SIZE, + NAME, + NUMBER, + PREPROCESSING, + SEQUENCE, + SET, + TEXT, + UNKNOWN_SYMBOL, +) +from ludwig.data.preprocessing import preprocess_for_prediction +from ludwig.explain.explainer import Explainer +from ludwig.explain.explanation import ExplanationsResult +from ludwig.explain.util import get_pred_col, replace_layer_with_copy +from ludwig.features.feature_utils import LudwigFeatureDict +from ludwig.models.ecd import ECD +from ludwig.utils.torch_utils import DEVICE + +logger = logging.getLogger(__name__) + +# These types as provided as integer values and passed through an embedding layer that breaks integrated gradients. +# As such, we need to take care to encode them before handing them to the explainer. +EMBEDDED_TYPES = {SEQUENCE, TEXT, CATEGORY, SET, DATE} + + +@dataclass +class ExplanationRunConfig: + """Mutable state containing runtime configuration for explanation process. + + This is useful for updating the batch size used during explanation so it can be propagated across calls to + `get_total_attribution`. + """ + + batch_size: int + + +def retry_with_halved_batch_size(run_config: ExplanationRunConfig): + """Function wrapper that retries an fn with a halved batch size. + + We want to maintain as large of a batch size as possible to maximize throughput. However, calculating explanations + requires significantly more memory, and the original batch sized used during training may be too large and cause a + CUDA OOM error, for example, if using GPUs. + + Will raise an error if a non-OOM error is raised, or if the batch size is reduced below 1 and the fn still fails. + """ + + def retry_with_halved_batch_size_fn(fn): + def retry_with_halved_batch_size_wrapper(*args, **kwargs): + latest_error = None + while run_config.batch_size >= MINIMUM_BATCH_SIZE: + try: + return fn(*args, **kwargs) + except RuntimeError as e: + latest_error = e + # PyTorch only generates Runtime errors for CUDA OOM. + gc.collect() + if "CUDA out of memory" in str(e) or isinstance(e, torch.cuda.OutOfMemoryError): + logger.exception(f"OOM at batch_size={run_config.batch_size}, halving and trying again") + run_config.batch_size //= 2 + else: + # Not a CUDA error + raise + + raise RuntimeError( + f"Ran into latest error {latest_error} during explanation. " + "If a CUDA out of memory error, then the batch size could not be reduced any further." + ) + + return retry_with_halved_batch_size_wrapper + + return retry_with_halved_batch_size_fn + + +class WrapperModule(torch.nn.Module): + """Model used by the explainer to generate predictions. + + Unlike Ludwig's ECD class, this wrapper takes individual args as inputs to the forward function. We derive the order + of these args from the order of the input_feature keys in ECD, which is guaranteed to be consistent (Python + dictionaries are ordered consistently), so we can map back to the input feature dictionary as a second step within + this wrapper. + """ + + def __init__(self, model: ECD, target: str): + super().__init__() + self.model = model + self.target = target + self.input_maps = LudwigFeatureDict() + self.input_maps.update( + { + arg_name: InputIdentity(arg_name) + for arg_name in self.model.input_features + if self.model.input_features.get(arg_name).type() not in EMBEDDED_TYPES + } + ) + + def forward(self, *args): + # Add back the dictionary structure so it conforms to ECD format. + input_features: LudwigFeatureDict = self.model.input_features + inputs = { + # Send the input through the identity layer so that we can use the output of the layer for attribution. + # Except for text/category features where we use the embedding layer for attribution. + feat_name: ( + feat_input + if input_features.get(feat_name).type() in EMBEDDED_TYPES + else self.input_maps.get(feat_name)(feat_input) + ) + for feat_name, feat_input in zip(input_features.keys(), args) + } + + outputs = self.model(inputs) + + # At this point we only have the raw logits, but to make explainability work we need the probabilities + # and predictions as well, so derive them. + predictions = {} + for of_name in self.model.output_features: + predictions[of_name] = self.model.output_features.get(of_name).predictions(outputs, of_name) + + pred_t = get_pred_col(predictions, self.target) + + # If the target feature is a non-scalar type (vector, set, etc.), sum it to get a scalar value. + # https://github.com/pytorch/captum/issues/377 + if len(pred_t.shape) > 1 and self.model.output_features.get(self.target).type() not in { + CATEGORY, + NUMBER, + BINARY, + }: + pred_t = torch.sum(pred_t.reshape(pred_t.shape[0], -1), dim=1) + + return pred_t + + +@PublicAPI(stability="experimental") +class IntegratedGradientsExplainer(Explainer): + def explain(self) -> ExplanationsResult: + """Explain the model's predictions using Integrated Gradients. + + Returns: + ExplanationsResult containing the explanations. + + `global_explanations`: Aggregate explanation for the entire input data. + + `row_explanations`: A list of explanations, one for each row in the input data. Each + explanation contains the integrated gradients for each label in the target feature's vocab with respect to + each input feature. + + `expected_values`: Of length [output feature cardinality]. Average convergence delta for each + label in the target feature's vocab. + """ + + # TODO(travis): add back skip encoders at the end in finally. Shouldn't be an issue in most cases as we + # typically perform explanations on a loaded model and don't use it to predict afterwards. + self.model.model.unskip() + self.model.model.to(DEVICE) + + input_features: LudwigFeatureDict = self.model.model.input_features + run_config = ExplanationRunConfig(batch_size=self.model.config_obj.trainer.batch_size) + + get_input_tensors_with_retry = retry_with_halved_batch_size(run_config)(get_input_tensors) + get_total_attribution_with_retry = retry_with_halved_batch_size(run_config)(get_total_attribution) + + # Convert input data into embedding tensors from the output of the model encoders. + inputs_encoded = get_input_tensors_with_retry(self.model, self.inputs_df, run_config) + sample_encoded = get_input_tensors_with_retry(self.model, self.sample_df, run_config) + baseline = get_baseline(self.model, sample_encoded) + + # Compute attribution for each possible output feature label separately. + expected_values = [] + for target_idx in tqdm(range(self.vocab_size), desc="Explain"): + total_attribution, feat_to_token_attributions, total_attribution_global = get_total_attribution_with_retry( + self.model, + self.target_feature_name, + target_idx if self.is_category_target else None, + inputs_encoded, + baseline, + len(self.inputs_df), + run_config, + ) + + # Aggregate token attributions + feat_to_token_attributions_global = {} + for feat_name, token_attributions in feat_to_token_attributions.items(): + token_attributions_global = defaultdict(float) + # sum attributions for each token + for token, token_attribution in (ta for tas in token_attributions for ta in tas): + token_attributions_global[token] += abs(token_attribution) + # divide by number of samples to get average attribution per token + token_attributions_global = { + token: token_attribution / max(0, len(token_attributions)) + for token, token_attribution in token_attributions_global.items() + } + # convert to list of tuples and sort by attribution + token_attributions_global = sorted(token_attributions_global.items(), key=lambda x: x[1], reverse=True) + # keep only top 100 tokens + token_attributions_global = token_attributions_global[:100] + feat_to_token_attributions_global[feat_name] = token_attributions_global + + self.global_explanation.add( + input_features.keys(), total_attribution_global, feat_to_token_attributions_global + ) + + for i, (feature_attributions, explanation) in enumerate(zip(total_attribution, self.row_explanations)): + # Add the feature attributions to the explanation object for this row. + explanation.add( + input_features.keys(), + feature_attributions, + {k: v[i] for k, v in feat_to_token_attributions.items()}, + ) + + # TODO(travis): for force plots, need something similar to SHAP E[X] + expected_values.append(0.0) + + if self.is_binary_target: + # For binary targets, we only need to compute attribution for the positive class (see below). + break + + # For binary targets, add an extra attribution for the negative class (false). + if self.is_binary_target: + le_true = self.global_explanation.label_explanations[0] + negated_attributions = le_true.to_array() * -1 + negated_token_attributions = { + fa.feature_name: [(t, -a) for t, a in fa.token_attributions] + for fa in le_true.feature_attributions + if fa.token_attributions is not None + } + # Prepend the negative class to the list of label explanations. + self.global_explanation.add( + input_features.keys(), negated_attributions, negated_token_attributions, prepend=True + ) + + for explanation in self.row_explanations: + le_true = explanation.label_explanations[0] + negated_attributions = le_true.to_array() * -1 + negated_token_attributions = { + fa.feature_name: [(t, -a) for t, a in fa.token_attributions] + for fa in le_true.feature_attributions + if fa.token_attributions is not None + } + # Prepend the negative class to the list of label explanations. + explanation.add(input_features.keys(), negated_attributions, negated_token_attributions, prepend=True) + + # TODO(travis): for force plots, need something similar to SHAP E[X] + expected_values.append(0.0) + + return ExplanationsResult(self.global_explanation, self.row_explanations, expected_values) + + +def get_input_tensors( + model: LudwigModel, input_set: pd.DataFrame, run_config: ExplanationRunConfig +) -> list[torch.Tensor]: + """Convert the input data into a list of variables, one for each input feature. + + Args: + model: The LudwigModel to use for encoding. + input_set: The input data to encode of shape [batch size, num input features]. + + Returns: + A list of variables, one for each input feature. Shape of each variable is [batch size, embedding size]. + """ + # Ignore sample_ratio and sample_size from the model config, since we want to explain all the data. + sample_ratio_bak = model.config_obj.preprocessing.sample_ratio + sample_size_bak = model.config_obj.preprocessing.sample_size + model.config_obj.preprocessing.sample_ratio = 1.0 + model.config_obj.preprocessing.sample_size = None + + config = model.config_obj.to_dict() + training_set_metadata = copy.deepcopy(model.training_set_metadata) + for feature in config[INPUT_FEATURES]: + preprocessing = training_set_metadata[feature[NAME]][PREPROCESSING] + if preprocessing.get("cache_encoder_embeddings"): + preprocessing["cache_encoder_embeddings"] = False + + # Convert raw input data into preprocessed tensor data + dataset, _ = preprocess_for_prediction( + config, + dataset=input_set, + training_set_metadata=training_set_metadata, + data_format="auto", + split="full", + include_outputs=False, + backend=model.backend, + callbacks=model.callbacks, + ) + + # Restore sample_ratio and sample_size + model.config_obj.preprocessing.sample_ratio = sample_ratio_bak + model.config_obj.preprocessing.sample_size = sample_size_bak + + # Make sure the number of rows in the preprocessed dataset matches the number of rows in the input data + preprocessed_rows = dataset.to_df().shape[0] + if preprocessed_rows != input_set.shape[0]: + raise RuntimeError( + f"Expected {input_set.shape[0]} rows in preprocessed dataset, but got {preprocessed_rows}. " + f"This is an internal error — please report it." + ) + + # Convert dataset into a dict of tensors, and split each tensor into batches to control GPU memory usage. + # Use dataset.get() rather than dataset.dataset[] directly so that LazyColumn values (audio/image + # stored as file-path arrays) are decoded into numpy arrays before torch conversion. + inputs = { + name: torch.from_numpy(np.array(dataset.get(feature.proc_column))).split(run_config.batch_size) + for name, feature in model.model.input_features.items() + } + + # Dict of lists to list of dicts + input_batches = [dict(zip(inputs, t)) for t in zip(*inputs.values())] + + # List of dicts to dict of lists + preproc_inputs = {k: torch.cat([d[k] for d in input_batches]) for k in input_batches[0]} + + data_to_predict = [v for _, v in preproc_inputs.items()] + tensors = [] + for t in data_to_predict: + # TODO(travis): Consider changing to `if not torch.is_floating_point(t.dtype)` to simplify, then handle bool + # case in this block. + if t.dtype == torch.int8 or t.dtype == torch.int16 or t.dtype == torch.int32 or t.dtype == torch.int64: + # Don't wrap input into a variable if it's an integer type, since it will be used as an index into the + # embedding table. We explain the output of the embedding table, not the input to the embedding table using + # LayerIntegratedGradients. + tensors.append(t) + else: + # Wrap input into a variable so torch will track the gradient and LayerIntegratedGradients can explain it. + if t.dtype == torch.bool: + t = t.to(torch.float32) + tensors.append(Variable(t, requires_grad=True)) + + return tensors + + +def get_baseline(model: LudwigModel, sample_encoded: list[Variable]) -> list[torch.Tensor]: + # TODO(travis): pre-compute this during training from the full training dataset. + input_features: LudwigFeatureDict = model.model.input_features + + baselines = [] + for sample_input, (name, feature) in zip(sample_encoded, input_features.items()): + metadata = model.training_set_metadata[name] + if feature.type() == TEXT: + PAD_IND = metadata.get("pad_idx", metadata.get("word_pad_idx")) + token_reference = TokenReferenceBase(reference_token_idx=PAD_IND) + baseline = token_reference.generate_reference(sequence_length=sample_input.shape[1], device=DEVICE) + elif feature.type() == CATEGORY: + most_popular_token = max(metadata["str2freq"], key=metadata["str2freq"].get) + most_popular_tok_idx = metadata["str2idx"].get(most_popular_token) + + # If an unknown is defined, use that as the baseline index, else use the most popular token + baseline_tok_idx = metadata["str2idx"].get(UNKNOWN_SYMBOL, most_popular_tok_idx) + baseline = torch.tensor(baseline_tok_idx, device=DEVICE) + elif feature.type() == IMAGE: + baseline = torch.zeros_like(sample_input[0], device=DEVICE) + else: + # For a robust baseline, we take the mean of all samples from the training data. + baseline = torch.mean(sample_input.float(), dim=0) + baselines.append(baseline.unsqueeze(0)) + + return baselines + + +def get_total_attribution( + model: LudwigModel, + target_feature_name: str, + target_idx: int | None, + feature_inputs: list[Variable], + baseline: list[torch.Tensor], + nsamples: int, + run_config: ExplanationRunConfig, +) -> tuple[npt.NDArray[np.float64], dict[str, list[list[tuple[str, float]]]]]: + """Compute the total attribution for each input feature for each row in the input data. + + Args: + model: The Ludwig model to explain. + target_feature_name: The name of the target feature to explain. + target_idx: The index of the target feature label to explain if the target feature is a category. + feature_inputs: The preprocessed input data as a list of tensors of length [num_features]. + baseline: The baseline input data as a list of tensors of length [num_features]. + nsamples: The total number of samples in the input data. + + Returns: + The token-attribution pair for each token in the input feature for each row in the input data. The members of + the output tuple are structured as follows: + + `total_attribution_rows`: (npt.NDArray[np.float64]) of shape [num_rows, num_features] + The total attribution for each input feature for each row in the input data. + + `feat_to_token_attributions`: (Dict[str, List[List[Tuple[str, float]]]]) with values of shape + [num_rows, seq_len, 2] + + `total_attribution_global`: (npt.NDArray[np.float64]) of shape [num_features] + The attribution for each input feature aggregated across all input data. + """ + input_features: LudwigFeatureDict = model.model.input_features + + # Configure the explainer, which includes wrapping the model so its interface conforms to + # the format expected by Captum. + model.model.zero_grad() + explanation_model = WrapperModule(model.model, target_feature_name) + + layers = [] + for feat_name, feat in input_features.items(): + if feat.type() in EMBEDDED_TYPES: + # Get embedding layer from encoder, which is the first child of the encoder. + target_layer = feat.encoder_obj.get_embedding_layer() + + # If the current layer matches any layer in the list, make a deep copy of the layer. + if len(layers) > 0 and any(target_layer == layer for layer in layers): + # Replace the layer with a deep copy of the layer to ensure that the attributions unique for each input + # feature that uses a shared layer. + # Recommended here: https://github.com/pytorch/captum/issues/794#issuecomment-1093021638 + replace_layer_with_copy(feat, target_layer) + target_layer = feat.encoder_obj.get_embedding_layer() # get the new copy + else: + # Get the wrapped input layer. + target_layer = explanation_model.input_maps.get(feat_name) + + layers.append(target_layer) + + explainer = LayerIntegratedGradients(explanation_model, layers) + + feature_inputs_splits = [ipt.split(run_config.batch_size) for ipt in feature_inputs] + baseline = [t.to(DEVICE) for t in baseline] + + total_attribution_rows = None + total_attribution_global = None + feat_to_token_attributions = defaultdict(list) + for input_batch in zip(*feature_inputs_splits): + input_batch = [ipt.to(DEVICE) for ipt in input_batch] + attribution = explainer.attribute( + tuple(input_batch), + baselines=tuple(baseline), + target=target_idx, + # https://captum.ai/docs/faq#i-am-facing-out-of-memory-oom-errors-when-using-captum-how-do-i-resolve-this + internal_batch_size=run_config.batch_size, + ) + + attributions_reduced = [] + for a in attribution: + a_reduced = a.detach().cpu() + if a_reduced.ndim == 2 or a_reduced.ndim == 3: + # Reduces category-level attributions of shape [batch_size, embedding_dim] by summing over the + # embedding dimension to get attributions of shape [batch_size]. + # Reduces token-level attributions of shape [batch_size, sequence_length, embedding_dim] by summing + # over the embedding dimension to get attributions of shape [batch_size, sequence_length]. We keep + # the sequence dimension so we can map the attributions to the tokens. + a_reduced = a_reduced.sum(dim=-1) + elif a_reduced.ndim == 4: + # Reduce pixel-level attributions of shape [batch_size, num_channels, height, width] by summing + # over the channel and spatial dimensions to get attributions of shape [batch_size]. + a_reduced = a_reduced.sum(dim=(1, 2, 3)) + attributions_reduced.append(a_reduced) + + for inputs, attrs, (name, feat) in zip(input_batch, attributions_reduced, input_features.items()): + if feat.type() == TEXT: + tok_attrs = get_token_attributions(model, name, inputs.detach().cpu(), attrs) + feat_to_token_attributions[name].append(tok_attrs) + + # Reduce attribution to [num_input_features, batch_size] by summing over the sequence dimension (if present). + attribution = [a.sum(dim=-1) if a.ndim == 2 else a for a in attributions_reduced] + attribution = np.stack(attribution) + + # Transpose to [batch_size, num_input_features] + attribution = attribution.T + + if total_attribution_rows is not None: + total_attribution_rows = np.concatenate([total_attribution_rows, attribution], axis=0) + else: + total_attribution_rows = attribution + + if total_attribution_global is not None: + total_attribution_global += attribution.sum(axis=0) + else: + total_attribution_global = attribution.sum(axis=0) + + total_attribution_global /= nsamples + + feat_to_token_attributions = {k: [e for lst in v for e in lst] for k, v in feat_to_token_attributions.items()} + + return total_attribution_rows, feat_to_token_attributions, total_attribution_global + + +def get_token_attributions( + model: LudwigModel, + feature_name: str, + input_ids: torch.Tensor, + token_attributions: torch.Tensor, +) -> list[list[tuple[str, float]]]: + """Convert token-level attributions to an array of token-attribution pairs of shape. + + [batch_size, sequence_length, 2]. + + Args: + model: The LudwigModel used to generate the attributions. + feature_name: The name of the feature for which the attributions were generated. + input_ids: The input ids of shape [batch_size, sequence_length]. + token_attributions: The token-level attributions of shape [batch_size, sequence_length]. + + Returns: + An array of token-attribution pairs of shape [batch_size, sequence_length, 2]. + """ + _integer_dtypes = (torch.int8, torch.int16, torch.int32, torch.int64) + if input_ids.dtype not in _integer_dtypes: + raise ValueError(f"input_ids must be an integer tensor (int8/int16/int32/int64), got dtype={input_ids.dtype}.") + + # Normalize token-level attributions to visualize the relative importance of each token. + norm = torch.linalg.norm(token_attributions, dim=1) + # Safe divide by zero by setting the norm to 1 if the norm is 0. + norm = torch.where(norm == 0, torch.ones_like(norm), norm) + token_attributions = token_attributions / norm.unsqueeze(-1) + + # map input ids to input tokens via the vocabulary + feature = model.training_set_metadata[feature_name] + vocab = feature.get("idx2str", feature.get("word_idx2str")) + idx2str = np.vectorize(lambda idx: vocab[idx]) + input_tokens = idx2str(input_ids) + + # add attribution to the input tokens + tok_attrs = [ + list(zip(t, a)) for t, a in zip(input_tokens, token_attributions.tolist()) + ] # [batch_size, sequence_length, 2] + + return tok_attrs diff --git a/ludwig/explain/captum_ray.py b/ludwig/explain/captum_ray.py new file mode 100644 index 0000000..8454813 --- /dev/null +++ b/ludwig/explain/captum_ray.py @@ -0,0 +1,212 @@ +from collections import defaultdict +from typing import Any + +import numpy as np +import pandas as pd +import ray +from torch.autograd import Variable +from tqdm import tqdm + +from ludwig.api import LudwigModel +from ludwig.api_annotations import PublicAPI +from ludwig.explain.captum import ( + ExplanationRunConfig, + get_baseline, + get_input_tensors, + get_total_attribution, + IntegratedGradientsExplainer, + retry_with_halved_batch_size, +) +from ludwig.explain.explanation import ExplanationsResult +from ludwig.features.feature_utils import LudwigFeatureDict +from ludwig.utils.torch_utils import get_torch_device + + +@PublicAPI(stability="experimental") +class RayIntegratedGradientsExplainer(IntegratedGradientsExplainer): + def __init__(self, *args, resources_per_task: dict[str, Any] | None = None, num_workers: int = 1, **kwargs): + super().__init__(*args, **kwargs) + self.resources_per_task = resources_per_task or {} + self.num_workers = num_workers + + def explain(self) -> ExplanationsResult: + """Explain the model's predictions using Integrated Gradients. + + # Return + + :return: ExplanationsResult containing the explanations. + `global_explanations`: (Explanation) Aggregate explanation for the entire input data. + + `row_explanations`: (List[Explanation]) A list of explanations, one for each row in the input data. Each + explanation contains the integrated gradients for each label in the target feature's vocab with respect to + each input feature. + + `expected_values`: (List[float]) of length [output feature cardinality] Average convergence delta for each + label in the target feature's vocab. + """ + self.model.model.cpu() + input_features: LudwigFeatureDict = self.model.model.input_features + model_ref = ray.put(self.model) + run_config = ExplanationRunConfig(batch_size=self.model.config_obj.trainer.batch_size) + + # Convert input data into embedding tensors from the output of the model encoders. + inputs_encoded_ref = get_input_tensors_task.options(**self.resources_per_task).remote( + model_ref, ray.put(self.inputs_df), run_config + ) + sample_encoded_ref = get_input_tensors_task.options(**self.resources_per_task).remote( + model_ref, ray.put(self.sample_df), run_config + ) + + inputs_encoded, run_config = ray.get(inputs_encoded_ref) + sample_encoded, run_config = ray.get(sample_encoded_ref) + baseline = get_baseline(self.model, sample_encoded) + + inputs_encoded_ref = ray.put(inputs_encoded) + baseline_ref = ray.put(baseline) + + if self.is_category_target: + # Evenly divide the list of labels among the desired number of workers (Ray tasks). + # For example, 4 GPUs -> 4 workers. We do this instead of creating nlabels tasks because + # there is significant overhead to spawning a Ray task. + target_splits = split_list(list(range(self.vocab_size)), self.num_workers) + else: + # No target index to compare against exists for number features. + # For binary targets, we only need to compute attribution for the positive class (see below). + # May need to revisit in the future for additional feature types. + target_splits = [[None]] + + # Compute attribution for each possible output feature label separately. + attrs_refs = [] + for target_indices in target_splits: + attrs_ref = get_total_attribution_task.options(**self.resources_per_task).remote( + model_ref, + self.target_feature_name, + target_indices, + inputs_encoded_ref, + baseline_ref, + len(self.inputs_df), + run_config, + ) + attrs_refs.append(attrs_ref) + + # Await the completion of our Ray tasks, then merge the results. + expected_values = [] + for attrs_ref in tqdm(attrs_refs, desc="Explain"): + attrs = ray.get(attrs_ref) + for total_attribution, feat_to_token_attributions, total_attribution_global in attrs: + # Aggregate token attributions + feat_to_token_attributions_global = {} + for feat_name, token_attributions in feat_to_token_attributions.items(): + token_attributions_global = defaultdict(float) + # sum attributions for each token + for token, token_attribution in (ta for tas in token_attributions for ta in tas): + token_attributions_global[token] += token_attribution + # divide by number of samples to get average attribution per token + token_attributions_global = { + token: token_attribution / max(0, len(token_attributions)) + for token, token_attribution in token_attributions_global.items() + } + # convert to list of tuples and sort by attribution + token_attributions_global = sorted( + token_attributions_global.items(), key=lambda x: x[1], reverse=True + ) + # keep only top 100 tokens + token_attributions_global = token_attributions_global[:100] + feat_to_token_attributions_global[feat_name] = token_attributions_global + + self.global_explanation.add( + input_features.keys(), total_attribution_global, feat_to_token_attributions_global + ) + + for i, (feature_attributions, explanation) in enumerate(zip(total_attribution, self.row_explanations)): + # Add the feature attributions to the explanation object for this row. + explanation.add( + input_features.keys(), + feature_attributions, + {k: v[i] for k, v in feat_to_token_attributions.items()}, + ) + + # TODO(travis): for force plots, need something similar to SHAP E[X] + expected_values.append(0.0) + + # For binary targets, add an extra attribution for the negative class (false). + if self.is_binary_target: + le_true = self.global_explanation.label_explanations[0] + negated_attributions = le_true.to_array() * -1 + negated_token_attributions = { + fa.feature_name: [(t, -a) for t, a in fa.token_attributions] + for fa in le_true.feature_attributions + if fa.token_attributions is not None + } + # Prepend the negative class to the list of label explanations. + self.global_explanation.add( + input_features.keys(), negated_attributions, negated_token_attributions, prepend=True + ) + + for explanation in self.row_explanations: + le_true = explanation.label_explanations[0] + negated_attributions = le_true.to_array() * -1 + negated_token_attributions = { + fa.feature_name: [(t, -a) for t, a in fa.token_attributions] + for fa in le_true.feature_attributions + if fa.token_attributions is not None + } + # Prepend the negative class to the list of label explanations. + explanation.add(input_features.keys(), negated_attributions, negated_token_attributions, prepend=True) + + # TODO(travis): for force plots, need something similar to SHAP E[X] + expected_values.append(0.0) + + return ExplanationsResult(self.global_explanation, self.row_explanations, expected_values) + + +@ray.remote(max_calls=1) +def get_input_tensors_task( + model: LudwigModel, df: pd.DataFrame, run_config: ExplanationRunConfig +) -> tuple[list[Variable], ExplanationRunConfig]: + model.model.unskip() + model.model.to(get_torch_device()) + try: + get_total_attribution_with_retry = retry_with_halved_batch_size(run_config)(get_input_tensors) + return get_total_attribution_with_retry(model, df, run_config), run_config + finally: + model.model.cpu() + + +@ray.remote(max_calls=1) +def get_total_attribution_task( + model: LudwigModel, + target_feature_name: str, + target_indices: list[int | None], + inputs_encoded: list[Variable], + baseline: list[Variable], + nsamples: int, + run_config: ExplanationRunConfig, +) -> list[np.array]: + model.model.unskip() + model.model.to(get_torch_device()) + try: + get_total_attribution_with_retry = retry_with_halved_batch_size(run_config)(get_total_attribution) + return [ + get_total_attribution_with_retry( + model=model, + target_feature_name=target_feature_name, + target_idx=target_idx, + feature_inputs=inputs_encoded, + baseline=baseline, + nsamples=nsamples, + run_config=run_config, + ) + for target_idx in tqdm(target_indices, desc="Explain") + ] + finally: + model.model.cpu() + + +def split_list(v, n): + """Splits a list into n roughly equal sub-lists. + + Source: https://stackoverflow.com/a/2135920 + """ + k, m = divmod(len(v), n) + return (v[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)) diff --git a/ludwig/explain/explainer.py b/ludwig/explain/explainer.py new file mode 100644 index 0000000..3e51fd1 --- /dev/null +++ b/ludwig/explain/explainer.py @@ -0,0 +1,72 @@ +from abc import ABCMeta, abstractmethod + +import pandas as pd + +from ludwig.api import LudwigModel +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, CATEGORY, TYPE +from ludwig.explain.explanation import Explanation, ExplanationsResult +from ludwig.explain.util import prepare_data + + +@DeveloperAPI +class Explainer(metaclass=ABCMeta): + def __init__( + self, + model: LudwigModel, + inputs_df: pd.DataFrame, + sample_df: pd.DataFrame, + target: str, + ): + """Constructor for the explainer. + + Args: + model: The LudwigModel to explain. + inputs_df: The input data to explain. + sample_df: A sample of the ground truth data. + target: The name of the target to explain. + """ + self.model = model + self.inputs_df = inputs_df + self.sample_df = sample_df + self.target = target + self.inputs_df, self.sample_df, self.feature_cols, self.target_feature_name = prepare_data( + model, inputs_df, sample_df, target + ) + + self.global_explanation = Explanation(self.target_feature_name) + self.row_explanations = [Explanation(self.target_feature_name) for _ in self.inputs_df.index] + + # Lookup from column name to output feature + config = self.model.config + self.output_feature_map = {feature["column"]: feature for feature in config["output_features"]} + + @property + def is_binary_target(self) -> bool: + """Whether the target is binary.""" + return self.output_feature_map[self.target_feature_name][TYPE] == BINARY + + @property + def is_category_target(self) -> bool: + """Whether the target is categorical.""" + return self.output_feature_map[self.target_feature_name][TYPE] == CATEGORY + + @property + def vocab_size(self) -> int: + """The vocab size of the target feature. + + For regression (number) this is 1, for binary it is 2, and for category it is the vocab size. + """ + if self.is_category_target: + return self.model.training_set_metadata[self.target_feature_name]["vocab_size"] + elif self.is_binary_target: + return 2 + return 1 + + @abstractmethod + def explain(self) -> ExplanationsResult: + """Explain the model's predictions. + + Returns: + ExplanationsResult containing the explanations. + """ diff --git a/ludwig/explain/explanation.py b/ludwig/explain/explanation.py new file mode 100644 index 0000000..d055d06 --- /dev/null +++ b/ludwig/explain/explanation.py @@ -0,0 +1,97 @@ +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt + +from ludwig.api_annotations import DeveloperAPI, PublicAPI + + +@DeveloperAPI +@dataclass +class FeatureAttribution: + """Stores the attribution for a single input feature.""" + + # The name of the input feature. + feature_name: str + + # The scalar attribution for the input feature. + attribution: float + + # (Optional) The attribution for each token in the input feature as an array of shape (seq_len, 2). + token_attributions: list[tuple[str, float]] = None + + +@DeveloperAPI +@dataclass +class LabelExplanation: + """Stores the feature attributions for a single label in the target feature's vocab.""" + + # The attribution for each input feature. + feature_attributions: list[FeatureAttribution] = field(default_factory=list) + + def add(self, feature_name: str, attribution: float, token_attributions: list[tuple[str, float]] | None = None): + """Add the attribution for a single input feature.""" + self.feature_attributions.append(FeatureAttribution(feature_name, attribution, token_attributions)) + + def to_array(self) -> npt.NDArray[np.float64]: + """Convert the explanation to a 1D array of shape (num_features,).""" + return np.array([fa.attribution for fa in self.feature_attributions]) + + +@DeveloperAPI +@dataclass +class Explanation: + """Stores the explanations for a single row of input data. + + Contains the feature attributions for each label in the target feature's vocab. + """ + + target: str + + # The explanations for each label in the vocab of the target feature. + label_explanations: list[LabelExplanation] = field(default_factory=list) + + def add( + self, + feat_names: list[str], + feat_attributions: npt.NDArray[np.float64], + feat_to_token_attributions: dict[str, list[tuple[str, float]]] | None = None, + prepend: bool = False, + ): + """Add the feature attributions for a single label.""" + if len(feat_names) != len(feat_attributions): + raise ValueError(f"Expected {len(feat_names)} feature attributions, got {len(feat_attributions)}.") + if len(self.label_explanations) > 0: + # Check that the feature attributions are the same shape as existing explanations. + expected_shape = self.label_explanations[0].to_array().shape + if expected_shape != feat_attributions.shape: + raise ValueError( + f"Expected feature attributions of shape {expected_shape}, got {feat_attributions.shape}." + ) + + le = LabelExplanation() + for i, feat_name in enumerate(feat_names): + le.add( + feat_name, + feat_attributions[i], + feat_to_token_attributions.get(feat_name) if feat_to_token_attributions else None, + ) + self.label_explanations.insert(0, le) if prepend else self.label_explanations.append(le) + + def to_array(self) -> npt.NDArray[np.float64]: + """Convert the explanation to a 2D array of shape (num_labels, num_features).""" + return np.array([le.to_array() for le in self.label_explanations]) + + +@PublicAPI(stability="experimental") +@dataclass +class ExplanationsResult: + # Aggregate explanation for the entire input data. + global_explanation: Explanation # GlobalExplanation + + # A list of explanations, one for each row in the input data. + # Each explanation contains the feature attributions for each label in the target feature's vocab. + row_explanations: list[Explanation] + + # Expected value for each label in the target feature's vocab. + expected_values: list[float] diff --git a/ludwig/explain/util.py b/ludwig/explain/util.py new file mode 100644 index 0000000..8574db4 --- /dev/null +++ b/ludwig/explain/util.py @@ -0,0 +1,111 @@ +from copy import deepcopy + +import pandas as pd +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import COLUMN, INPUT_FEATURES, PREPROCESSING, SPLIT +from ludwig.data.split import get_splitter +from ludwig.features.base_feature import BaseFeature + + +def filter_cols(df, cols): + cols = {c.lower() for c in cols} + retain_cols = [c for c in df.columns if c.lower() in cols] + return df[retain_cols] + + +def prepare_data(model: LudwigModel, inputs_df: pd.DataFrame, sample_df: pd.DataFrame, target: str): + config = model.config + feature_cols = [feature[COLUMN] for feature in config[INPUT_FEATURES]] + if SPLIT in config.get(PREPROCESSING, {}): + # Keep columns required for Ludwig preprocessing + splitter = get_splitter(**config[PREPROCESSING][SPLIT]) + feature_cols += splitter.required_columns + target_feature_name = get_feature_name(model, target) + + inputs_df = filter_cols(inputs_df, feature_cols) + if sample_df is not None: + sample_df = filter_cols(sample_df, feature_cols) + + return inputs_df, sample_df, feature_cols, target_feature_name + + +def get_pred_col(preds, target): + t = target.lower() + for c in preds: + if c.lower() == t: + if "probabilities" in preds[c]: + return preds[c]["probabilities"] + else: + return preds[c]["predictions"] + raise ValueError(f"Unable to find target column {t} in {preds.keys()}") + + +def get_feature_name(model: LudwigModel, target: str) -> str: + t = target.lower() + for c in model.training_set_metadata: + if c.lower() == t: + return c + raise ValueError(f"Unable to find target column {t} in {model.training_set_metadata.keys()}") + + +def get_absolute_module_key_from_submodule(module: torch.nn.Module, submodule: torch.nn.Module): + """Get the absolute module key for each param in the target layer. + + Assumes that the keys in the submodule are relative to the module. + + We find the params from the submodule in the module by comparing the data + pointers, since the data returned by named_parameters is by reference. + More information on checking if tensors point to the same place in storage can be found here: + https://discuss.pytorch.org/t/any-way-to-check-if-two-tensors-have-the-same-base/44310/2 + """ + absolute_keys = [] + for module_key, module_param in module.named_parameters(): + for _, submodule_param in submodule.named_parameters(): + if submodule_param.data_ptr() == module_param.data_ptr(): + absolute_keys.append(module_key) + break + return absolute_keys + + +def replace_layer_with_copy(feat: BaseFeature, target_layer: torch.nn.Module): + """Replaces a layer in a feature with a copy of the layer in-place. + + This is useful in a tied weights scenario, where a single encoder may be used by multiple features. If we leave + as-is, Captum complains about the resulting computation graph. The solution is to create an identical + (deep) copy of the layer fed into Captum: https://github.com/pytorch/captum/issues/794#issuecomment-1093021638 + + This is safe to do during the explain step because we are essentially running inference, and no model artifacts are + being saved during the explain step. + + TODO(geoffrey): if a user ever wants to train immediately after explain (i.e. w/o loading weights from the disk), + we might want to implement this as a context so that we can restore the original encoder object at the end. + Will defer this implementation for now because that scenario seems unlikely. + + At a high-level the approach is the following: + 1. Create a deep-copy of the entire encoder object and set it as the feature's encoder object + 2. Replace the tensors in the copied encoder object with the tensors from the original encoder object, except for + the tensors in the target layer. We want to explain these tensors, so we want to keep them as deep copies. + + This approach ensures that at most 2 copies of the encoder object are in memory at any given time. + """ + with torch.no_grad(): + # Get the original encoder object and a mapping from param names to the params themselves. + orig_encoder_obj = feat.encoder_obj + orig_encoder_obj_state_dict = orig_encoder_obj.state_dict() + + # Deep copy the original encoder object and set the copy as this feature's encoder object. + copy_encoder_obj = deepcopy(orig_encoder_obj) + feat.encoder_obj = copy_encoder_obj + + # We have to get the absolute module key in order to do string matching because the target_layer keys are + # relative to itself. If we were to leave it as-is and attempt to suffix match, we may get duplicates for + # common layers i.e. "LayerNorm.weight" and "LayerNorm.bias". Getting the absolute module key ensures we + # use values like "transformer.module.embedding.LayerNorm.weight" instead. + keys_to_keep_copy = get_absolute_module_key_from_submodule(orig_encoder_obj, target_layer) + + # Get the tensors to keep from the copied encoder object. These are the tensors in the target layer. + for key, param in copy_encoder_obj.named_parameters(): + if key not in keys_to_keep_copy: + param.data = orig_encoder_obj_state_dict[key].data diff --git a/ludwig/export.py b/ludwig/export.py new file mode 100644 index 0000000..fdc13dc --- /dev/null +++ b/ludwig/export.py @@ -0,0 +1,127 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import sys + +from ludwig.api import LudwigModel +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + + +def export_mlflow(model_path, output_path="mlflow", registered_model_name=None, callbacks=None, **kwargs): + """Exports a trained Ludwig model as an MLflow model. + + Args: + model_path: filepath to the trained Ludwig model. + output_path: output directory for the MLflow model. + registered_model_name: register model with this name. Defaults to None. + """ + logger.info(f"Loading Ludwig model from {model_path}") + + callbacks = callbacks or [] + for callback in callbacks: + callback.on_cmdline("export_mlflow", model_path=model_path, output_path=output_path) + + from ludwig.contribs.mlflow.model import export_model as mlflow_export + + mlflow_export(model_path, output_path, registered_model_name) + + +def export_model(model_path, output_path, format="safetensors", **kwargs): + """Exports a trained Ludwig model in various formats. + + Args: + model_path: filepath to the trained Ludwig model. + output_path: output directory for the exported model. + format: export format: safetensors, torch_export, onnx. + """ + logger.info(f"Loading Ludwig model from {model_path}") + model = LudwigModel.load(model_path) + model.export_model(output_path, format=format) + + +def cli_export_mlflow(sys_argv): + parser = argparse.ArgumentParser( + description="This script exports a trained Ludwig model to MLflow format", + prog="ludwig export_mlflow", + usage="%(prog)s [options]", + ) + parser.add_argument("-m", "--model_path", help="path to the trained model", required=True) + parser.add_argument("-o", "--output_path", type=str, default="mlflow", help="output path") + parser.add_argument("-rmn", "--registered_model_name", type=str, default=None, help="registered model name") + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="logging level", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + print_ludwig("Export MLflow", LUDWIG_VERSION) + export_mlflow(**vars(args)) + + +def cli_export_model(sys_argv): + parser = argparse.ArgumentParser( + description="This script exports a trained Ludwig model to various formats (safetensors, torch_export, onnx)", + prog="ludwig export_model", + usage="%(prog)s [options]", + ) + parser.add_argument("-m", "--model_path", help="path to the trained model", required=True) + parser.add_argument("-o", "--output_path", type=str, default="exported_model", help="output path") + parser.add_argument( + "-f", + "--format", + type=str, + default="safetensors", + choices=["safetensors", "torch_export", "onnx"], + help="export format", + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="logging level", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + args = parser.parse_args(sys_argv) + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + print_ludwig("Export Model", LUDWIG_VERSION) + export_model(**vars(args)) + + +def cli(sys_argv): + sub = sys.argv[1] if len(sys.argv) > 1 else None + if sub == "mlflow": + cli_export_mlflow(sys.argv[2:]) + elif sub == "model": + cli_export_model(sys.argv[2:]) + else: + print(f"Unknown export subcommand: {sub}") + print("Available: mlflow, model") + sys.exit(1) + + +if __name__ == "__main__": + cli(sys.argv) diff --git a/ludwig/features/__init__.py b/ludwig/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/features/anomaly_feature.py b/ludwig/features/anomaly_feature.py new file mode 100644 index 0000000..f2c81f9 --- /dev/null +++ b/ludwig/features/anomaly_feature.py @@ -0,0 +1,414 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Anomaly detection output feature using Deep One-Class Classification methods. + +This module provides the AnomalyOutputFeature, which implements three hypersphere-based +anomaly detection objectives: + +- Deep SVDD (default): Trains a neural network to map all normal inputs inside a compact + hypersphere. The squared distance ||z - c||^2 from the encoder output z to the center c + is the anomaly score. Geometric and interpretable; works well for homogeneous normal data. + Reference: Ruff et al., "Deep One-Class Classification", ICML 2018. + +- Deep SAD: Semi-supervised extension of SVDD. Normal examples are pulled toward c while + confirmed anomaly examples (target=1) are pushed away. Useful when a small number of + labeled anomalies are available at training time. + Reference: Ruff et al., "Deep Semi-Supervised Anomaly Detection", ICLR 2020. + +- DROCC: Prevents collapse (all representations mapping to c) by adding an adversarial + perturbation term. Recommended when using expressive encoders such as transformers. + Reference: Goyal et al., "DROCC: Deep Robust One-Class Classification", ICML 2020. + +Multimodal anomaly detection works natively through Ludwig's ECD architecture: add any +combination of input features (text, image, audio, tabular) and the combiner will fuse +them before the anomaly decoder. + +Center initialization: + The hypersphere center c must be initialized after the first epoch by calling + ``output_feature.initialize_center(center_tensor)``. This is typically handled + automatically by the Ludwig trainer when an anomaly output feature is detected. + The center is set to the mean of all encoder outputs on the training set. +""" + +import logging + +import numpy as np +import torch + +from ludwig.constants import ( + ANOMALY, + ANOMALY_SCORE, + COLUMN, + HIDDEN, + NAME, + PREDICTIONS, + PROC_COLUMN, +) +from ludwig.features.base_feature import FeaturePreprocessingMixin, OutputFeature, PredictModule +from ludwig.schema.features.anomaly_feature import AnomalyOutputFeatureConfig +from ludwig.types import ( + FeatureConfigDict, + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.types import DataFrame + +logger = logging.getLogger(__name__) + + +class _AnomalyPredict(PredictModule): + """Converts raw anomaly scores to predictions (is_anomaly bool) using a threshold. + + Args: + threshold: Float threshold above which anomaly scores are classified as anomalous. + Use ``float('inf')`` to disable hard classification (only anomaly_score is returned). + """ + + def __init__(self, threshold: float = float("inf")): + super().__init__() + self.threshold = threshold + self.anomaly_score_key = ANOMALY_SCORE + + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + anomaly_score = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.anomaly_score_key) + # Threshold-based binary classification. + predictions = anomaly_score >= self.threshold + return { + self.anomaly_score_key: anomaly_score, + self.predictions_key: predictions, + } + + +class AnomalyFeatureMixin(FeaturePreprocessingMixin): + """Mixin providing preprocessing utilities for anomaly output features. + + Anomaly detection is typically unsupervised — the target column contains 0 (normal) or 1 (anomaly) labels for + evaluation, or -1 for unlabeled examples (used by Deep SAD). If no target column is present in the dataset, the + feature runs in fully unsupervised mode. + """ + + @staticmethod + def type() -> str: + return ANOMALY + + @staticmethod + def cast_column(column: DataFrame, backend) -> DataFrame: + """Cast the target anomaly label column to float32. + + Labels should be 0 (normal), 1 (anomaly), or -1 (unlabeled for Deep SAD). If the column contains NaN values + (unlabeled), they are filled with -1. + """ + return column.fillna(-1).astype(np.float32) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column: DataFrame, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + return {} + + @staticmethod + def add_feature_data( + feature_config: FeatureConfigDict, + input_df: DataFrame, + proc_df: dict[str, DataFrame], + metadata: TrainingSetMetadataDict, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input: bool, + ) -> None: + column_name = feature_config[COLUMN] + if column_name in input_df.columns: + # Labels present: cast to float32 (0=normal, 1=anomaly, -1=unlabeled). + col = input_df[column_name].fillna(-1).astype(np.float32) + else: + # Fully unsupervised: fill with -1 (unlabeled) so the loss still works. + logger.info( + f"Anomaly feature '{feature_config[NAME]}': no target column found in dataset. " + "Running in fully unsupervised mode (all labels set to -1)." + ) + col = backend.df_engine.from_pandas( + np.full(len(input_df), -1.0, dtype=np.float32), + ) + + proc_df[feature_config[PROC_COLUMN]] = col + return proc_df + + +class AnomalyOutputFeature(AnomalyFeatureMixin, OutputFeature): + """Output feature for anomaly detection using Deep One-Class Classification. + + This feature maps input representations to an anomaly score via a hypersphere decoder. + The anomaly score is the squared distance from the encoder output to a learned center c: + + anomaly_score = ||encoder_output - c||^2 + + Training objectives (controlled by the ``loss.type`` config field): + - ``deep_svdd``: Hard or soft boundary SVDD. Minimizes mean(||z - c||^2) for all points. + For soft boundary, a radius R is estimated as the nu-th quantile of distances. + - ``deep_sad``: Semi-supervised. Normal/unlabeled points pulled toward c; labeled anomalies + (target=1) pushed away from c via inverted distance. + - ``drocc``: Adds adversarial perturbations to prevent collapse. Recommended for + expressive encoders. + + Center initialization: + After the first epoch (or at any time), call ``initialize_center(center)`` to set the + hypersphere center c. The trainer does this automatically for anomaly output features. + Until initialization, the center defaults to zeros. + + Predictions: + - ``anomaly_score``: Float scalar per sample (primary output for ranking and metrics). + - ``predictions``: Bool per sample — True if anomaly_score >= threshold. + + Threshold: + Configure via the ``threshold`` field in the output feature config: + - ``"auto"``: Automatically select the threshold as the ``threshold_percentile``-th + percentile of validation-set anomaly scores after training. + - A float value: use as a fixed decision boundary. + + Multimodal: + Works with any ECD input features. The combiner fuses inputs before the anomaly decoder. + """ + + def __init__( + self, + output_feature_config: AnomalyOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self._threshold = output_feature_config.threshold + self._threshold_percentile = getattr(output_feature_config, "threshold_percentile", 95.0) + super().__init__(output_feature_config, output_features, **kwargs) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + @staticmethod + def type() -> str: + return ANOMALY + + def initialize_center(self, center: torch.Tensor) -> None: + """Initialize the hypersphere center c from the mean of first-epoch encoder outputs. + + Delegates to the underlying AnomalyDecoder. Call this after collecting encoder outputs + for the entire training set at the end of epoch 0. + + Args: + center: Tensor of shape ``[latent_dim]`` — typically ``encoder_outputs.mean(dim=0)``. + """ + self.decoder_obj.initialize_center(center) + + @property + def center_initialized(self) -> bool: + """Whether the hypersphere center has been initialized.""" + return getattr(self.decoder_obj, "_center_initialized", False) + + def logits(self, inputs: dict[str, torch.Tensor], **kwargs) -> dict[str, torch.Tensor]: + """Compute anomaly scores from hidden representations. + + Args: + inputs: Dict with at least key ``HIDDEN`` — the decoder input. + + Returns: + Dict with ``anomaly_score`` key containing per-sample distances ``||z - c||^2``. + """ + hidden = inputs[HIDDEN] + anomaly_score = self.decoder_obj(hidden) + return {ANOMALY_SCORE: anomaly_score} + + def create_predict_module(self) -> PredictModule: + # Resolve numeric threshold; "auto" starts as inf and is updated after training. + if isinstance(self._threshold, str) and self._threshold == "auto": + threshold = float("inf") + else: + threshold = float(self._threshold) + return _AnomalyPredict(threshold=threshold) + + def get_prediction_set(self) -> set[str]: + return {ANOMALY_SCORE, PREDICTIONS} + + @classmethod + def get_output_dtype(cls) -> torch.dtype: + return torch.float32 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + def train_loss(self, targets: torch.Tensor, predictions: dict[str, torch.Tensor], feature_name: str): + """Compute the anomaly detection training loss. + + Args: + targets: Ground-truth labels (0=normal, 1=anomaly, -1=unlabeled). Shape [batch]. + predictions: Dict of tensors including ``anomaly_score``. + feature_name: Feature name used to look up the correct tensor. + + Returns: + Scalar loss tensor. + """ + prediction_key = output_feature_utils.get_feature_concat_name( + feature_name, type(self.train_loss_function).get_loss_inputs() + ) + return self.train_loss_function(predictions[prediction_key], targets) + + def eval_loss(self, targets: torch.Tensor, predictions: dict[str, torch.Tensor]): + prediction_key = type(self.train_loss_function).get_loss_inputs() + return self.eval_loss_metric.get_current_value(predictions[prediction_key].detach(), targets) + + def update_metrics(self, targets: torch.Tensor, predictions: dict[str, torch.Tensor]) -> None: + """Update evaluation metrics with current batch. + + Args: + targets: Ground-truth labels (0=normal, 1=anomaly, -1=unlabeled). Shape [batch]. + predictions: Dict of tensors including ``anomaly_score``. + """ + from ludwig.modules.metric_registry import get_metric_tensor_input + + for metric_name, metric_fn in self._metric_functions.items(): + prediction_key = get_metric_tensor_input(metric_name) + metric_fn = metric_fn.to(predictions[prediction_key].device) + metric_fn.update(predictions[prediction_key].detach(), targets) + + def auto_set_threshold(self, validation_anomaly_scores: torch.Tensor) -> None: + """Set the anomaly threshold automatically from validation-set anomaly scores. + + After training, call this with the anomaly scores from the validation set to + automatically determine the decision threshold. The threshold is set to the + ``threshold_percentile``-th percentile of validation scores. + + Args: + validation_anomaly_scores: 1D tensor of anomaly scores from the validation set. + """ + if isinstance(self._threshold, str) and self._threshold == "auto": + threshold_value = float(torch.quantile(validation_anomaly_scores, self._threshold_percentile / 100.0)) + logger.info( + f"Anomaly feature '{self.feature_name}': auto-selected threshold " + f"{threshold_value:.6f} ({self._threshold_percentile}th percentile of validation scores)." + ) + self._threshold = threshold_value + # Update the predict module's threshold too. + if hasattr(self, "_prediction_module") and self._prediction_module is not None: + self._prediction_module.module.threshold = threshold_value + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + """Calculate overall anomaly detection statistics. + + Computes AUROC and F1-max over all thresholds when ground-truth labels are available. + If only unlabeled data is available (all targets == -1), returns an empty dict. + + Args: + predictions: Dict with keys ``anomaly_score`` and ``predictions``. + targets: 1D array of ground-truth labels (0, 1, or -1). + train_set_metadata: Unused metadata. + + Returns: + Dict of overall statistics. + """ + overall_stats = {} + scores = np.array(predictions.get(ANOMALY_SCORE, [])) + preds = np.array(predictions.get(PREDICTIONS, [])) + targets = np.array(targets) + + # Only compute label-dependent stats if labeled data is available. + labeled_mask = targets >= 0 + if labeled_mask.sum() < 2: + logger.info("Anomaly feature: no labeled validation data available; skipping AUROC/F1 stats.") + return overall_stats + + labeled_scores = scores[labeled_mask] + labeled_targets = targets[labeled_mask] + labeled_preds = preds[labeled_mask] + + if len(np.unique(labeled_targets)) < 2: + logger.info("Anomaly feature: only one class in labeled data; skipping AUROC/F1 stats.") + return overall_stats + + try: + from sklearn.metrics import roc_auc_score + + overall_stats["roc_auc"] = float(roc_auc_score(labeled_targets, labeled_scores)) + except Exception as e: + logger.warning(f"Could not compute AUROC: {e}") + + try: + from sklearn.metrics import f1_score, precision_score, recall_score + + overall_stats["f1"] = float(f1_score(labeled_targets, labeled_preds, zero_division=0)) + overall_stats["precision"] = float(precision_score(labeled_targets, labeled_preds, zero_division=0)) + overall_stats["recall"] = float(recall_score(labeled_targets, labeled_preds, zero_division=0)) + except Exception as e: + logger.warning(f"Could not compute F1/precision/recall: {e}") + + # F1-max: best F1 over all thresholds. + try: + from sklearn.metrics import f1_score as sk_f1 + + best_f1 = 0.0 + best_threshold = None + for t in np.unique(labeled_scores): + preds_t = (labeled_scores >= t).astype(int) + f1 = sk_f1(labeled_targets, preds_t, zero_division=0) + if f1 > best_f1: + best_f1 = f1 + best_threshold = float(t) + overall_stats["f1_max"] = best_f1 + overall_stats["f1_max_threshold"] = best_threshold + except Exception as e: + logger.warning(f"Could not compute F1-max: {e}") + + return overall_stats + + def postprocess_predictions( + self, + result, + metadata, + ) -> FeaturePostProcessingOutputDict: + """Post-process model output columns for saving. + + Renames ``anomaly_score`` and ``predictions`` columns with the feature name prefix. + + Args: + result: DataFrame with model output columns. + metadata: Feature metadata (unused for anomaly). + + Returns: + Modified result DataFrame. + """ + # Anomaly score is already float — no further post-processing needed. + return result + + @staticmethod + def get_schema_cls(): + return AnomalyOutputFeatureConfig + + @classmethod + def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "float32" diff --git a/ludwig/features/audio_feature.py b/ludwig/features/audio_feature.py new file mode 100644 index 0000000..815cd1a --- /dev/null +++ b/ludwig/features/audio_feature.py @@ -0,0 +1,675 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os +from pathlib import Path + +import numpy as np +import pandas as pd +import torch +import torchaudio +from packaging import version + +from ludwig.constants import AUDIO, AUDIO_FEATURE_KEYS, COLUMN, NAME, PROC_COLUMN, SRC, TYPE +from ludwig.data.lazy_utils import resolve_lazy_cache_dir +from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin +from ludwig.features.sequence_feature import SequenceInputFeature +from ludwig.schema.features.audio_feature import AudioInputFeatureConfig +from ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict +from ludwig.utils.audio_utils import ( + calculate_mean, + calculate_var, + get_default_audio, + get_fbank, + get_group_delay, + get_length_in_samp, + get_max_length_stft_based, + get_non_symmetric_length, + get_phase_stft_magnitude, + get_stft_magnitude, + is_torch_audio_tuple, + read_audio_from_bytes_obj, + read_audio_from_path, +) +from ludwig.utils.data_utils import get_abs_path +from ludwig.utils.fs_utils import has_remote_protocol +from ludwig.utils.misc_utils import set_default_value +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + +_TORCH_200 = version.parse(torch.__version__) >= version.parse("2.0.0") + + +def _extract_audio_path(entry: dict) -> str | None: + """Return a usable on-disk path from a HuggingFace-style audio dict, or ``None``. + + HuggingFace ``Audio`` columns deliver dicts with at least ``"array"`` and + ``"sampling_rate"`` keys. When the underlying file is still cached on disk + the dict also contains a ``"path"`` key whose value is the absolute path. + + Parameters + ---------- + entry: + A dict-like object representing one audio sample. + + Returns + ------- + str | None + The existing on-disk path when ``entry["path"]`` exists and the file is + present on disk; ``None`` otherwise. + """ + path = entry.get("path") if isinstance(entry, dict) else None + if path and isinstance(path, str) and os.path.isfile(path): + return path + return None + + +def _cache_audio_column_to_disk( + column, + cache_dir: Path, + feature_name: str, + sampling_rate: int = 16_000, +) -> list[str]: + """Write every audio entry in *column* to WAV files in *cache_dir*. + + Entries that are dicts with an existing on-disk ``"path"`` are reused + without copying. All other entries (dicts with only ``"array"`` / + ``"sampling_rate"``, or bare ``torch.Tensor`` objects) are saved as + 16-bit WAV files. Files that already exist in *cache_dir* are not + overwritten (idempotent behaviour), which makes re-runs after a crash + safe. + + Parameters + ---------- + column: + An iterable of audio entries. Each entry may be: + + * a ``dict`` with at least ``"array"`` (numpy or Tensor) and + ``"sampling_rate"`` (int) keys, and optionally ``"path"``; + * a bare ``torch.Tensor`` with shape ``(channels, samples)`` or + ``(samples,)`` (uses *sampling_rate* as the sample rate). + cache_dir: + Directory in which to write WAV files. Must already exist. + feature_name: + Used as a filename prefix so that files are recognisable during + debugging. + sampling_rate: + Fallback sample rate used when an entry is a bare Tensor and does + not carry its own sampling-rate information. + + Returns + ------- + list[str] + Ordered list of absolute paths (one per entry), suitable for direct + use as a ``LazyColumn`` path list. + + Raises + ------ + ValueError + If an entry has an unrecognised type that cannot be converted to a + waveform tensor. + """ + paths: list[str] = [] + for idx, entry in enumerate(column): + # --- Try to reuse an existing on-disk path --- + if isinstance(entry, dict): + existing = _extract_audio_path(entry) + if existing is not None: + paths.append(existing) + continue + + # --- Build (waveform, sr) tuple from the entry --- + if isinstance(entry, dict): + array = entry["array"] + sr = int(entry.get("sampling_rate", sampling_rate)) + if isinstance(array, np.ndarray): + waveform = torch.from_numpy(array).float() + elif isinstance(array, torch.Tensor): + waveform = array.float() + else: + raise ValueError( + f"Audio entry [{idx}] in feature '{feature_name}' has unrecognised " + f"array type {type(array).__name__!r}. Expected numpy.ndarray or torch.Tensor." + ) + elif isinstance(entry, torch.Tensor): + waveform = entry.float() + sr = sampling_rate + else: + raise ValueError( + f"Audio entry [{idx}] in feature '{feature_name}' has unrecognised " + f"type {type(entry).__name__!r}. Expected dict or torch.Tensor." + ) + + # Ensure shape is (channels, samples) + if waveform.ndim == 1: + waveform = waveform.unsqueeze(0) + + dest_path = str(cache_dir / f"{feature_name}_{idx:08d}.wav") + if not os.path.isfile(dest_path): + torchaudio.save(dest_path, waveform, sr) + paths.append(dest_path) + + return paths + + +class _AudioPreprocessing(BasePreprocessingModule): + audio_feature_dict: dict[str, float | int | str] + + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.audio_feature_dict = { + key: value + for key, value in metadata["preprocessing"].items() + if key in AUDIO_FEATURE_KEYS and value is not None + } + self.feature_dim = metadata["feature_dim"] + self.max_length = metadata["max_length"] + self.padding_value = metadata["preprocessing"]["padding_value"] + self.normalization_type = metadata["preprocessing"]["norm"] + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if not torch.jit.isinstance(v, list[tuple[torch.Tensor, int]]): + raise ValueError(f"Unsupported input: {v}") + + processed_audio_matrix = [] + for audio, sampling_rate_in_hz in v: + processed_audio = AudioFeatureMixin._transform_to_feature( + audio, + sampling_rate_in_hz, + self.audio_feature_dict, + self.feature_dim, + self.max_length, + self.padding_value, + self.normalization_type, + ) + processed_audio_matrix.append(processed_audio) + return torch.stack(processed_audio_matrix) + + +class AudioFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return AUDIO + + @staticmethod + def cast_column(column, backend): + return column + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + first_audio_entry = column.head(1).iloc[0] + if isinstance(first_audio_entry, dict): + sampling_rate_in_hz = int(first_audio_entry.get("sampling_rate", 16_000)) + elif isinstance(first_audio_entry, torch.Tensor): + # Bare tensor — cannot infer sample rate; fall back to 16 kHz default. + sampling_rate_in_hz = 16_000 + else: + _, sampling_rate_in_hz = torchaudio.load(first_audio_entry) + + feature_dim = AudioFeatureMixin._get_feature_dim(preprocessing_parameters, sampling_rate_in_hz) + audio_file_length_limit_in_s = preprocessing_parameters["audio_file_length_limit_in_s"] + max_length = AudioFeatureMixin._get_max_length_feature( + preprocessing_parameters, sampling_rate_in_hz, audio_file_length_limit_in_s + ) + return { + "feature_dim": feature_dim, + "sampling_rate_in_hz": sampling_rate_in_hz, + "max_length": max_length, + "reshape": (max_length, feature_dim), + } + + @staticmethod + def _get_feature_dim(preprocessing_parameters: PreprocessingConfigDict, sampling_rate_in_hz): + feature_type = preprocessing_parameters[TYPE] + + if feature_type == "raw": + feature_dim = 1 + elif feature_type == "stft_phase": + feature_dim_symmetric = get_length_in_samp( + preprocessing_parameters["window_length_in_s"], sampling_rate_in_hz + ) + feature_dim = 2 * get_non_symmetric_length(feature_dim_symmetric) + elif feature_type in ["stft", "group_delay"]: + feature_dim_symmetric = get_length_in_samp( + preprocessing_parameters["window_length_in_s"], sampling_rate_in_hz + ) + feature_dim = get_non_symmetric_length(feature_dim_symmetric) + elif feature_type == "fbank": + feature_dim = preprocessing_parameters["num_filter_bands"] + else: + raise ValueError(f"{feature_type} is not recognized.") + + return feature_dim + + @staticmethod + def _process_in_memory( + column, + audio_feature_dict, + feature_dim, + max_length, + padding_value, + normalization_type, + audio_file_length_limit_in_s, + backend, + ): + df_engine = backend.df_engine + if _TORCH_200: + # Read audio from path if the version of torch is >= 2.0.0. + raw_audio = backend.read_binary_files(column, map_fn=read_audio_from_path) + else: + raw_audio = backend.read_binary_files(column, map_fn=read_audio_from_bytes_obj) + + try: + default_audio = get_default_audio([audio for audio in raw_audio if is_torch_audio_tuple(audio)]) + except RuntimeError as e: + raise RuntimeError(f"Unable to process audio files provided: {e}") from e + + raw_audio = df_engine.map_objects(raw_audio, lambda row: row if is_torch_audio_tuple(row) else default_audio) + processed_audio = df_engine.map_objects( + raw_audio, + lambda row: AudioFeatureMixin._transform_to_feature( + audio=row[0], + sampling_rate_in_hz=row[1], + audio_feature_dict=audio_feature_dict, + feature_dim=feature_dim, + max_length=max_length, + padding_value=padding_value, + normalization_type=normalization_type, + ).numpy(), # non-torchscript preprocessing requires np.ndarray + ) + + audio_stats = df_engine.map_objects( + raw_audio, + lambda row: AudioFeatureMixin._get_stats( + audio=row[0], + sampling_rate_in_hz=row[1], + max_length_in_s=audio_file_length_limit_in_s, + ), + ) + + def reduce(series): + merged_stats = None + for audio_stats in series: + if merged_stats is None: + merged_stats = audio_stats.copy() + else: + AudioFeatureMixin._merge_stats(merged_stats, audio_stats) + return merged_stats + + merged_stats = df_engine.reduce_objects(audio_stats, reduce) + merged_stats["mean"] = calculate_mean(merged_stats["sum"], merged_stats["count"]) + merged_stats["var"] = calculate_var(merged_stats["sum"], merged_stats["sum2"], merged_stats["count"]) + merged_stats["std"] = np.sqrt(merged_stats["var"] / float(merged_stats["count"])) + print_statistics = ( + "{} audio files loaded.\n" + "Statistics of audio file lengths:\n" + "- mean: {:.4f}\n" + "- std: {:.4f}\n" + "- max: {:.4f}\n" + "- min: {:.4f}\n" + "- cropped audio_files: {}\n" + "Max length was given as {}s" + ).format( + merged_stats["count"], + merged_stats["mean"], + merged_stats["std"], + merged_stats["max"], + merged_stats["min"], + merged_stats["cropped"], + audio_file_length_limit_in_s, + ) + logger.debug(print_statistics) + return processed_audio + + @staticmethod + def _transform_to_feature( + audio: torch.Tensor, + sampling_rate_in_hz: int, + audio_feature_dict: dict[str, float | int | str], + feature_dim: int, + max_length: int, + padding_value: float, + normalization_type: str | None = None, + type_key: str = TYPE, + ): + feature_type: str = str(audio_feature_dict[type_key]) + if feature_type == "raw": + audio_feature = torch.unsqueeze(audio[0], dim=-1) + elif feature_type in ["stft", "stft_phase", "group_delay", "fbank"]: + audio_feature = AudioFeatureMixin._get_2D_feature( + audio, feature_type, audio_feature_dict, sampling_rate_in_hz + ) + audio_feature = torch.transpose(audio_feature, 0, 1) + else: + raise ValueError(f"{feature_type} is not recognized.") + + # Outer conditional is type refinement from Union[str, None] to str + if normalization_type is not None: + if normalization_type == "per_file": + mean = torch.mean(audio_feature, dim=0) + std = torch.std(audio_feature, dim=0) + audio_feature = torch.divide((audio_feature - mean), std + 1.0e-10) + elif normalization_type == "global": + raise ValueError("not implemented yet") + + feature_length = audio_feature.shape[0] + broadcast_feature_length = min(feature_length, max_length) + audio_feature_padded = torch.full( + (max_length, feature_dim), padding_value, dtype=torch.float32, device=audio_feature.device + ) + audio_feature_padded[:broadcast_feature_length, :] = audio_feature[:max_length, :] + + return audio_feature_padded + + @staticmethod + def _get_stats(audio, sampling_rate_in_hz, max_length_in_s): + audio_length_in_s = audio.shape[-1] / float(sampling_rate_in_hz) + return { + "count": 1, + "sum": audio_length_in_s, + "sum2": audio_length_in_s * audio_length_in_s, + "min": audio_length_in_s, + "max": audio_length_in_s, + "cropped": 1 if audio_length_in_s > max_length_in_s else 0, + } + + @staticmethod + def _merge_stats(merged_stats, audio_stats): + merged_stats["count"] += audio_stats["count"] + merged_stats["sum"] += audio_stats["sum"] + merged_stats["sum2"] += audio_stats["sum2"] + merged_stats["min"] = min(merged_stats["min"], audio_stats["min"]) + merged_stats["max"] = max(merged_stats["max"], audio_stats["max"]) + merged_stats["cropped"] += audio_stats["cropped"] + + @staticmethod + def _make_lazy_decode_fn( + audio_feature_dict: dict, + feature_dim: int, + max_length: int, + padding_value: float, + normalization_type: str | None, + ): + """Return a per-path decode function suitable for use in ``LazyColumn``. + + The returned callable is stateless (captures only plain values) so it + is safe to share across DataLoader worker threads. + """ + + def decode(path: str) -> np.ndarray: + audio, sr = read_audio_from_path(path) + if not is_torch_audio_tuple((audio, sr)): + default_audio, default_sr = get_default_audio([(audio, sr)]) + audio, sr = default_audio, default_sr + return AudioFeatureMixin._transform_to_feature( + audio=audio, + sampling_rate_in_hz=sr, + audio_feature_dict=audio_feature_dict, + feature_dim=feature_dim, + max_length=max_length, + padding_value=padding_value, + normalization_type=normalization_type, + ).numpy() + + decode.__name__ = "audio_lazy_decode" + return decode + + @staticmethod + def _get_2D_feature( + audio: torch.Tensor, + feature_type: str, + audio_feature_dict: dict[str, float | int | str], + sampling_rate_in_hz: int, + ) -> torch.Tensor: + window_length_in_s = audio_feature_dict["window_length_in_s"] + window_shift_in_s = audio_feature_dict["window_shift_in_s"] + assert torch.jit.isinstance(window_length_in_s, float) + assert torch.jit.isinstance(window_shift_in_s, float) + + window_length_in_samp = get_length_in_samp(window_length_in_s, sampling_rate_in_hz) + + if "num_fft_points" in audio_feature_dict: + num_fft_points = audio_feature_dict["num_fft_points"] + assert torch.jit.isinstance(num_fft_points, int) + + if num_fft_points < window_length_in_samp: + raise ValueError( + f"num_fft_points: {num_fft_points} < window length in " + f"samples: {window_length_in_s} (corresponds to window length" + f" in s: {window_length_in_samp}" + ) + else: + num_fft_points = window_length_in_samp + + if "window_type" in audio_feature_dict: + window_type = audio_feature_dict["window_type"] + assert torch.jit.isinstance(window_type, str) + else: + window_type = "hamming" + + if feature_type == "stft_phase": + return get_phase_stft_magnitude( + audio, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type + ) + elif feature_type == "stft": + return get_stft_magnitude( + audio, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type + ) + elif feature_type == "group_delay": + return get_group_delay( + audio, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type + ) + elif feature_type == "fbank": + num_filter_bands = audio_feature_dict["num_filter_bands"] + assert torch.jit.isinstance(num_filter_bands, int) + + return get_fbank( + audio, + sampling_rate_in_hz, + window_length_in_s, + window_shift_in_s, + num_fft_points, + window_type, + num_filter_bands, + ) + else: + raise ValueError(f'feature_type "{feature_type}" is not recognized.') + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + set_default_value(feature_config["preprocessing"], "in_memory", preprocessing_parameters["in_memory"]) + + name = feature_config[NAME] + column = input_df[feature_config[COLUMN]] + + num_audio_files = len(column) + if num_audio_files == 0: + raise ValueError("There are no audio files in the dataset provided.") + + first_audio_entry = next(iter(column)) + logger.debug(f"Detected audio feature type is {type(first_audio_entry)}") + + src_path = None + if SRC in metadata: + if isinstance(first_audio_entry, str) and not has_remote_protocol(first_audio_entry): + src_path = os.path.dirname(os.path.abspath(metadata.get(SRC))) + abs_path_column = backend.df_engine.map_objects( # This gets the CSV file path + column, lambda row: get_abs_path(src_path, row) if isinstance(row, str) else row + ) + + num_audio_utterances = len(input_df[feature_config[COLUMN]]) + padding_value = preprocessing_parameters["padding_value"] + normalization_type = preprocessing_parameters["norm"] + + feature_dim = metadata[name]["feature_dim"] + max_length = metadata[name]["max_length"] + audio_feature_dict = { + key: value + for key, value in preprocessing_parameters.items() + if key in AUDIO_FEATURE_KEYS and value is not None + } + audio_file_length_limit_in_s = preprocessing_parameters["audio_file_length_limit_in_s"] + + if num_audio_utterances == 0: + raise ValueError("There are no audio files in the dataset provided.") + + if preprocessing_parameters.get("mode", "lazy") != "eager": + # Lazy path: store file paths as a string Series. The actual audio + # decode happens per-batch inside PandasDataset via LazyColumn. + # This bounds peak memory to batch_size × clip_size instead of N × clip_size. + # reshape is overridden to None so preprocessing.py skips the flatten step + # for path columns, and PandasDataset skips the reshape-restore step. + if isinstance(first_audio_entry, str): + # Input is already a local/remote path — use abs_path_column directly so + # that the DataFrame index is preserved after sampling/filtering operations. + proc_df[feature_config[PROC_COLUMN]] = abs_path_column + else: + # In-memory data (HF audio dicts, bare Tensors) — cache to disk first. + cache_dir = resolve_lazy_cache_dir( + preprocessing_parameters.get("lazy_cache_dir"), + name, + ) + logger.info(f"Audio feature '{name}': caching in-memory audio to {cache_dir} for lazy decoding.") + raw_column = abs_path_column.tolist() if hasattr(abs_path_column, "tolist") else list(abs_path_column) + path_list = _cache_audio_column_to_disk( + raw_column, + cache_dir, + name, + sampling_rate=metadata[name].get("sampling_rate_in_hz", 16_000), + ) + # Reconstruct a Series using the original index so that it aligns + # correctly with proc_df (which may have a non-0-based index after sampling). + if hasattr(abs_path_column, "compute"): # Dask Series + orig_index = abs_path_column.index.compute() + else: + orig_index = abs_path_column.index + proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.from_pandas( + pd.Series(path_list, dtype=object, index=orig_index) + ) + metadata[name]["lazy"] = True # backward compat for ray.py + metadata[name]["mode"] = preprocessing_parameters.get("mode", "lazy") + metadata[name]["prefetch_size"] = preprocessing_parameters.get("prefetch_size") + metadata[name]["reshape"] = None # paths are 1-D strings — no reshape needed + # Persist decode params in metadata so PandasDataset can reconstruct the fn + metadata[name]["lazy_audio_params"] = { + "audio_feature_dict": audio_feature_dict, + "feature_dim": feature_dim, + "max_length": max_length, + "padding_value": padding_value, + "normalization_type": normalization_type, + } + else: + # Eager path (legacy): decode all files upfront into a numpy array. + audio_features = AudioFeatureMixin._process_in_memory( + abs_path_column, + audio_feature_dict, + feature_dim, + max_length, + padding_value, + normalization_type, + audio_file_length_limit_in_s, + backend, + ) + proc_df[feature_config[PROC_COLUMN]] = audio_features + + return proc_df + + @staticmethod + def _get_max_length_feature( + preprocessing_parameters: PreprocessingConfigDict, sampling_rate_in_hz, audio_length_limit_in_s + ): + feature_type = preprocessing_parameters[TYPE] + audio_length_limit_in_samp = audio_length_limit_in_s * sampling_rate_in_hz + + if not audio_length_limit_in_samp.is_integer(): + raise ValueError( + "Audio_file_length_limit has to be chosen " + f"so that {audio_length_limit_in_s} (in s) * {sampling_rate_in_hz} (sampling rate in Hz) " + "is an integer." + ) + audio_length_limit_in_samp = int(audio_length_limit_in_samp) + + if feature_type == "raw": + return audio_length_limit_in_samp + elif feature_type in ["stft", "stft_phase", "group_delay", "fbank"]: + window_length_in_s = preprocessing_parameters["window_length_in_s"] + window_shift_in_s = preprocessing_parameters["window_shift_in_s"] + return get_max_length_stft_based( + audio_length_limit_in_samp, window_length_in_s, window_shift_in_s, sampling_rate_in_hz + ) + else: + raise ValueError(f"{feature_type} is not recognized.") + + +class AudioInputFeature(AudioFeatureMixin, SequenceInputFeature): + def __init__(self, input_feature_config: AudioInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, encoder_obj=encoder_obj, **kwargs) + + if not getattr(self.encoder_obj.config, "embedding_size", None): + raise ValueError('embedding_size has to be defined - check "update_config_with_metadata()"') + if not getattr(self.encoder_obj.config, "max_sequence_length", None): + raise ValueError('max_sequence_length has to be defined - check "update_config_with_metadata()"') + + def forward(self, inputs, mask=None): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Audio feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if inputs.dtype != torch.float32: + raise ValueError(f"Audio feature inputs dtype must be float32, got {inputs.dtype}.") + if len(inputs.shape) != 3: + raise ValueError( + f"Audio feature inputs must be 3D (batch x time x features), got shape {tuple(inputs.shape)}." + ) + + encoder_output = self.encoder_obj(inputs, mask=mask) + + return encoder_output + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.encoder_obj.config.max_sequence_length, self.encoder_obj.config.embedding_size]) + + @property + def input_dtype(self): + return torch.float32 + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.max_sequence_length = feature_metadata["max_length"] + feature_config.encoder.embedding_size = feature_metadata["feature_dim"] + feature_config.encoder.should_embed = False + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _AudioPreprocessing(metadata) + + @staticmethod + def get_schema_cls(): + return AudioInputFeatureConfig diff --git a/ludwig/features/bag_feature.py b/ludwig/features/bag_feature.py new file mode 100644 index 0000000..53045a1 --- /dev/null +++ b/ludwig/features/bag_feature.py @@ -0,0 +1,130 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from collections import Counter + +import numpy as np +import torch + +from ludwig.constants import BAG, COLUMN, NAME, PROC_COLUMN +from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin, InputFeature +from ludwig.features.feature_utils import set_str_to_idx +from ludwig.features.set_feature import _SetPreprocessing +from ludwig.schema.features.bag_feature import BagInputFeatureConfig +from ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict +from ludwig.utils.strings_utils import create_vocabulary + +logger = logging.getLogger(__name__) + + +class BagFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return BAG + + @staticmethod + def cast_column(column, backend): + return column.astype(str) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + vocabulary = create_vocabulary( + column, + preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + processor=backend.df_engine, + ) + return { + "idx2str": vocabulary.vocab, + "str2idx": vocabulary.str2idx, + "str2freq": vocabulary.str2freq, + "vocab_size": len(vocabulary.str2idx), + "max_set_size": vocabulary.max_sequence_length, + } + + @staticmethod + def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend): + def to_vector(set_str): + bag_vector = np.zeros((len(metadata["str2idx"]),), dtype=np.float32) + col_counter = Counter(set_str_to_idx(set_str, metadata["str2idx"], preprocessing_parameters["tokenizer"])) + + bag_vector[list(col_counter.keys())] = list(col_counter.values()) + return bag_vector + + return backend.df_engine.map_objects(column, to_vector) + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + proc_df[feature_config[PROC_COLUMN]] = BagFeatureMixin.feature_data( + input_df[feature_config[COLUMN]], + metadata[feature_config[NAME]], + preprocessing_parameters, + backend, + ) + return proc_df + + +class BagInputFeature(BagFeatureMixin, InputFeature): + def __init__(self, input_feature_config: BagInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Bag feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + + encoder_output = self.encoder_obj(inputs) + + return encoder_output + + @property + def input_shape(self) -> torch.Size: + return torch.Size([len(self.encoder_obj.config.vocab)]) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.vocab = feature_metadata["idx2str"] + + @staticmethod + def get_schema_cls(): + return BagInputFeatureConfig + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _SetPreprocessing(metadata, is_bag=True) diff --git a/ludwig/features/base_feature.py b/ludwig/features/base_feature.py new file mode 100644 index 0000000..c7a8689 --- /dev/null +++ b/ludwig/features/base_feature.py @@ -0,0 +1,655 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + +import torch +from torch import Tensor + +from ludwig.constants import ( + ENCODER_OUTPUT_STATE, + HIDDEN, + LENGTHS, + LOGITS, + LOSS, + PREDICTIONS, + PROBABILITIES, +) +from ludwig.decoders.registry import get_decoder_cls +from ludwig.encoders.registry import get_encoder_cls +from ludwig.features.feature_utils import get_input_size_with_dependencies +from ludwig.modules.fully_connected_modules import FCStack +from ludwig.modules.loss_modules import create_loss +from ludwig.modules.metric_modules import LossMetric, LudwigMetric, MeanMetric +from ludwig.modules.metric_registry import get_metric_classes, get_metric_cls, get_metric_tensor_input +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.schema.features.base import BaseFeatureConfig, BaseOutputFeatureConfig +from ludwig.types import ( + FeatureConfigDict, + FeatureMetadataDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.calibration import CalibrationModule +from ludwig.utils.torch_utils import LudwigModule +from ludwig.utils.types import DataFrame, PreprocessingInput + +logger = logging.getLogger(__name__) + + +class BasePreprocessingModule(torch.nn.Module): + """Shared base class for all feature preprocessing modules. + + All concrete preprocessing modules (``_CategoryPreprocessing``, ``_NumberPreprocessing``, etc.) must + inherit from this class and override ``forward``. The base class establishes the common interface so + that ``create_preproc_module`` can advertise a concrete return type and callers can use + ``isinstance(module, BasePreprocessingModule)`` checks. + + Subclasses must be TorchScript-compatible: avoid ABC ``@abstractmethod`` decorators and keep + all method signatures fully concrete-typed. + """ + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + raise NotImplementedError("Subclasses must implement forward()") + + +class BasePostprocessingModule(torch.nn.Module): + """Shared base class for all feature postprocessing modules. + + All concrete postprocessing modules (``_CategoryPostprocessing``, ``_NumberPostprocessing``, etc.) must + inherit from this class and override ``forward``. + """ + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> dict[str, Any]: + raise NotImplementedError("Subclasses must implement forward()") + + +class FeaturePreprocessingMixin(ABC): + """Parent class for feature mixins. + + Feature mixins support preprocessing functionality shared across input and output features. + """ + + @staticmethod + @abstractmethod + def type() -> str: + """Returns the type of feature this mixin supports.""" + raise NotImplementedError + + @staticmethod + @abstractmethod + def cast_column(column: DataFrame, backend) -> DataFrame: + """Returns a copy of the dataset column for the given feature, potentially after a type cast. + + Args: + column: Pandas column of values. + backend: (Union[Backend, str]) Backend to use for feature data processing. + """ + raise NotImplementedError + + @staticmethod + @abstractmethod + def get_feature_meta( + config: ModelConfigDict, + column: DataFrame, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + """Returns a dictionary of feature metadata. + + Args: + config: Ludwig model config dict. + column: Pandas column of values. + preprocessing_parameters: Preprocessing configuration for this feature. + backend: (Union[Backend, str]) Backend to use for feature data processing. + """ + raise NotImplementedError + + @staticmethod + @abstractmethod + def add_feature_data( + feature_config: FeatureConfigDict, + input_df: DataFrame, + proc_df: dict[str, DataFrame], + metadata: TrainingSetMetadataDict, + preprocessing_parameters: PreprocessingConfigDict, + backend, # Union[Backend, str] + skip_save_processed_input: bool, + ) -> None: + """Runs preprocessing on the input_df and stores results in the proc_df and metadata dictionaries. + + Args: + feature_config: Feature configuration. + input_df: Pandas column of values. + proc_df: Dict of processed columns of data. Feature data is added to this. + metadata: Metadata returned by get_feature_meta(). Additional information may be added to this. + preprocessing_parameters: Preprocessing configuration for this feature. + backend: (Union[Backend, str]) Backend to use for feature data processing. + skip_save_processed_input: Whether to skip saving the processed input. + """ + raise NotImplementedError + + +@dataclass +class NonPropertyModuleWrapper: + """Used to prevent the PredictModule from showing up as an attribute on the feature module. + + This is necessary to avoid inflight errors from some distributed strategies that may believe a param is still in the + process of being processed asynchronously (allgathered, etc.). + """ + + module: torch.nn.Module + + +class PredictModule(torch.nn.Module): + """Base class for all modules that convert model outputs to predictions. + + Explicit member variables needed here for scripting, as Torchscript will not be able to recognize global variables + during scripting. + """ + + def __init__(self): + super().__init__() + self.predictions_key = PREDICTIONS + self.probabilities_key = PROBABILITIES + self.logits_key = LOGITS + + +class BaseFeature: + """Base class for all features. + + Note that this class is not-cooperative (does not forward kwargs), so when constructing feature class hierarchies, + there should be only one parent class that derives from base feature. Other functionality should be put into mixin + classes to avoid the diamond pattern. + """ + + def __init__(self, feature: BaseFeatureConfig): + super().__init__() + + if not feature.name: + raise ValueError( + "Feature config is missing a 'name' field.\n" + "Every feature must have a unique name. " + "Check your config's input_features and output_features lists." + ) + self.feature_name = feature.name + + if not feature.column: + feature.column = self.feature_name + self.column = feature.column + + self.proc_column = feature.proc_column + + +class InputFeature(BaseFeature, LudwigModule, ABC): + """Parent class for all input features.""" + + def create_sample_input(self, batch_size: int = 2) -> torch.Tensor: + # Used by get_model_inputs(), which is used for tracing-based torchscript generation. + return torch.rand([batch_size, *self.input_shape]).to(self.input_dtype) + + def unskip(self) -> "InputFeature": + """Convert feature using passthrough wrapper back to full encoder.""" + return self + + @staticmethod + @abstractmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs) -> None: + pass + + def update_config_after_module_init(self, feature_config) -> None: + """Updates the config after the torch.nn.Module objects have been initialized.""" + + def initialize_encoder(self, encoder_config): + encoder_cls = get_encoder_cls(self.type(), encoder_config.type) + encoder_params_dict = encoder_config.to_dict() + + # HF text encoders (auto_transformer, bert, etc.) handle adapters natively + # via their _wrap_transformer() method. For those, pass adapter through. + # For all other encoders, remove adapter from kwargs (they don't expect it) + # and apply it generically via PEFT after construction. + has_native_adapter_support = hasattr(encoder_cls, "_wrap_transformer") + if not has_native_adapter_support: + encoder_params_dict.pop("adapter", None) + + encoder = encoder_cls(encoder_config=encoder_config, **encoder_params_dict) + + # Apply generic PEFT adapter for pretrained encoders without native adapter support + adapter_config = getattr(encoder_config, "adapter", None) + if ( + adapter_config + and isinstance(adapter_config, dict) + and encoder_config.is_pretrained() + and not has_native_adapter_support + ): + encoder = self._apply_adapter(encoder, adapter_config) + + return encoder + + @staticmethod + def _apply_adapter(encoder, adapter_config: dict): + """Apply a PEFT adapter to a pretrained encoder for parameter-efficient fine-tuning. + + This enables using LoRA, DoRA, VeRA, etc. on pretrained text encoders (BERT, etc.) and image encoders (TIMM, + torchvision) in ECD mode, not just for LLMs. + """ + import logging + + logger = logging.getLogger(__name__) + + try: + from peft import get_peft_model + + adapter_type = adapter_config.get("type", "lora") + + # Look up the Ludwig adapter config class and convert to PEFT config + from ludwig.schema.llms.peft import adapter_registry + + if adapter_type not in adapter_registry: + logger.warning(f"Unknown adapter type '{adapter_type}' for encoder. Skipping.") + return encoder + + ludwig_adapter_cls = adapter_registry[adapter_type] + ludwig_adapter = ludwig_adapter_cls.model_validate(adapter_config) + peft_config = ludwig_adapter.to_config() + + # Apply PEFT to the encoder's underlying model + # For HF text encoders, the transformer is usually at encoder.transformer + # For TIMM image encoders, the model is the encoder itself + target = getattr(encoder, "transformer", None) or getattr(encoder, "model", encoder) + wrapped = get_peft_model(target, peft_config) + + # Replace the model in the encoder + if hasattr(encoder, "transformer"): + encoder.transformer = wrapped + elif hasattr(encoder, "model"): + encoder.model = wrapped + else: + encoder = wrapped + + trainable = sum(p.numel() for p in wrapped.parameters() if p.requires_grad) + total = sum(p.numel() for p in wrapped.parameters()) + logger.info( + f"Applied {adapter_type} adapter to encoder: " + f"{trainable:,} trainable / {total:,} total params ({100 * trainable / total:.2f}%)" + ) + + except ImportError: + logger.warning("PEFT not installed. Cannot apply adapter to encoder. pip install peft") + except Exception: + logger.warning("Failed to apply adapter to encoder.", exc_info=True) + + return encoder + + @classmethod + def get_preproc_input_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "string" + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + raise NotImplementedError("Torchscript tracing not supported for feature") + + +class OutputFeature(BaseFeature, LudwigModule, ABC): + """Parent class for all output features.""" + + def __init__( + self, + feature: BaseOutputFeatureConfig, + other_output_features: dict[str, "OutputFeature"], + *args, + **kwargs, + ): + """Defines defaults, overwrites them based on the feature dictionary, and sets up dependencies. + + Any output feature can depend on one or more other output features. The `other_output_features` input dictionary + should contain entries for any dependent output features, which is accomplished by constructing output features + in topographically sorted order. Attributes of any dependent output features are used to properly initialize + this feature's sizes. + """ + super().__init__(feature) + + # List of names of metrics that this OutputFeature computes. + self.metric_names = [] + self.loss = feature.loss + self.reduce_input = feature.reduce_input + self.reduce_dependencies = feature.reduce_dependencies + + # List of feature names that this output feature is dependent on. + self.dependencies = feature.dependencies + + logger.debug(" output feature fully connected layers") + logger.debug(" FCStack") + + self.input_size = get_input_size_with_dependencies(feature.input_size, self.dependencies, other_output_features) + feature.input_size = self.input_size + + self.fc_stack = FCStack( + first_layer_input_size=self.input_size, + layers=feature.decoder.fc_layers, + num_layers=feature.decoder.num_fc_layers, + default_output_size=feature.decoder.fc_output_size, + default_use_bias=feature.decoder.fc_use_bias, + default_weights_initializer=feature.decoder.fc_weights_initializer, + default_bias_initializer=feature.decoder.fc_bias_initializer, + default_norm=feature.decoder.fc_norm, + default_norm_params=feature.decoder.fc_norm_params, + default_activation=feature.decoder.fc_activation, + default_dropout=feature.decoder.fc_dropout, + ) + self._calibration_module = self.create_calibration_module(feature) + self._prediction_module = NonPropertyModuleWrapper(self.create_predict_module()) + + # set up two sequence reducers, one for inputs and other for dependencies + self.reduce_sequence_input = SequenceReducer(reduce_mode=self.reduce_input) + if self.dependencies: + self.dependency_reducers = torch.nn.ModuleDict() + # todo: re-evaluate need for separate handling of `attention` reducer + # currently this code does not support `attention` + for dependency in self.dependencies: + self.dependency_reducers[dependency] = SequenceReducer(reduce_mode=self.reduce_dependencies) + + def create_sample_output(self, batch_size: int = 2) -> torch.Tensor: + output_shape = self.output_shape + shape = [batch_size, *self.output_shape] if output_shape != torch.Size([1]) else [batch_size] + return torch.rand(shape).to(self.get_output_dtype()) + + @abstractmethod + def get_prediction_set(self) -> set[str]: + """Returns the set of tensor keys returned by this feature's PredictModule.""" + raise NotImplementedError("OutputFeature is missing implementation for get_prediction_set.") + + @classmethod + @abstractmethod + def get_output_dtype(cls) -> torch.dtype: + """Returns the Tensor data type feature outputs.""" + + def initialize_decoder(self, decoder_config) -> torch.nn.Module: + # Input to the decoder is the output feature's FC hidden layer. + decoder_config.input_size = self.fc_stack.output_shape[-1] + decoder_cls = get_decoder_cls(self.type(), decoder_config.type) + decoder_params_dict = decoder_config.to_dict() + return decoder_cls(decoder_config=decoder_config, **decoder_params_dict) + + def train_loss(self, targets: Tensor, predictions: dict[str, Tensor], feature_name) -> tuple[Tensor, dict]: + loss_class = type(self.train_loss_function) + prediction_key = output_feature_utils.get_feature_concat_name(feature_name, loss_class.get_loss_inputs()) + return self.train_loss_function(predictions[prediction_key], targets) + + def eval_loss(self, targets: Tensor, predictions: dict[str, Tensor]) -> Tensor: + loss_class = type(self.train_loss_function) + prediction_key = loss_class.get_loss_inputs() + preds = predictions[prediction_key].detach() + if isinstance(self.eval_loss_metric, MeanMetric): + # MeanMetric stores the running average externally; get_current_value() computes the + # batch loss without touching that state (metrics are updated in update_metrics()). + return self.eval_loss_metric.get_current_value(preds, targets) + # For non-MeanMetric eval_loss_metrics (e.g. MSEMetric / MeanSquaredError), calling + # self.eval_loss_metric(preds, targets) would invoke forward() which updates the metric's + # running sum — double-counting each batch because update_metrics() already called update(). + # Compute the batch loss directly via the stateless train_loss_function instead. + with torch.no_grad(): + return self.train_loss_function(preds, targets) + + def _setup_loss(self) -> None: + self.train_loss_function = create_loss(self.loss) + self._eval_loss_metric = NonPropertyModuleWrapper(get_metric_cls(self.type(), self.loss.type)(config=self.loss)) + + def _setup_metrics(self) -> None: + kwargs = {} + for name, cls in get_metric_classes(self.type()).items(): + if cls.can_report(self) and isinstance(cls, LossMetric): + kwargs[name] = cls(config=self.loss, **self.metric_kwargs()) + elif cls.can_report(self): + kwargs[name] = cls(**self.metric_kwargs()) + self._metric_functions = { + LOSS: self.eval_loss_metric, + **kwargs, + } + self.metric_names = sorted(list(self._metric_functions.keys())) + + def create_calibration_module(self, feature: BaseOutputFeatureConfig) -> CalibrationModule: + """Creates and returns a CalibrationModule that converts logits to a probability distribution.""" + return None + + @property + def eval_loss_metric(self) -> LudwigMetric: + return self._eval_loss_metric.module + + @property + def calibration_module(self) -> torch.nn.Module: + """Returns the CalibrationModule used to convert logits to a probability distribution.""" + return self._calibration_module + + @abstractmethod + def create_predict_module(self) -> PredictModule: + """Creates and returns a `nn.Module` that converts raw model outputs (logits) to predictions. + + This module is needed when generating the Torchscript model using scripting. + """ + raise NotImplementedError() + + @property + def prediction_module(self) -> PredictModule: + """Returns the PredictModule used to convert model outputs to predictions.""" + return self._prediction_module.module + + def predictions(self, all_decoder_outputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + """Computes actual predictions from the outputs of feature decoders. + + TODO(Justin): Consider refactoring this to accept feature-specific decoder outputs. + + Args: + all_decoder_outputs: A dictionary of {feature name}::{tensor_name} -> output tensor. + Returns: + Dictionary of tensors with predictions as well as any additional tensors that may be + necessary for computing evaluation metrics. + """ + return self.prediction_module(all_decoder_outputs, feature_name) + + @abstractmethod + def logits(self, combiner_outputs: dict[str, torch.Tensor], target=None, **kwargs) -> dict[str, torch.Tensor]: + """Unpacks and feeds combiner_outputs to the decoder. Invoked as part of the output feature's forward pass. + + If target is not None, then we are in training. + + Args: + combiner_outputs: Dictionary of tensors from the combiner's forward pass. + Returns: + Dictionary of decoder's output tensors (non-normalized), as well as any additional + tensors that may be necessary for computing predictions or evaluation metrics. + """ + raise NotImplementedError("OutputFeature is missing logits() implementation.") + + def metric_kwargs(self) -> dict[str, Any]: + """Returns arguments that are used to instantiate an instance of each metric class.""" + return {} + + def update_metrics(self, targets: Tensor, predictions: dict[str, Tensor]) -> None: + """Updates metrics with the given targets and predictions. + + Args: + targets: Tensor with target values for this output feature. + predictions: Dict of tensors returned by predictions(). + """ + for metric_name, metric_fn in self._metric_functions.items(): + prediction_key = get_metric_tensor_input(metric_name) + metric_fn = metric_fn.to(predictions[prediction_key].device) + metric_fn.update(predictions[prediction_key].detach(), targets) + + def get_metrics(self) -> dict[str, float]: + # NOTE: do NOT wrap metric_fn.compute() in an explicit sync_context() call here. + # + # torchmetrics wraps every compute() internally in sync_context(). Ludwig overrides + # LudwigMetric.sync_context() to fall back to torch.distributed.gather_all_tensors + # when the registered Ludwig strategy provides no gather function but + # torch.distributed is already initialised (the Ray TorchTrainer / eval_fn case). + # + # Adding a manual outer sync_context() would cause torchmetrics to see + # _is_synced=True when its own inner sync_context() runs, raising: + # TorchMetricsUserError: The Metric has already been synced. + # + # See LudwigMetric.sync_context() in metric_modules.py for the full explanation. + metric_vals = {} + for metric_name, metric_fn in self._metric_functions.items(): + try: + computed_metric = metric_fn.compute() + except Exception as e: + logger.exception(f"Caught exception computing metric: {metric_name} with error: {e}.") + continue + + # Metrics from torchmetrics can be a straightforward tensor. + if isinstance(computed_metric, Tensor): + metric_vals[metric_name] = computed_metric.detach().cpu().numpy().item() + else: + # Metrics from torchmetrics can be a dict of tensors. + # For example, ROUGE is returned as a dictionary of tensors. + # Unpack. + for sub_metric_name, metric in computed_metric.items(): + metric_vals[sub_metric_name] = metric.detach().cpu().numpy().item() + return metric_vals + + def reset_metrics(self) -> None: + for _, metric_fn in self._metric_functions.items(): + if metric_fn is not None: + metric_fn.reset() + + def forward( + self, + combiner_outputs: dict[str, torch.Tensor], + other_output_feature_outputs: dict[str, torch.Tensor], + mask: torch.Tensor | None = None, + target: torch.Tensor | None = None, + ) -> dict[str, torch.Tensor]: + """Forward pass that takes in output from the combiner, and passes it through to the decoder. + + Args: + combiner_outputs: Dict of outputs from the combiner. + other_output_feature_outputs: Dict of tensors from other output features. Used for resolving dependencies. + mask: (Unused). Tensor for masking. + target: Tensor with targets. During training, targets != None. During prediction, targets = None. + + Returns: + Dict of output tensors, with at least 'last_hidden' and 'logits' as keys, as well as any additional tensor + results from the decoder. + """ + # extract the combined hidden layer + combiner_hidden = combiner_outputs["combiner_output"] + hidden = self.prepare_decoder_inputs(combiner_hidden, other_output_feature_outputs, mask=mask) + + # ================ Predictions ================ + logits_input = {HIDDEN: hidden} + # pass supplemental data from encoders to decoder + if ENCODER_OUTPUT_STATE in combiner_outputs: + logits_input[ENCODER_OUTPUT_STATE] = combiner_outputs[ENCODER_OUTPUT_STATE] + if LENGTHS in combiner_outputs: + logits_input[LENGTHS] = combiner_outputs[LENGTHS] + + logits = self.logits(logits_input, target=target) + + # For binary and number features, self.logits() is a tensor. + # There are two special cases where self.logits() is a dict: + # categorical + # keys: logits, projection_input + # sequence + # keys: logits + if isinstance(logits, Tensor): + logits = {"logits": logits} + + # For multi-class features, we must choose a consistent tuple subset. + return { + # last_hidden used for dependencies processing + "last_hidden": hidden, + **logits, + } + + @abstractmethod + def postprocess_predictions( + self, + result: dict[str, Tensor], + metadata: TrainingSetMetadataDict, + ) -> dict[str, list]: + raise NotImplementedError + + @classmethod + def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "string" + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + raise NotImplementedError("Torchscript tracing not supported for feature") + + @staticmethod + @abstractmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs) -> None: + pass + + @staticmethod + @abstractmethod + def calculate_overall_stats(predictions, targets, train_set_metadata) -> dict: + pass + + def output_specific_fully_connected(self, inputs, mask=None) -> torch.Tensor: + feature_hidden = inputs + original_feature_hidden = inputs + + # flatten inputs + if len(original_feature_hidden.shape) > 2: + feature_hidden = torch.reshape(feature_hidden, (-1, list(feature_hidden.shape)[-1])) + + # pass it through fc_stack + feature_hidden = self.fc_stack(feature_hidden, mask=mask) + feature_hidden_size = feature_hidden.shape[-1] + + # reshape back to original first and second dimension + if len(original_feature_hidden.shape) > 2: + sequence_length = original_feature_hidden.shape[1] + feature_hidden = torch.reshape(feature_hidden, (-1, sequence_length, feature_hidden_size)) + + return feature_hidden + + def prepare_decoder_inputs( + self, combiner_hidden: Tensor, other_output_features: dict[str, Tensor], mask=None + ) -> Tensor: + """Takes the combiner output and the outputs of other outputs features computed so far and performs: + + - reduction of combiner outputs (if needed) + - concatenating the outputs of dependent features (if needed) + - output_specific fully connected layers (if needed) + + Args: + combiner_hidden: hidden state of the combiner + other_output_features: output tensors from other output features + """ + # ================ Reduce Inputs ================ + feature_hidden = combiner_hidden + if self.reduce_input is not None and len(combiner_hidden.shape) > 2: + feature_hidden = self.reduce_sequence_input(combiner_hidden) + + # ================ Concat Dependencies ================ + if self.dependencies: + feature_hidden = output_feature_utils.concat_dependencies( + self.column, self.dependencies, self.dependency_reducers, feature_hidden, other_output_features + ) + + # ================ Output-wise Fully Connected ================ + feature_hidden = self.output_specific_fully_connected(feature_hidden, mask=mask) + + return feature_hidden diff --git a/ludwig/features/binary_feature.py b/ludwig/features/binary_feature.py new file mode 100644 index 0000000..8ff75b7 --- /dev/null +++ b/ludwig/features/binary_feature.py @@ -0,0 +1,409 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import numpy as np +import torch + +from ludwig.constants import BINARY, COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROBABILITIES, PROBABILITY, PROC_COLUMN +from ludwig.error import InputDataError +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + FeaturePreprocessingMixin, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.schema.features.binary_feature import BinaryInputFeatureConfig, BinaryOutputFeatureConfig +from ludwig.types import ( + FeatureConfigDict, + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import calibration, output_feature_utils, strings_utils +from ludwig.utils.eval_utils import ( + average_precision_score, + ConfusionMatrix, + precision_recall_curve, + roc_auc_score, + roc_curve, +) +from ludwig.utils.types import DataFrame, PreprocessingInput + +logger = logging.getLogger(__name__) + + +class _BinaryPreprocessing(BasePreprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + str2bool = metadata.get("str2bool") + self.str2bool = str2bool or dict.fromkeys(strings_utils.BOOL_TRUE_STRS, True) + self.should_lower = str2bool is None + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if torch.jit.isinstance(v, list[tuple[torch.Tensor, int]]): + raise ValueError(f"Unsupported input: {v}") + + if torch.jit.isinstance(v, list[torch.Tensor]): + v = torch.stack(v) + + if torch.jit.isinstance(v, torch.Tensor): + return v.to(dtype=torch.float32) + + v = [s.strip() for s in v] + if self.should_lower: + v = [s.lower() for s in v] + indices = [self.str2bool.get(s, False) for s in v] + return torch.tensor(indices, dtype=torch.float32) + + +class _BinaryPostprocessing(BasePostprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + bool2str = metadata.get("bool2str") + self.bool2str = dict(enumerate(bool2str)) if bool2str is not None else None + self.predictions_key = PREDICTIONS + self.probabilities_key = PROBABILITIES + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key) + + if self.bool2str is not None: + predictions = predictions.to(dtype=torch.int32) + predictions = [self.bool2str.get(pred, self.bool2str[0]) for pred in predictions] + + probabilities = torch.stack([1 - probabilities, probabilities], dim=-1) + + return { + self.predictions_key: predictions, + self.probabilities_key: probabilities, + } + + +class _BinaryPredict(PredictModule): + def __init__(self, threshold, calibration_module=None): + super().__init__() + self.threshold = threshold + self.calibration_module = calibration_module + + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + + if self.calibration_module is not None: + probabilities = self.calibration_module(logits) + else: + probabilities = torch.sigmoid(logits) + + predictions = probabilities >= self.threshold + return { + self.probabilities_key: probabilities, + self.predictions_key: predictions, + self.logits_key: logits, + } + + +class BinaryFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return BINARY + + @staticmethod + def cast_column(column, backend): + """Cast column of dtype object to bool. + + Unchecked casting to boolean when given a column of dtype object converts all non-empty cells to True. We check + the values of the column directly and manually determine the best dtype to use. + """ + values = backend.df_engine.compute(column.drop_duplicates()) + + if strings_utils.values_are_pandas_numbers(values): + # If numbers, convert to float so it can be converted to bool + column = column.astype(float).astype(bool) + elif strings_utils.values_are_pandas_bools(values): + # If booleans, manually assign boolean values + column = backend.df_engine.map_objects( + column, lambda x: x.lower() in strings_utils.PANDAS_TRUE_STRS + ).astype(bool) + else: + # If neither numbers or booleans, they are strings (objects) + column = column.astype(object) + return column + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column: DataFrame, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + if column.dtype != object: + return {} + + distinct_values = backend.df_engine.compute(column.drop_duplicates()) + if len(distinct_values) > 2: + raise InputDataError( + column.name, BINARY, f"expects 2 distinct values, found {distinct_values.values.tolist()}" + ) + if preprocessing_parameters["fallback_true_label"]: + fallback_true_label = preprocessing_parameters["fallback_true_label"] + else: + fallback_true_label = sorted(distinct_values)[0] + preprocessing_parameters["fallback_true_label"] = fallback_true_label + + try: + str2bool = {v: strings_utils.str2bool(v) for v in distinct_values} + except Exception as e: + logger.warning( + f"Binary feature {column.name} has at least 1 unconventional boolean value: {e}. " + f"We will now interpret {fallback_true_label} as 1 and the other values as 0. " + f"If this is incorrect, please use the category feature type or " + f"manually specify the true value with `preprocessing.fallback_true_label`." + ) + str2bool = {v: strings_utils.str2bool(v, fallback_true_label) for v in distinct_values} + + bool2str = [k for k, v in sorted(str2bool.items(), key=lambda item: item[1])] + return {"str2bool": str2bool, "bool2str": bool2str, "fallback_true_label": fallback_true_label} + + @staticmethod + def add_feature_data( + feature_config: FeatureConfigDict, + input_df: DataFrame, + proc_df: dict[str, DataFrame], + metadata: TrainingSetMetadataDict, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input: bool, + ) -> None: + column = input_df[feature_config[COLUMN]] + + if column.dtype == object: + metadata = metadata[feature_config[NAME]] + if "str2bool" in metadata: + column = backend.df_engine.map_objects(column, lambda x: metadata["str2bool"][str(x)]) + else: + # No predefined mapping from string to bool, so compute it directly + column = backend.df_engine.map_objects(column, strings_utils.str2bool) + + proc_df[feature_config[PROC_COLUMN]] = column.astype(np.bool_) + + return proc_df + + +class BinaryInputFeature(BinaryFeatureMixin, InputFeature): + def __init__(self, input_feature_config: BinaryInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + input_feature_config.encoder.input_size = self.input_shape[-1] + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Binary feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + _valid_dtypes = (torch.bool, torch.int64, torch.float32) + if inputs.dtype not in _valid_dtypes: + raise ValueError(f"Binary feature inputs dtype must be one of {_valid_dtypes}, got {inputs.dtype}.") + if not (len(inputs.shape) == 1 or (len(inputs.shape) == 2 and inputs.shape[1] == 1)): + raise ValueError( + f"Binary feature inputs must be 1D or 2D with shape[1]==1, got shape {tuple(inputs.shape)}." + ) + + if len(inputs.shape) == 1: + inputs = inputs[:, None] + + # Inputs to the binary encoder could be of dtype torch.bool. Linear layer + # weights are of dtype torch.float32. The inputs and the weights need to + # be of the same dtype. + if inputs.dtype == torch.bool: + inputs = inputs.type(torch.float32) + + encoder_outputs = self.encoder_obj(inputs) + return encoder_outputs + + @property + def input_dtype(self): + return torch.bool + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + @staticmethod + def get_schema_cls(): + return BinaryInputFeatureConfig + + def create_sample_input(self, batch_size: int = 2): + return torch.rand([batch_size]) > 0.5 + + @classmethod + def get_preproc_input_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "string" if metadata.get("str2bool") else "int32" + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _BinaryPreprocessing(metadata) + + +class BinaryOutputFeature(BinaryFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: BinaryOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self.threshold = output_feature_config.threshold + super().__init__(output_feature_config, output_features, **kwargs) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, **kwargs): + hidden = inputs[HIDDEN] + return self.decoder_obj(hidden) + + def create_calibration_module(self, feature: BinaryOutputFeatureConfig) -> torch.nn.Module: + """Creates the appropriate calibration module based on the feature config. + + Today, only one type of calibration ("temperature_scaling") is available, but more options may be supported in + the future. + """ + if feature.calibration: + calibration_cls = calibration.get_calibration_cls(BINARY, "temperature_scaling") + return calibration_cls(binary=True) + return None + + def create_predict_module(self) -> PredictModule: + # A lot of code assumes output features have a prediction module, but if we are using a passthrough + # decoder then there is no threshold. + threshold = getattr(self, "threshold", 0.5) + return _BinaryPredict(threshold, calibration_module=self.calibration_module) + + def get_prediction_set(self): + return {PREDICTIONS, PROBABILITIES, LOGITS} + + @classmethod + def get_output_dtype(cls): + return torch.bool + + @property + def output_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + overall_stats = {} + confusion_matrix = ConfusionMatrix(targets, predictions[PREDICTIONS], labels=["False", "True"]) + overall_stats["confusion_matrix"] = confusion_matrix.cm.tolist() + overall_stats["overall_stats"] = confusion_matrix.stats() + overall_stats["per_class_stats"] = confusion_matrix.per_class_stats() + fpr, tpr, thresholds = roc_curve(targets, predictions[PROBABILITIES]) + overall_stats["roc_curve"] = { + "false_positive_rate": fpr.tolist(), + "true_positive_rate": tpr.tolist(), + } + overall_stats["roc_auc_macro"] = roc_auc_score(targets, predictions[PROBABILITIES], average="macro") + overall_stats["roc_auc_micro"] = roc_auc_score(targets, predictions[PROBABILITIES], average="micro") + ps, rs, thresholds = precision_recall_curve(targets, predictions[PROBABILITIES]) + overall_stats["precision_recall_curve"] = { + "precisions": ps.tolist(), + "recalls": rs.tolist(), + } + overall_stats["average_precision_macro"] = average_precision_score( + targets, predictions[PROBABILITIES], average="macro" + ) + overall_stats["average_precision_micro"] = average_precision_score( + targets, predictions[PROBABILITIES], average="micro" + ) + overall_stats["average_precision_samples"] = average_precision_score( + targets, predictions[PROBABILITIES], average="samples" + ) + + return overall_stats + + def postprocess_predictions( + self, + result, + metadata, + ): + class_names = ["False", "True"] + if "bool2str" in metadata: + class_names = metadata["bool2str"] + + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + if predictions_col in result: + if "bool2str" in metadata: + result[predictions_col] = result[predictions_col].map( + lambda pred: metadata["bool2str"][pred], + ) + + probabilities_col = f"{self.feature_name}_{PROBABILITIES}" + if probabilities_col in result: + false_col = f"{probabilities_col}_{class_names[0]}" + true_col = f"{probabilities_col}_{class_names[1]}" + prob_col = f"{self.feature_name}_{PROBABILITY}" + + result = result.assign( + **{ + false_col: lambda x: 1 - x[probabilities_col], + true_col: lambda x: x[probabilities_col], + prob_col: np.where( + result[probabilities_col] > 0.5, result[probabilities_col], 1 - result[probabilities_col] + ), + probabilities_col: result[probabilities_col].map(lambda x: [1 - x, x]), + }, + ) + + return result + + @staticmethod + def get_schema_cls(): + return BinaryOutputFeatureConfig + + @classmethod + def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "string" if metadata.get("bool2str") else "int32" + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _BinaryPostprocessing(metadata) + + def metric_kwargs(self) -> dict: + """Returns arguments that are used to instantiate an instance of each metric class.""" + return {"task": "binary"} diff --git a/ludwig/features/category_feature.py b/ludwig/features/category_feature.py new file mode 100644 index 0000000..732cc72 --- /dev/null +++ b/ludwig/features/category_feature.py @@ -0,0 +1,554 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from typing import Any + +import numpy as np +import torch + +from ludwig.constants import ( + CATEGORY, + CATEGORY_DISTRIBUTION, + COLUMN, + HIDDEN, + LOGITS, + NAME, + PREDICTIONS, + PREPROCESSING, + PROBABILITIES, + PROBABILITY, + PROC_COLUMN, + PROJECTION_INPUT, +) +from ludwig.error import InputDataError +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + FeaturePreprocessingMixin, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.features.vector_feature import VectorFeatureMixin +from ludwig.schema.features.category_feature import ( + CategoryDistributionOutputFeatureConfig, + CategoryInputFeatureConfig, + CategoryOutputFeatureConfig, +) +from ludwig.schema.features.loss.loss import CORNLossConfig +from ludwig.types import ( + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import calibration, output_feature_utils +from ludwig.utils.eval_utils import ConfusionMatrix +from ludwig.utils.math_utils import int_type, softmax +from ludwig.utils.strings_utils import create_vocabulary_single_token, UNKNOWN_SYMBOL +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + + +class _CategoryPreprocessing(BasePreprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.str2idx = metadata["str2idx"] + if UNKNOWN_SYMBOL in self.str2idx: + self.unk = self.str2idx[UNKNOWN_SYMBOL] + else: + # self.unk is set to 0 to comply with Torchscript type tracing and will + # likely not be used during training, but potentially during inference + self.unk = 0 + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if not torch.jit.isinstance(v, list[str]): + raise ValueError(f"Unsupported input: {v}") + + indices = [self.str2idx.get(s.strip(), self.unk) for s in v] + return torch.tensor(indices, dtype=torch.int32) + + +class _CategoryPostprocessing(BasePostprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.idx2str = dict(enumerate(metadata["idx2str"])) + self.unk = UNKNOWN_SYMBOL + self.predictions_key = PREDICTIONS + self.probabilities_key = PROBABILITIES + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key) + + inv_preds = [self.idx2str.get(pred, self.unk) for pred in predictions] + + return { + self.predictions_key: inv_preds, + self.probabilities_key: probabilities, + } + + +class _CategoryPredict(PredictModule): + def __init__(self, calibration_module=None, use_cumulative_probs=False): + super().__init__() + self.calibration_module = calibration_module + + # Derive the label from the cumulative probability distribution of the ordered category logits. + # Taken from CORN loss implementation: + # https://github.com/Raschka-research-group/coral-pytorch/blob/main/coral_pytorch/dataset.py#L123 + self.use_cumulative_probs = use_cumulative_probs + + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + + if self.use_cumulative_probs: + if self.calibration_module is not None: + probabilities = self.calibration_module(logits) + else: + probabilities = torch.sigmoid(logits) + probabilities = torch.cumprod(probabilities, dim=1) + + predict_levels = probabilities > 0.5 + predictions = torch.sum(predict_levels, dim=1) + else: + if self.calibration_module is not None: + probabilities = self.calibration_module(logits) + else: + probabilities = torch.softmax(logits, -1) + predictions = torch.argmax(probabilities, -1) + + predictions = predictions.long() + + # EXPECTED SHAPE OF RETURNED TENSORS + # predictions: [batch_size] + # probabilities: [batch_size, num_classes] + # logits: [batch_size, num_classes] + return {self.predictions_key: predictions, self.probabilities_key: probabilities, self.logits_key: logits} + + +class CategoryFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return CATEGORY + + @staticmethod + def cast_column(column, backend): + return column.astype(str) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + idx2str, str2idx, str2freq = create_vocabulary_single_token( + column, + num_most_frequent=preprocessing_parameters["most_common"], + processor=backend.df_engine, + ) + + if preprocessing_parameters.get("vocab"): # Check that vocab is non-empty + # If vocab was explciitly provided, override the inferred vocab + idx2str = preprocessing_parameters["vocab"] + str2idx = {s: i for i, s in enumerate(idx2str)} + str2freq = {k: str2freq.get(k, 0) for k in idx2str} + + if "fallback_label" in preprocessing_parameters: + # This is a category output feature for LLMs + # Check if the fallback label is in the vocab, if not add it. + if preprocessing_parameters["fallback_label"] not in str2idx: + str2idx[preprocessing_parameters["fallback_label"]] = len(str2idx) + idx2str.append(preprocessing_parameters["fallback_label"]) + str2freq[preprocessing_parameters["fallback_label"]] = 0 + + vocab_size = len(str2idx) + if not is_input_feature and vocab_size <= 1: + # Category output feature with vocab size 1 + raise InputDataError( + column.name, + CATEGORY, + f""" + At least 2 distinct values are required for category output features, but column + only contains {idx2str!s}. + """, + ) + if vocab_size <= 1: + # Category input feature with vocab size 1 + logger.info( + f"Input feature '{column.name}' contains only 1 distinct value {idx2str!s}. This is not useful" + " for machine learning models because this feature has zero variance. Consider removing this feature" + " from your input features." + ) + return {"idx2str": idx2str, "str2idx": str2idx, "str2freq": str2freq, "vocab_size": vocab_size} + + @staticmethod + def feature_data(backend, column, metadata): + def __replace_token_with_idx(value: Any, metadata: TrainingSetMetadataDict, fallback_symbol_idx: int) -> int: + stripped_value = value.strip() + if stripped_value in metadata["str2idx"]: + return metadata["str2idx"][stripped_value] + logger.warning(f""" + Encountered unknown symbol '{stripped_value}' for '{column.name}' during category + feature preprocessing. This should never happen during training. If this happens during + inference, this may be an indication that not all possible symbols were present in your + training set. Consider re-splitting your data to ensure full representation, or setting + preprocessing.most_common parameter to be smaller than this feature's total vocabulary + size, {len(metadata["str2idx"])}, which will ensure that the model is architected and + trained with an UNKNOWN symbol. Returning the index for the most frequent symbol, + {metadata["idx2str"][fallback_symbol_idx]}, instead. + """) + return fallback_symbol_idx + + # No unknown symbol in Metadata from preprocessing means that all values + # should be mappable to vocabulary + if UNKNOWN_SYMBOL not in metadata["str2idx"]: + # If no unknown is defined, just use the most popular token's index as the fallback index + most_popular_token = max(metadata["str2freq"], key=metadata["str2freq"].get) + most_popular_token_idx = metadata["str2idx"].get(most_popular_token) + return backend.df_engine.map_objects( + column, + lambda x: __replace_token_with_idx(x, metadata, most_popular_token_idx), + meta=(column.name, int), + ).astype(int_type(metadata["vocab_size"])) + else: + return backend.df_engine.map_objects( + column, + lambda x: ( + metadata["str2idx"][x.strip()] + if x.strip() in metadata["str2idx"] + else metadata["str2idx"][UNKNOWN_SYMBOL] + ), + meta=(column.name, int), + ).astype(int_type(metadata["vocab_size"])) + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + proc_df[feature_config[PROC_COLUMN]] = CategoryFeatureMixin.feature_data( + backend, + input_df[feature_config[COLUMN]], + metadata[feature_config[NAME]], + ) + + return proc_df + + +class CategoryDistributionFeatureMixin(VectorFeatureMixin): + @staticmethod + def type(): + return CATEGORY_DISTRIBUTION + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + idx2str = preprocessing_parameters["vocab"] + str2idx = {s: i for i, s in enumerate(idx2str)} + return { + "preprocessing": preprocessing_parameters, + "idx2str": idx2str, + "str2idx": str2idx, + "vocab_size": len(idx2str), + } + + +class CategoryInputFeature(CategoryFeatureMixin, InputFeature): + def __init__(self, input_feature_config: CategoryInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Category feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + _valid_dtypes = (torch.int8, torch.int16, torch.int32, torch.int64) + if inputs.dtype not in _valid_dtypes: + raise ValueError(f"Category feature inputs dtype must be one of {_valid_dtypes}, got {inputs.dtype}.") + if not (len(inputs.shape) == 1 or (len(inputs.shape) == 2 and inputs.shape[1] == 1)): + raise ValueError( + f"Category feature inputs must be 1D or 2D with shape[1]==1, got shape {tuple(inputs.shape)}." + ) + + inputs = inputs.reshape(-1, 1) + if inputs.dtype == torch.int8 or inputs.dtype == torch.int16: + inputs = inputs.type(torch.int) + encoder_output = self.encoder_obj(inputs) + + return encoder_output + + @property + def input_dtype(self): + return torch.int32 + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self.encoder_obj.output_shape) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.vocab = feature_metadata["idx2str"] + feature_config.encoder.skip = feature_metadata[PREPROCESSING].get("cache_encoder_embeddings", False) + + @staticmethod + def get_schema_cls(): + return CategoryInputFeatureConfig + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _CategoryPreprocessing(metadata) + + +class CategoryOutputFeature(CategoryFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: CategoryOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self.num_classes = output_feature_config.num_classes + self.top_k = output_feature_config.top_k + + # TODO(travis): make this more general to other cumulative loss functions + self.use_cumulative_probs = isinstance(output_feature_config.loss, CORNLossConfig) + + super().__init__(output_feature_config, output_features, **kwargs) + if hasattr(output_feature_config.decoder, "num_classes"): + output_feature_config.decoder.num_classes = output_feature_config.num_classes + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, **kwargs): # hidden + hidden = inputs[HIDDEN] + + # EXPECTED SHAPES FOR RETURNED TENSORS + # logits: shape [batch_size, num_classes] + # hidden: shape [batch_size, size of final fully connected layer] + return {LOGITS: self.decoder_obj(hidden), PROJECTION_INPUT: hidden} + + def create_calibration_module(self, feature: CategoryOutputFeatureConfig) -> torch.nn.Module: + """Creates the appropriate calibration module based on the feature config. + + Today, only one type of calibration ("temperature_scaling") is available, but more options may be supported in + the future. + """ + if feature.calibration: + calibration_cls = calibration.get_calibration_cls(CATEGORY, "temperature_scaling") + return calibration_cls(num_classes=self.num_classes) + return None + + def create_predict_module(self) -> PredictModule: + return _CategoryPredict( + calibration_module=self.calibration_module, use_cumulative_probs=self.use_cumulative_probs + ) + + def get_prediction_set(self): + return {PREDICTIONS, PROBABILITIES, LOGITS} + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @classmethod + def get_output_dtype(cls): + return torch.int64 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([1]) + + def metric_kwargs(self): + return {"top_k": self.top_k, "num_classes": self.num_classes} + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.num_classes = feature_metadata["vocab_size"] + feature_config.top_k = min(feature_config.num_classes, feature_config.top_k) + + # If labels are provided, then this is a classification task for LLMs + if hasattr(feature_config.preprocessing, "vocab"): + # Enrich the feature config's decoder with str2idx + feature_config.decoder.str2idx = feature_metadata["str2idx"] + + if isinstance(feature_config.loss.class_weights, (list, tuple)): + if len(feature_config.loss.class_weights) != feature_config.num_classes: + raise ValueError( + f"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with " + f"the number of classes ({feature_config.num_classes}) for feature {feature_config.column}. " + "Check the metadata JSON file to see the classes " + "and their order and consider there needs to be a weight " + "for the class too." + ) + + if isinstance(feature_config.loss.class_weights, dict): + if feature_metadata["str2idx"].keys() != feature_config.loss.class_weights.keys(): + raise ValueError( + f"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with " + f"the classes ({feature_metadata['str2idx'].keys()}) of feature {feature_config.column}. " + "Check the metadata JSON file to see the classes " + "and consider there needs to be a weight " + "for the class too." + ) + else: + class_weights = feature_config.loss.class_weights + idx2str = feature_metadata["idx2str"] + class_weights_list = [class_weights[s] for s in idx2str] + feature_config.loss.class_weights = class_weights_list + + if feature_config.loss.class_similarities_temperature > 0: + if feature_config.loss.class_similarities is not None: + similarities = feature_config.loss.class_similarities + temperature = feature_config.loss.class_similarities_temperature + + curr_row = 0 + first_row_length = 0 + is_first_row = True + for row in similarities: + if is_first_row: + first_row_length = len(row) + is_first_row = False + curr_row += 1 + else: + curr_row_length = len(row) + if curr_row_length != first_row_length: + raise ValueError( + f"The length of row {curr_row} of the class_similarities " + f"of {feature_config.column} is {curr_row_length}, different from the length of " + f"the first row {first_row_length}. All rows must have " + "the same length." + ) + else: + curr_row += 1 + all_rows_length = first_row_length + + if all_rows_length != len(similarities): + raise ValueError( + f"The class_similarities matrix of {feature_config.column} has " + f"{len(similarities)} rows and {all_rows_length} columns, " + "their number must be identical." + ) + + if all_rows_length != feature_config.num_classes: + raise ValueError( + f"The size of the class_similarities matrix of {feature_config.column} is " + f"{all_rows_length}, different from the number of classes ({feature_config.num_classes}). " + "Check the metadata JSON file to see the classes " + "and their order and " + "consider class too." + ) + + similarities = np.array(similarities, dtype=np.float32) + for i in range(len(similarities)): + similarities[i, :] = softmax(similarities[i, :], temperature=temperature) + + feature_config.loss.class_similarities = similarities + else: + raise ValueError( + "class_similarities_temperature > 0, " + "but no class_similarities are provided " + f"for feature {feature_config.column}" + ) + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + overall_stats = {} + confusion_matrix = ConfusionMatrix(targets, predictions[PREDICTIONS], labels=train_set_metadata["idx2str"]) + overall_stats["confusion_matrix"] = confusion_matrix.cm.tolist() + overall_stats["overall_stats"] = confusion_matrix.stats() + overall_stats["per_class_stats"] = confusion_matrix.per_class_stats() + + return overall_stats + + def postprocess_predictions( + self, + predictions, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + if predictions_col in predictions: + if "idx2str" in metadata: + predictions[predictions_col] = predictions[predictions_col].map(lambda pred: metadata["idx2str"][pred]) + + probabilities_col = f"{self.feature_name}_{PROBABILITIES}" + if probabilities_col in predictions: + prob_col = f"{self.feature_name}_{PROBABILITY}" + predictions[prob_col] = predictions[probabilities_col].map(max) + predictions[probabilities_col] = predictions[probabilities_col].map(lambda pred: pred.tolist()) + if "idx2str" in metadata: + for i, label in enumerate(metadata["idx2str"]): + key = f"{probabilities_col}_{label}" + + # Use default param to force a capture before the loop completes, see: + # https://stackoverflow.com/questions/2295290/what-do-lambda-function-closures-capture + predictions[key] = predictions[probabilities_col].map(lambda prob, i=i: prob[i]) + + top_k_col = f"{self.feature_name}_predictions_top_k" + if top_k_col in predictions: + if "idx2str" in metadata: + predictions[top_k_col] = predictions[top_k_col].map( + lambda pred_top_k: [metadata["idx2str"][pred] for pred in pred_top_k] + ) + + return predictions + + @staticmethod + def get_schema_cls(): + return CategoryOutputFeatureConfig + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _CategoryPostprocessing(metadata) + + +class CategoryDistributionOutputFeature(CategoryDistributionFeatureMixin, CategoryOutputFeature): + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @classmethod + def get_output_dtype(cls): + return torch.float32 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.num_classes]) + + @staticmethod + def get_schema_cls(): + return CategoryDistributionOutputFeatureConfig diff --git a/ludwig/features/date_feature.py b/ludwig/features/date_feature.py new file mode 100644 index 0000000..e6bd7aa --- /dev/null +++ b/ludwig/features/date_feature.py @@ -0,0 +1,167 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from datetime import date, datetime + +import numpy as np +import torch + +from ludwig.constants import COLUMN, DATE, DATE_VECTOR_LENGTH, PROC_COLUMN +from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin, InputFeature +from ludwig.schema.features.date_feature import DateInputFeatureConfig +from ludwig.types import ( + FeatureConfigDict, + FeatureMetadataDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils.date_utils import create_vector_from_datetime_obj, parse_datetime +from ludwig.utils.types import DataFrame, PreprocessingInput + +logger = logging.getLogger(__name__) + + +class _DatePreprocessing(BasePreprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if torch.jit.isinstance(v, list[torch.Tensor]): + v = torch.stack(v) + + if torch.jit.isinstance(v, torch.Tensor): + return v.to(dtype=torch.int) + else: + raise ValueError(f"Unsupported input: {v}") + + +class DateFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return DATE + + @staticmethod + def cast_column(column, backend): + return column + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + return {"preprocessing": preprocessing_parameters} + + @staticmethod + def date_to_list(date_value, datetime_format, preprocessing_parameters): + try: + if isinstance(date_value, datetime): + datetime_obj = date_value + elif isinstance(date_value, date): + datetime_obj = datetime.combine(date=date_value, time=datetime.min.time()) + elif isinstance(date_value, str) and datetime_format is not None: + try: + datetime_obj = datetime.strptime(date_value, datetime_format) + except ValueError: + datetime_obj = parse_datetime(date_value) + else: + datetime_obj = parse_datetime(date_value) + except Exception as e: + logger.error( + f"Error parsing date: '{date_value}' with error '{e}' " + "Please provide a datetime format that parses it " + "in the preprocessing section of the date feature " + "in the config. " + "The preprocessing fill in value will be used." + "For more details: " + "https://ludwig-ai.github.io/ludwig-docs/latest/configuration/features/date_features/#date-features-preprocessing" + ) + fill_value = preprocessing_parameters["fill_value"] + if fill_value != "": + datetime_obj = parse_datetime(fill_value) + else: + datetime_obj = datetime.now() + + return create_vector_from_datetime_obj(datetime_obj) + + @staticmethod + def add_feature_data( + feature_config: FeatureConfigDict, + input_df: DataFrame, + proc_df: dict[str, DataFrame], + metadata: TrainingSetMetadataDict, + preprocessing_parameters: PreprocessingConfigDict, + backend, # Union[Backend, str] + skip_save_processed_input: bool, + ) -> None: + datetime_format = preprocessing_parameters["datetime_format"] + proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_objects( + input_df[feature_config[COLUMN]], + lambda x: np.array( + DateFeatureMixin.date_to_list(x, datetime_format, preprocessing_parameters), dtype=np.int32 + ), + ) + return proc_df + + +class DateInputFeature(DateFeatureMixin, InputFeature): + def __init__(self, input_feature_config: DateInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Date feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + _valid_dtypes = (torch.int16, torch.int32, torch.int64, torch.float32) + if inputs.dtype not in _valid_dtypes: + raise ValueError(f"Date feature inputs dtype must be one of {_valid_dtypes}, got {inputs.dtype}.") + inputs_encoded = self.encoder_obj(inputs) + return inputs_encoded + + @property + def input_dtype(self): + return torch.int32 + + @property + def input_shape(self) -> torch.Size: + return torch.Size([DATE_VECTOR_LENGTH]) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + def create_sample_input(self, batch_size: int = 2): + date = [2013, 2, 26, 1, 57, 0, 0, 0, 0] + return torch.Tensor([date for _ in range(batch_size)]).type(torch.int32) + + @staticmethod + def get_schema_cls(): + return DateInputFeatureConfig + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _DatePreprocessing(metadata) diff --git a/ludwig/features/feature_registries.py b/ludwig/features/feature_registries.py new file mode 100644 index 0000000..cb8f607 --- /dev/null +++ b/ludwig/features/feature_registries.py @@ -0,0 +1,140 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from typing import Any, TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + ANOMALY, + AUDIO, + BAG, + BINARY, + CATEGORY, + CATEGORY_DISTRIBUTION, + DATE, + H3, + IMAGE, + NUMBER, + SEQUENCE, + SET, + TEXT, + TIMESERIES, + VECTOR, +) +from ludwig.features.anomaly_feature import AnomalyFeatureMixin, AnomalyOutputFeature +from ludwig.features.audio_feature import AudioFeatureMixin, AudioInputFeature +from ludwig.features.bag_feature import BagFeatureMixin, BagInputFeature +from ludwig.features.binary_feature import BinaryFeatureMixin, BinaryInputFeature, BinaryOutputFeature +from ludwig.features.category_feature import ( + CategoryDistributionFeatureMixin, + CategoryDistributionOutputFeature, + CategoryFeatureMixin, + CategoryInputFeature, + CategoryOutputFeature, +) +from ludwig.features.date_feature import DateFeatureMixin, DateInputFeature +from ludwig.features.h3_feature import H3FeatureMixin, H3InputFeature +from ludwig.features.image_feature import ImageFeatureMixin, ImageInputFeature, ImageOutputFeature +from ludwig.features.number_feature import NumberFeatureMixin, NumberInputFeature, NumberOutputFeature +from ludwig.features.sequence_feature import SequenceFeatureMixin, SequenceInputFeature, SequenceOutputFeature +from ludwig.features.set_feature import SetFeatureMixin, SetInputFeature, SetOutputFeature +from ludwig.features.text_feature import TextFeatureMixin, TextInputFeature, TextOutputFeature +from ludwig.features.timeseries_feature import TimeseriesFeatureMixin, TimeseriesInputFeature, TimeseriesOutputFeature +from ludwig.features.vector_feature import VectorFeatureMixin, VectorInputFeature, VectorOutputFeature +from ludwig.utils.misc_utils import get_from_registry + +if TYPE_CHECKING: + from ludwig.models.base import BaseModel + from ludwig.schema.model_types.base import ModelConfig + + +@DeveloperAPI +def get_base_type_registry() -> dict: + return { + TEXT: TextFeatureMixin, + CATEGORY: CategoryFeatureMixin, + SET: SetFeatureMixin, + BAG: BagFeatureMixin, + BINARY: BinaryFeatureMixin, + NUMBER: NumberFeatureMixin, + SEQUENCE: SequenceFeatureMixin, + TIMESERIES: TimeseriesFeatureMixin, + IMAGE: ImageFeatureMixin, + AUDIO: AudioFeatureMixin, + H3: H3FeatureMixin, + DATE: DateFeatureMixin, + VECTOR: VectorFeatureMixin, + CATEGORY_DISTRIBUTION: CategoryDistributionFeatureMixin, + ANOMALY: AnomalyFeatureMixin, + } + + +@DeveloperAPI +def get_input_type_registry() -> dict: + return { + TEXT: TextInputFeature, + NUMBER: NumberInputFeature, + BINARY: BinaryInputFeature, + CATEGORY: CategoryInputFeature, + SET: SetInputFeature, + SEQUENCE: SequenceInputFeature, + IMAGE: ImageInputFeature, + AUDIO: AudioInputFeature, + TIMESERIES: TimeseriesInputFeature, + BAG: BagInputFeature, + H3: H3InputFeature, + DATE: DateInputFeature, + VECTOR: VectorInputFeature, + } + + +@DeveloperAPI +def get_output_type_registry() -> dict: + return { + CATEGORY: CategoryOutputFeature, + BINARY: BinaryOutputFeature, + NUMBER: NumberOutputFeature, + SEQUENCE: SequenceOutputFeature, + SET: SetOutputFeature, + TEXT: TextOutputFeature, + TIMESERIES: TimeseriesOutputFeature, + VECTOR: VectorOutputFeature, + CATEGORY_DISTRIBUTION: CategoryDistributionOutputFeature, + IMAGE: ImageOutputFeature, + ANOMALY: AnomalyOutputFeature, + } + + +def update_config_with_metadata(config_obj: "ModelConfig", training_set_metadata: dict[str, Any]): + # populate input features fields depending on data + for input_feature in config_obj.input_features: + feature = get_from_registry(input_feature.type, get_input_type_registry()) + feature.update_config_with_metadata(input_feature, training_set_metadata[input_feature.name]) + + # populate output features fields depending on data + for output_feature in config_obj.output_features: + feature = get_from_registry(output_feature.type, get_output_type_registry()) + feature.update_config_with_metadata(output_feature, training_set_metadata[output_feature.name]) + + +def update_config_with_model(config_obj: "ModelConfig", model: "BaseModel"): + """Updates the config with the final input feature params given a model. + + This function should only be called to update the config after the model is initialized. Currently only implemented + for input features because it is only relevant for HuggingFace text encoders. HuggingFace text encoders only know + their final config after class initialization. + """ + for input_feature in config_obj.input_features: + model_input_feature = model.input_features.get(input_feature.name) + model_input_feature.update_config_after_module_init(input_feature) diff --git a/ludwig/features/feature_utils.py b/ludwig/features/feature_utils.py new file mode 100644 index 0000000..940b24a --- /dev/null +++ b/ludwig/features/feature_utils.py @@ -0,0 +1,210 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import re + +import numpy as np +import torch + +from ludwig.constants import NAME, PREPROCESSING, SEQUENCE, TEXT, TIMESERIES, TYPE +from ludwig.utils.data_utils import hash_dict +from ludwig.utils.strings_utils import get_tokenizer_from_registry, UNKNOWN_SYMBOL + +SEQUENCE_TYPES = {SEQUENCE, TEXT, TIMESERIES} +FEATURE_NAME_SUFFIX = "__ludwig" +FEATURE_NAME_SUFFIX_LENGTH = len(FEATURE_NAME_SUFFIX) + + +def should_regularize(regularize_layers): + regularize = False + if (isinstance(regularize_layers, bool) and regularize_layers) or ( + isinstance(regularize_layers, (list, tuple)) and regularize_layers and regularize_layers[-1] + ): + regularize = True + return regularize + + +def set_str_to_idx(set_string, feature_dict, tokenizer_name): + try: + tokenizer = get_tokenizer_from_registry(tokenizer_name)() + except ValueError: + raise Exception(f"Tokenizer {tokenizer_name} not supported") + + out = [feature_dict.get(item, feature_dict[UNKNOWN_SYMBOL]) for item in tokenizer(set_string)] + + return np.array(out, dtype=np.int32) + + +def compute_token_probabilities( + probabilities: list | tuple | np.ndarray, +) -> np.ndarray: + """Gets the maximum probability per timestep. + + Args: + probabilities: An iterable of iterables or np.ndarray with shape (sequence_length, num_classes) + where each inner iterable or np.ndarray is the probability distribution for a single timestep. + Returns: + An np.ndarray with shape (sequence_length,) containing the maximum probability for each timestep. + """ + if isinstance(probabilities, (list, tuple)): + if not hasattr(probabilities[0], "__len__"): + raise ValueError( + "Received token probabilities as a flat 1D list. Expected list of list of probabilities " + "(sequence_length, vocab_size)." + ) + max_probs = [] + for timestep_probs in probabilities: + max_probs.append(np.max(timestep_probs)) + max_probs = np.array(max_probs) + elif isinstance(probabilities, np.ndarray): + if len(probabilities.shape) != 2: + raise ValueError( + f"Received token probabilities with non 2D shape: {probabilities.shape}. Expected shape: " + "(sequence_length, vocab_size)." + ) + max_probs = np.max(probabilities, axis=-1) + else: + raise ValueError(f"probabilities type must be in [list, tuple, np.ndarray]. Got {type(probabilities)}") + return max_probs + + +def compute_sequence_probability( + sequence_probabilities: np.ndarray, + max_sequence_length: int | None = None, + return_log_prob: bool = True, +) -> float: + """Computes the sequence level probability. + + Args: + sequence_probabilities: An iterable of iterables or np.ndarray with shape (sequence_length,) + max_sequence_length: The maximum sequence length to use. If None, uses the first dim of `sequence_probabilities` + return_log_prob: Whether to return the log probability. Defaults to True. + """ + if max_sequence_length is None: + max_sequence_length = sequence_probabilities.shape[0] + + sequence_probabilities = sequence_probabilities[:max_sequence_length] + + if return_log_prob: + return np.sum(np.log(np.clip(sequence_probabilities, 1e-10, 1.0))) + else: + return np.prod(sequence_probabilities) + + +def sanitize(name): + """Replaces invalid id characters.""" + return re.sub("\\W|^(?=\\d)", "_", name) + + +def compute_feature_hash(feature: dict) -> str: + """This function computes a hash for each feature based on the preprocessing dictionary associated with each + feature, as well as the feature's type. + + Args: + feature: Feature dictionary + + Returns: Feature hash name + """ + feature_data = { + "preprocessing": feature.get(PREPROCESSING, {}), + "type": feature[TYPE], + } + return sanitize(feature[NAME]) + "_" + hash_dict(feature_data).decode("ascii") + + +def get_input_size_with_dependencies( + combiner_output_size: int, + dependencies: list[str], + other_output_features, # Dict[str, "OutputFeature"] +): + """Returns the input size for the first layer of this output feature's FC stack, accounting for dependencies on + other output features. + + In the forward pass, the hidden states of any dependent output features get concatenated with the combiner's output. + If this output feature depends on other output features, then the input size for this feature's FCStack is the sum + of the output sizes of other output features + the combiner's output size. + """ + input_size_with_dependencies = combiner_output_size + for feature_name in dependencies: + if other_output_features[feature_name].fc_stack.num_layers: + input_size_with_dependencies += other_output_features[feature_name].fc_stack.output_shape[-1] + else: + # 0-layer FCStack. Use the output feature's input size. + input_size_with_dependencies += other_output_features[feature_name].input_size + return input_size_with_dependencies + + +def get_module_dict_key_from_name(name: str, feature_name_suffix: str = FEATURE_NAME_SUFFIX) -> str: + """Returns a key that's guaranteed to be compatible with torch.""" + key = name.replace(".", "__ludwig_punct_period__") + return key + feature_name_suffix + + +def get_name_from_module_dict_key(key: str, feature_name_suffix_length: int = FEATURE_NAME_SUFFIX_LENGTH) -> str: + """Reverse of get_module_dict_key_from_name.""" + name = key.replace("__ludwig_punct_period__", ".") + return name[:-feature_name_suffix_length] + + +class LudwigFeatureDict(torch.nn.Module): + """Torch ModuleDict wrapper that permits keys with any name. + + Torch's ModuleDict implementation doesn't allow certain keys to be used if they conflict with existing class + attributes, e.g. + + > torch.nn.ModuleDict({'type': torch.nn.Module()}) # Raises KeyError. + + This class is a simple wrapper around torch's ModuleDict that mitigates possible conflicts by using a key-suffixing + protocol. + + This is also tracked in Pytorch: https://github.com/pytorch/pytorch/issues/71203. + """ + + def __init__(self): + super().__init__() + self.module_dict = torch.nn.ModuleDict() + self.internal_key_to_original_name_map = {} + + def get(self, key) -> torch.nn.Module: + return self.module_dict[get_module_dict_key_from_name(key)] + + def set(self, key: str, module: torch.nn.Module) -> None: + module_dict_key_name = get_module_dict_key_from_name(key) + self.internal_key_to_original_name_map[module_dict_key_name] = key + self.module_dict[module_dict_key_name] = module + + def __len__(self) -> int: + return len(self.module_dict) + + def __next__(self) -> None: + return next(iter(self)) + + def __iter__(self) -> None: + return iter(self.keys()) + + def keys(self) -> list[str]: + return [get_name_from_module_dict_key(feature_name) for feature_name in self.internal_key_to_original_name_map] + + def values(self) -> list[torch.nn.Module]: + return [module for _, module in self.module_dict.items()] + + def items(self) -> list[tuple[str, torch.nn.Module]]: + return [ + (get_name_from_module_dict_key(feature_name), module) for feature_name, module in self.module_dict.items() + ] + + def update(self, modules: dict[str, torch.nn.Module]) -> None: + for feature_name, module in modules.items(): + self.set(feature_name, module) diff --git a/ludwig/features/h3_feature.py b/ludwig/features/h3_feature.py new file mode 100644 index 0000000..0e57f9f --- /dev/null +++ b/ludwig/features/h3_feature.py @@ -0,0 +1,159 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import numpy as np +import torch + +from ludwig.constants import COLUMN, H3, H3_VECTOR_LENGTH, MAX_H3_RESOLUTION, PROC_COLUMN +from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin, InputFeature +from ludwig.schema.features.h3_feature import H3InputFeatureConfig +from ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict +from ludwig.utils.h3_util import h3_to_components +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + +H3_PADDING_VALUE = 7 + + +class _H3Preprocessing(BasePreprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.max_h3_resolution = MAX_H3_RESOLUTION + self.h3_padding_value = H3_PADDING_VALUE + self.computed_fill_value = float(metadata["preprocessing"]["computed_fill_value"]) + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if torch.jit.isinstance(v, list[torch.Tensor]): + v = torch.stack(v) + + if not torch.jit.isinstance(v, torch.Tensor): + raise ValueError(f"Unsupported input: {v}") + + v = torch.nan_to_num(v, nan=self.computed_fill_value) + v = v.long() + + outputs: list[torch.Tensor] = [] + for v_i in v: + components = h3_to_components(v_i) + header: list[int] = [ + components.mode, + components.edge, + components.resolution, + components.base_cell, + ] + cells_padding: list[int] = [self.h3_padding_value] * (self.max_h3_resolution - len(components.cells)) + output = torch.tensor(header + components.cells + cells_padding, dtype=torch.uint8, device=v.device) + outputs.append(output) + + return torch.stack(outputs) + + +class H3FeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return H3 + + @staticmethod + def cast_column(column, backend): + try: + return column.astype(int) + except ValueError: + logger.warning("H3Feature could not be read as int directly. Reading as float and converting to int.") + return column.astype(float).astype(int) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + return {} + + @staticmethod + def h3_to_list(h3_int): + components = h3_to_components(h3_int) + header = [components.mode, components.edge, components.resolution, components.base_cell] + cells_padding = [H3_PADDING_VALUE] * (MAX_H3_RESOLUTION - len(components.cells)) + return header + components.cells + cells_padding + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + column = input_df[feature_config[COLUMN]] + if column.dtype == object: + column = backend.df_engine.map_objects(column, int) + column = backend.df_engine.map_objects(column, H3FeatureMixin.h3_to_list) + + proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_objects( + column, lambda x: np.array(x, dtype=np.uint8) + ) + return proc_df + + +class H3InputFeature(H3FeatureMixin, InputFeature): + def __init__(self, input_feature_config: H3InputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"H3 feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if inputs.dtype not in (torch.uint8, torch.int64): + raise ValueError(f"H3 feature inputs dtype must be uint8 or int64, got {inputs.dtype}.") + if len(inputs.shape) != 2: + raise ValueError(f"H3 feature inputs must be 2D, got shape {tuple(inputs.shape)}.") + + inputs_encoded = self.encoder_obj(inputs) + + return inputs_encoded + + @property + def input_dtype(self): + return torch.uint8 + + @property + def input_shape(self) -> torch.Size: + return torch.Size([H3_VECTOR_LENGTH]) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _H3Preprocessing(metadata) + + @staticmethod + def get_schema_cls(): + return H3InputFeatureConfig diff --git a/ludwig/features/image_feature.py b/ludwig/features/image_feature.py new file mode 100644 index 0000000..c230fc4 --- /dev/null +++ b/ludwig/features/image_feature.py @@ -0,0 +1,1384 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os +import warnings +from collections import Counter +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial +from pathlib import Path +from typing import Any + +import numpy as np +import torch +from torchvision import transforms +from torchvision.transforms import functional as F +from torchvision.transforms.functional import normalize + +from ludwig.constants import ( + COLUMN, + ENCODER, + HEIGHT, + IMAGE, + IMAGENET1K, + INFER_IMAGE_DIMENSIONS, + INFER_IMAGE_MAX_HEIGHT, + INFER_IMAGE_MAX_WIDTH, + INFER_IMAGE_NUM_CLASSES, + INFER_IMAGE_SAMPLE_SIZE, + LOGITS, + NAME, + NUM_CHANNELS, + PREDICTIONS, + PREPROCESSING, + PROC_COLUMN, + REQUIRES_EQUAL_DIMENSIONS, + SRC, + TYPE, + WIDTH, +) +from ludwig.data.lazy_utils import resolve_lazy_cache_dir +from ludwig.encoders.base import Encoder +from ludwig.encoders.image.torchvision import TVModelVariant +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + FeaturePreprocessingMixin, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.schema.features.augmentation.base import BaseAugmentationConfig +from ludwig.schema.features.augmentation.image import ( + AutoAugmentationConfig, + RandomBlurConfig, + RandomBrightnessConfig, + RandomContrastConfig, + RandomHorizontalFlipConfig, + RandomRotateConfig, + RandomVerticalFlipConfig, +) +from ludwig.schema.features.image_feature import ImageInputFeatureConfig, ImageOutputFeatureConfig +from ludwig.types import ( + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.augmentation_utils import get_augmentation_op, register_augmentation_op +from ludwig.utils.data_utils import get_abs_path +from ludwig.utils.dataframe_utils import is_dask_series_or_df +from ludwig.utils.fs_utils import has_remote_protocol +from ludwig.utils.image_utils import ( + get_class_mask_from_image, + get_gray_default_image, + get_image_from_class_mask, + get_unique_channels, + grayscale, + num_channels_in_image, + read_image_from_bytes_obj, + read_image_from_path, + resize_image, + ResizeChannels, + torchvision_model_registry, +) +from ludwig.utils.misc_utils import set_default_value +from ludwig.utils.types import PreprocessingInput, Series + +# constants used for Ludwig image preprocessing +IMAGENET1K_MEAN = [0.485, 0.456, 0.406] +IMAGENET1K_STD = [0.229, 0.224, 0.225] + + +logger = logging.getLogger(__name__) + + +def _extract_image_path(entry: object) -> str | None: + """Return a usable on-disk path from a PIL Image or dict entry, or ``None``. + + PIL Images opened from disk retain their source path in the ``.filename`` + attribute. HuggingFace ``Image`` columns may deliver dicts that contain a + ``"path"`` key pointing to the cached file. + + Parameters + ---------- + entry: + A single image entry from a dataset column. May be a ``PIL.Image.Image`` + with a ``.filename`` attribute, a dict with a ``"path"`` key, or any + other type. + + Returns + ------- + str | None + The existing on-disk path when one is found and the file is present on + disk; ``None`` otherwise. + """ + # PIL Image opened from a file retains its filename + filename = getattr(entry, "filename", None) + if filename and isinstance(filename, str) and os.path.isfile(filename): + return filename + + # HuggingFace-style dict with a "path" key + if isinstance(entry, dict): + path = entry.get("path") + if path and isinstance(path, str) and os.path.isfile(path): + return path + + return None + + +def _cache_image_column_to_disk( + column, + cache_dir: Path, + feature_name: str, +) -> list[str]: + """Write every image entry in *column* to PNG files in *cache_dir*. + + Entries that already have an accessible on-disk path (PIL ``.filename`` or + dict ``"path"``) are reused without copying. All other entries (in-memory + PIL Images, raw bytes, or numpy arrays) are saved as PNG files. Files + that already exist in *cache_dir* are not overwritten (idempotent + behaviour), which makes re-runs after a crash safe. + + Parameters + ---------- + column: + An iterable of image entries. Each entry may be: + + * a ``PIL.Image.Image`` — with or without a ``.filename`` attribute; + * ``bytes`` containing a valid encoded image; + * a ``numpy.ndarray`` with shape ``(H, W)``, ``(H, W, C)``, or + ``(C, H, W)`` and dtype ``uint8``; + * a dict with at least a ``"bytes"`` or ``"path"`` key (HuggingFace + ``Image`` column format). + cache_dir: + Directory in which to write PNG files. Must already exist. + feature_name: + Used as a filename prefix so that files are recognisable during + debugging. + + Returns + ------- + list[str] + Ordered list of absolute paths (one per entry), suitable for direct + use as a ``LazyColumn`` path list. + + Raises + ------ + ValueError + If an entry has an unrecognised type that cannot be converted to a PIL + Image for saving. + """ + # Lazy import — PIL is an optional dependency; fail loudly only when used. + from PIL import Image as PILImage + + paths: list[str] = [] + for idx, entry in enumerate(column): + # --- Try to reuse an existing on-disk path --- + existing = _extract_image_path(entry) + if existing is not None: + paths.append(existing) + continue + + # --- Convert entry to a PIL Image --- + if isinstance(entry, PILImage.Image): + pil_img = entry + elif isinstance(entry, bytes): + import io + + pil_img = PILImage.open(io.BytesIO(entry)).copy() + elif isinstance(entry, np.ndarray): + # Handle (C, H, W) → (H, W, C) for RGB + if entry.ndim == 3 and entry.shape[0] in (1, 3, 4): + entry = np.transpose(entry, (1, 2, 0)) + pil_img = PILImage.fromarray(entry.astype(np.uint8)) + elif isinstance(entry, dict): + # HuggingFace Image column format: {"bytes": , "path": } + raw_bytes = entry.get("bytes") + if raw_bytes: + import io + + pil_img = PILImage.open(io.BytesIO(raw_bytes)).copy() + else: + raise ValueError( + f"Image entry [{idx}] in feature '{feature_name}' is a dict but contains " + "neither a usable 'path' nor 'bytes' key." + ) + else: + # Try torch.Tensor as a last resort (optional dependency) + try: + import torch + + if isinstance(entry, torch.Tensor): + arr = entry.detach().cpu().numpy() + # Handle (C, H, W) → (H, W, C) for colour tensors + if arr.ndim == 3 and arr.shape[0] in (1, 3, 4): + arr = np.transpose(arr, (1, 2, 0)) + pil_img = PILImage.fromarray(arr.astype(np.uint8)) + else: + raise TypeError + except (ImportError, TypeError): + raise ValueError( + f"Image entry [{idx}] in feature '{feature_name}' has unrecognised " + f"type {type(entry).__name__!r}. " + "Expected PIL.Image, bytes, numpy.ndarray, torch.Tensor, or dict." + ) + + dest_path = str(cache_dir / f"{feature_name}_{idx:08d}.png") + if not os.path.isfile(dest_path): + pil_img.save(dest_path, format="PNG") + paths.append(dest_path) + + return paths + + +### +# Image specific augmentation operations +### +@register_augmentation_op(name="auto_augmentation", features=IMAGE) +class AutoAugment(torch.nn.Module): + def __init__(self, config: AutoAugmentationConfig): + super().__init__() + self.auto_augmentation_method = config.method + self.augmentation_method = self.get_augmentation_method() + + def get_augmentation_method(self): + if self.auto_augmentation_method == "trivial_augment": + return transforms.TrivialAugmentWide() + if self.auto_augmentation_method == "auto_augment": + return transforms.AutoAugment() + if self.auto_augmentation_method == "rand_augment": + return transforms.RandAugment() + raise ValueError(f"Unsupported auto-augmentation method: {self.auto_augmentation_method}") + + def forward(self, imgs: torch.Tensor) -> torch.Tensor: + method = self.augmentation_method + uint8imgs = imgs.to(torch.uint8) + augmented_imgs = method(uint8imgs) + + return augmented_imgs.to(torch.float32) + + +@register_augmentation_op(name="random_vertical_flip", features=IMAGE) +class RandomVFlip(torch.nn.Module): + def __init__( + self, + config: RandomVerticalFlipConfig, + ): + super().__init__() + + def forward(self, imgs): + if torch.rand(1) < 0.5: + imgs = F.vflip(imgs) + + return imgs + + +@register_augmentation_op(name="random_horizontal_flip", features=IMAGE) +class RandomHFlip(torch.nn.Module): + def __init__( + self, + config: RandomHorizontalFlipConfig, + ): + super().__init__() + + def forward(self, imgs): + if torch.rand(1) < 0.5: + imgs = F.hflip(imgs) + + return imgs + + +@register_augmentation_op(name="random_rotate", features=IMAGE) +class RandomRotate(torch.nn.Module): + def __init__(self, config: RandomRotateConfig): + super().__init__() + self.degree = config.degree + + def forward(self, imgs): + if torch.rand(1) < 0.5: + # map angle to interval (-degree, +degree) + angle = (torch.rand(1) * 2 * self.degree - self.degree).item() + return F.rotate(imgs, angle) + else: + return imgs + + +@register_augmentation_op(name="random_contrast", features=IMAGE) +class RandomContrast(torch.nn.Module): + def __init__(self, config: RandomContrastConfig): + super().__init__() + self.min_contrast = config.min + self.contrast_adjustment_range = config.max - config.min + + def forward(self, imgs): + if torch.rand(1) < 0.5: + # random contrast adjustment + adjust_factor = (torch.rand(1) * self.contrast_adjustment_range + self.min_contrast).item() + return F.adjust_contrast(imgs, adjust_factor) + else: + return imgs + + +@register_augmentation_op(name="random_brightness", features=IMAGE) +class RandomBrightness(torch.nn.Module): + def __init__(self, config: RandomBrightnessConfig): + super().__init__() + self.min_brightness = config.min + self.brightness_adjustment_range = config.max - config.min + + def forward(self, imgs): + if torch.rand(1) < 0.5: + # random contrast adjustment + adjust_factor = (torch.rand(1) * self.brightness_adjustment_range + self.min_brightness).item() + return F.adjust_brightness(imgs, adjust_factor) + else: + return imgs + + +@register_augmentation_op(name="random_blur", features=IMAGE) +class RandomBlur(torch.nn.Module): + def __init__(self, config: RandomBlurConfig): + super().__init__() + self.kernel_size = [config.kernel_size, config.kernel_size] + + def forward(self, imgs): + if torch.rand(1) < 0.5: + imgs = F.gaussian_blur(imgs, self.kernel_size) + + return imgs + + +class ImageAugmentation(torch.nn.Module): + def __init__( + self, + augmentation_list: list[BaseAugmentationConfig], + normalize_mean: list[float] | None = None, + normalize_std: list[float] | None = None, + ): + super().__init__() + + logger.debug(f"Creating augmentation pipeline: {augmentation_list}") + + self.normalize_mean = normalize_mean + self.normalize_std = normalize_std + + if self.training: + self.augmentation_steps = torch.nn.Sequential() + for aug_config in augmentation_list: + try: + aug_op = get_augmentation_op(IMAGE, aug_config.type) + self.augmentation_steps.append(aug_op(aug_config)) + except KeyError: + raise ValueError(f"Invalid augmentation operation specification: {aug_config}") + else: + self.augmentation_steps = None + + def forward(self, imgs): + if self.augmentation_steps: + # convert from float to uint8 values - this is required for the augmentation + imgs = self._convert_back_to_uint8(imgs) + + logger.debug("Executing augmentation pipeline steps: %s", self.augmentation_steps) + imgs = self.augmentation_steps(imgs) + + # convert back to float32 values and renormalize if needed + imgs = self._renormalize_image(imgs) + + return imgs + + # function to partially undo the TorchVision ImageClassification transformation. + # back out the normalization step and convert from float32 to uint8 dtype + # to make the tensor displayable as an image + # crop size remains the same + def _convert_back_to_uint8(self, images): + if self.normalize_mean: + mean = torch.as_tensor(self.normalize_mean, dtype=torch.float32).view(-1, 1, 1) + std = torch.as_tensor(self.normalize_std, dtype=torch.float32).view(-1, 1, 1) + return images.mul(std).add(mean).mul(255.0).type(torch.uint8) + else: + return images.mul(255.0).type(torch.uint8) + + # function to redo part of the TorchVision ImageClassification transformation. + # convert uint8 to float32 + # apply the imagenet1k normalization + def _renormalize_image(self, images): + if self.normalize_mean: + mean = torch.as_tensor(self.normalize_mean, dtype=torch.float32).view(-1, 1, 1) + std = torch.as_tensor(self.normalize_std, dtype=torch.float32).view(-1, 1, 1) + return images.type(torch.float32).div(255.0).sub(mean).div(std) + else: + return images.type(torch.float32).div(255.0) + + +@dataclass +class ImageTransformMetadata: + height: int + width: int + num_channels: int + + +def _get_torchvision_transform( + torchvision_parameters: TVModelVariant, +) -> tuple[torch.nn.Module, ImageTransformMetadata]: + """Returns a torchvision transform that is compatible with the model variant. + + Note that the raw torchvision transform is not returned. Instead, a Sequential module that includes + image resizing is returned. This is because the raw torchvision transform assumes that the input image has + three channels, which is not always the case with images input into Ludwig. + + Args: + torchvision_parameters: The parameters for the torchvision model variant. + Returns: + (torchvision_transform, transform_metadata): A torchvision transform and the metadata for the transform. + """ + torchvision_transform_raw = torchvision_parameters.model_weights.DEFAULT.transforms() + torchvision_transform = torch.nn.Sequential( + ResizeChannels(num_channels=3), + torchvision_transform_raw, + ) + transform_metadata = ImageTransformMetadata( + height=torchvision_transform_raw.crop_size[0], + width=torchvision_transform_raw.crop_size[0], + num_channels=len(torchvision_transform_raw.mean), + ) + return (torchvision_transform, transform_metadata) + + +def _get_torchvision_parameters(model_type: str, model_variant: str) -> TVModelVariant: + return torchvision_model_registry.get(model_type).get(model_variant) + + +def is_torchvision_encoder(encoder_obj: Encoder) -> bool: + # TODO(travis): do this through an interface rather than conditional logic + from ludwig.encoders.image.torchvision import TVBaseEncoder + + return isinstance(encoder_obj, TVBaseEncoder) + + +class _ImagePreprocessing(BasePreprocessingModule): + """Torchscript-enabled version of preprocessing done by ImageFeatureMixin.add_feature_data.""" + + def __init__( + self, + metadata: TrainingSetMetadataDict, + torchvision_transform: torch.nn.Module | None = None, + transform_metadata: ImageTransformMetadata | None = None, + ): + super().__init__() + + self.resize_method = metadata["preprocessing"]["resize_method"] + self.torchvision_transform = torchvision_transform + if transform_metadata is not None: + self.height = transform_metadata.height + self.width = transform_metadata.width + self.num_channels = transform_metadata.num_channels + self.channel_class_map = torch.Tensor([]) + else: + self.height = metadata["preprocessing"]["height"] + self.width = metadata["preprocessing"]["width"] + self.num_channels = metadata["preprocessing"]["num_channels"] + self.channel_class_map = torch.ByteTensor(metadata["preprocessing"]["channel_class_map"]) + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + """Takes a list of images and adjusts the size and number of channels as specified in the metadata. + + If `v` is already a torch.Tensor, we assume that the images are already preprocessed to be the same size. + """ + # Nested conditional is a workaround to short-circuit boolean evaluation. + if not torch.jit.isinstance(v, list[torch.Tensor]): + if not torch.jit.isinstance(v, torch.Tensor): + raise ValueError(f"Unsupported input: {v}") + + if self.torchvision_transform is not None: + # perform pre-processing for torchvision pretrained model encoders + if torch.jit.isinstance(v, list[torch.Tensor]): + imgs = [self.torchvision_transform(img) for img in v] + else: + # convert batch of image tensors to a list and then run torchvision pretrained + # model transforms on each image + imgs = [self.torchvision_transform(img) for img in torch.unbind(v)] + + # collect the list of images into a batch + imgs_stacked = torch.stack(imgs) + else: + # perform pre-processing for Ludwig defined image encoders + if torch.jit.isinstance(v, list[torch.Tensor]): + imgs = [resize_image(img, (self.height, self.width), self.resize_method) for img in v] + imgs_stacked = torch.stack(imgs) + else: + imgs_stacked = v + + _, num_channels, height, width = imgs_stacked.shape + + # Ensure images are the size expected by the model + if height != self.height or width != self.width: + imgs_stacked = resize_image(imgs_stacked, (self.height, self.width), self.resize_method) + + # Ensures images have the number of channels expected by the model + if num_channels != self.num_channels: + if self.num_channels == 1: + imgs_stacked = grayscale(imgs_stacked) + elif num_channels < self.num_channels: + extra_channels = self.num_channels - num_channels + imgs_stacked = torch.nn.functional.pad(imgs_stacked, [0, 0, 0, 0, 0, extra_channels]) + else: + raise ValueError( + f"Number of channels cannot be reconciled. metadata.num_channels = " + f"{self.num_channels}, but imgs.shape[1] = {num_channels}" + ) + + # Create class-masked images if required + if self.channel_class_map.shape[0]: + masks = [] + for img in imgs_stacked: + mask = get_class_mask_from_image(self.channel_class_map, img) + masks.append(mask) + imgs_stacked = torch.stack(masks) + else: + imgs_stacked = imgs_stacked.type(torch.float32) / 255 + + return imgs_stacked + + +class _ImagePostprocessing(BasePostprocessingModule): + def __init__(self): + super().__init__() + self.logits_key = LOGITS + self.predictions_key = PREDICTIONS + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + logits = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.logits_key) + + return {self.predictions_key: predictions, self.logits_key: logits} + + +class _ImagePredict(PredictModule): + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + predictions = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.predictions_key) + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + + return {self.predictions_key: predictions, self.logits_key: logits} + + +class ImageFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return IMAGE + + @staticmethod + def cast_column(column, backend): + return column + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + return {PREPROCESSING: preprocessing_parameters} + + @staticmethod + def _read_image_if_bytes_obj_and_resize( + img_entry: bytes | torch.Tensor | np.ndarray | str, + img_width: int, + img_height: int, + should_resize: bool, + num_channels: int, + resize_method: str, + user_specified_num_channels: bool, + standardize_image: str, + channel_class_map: torch.Tensor, + ) -> np.ndarray | None: + """Helper method to read and resize an image according to model definition. If the user doesn't specify a + number of channels, we use the first image in the dataset as the source of truth. If any image in the + dataset doesn't have the same number of channels as the first image, raise an exception. + + If the user specifies a number of channels, we try to convert all the images to the specifications by + dropping channels/padding 0 channels. + + Args: + img_entry: if str, file path to the image; otherwise a torch.Tensor, np.ndarray, or bytes of the image. + img_width: expected width of the image. + img_height: expected height of the image. + should_resize: Should the image be resized? + resize_method: type of resizing method. + num_channels: expected number of channels in the first image. + user_specified_num_channels: did the user specify num channels? + standardize_image: specifies whether to standardize image with imagenet1k specifications. + channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class. + + Returns: + image object as a numpy array. + """ + + if isinstance(img_entry, bytes): + img = read_image_from_bytes_obj(img_entry, num_channels) + elif isinstance(img_entry, str): + img = read_image_from_path(img_entry, num_channels) + elif isinstance(img_entry, np.ndarray): + img = torch.from_numpy(np.array(img_entry, copy=True)).permute(2, 0, 1) + else: + img = img_entry + + if not isinstance(img, torch.Tensor): + warnings.warn(f"Image with value {img} cannot be read") + return None + + img_num_channels = num_channels_in_image(img) + # Convert to grayscale if needed. + if num_channels == 1 and img_num_channels != 1: + img = grayscale(img) + img_num_channels = 1 + + if should_resize: + img = resize_image(img, (img_height, img_width), resize_method) + + if user_specified_num_channels: + # Number of channels is specified by the user + # img_padded = np.zeros((img_height, img_width, num_channels), + # dtype=np.uint8) + # min_num_channels = min(num_channels, img_num_channels) + # img_padded[:, :, :min_num_channels] = img[:, :, :min_num_channels] + # img = img_padded + if num_channels > img_num_channels: + extra_channels = num_channels - img_num_channels + img = torch.nn.functional.pad(img, [0, 0, 0, 0, 0, extra_channels]) + + if img_num_channels != num_channels: + logger.warning( + f"Image has {img_num_channels} channels, where as {num_channels} " + "channels are expected. Dropping/adding channels " + "with 0s as appropriate" + ) + else: + # If the image isn't like the first image, raise exception + if img_num_channels != num_channels: + raise ValueError( + f"Image has {img_num_channels} channels, unlike the first image, which " + f"has {num_channels} channels. Make sure all the images have the same " + "number of channels or use the num_channels property in " + "image preprocessing" + ) + + if img.shape[1] != img_height or img.shape[2] != img_width: + raise ValueError( + "Images are not of the same size. " + f"Expected size is {[img_height, img_width, num_channels]}, " + f"current image size is {img.shape}." + "Images are expected to be all of the same size " + "or explicit image width and height are expected " + "to be provided. " + "Additional information: " + "https://ludwig-ai.github.io/ludwig-docs/latest/configuration/features/image_features" + "#image-features-preprocessing" + ) + + # Create class-masked image if required + if channel_class_map.shape[0]: + img = get_class_mask_from_image(channel_class_map, img) + else: + # casting and rescaling + img = img.type(torch.float32) / 255 + + if standardize_image == IMAGENET1K: + img = normalize(img, mean=IMAGENET1K_MEAN, std=IMAGENET1K_STD) + + return img.numpy() + + @staticmethod + def _read_image_with_pretrained_transform( + img_entry: bytes | torch.Tensor | np.ndarray, + transform_fn: Callable, + ) -> np.ndarray | None: + if isinstance(img_entry, bytes): + img = read_image_from_bytes_obj(img_entry) + elif isinstance(img_entry, str): + img = read_image_from_path(img_entry) + elif isinstance(img_entry, np.ndarray): + img = torch.from_numpy(img_entry).permute(2, 0, 1) + else: + img = img_entry + + if not isinstance(img, torch.Tensor): + warnings.warn(f"Image with value {img} cannot be read") + return None + + img = transform_fn(img) + + return img.numpy() + + @staticmethod + def _make_lazy_decode_fn( + img_width: int, + img_height: int, + should_resize: bool, + num_channels: int, + resize_method: str, + user_specified_num_channels: bool, + standardize_image: str, + channel_class_map, + default_image: np.ndarray, + ): + """Return a per-path decode function suitable for use in ``LazyColumn``. + + The returned callable is stateless (captures only plain values / arrays) + so it is safe to share across DataLoader worker threads. + """ + _resize_fn = partial( + ImageFeatureMixin._read_image_if_bytes_obj_and_resize, + img_width=img_width, + img_height=img_height, + should_resize=should_resize, + num_channels=num_channels, + resize_method=resize_method, + user_specified_num_channels=user_specified_num_channels, + standardize_image=standardize_image, + channel_class_map=channel_class_map, + ) + + def decode(path_or_bytes) -> np.ndarray: + result = _resize_fn(path_or_bytes) + if not isinstance(result, np.ndarray): + return default_image + return result + + decode.__name__ = "image_lazy_decode" + return decode + + @staticmethod + def _set_image_and_height_equal_for_encoder( + width: int, height: int, preprocessing_parameters: dict, encoder_type: str + ) -> tuple[int, int]: + """Some pretrained image encoders require images with the same dimension, or images with a specific width + and heigh values. The returned width and height are set based on compatibility with the downstream encoder + using the encoder parameters for the feature. + + Args: + width: Represents the width of the image. This is either specified in the user config, or inferred using + a sample of images. + height: Represents the height of the image. This is either specified in the user config, or inferred using + a sample of images. + preprocessing_parameters: Parameters defining how the image feature should be preprocessed + encoder_type: The name of the encoder + + Return: + (width, height) Updated width and height so that they are equal + """ + + if preprocessing_parameters[REQUIRES_EQUAL_DIMENSIONS] and height != width: + width = height = min(width, height) + # Update preprocessing parameters dictionary to reflect new height and width values + preprocessing_parameters["width"] = width + preprocessing_parameters["height"] = height + logger.info(f"Set image feature height and width to {width} to be compatible with {encoder_type} encoder.") + return width, height + + @staticmethod + def _infer_image_size( + image_sample: list[torch.Tensor], + max_height: int, + max_width: int, + preprocessing_parameters: dict, + encoder_type: str, + ) -> tuple[int, int]: + """Infers the size to use from a group of images. The returned height will be the average height of images + in image_sample rounded to the nearest integer, or max_height. Likewise for width. + + Args: + image_sample: Sample of images to use to infer image size. Must be formatted as [channels, height, width]. + max_height: Maximum height. + max_width: Maximum width. + preprocessing_parameters: Parameters defining how the image feature should be preprocessed + encoder_type: The name of the encoder + + Return: + (height, width) The inferred height and width. + """ + + height_avg = sum(x.shape[1] for x in image_sample) / len(image_sample) + width_avg = sum(x.shape[2] for x in image_sample) / len(image_sample) + height = min(int(round(height_avg)), max_height) + width = min(int(round(width_avg)), max_width) + + # Update height and width if the downstream encoder requires images + # with the same dimension or specific width and height values + width, height = ImageFeatureMixin._set_image_and_height_equal_for_encoder( + width, height, preprocessing_parameters, encoder_type + ) + + logger.debug(f"Inferring height: {height} and width: {width}") + return height, width + + @staticmethod + def _infer_number_of_channels(image_sample: list[torch.Tensor]): + """Infers the channel depth to use from a group of images. + + We make the assumption that the majority of datasets scraped from the web will be RGB, so if we get a mixed bag + of images we should default to that. However, if the majority of the sample images have a specific channel depth + (other than 3) this is probably intentional so we keep it, but log an info message. + """ + n_images = len(image_sample) + channel_frequency = Counter([num_channels_in_image(x) for x in image_sample]) + if channel_frequency[1] > n_images / 2: + # If the majority of images in sample are 1 channel, use 1. + num_channels = 1 + elif channel_frequency[2] > n_images / 2: + # If the majority of images in sample are 2 channel, use 2. + num_channels = 2 + elif channel_frequency[4] > n_images / 2: + # If the majority of images in sample are 4 channel, use 4. + num_channels = 4 + else: + # Default case: use 3 channels. + num_channels = 3 + logger.info(f"Inferring num_channels from the first {n_images} images.") + logger.info("\n".join([f" images with {k} channels: {v}" for k, v in sorted(channel_frequency.items())])) + if num_channels == max(channel_frequency, key=channel_frequency.get): + logger.info( + f"Using {num_channels} channels because it is the majority in sample. If an image with" + f" a different depth is read, will attempt to convert to {num_channels} channels." + ) + else: + logger.info(f"Defaulting to {num_channels} channels.") + logger.info( + "To explicitly set the number of channels, define num_channels in the preprocessing dictionary of " + "the image input feature config." + ) + return num_channels + + @staticmethod + def _infer_image_num_classes( + image_sample: list[torch.Tensor], + num_channels: int, + num_classes: int, + ) -> torch.Tensor: + """Infers the number of channel classes from a group of images (for image segmentation). The returned + tensor contains the channel value for each class, where dim=0 is the class. + + Args: + image_sample: Sample of images to use to infer image size. Must be formatted as [channels, height, width]. + num_channels: Expected number of channels + num_classes: Expected number of channel classes or None + + Return: + channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class. + """ + n_images = len(image_sample) + logger.info(f"Inferring num_classes from the first {n_images} images.") + channel_class_map = get_unique_channels(image_sample, num_channels, num_classes) + + inferred_num_classes = channel_class_map.shape[0] + if num_classes: + if num_classes < inferred_num_classes: + raise ValueError( + f"Images inferred num classes {inferred_num_classes} exceeds `num_classes` {num_classes}." + ) + elif num_classes > inferred_num_classes: + logger.warning( + f"Images inferred num classes {inferred_num_classes} does not match `num_classes` {num_classes}. " + f"Using inferred num classes {inferred_num_classes}." + ) + + return channel_class_map + + @staticmethod + def _finalize_preprocessing_parameters( + preprocessing_parameters: dict, + encoder_type: str, + column: Series, + ) -> tuple: + """Helper method to determine the height, width and number of channels for preprocessing the image data. + + This is achieved by looking at the parameters provided by the user. When there are some missing parameters, we + fall back on to the first image in the dataset. The assumption being that all the images in the data are + expected be of the same size with the same number of channels. + + Args: + preprocessing_parameters: Parameters defining how the image feature should be preprocessed + encoder_type: The name of the encoder + column: The data itself. Can be a Pandas, Modin or Dask series. + """ + + explicit_height_width = preprocessing_parameters[HEIGHT] or preprocessing_parameters[WIDTH] + explicit_num_channels = NUM_CHANNELS in preprocessing_parameters and preprocessing_parameters[NUM_CHANNELS] + + if preprocessing_parameters[INFER_IMAGE_DIMENSIONS] and not (explicit_height_width and explicit_num_channels): + sample_size = min(len(column), preprocessing_parameters[INFER_IMAGE_SAMPLE_SIZE]) + else: + sample_size = 1 # Take first image + + sample = [] + sample_num_bytes = [] + failed_entries = [] + for image_entry in column.head(sample_size): + if isinstance(image_entry, bytes): + image = read_image_from_bytes_obj(image_entry) + elif isinstance(image_entry, str): + # Tries to read image as PNG or numpy file from the path. + image, num_bytes = read_image_from_path(image_entry, return_num_bytes=True) + if num_bytes is not None: + sample_num_bytes.append(num_bytes) + else: + image = image_entry + + if isinstance(image, torch.Tensor): + sample.append(image) + elif isinstance(image, np.ndarray): + sample.append(torch.from_numpy(image).permute(2, 0, 1)) + else: + failed_entries.append(image_entry) + if len(sample) == 0: + failed_entries_repr = "\n\t- ".join(failed_entries) + raise ValueError( + f"Images dimensions cannot be inferred. Failed to read {sample_size} images as samples:" + f"\n\t- {failed_entries_repr}." + ) + + should_resize = False + if explicit_height_width: + should_resize = True + try: + height = int(preprocessing_parameters[HEIGHT]) + width = int(preprocessing_parameters[WIDTH]) + # Update height and width if the downstream encoder requires images + # with the same dimension or specific width and height values + width, height = ImageFeatureMixin._set_image_and_height_equal_for_encoder( + width, height, preprocessing_parameters, encoder_type + ) + except ValueError as e: + raise ValueError("Image height and width must be set and have positive integer values: " + str(e)) + if height <= 0 or width <= 0: + raise ValueError("Image height and width must be positive integers") + else: + # User hasn't specified height and width. + # Default to inferring from sample or first image. + if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]: + should_resize = True + height, width = ImageFeatureMixin._infer_image_size( + sample, + max_height=preprocessing_parameters[INFER_IMAGE_MAX_HEIGHT], + max_width=preprocessing_parameters[INFER_IMAGE_MAX_WIDTH], + preprocessing_parameters=preprocessing_parameters, + encoder_type=encoder_type, + ) + else: + raise ValueError( + "Explicit image width/height are not set, infer_image_dimensions is false, " + "and first image cannot be read, so image dimensions are unknown" + ) + + if explicit_num_channels: + # User specified num_channels in the model/feature config + user_specified_num_channels = True + num_channels = preprocessing_parameters[NUM_CHANNELS] + else: + user_specified_num_channels = False + if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]: + user_specified_num_channels = True + num_channels = ImageFeatureMixin._infer_number_of_channels(sample) + elif len(sample) > 0: + num_channels = num_channels_in_image(sample[0]) + else: + raise ValueError( + "Explicit image num channels is not set, infer_image_dimensions is false, " + "and first image cannot be read, so image num channels is unknown" + ) + + if not isinstance(num_channels, int): + raise ValueError(f"Number of image channels needs to be an integer, got {type(num_channels).__name__}.") + + average_file_size = np.mean(sample_num_bytes) if sample_num_bytes else None + + standardize_image = preprocessing_parameters["standardize_image"] + if standardize_image == "imagenet1k" and num_channels != 3: + warnings.warn( + f"'standardize_image=imagenet1k' is defined only for 'num_channels=3' but " + f"detected 'num_channels={num_channels}'. For this situation setting 'standardize_image=None'.", + RuntimeWarning, + ) + standardize_image = None + + if preprocessing_parameters[INFER_IMAGE_NUM_CLASSES] or preprocessing_parameters["num_classes"]: + channel_class_map = ImageFeatureMixin._infer_image_num_classes( + sample, num_channels, preprocessing_parameters["num_classes"] + ) + else: + channel_class_map = torch.Tensor([]) + + return ( + should_resize, + width, + height, + num_channels, + user_specified_num_channels, + average_file_size, + standardize_image, + channel_class_map, + ) + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + set_default_value(feature_config[PREPROCESSING], "in_memory", preprocessing_parameters["in_memory"]) + + name = feature_config[NAME] + column = input_df[feature_config[COLUMN]] + encoder_type = feature_config[ENCODER][TYPE] if ENCODER in feature_config else None + + src_path = None + if SRC in metadata: + src_path = os.path.dirname(os.path.abspath(metadata.get(SRC))) + abs_path_column = backend.df_engine.map_objects( + column, + lambda row: get_abs_path(src_path, row) if isinstance(row, str) and not has_remote_protocol(row) else row, + ) + + # determine if specified encoder is a torchvision model + model_type = feature_config[ENCODER].get("type", None) if ENCODER in feature_config else None + model_variant = feature_config[ENCODER].get("model_variant") if ENCODER in feature_config else None + if model_variant: + torchvision_parameters = _get_torchvision_parameters(model_type, model_variant) + else: + torchvision_parameters = None + + if torchvision_parameters: + logger.warning( + f"Using the transforms specified for the torchvision model {model_type} {model_variant} " + f"This includes setting the number of channels is 3 and resizing the image to the needs of the model." + ) + + torchvision_transform, transform_metadata = _get_torchvision_transform(torchvision_parameters) + + # torchvision_parameters is not None + # perform torchvision model transformations + read_image_if_bytes_obj_and_resize = partial( + ImageFeatureMixin._read_image_with_pretrained_transform, + transform_fn=torchvision_transform, + ) + average_file_size = None + + # save weight specification in preprocessing section + preprocessing_parameters["torchvision_model_default_weights"] = ( + f"{torchvision_parameters.model_weights.DEFAULT}" + ) + + # add torchvision model id to preprocessing section for torchscript + preprocessing_parameters["torchvision_model_type"] = model_type + preprocessing_parameters["torchvision_model_variant"] = model_variant + + # get required setup parameters for in_memory = False processing + height = transform_metadata.height + width = transform_metadata.width + num_channels = transform_metadata.num_channels + channel_class_map = torch.Tensor([]) + else: + # torchvision_parameters is None + # perform Ludwig specified transformations + ( + should_resize, + width, + height, + num_channels, + user_specified_num_channels, + average_file_size, + standardize_image, + channel_class_map, + ) = ImageFeatureMixin._finalize_preprocessing_parameters( + preprocessing_parameters, encoder_type, abs_path_column + ) + + metadata[name][PREPROCESSING]["height"] = height + metadata[name][PREPROCESSING]["width"] = width + metadata[name][PREPROCESSING]["num_channels"] = num_channels + metadata[name][PREPROCESSING]["num_classes"] = channel_class_map.shape[0] + metadata[name][PREPROCESSING]["channel_class_map"] = channel_class_map.tolist() + + read_image_if_bytes_obj_and_resize = partial( + ImageFeatureMixin._read_image_if_bytes_obj_and_resize, + img_width=width, + img_height=height, + should_resize=should_resize, + num_channels=num_channels, + resize_method=preprocessing_parameters["resize_method"], + user_specified_num_channels=user_specified_num_channels, + standardize_image=standardize_image, + channel_class_map=channel_class_map, + ) + + # TODO: alternatively use get_average_image() for unreachable images + if channel_class_map.shape[0]: + default_image = get_gray_default_image(1, height, width).squeeze(0) + metadata[name]["reshape"] = (height, width) + else: + default_image = get_gray_default_image(num_channels, height, width) + metadata[name]["reshape"] = (num_channels, height, width) + + import pandas as pd + + sample_entry = abs_path_column.iloc[0] if hasattr(abs_path_column, "iloc") else next(iter(abs_path_column)) + if preprocessing_parameters.get("mode", "lazy") != "eager" and not torchvision_parameters: + # Lazy path: store file paths as a string Series. The actual image + # decode happens per-batch inside PandasDataset via LazyColumn. + # This bounds peak memory to batch_size × image_size instead of N × image_size. + if isinstance(sample_entry, str): + # Input is already a local/remote path — use abs_path_column directly so + # that the DataFrame index is preserved after sampling/filtering operations. + proc_df[feature_config[PROC_COLUMN]] = abs_path_column + else: + # In-memory data (HF PIL Images, raw bytes, numpy arrays) — cache to disk first. + cache_dir = resolve_lazy_cache_dir( + preprocessing_parameters.get("lazy_cache_dir"), + name, + ) + logger.info(f"Image feature '{name}': caching in-memory images to {cache_dir} for lazy decoding.") + raw_column = abs_path_column.tolist() if hasattr(abs_path_column, "tolist") else list(abs_path_column) + path_list = _cache_image_column_to_disk(raw_column, cache_dir, name) + # Reconstruct a Series using the original index so that it aligns + # correctly with proc_df (which may have a non-0-based index after sampling). + if hasattr(abs_path_column, "compute"): # Dask Series + orig_index = abs_path_column.index.compute() + else: + orig_index = abs_path_column.index + proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.from_pandas( + pd.Series(path_list, dtype=object, index=orig_index) + ) + metadata[name]["lazy"] = True # backward compat for ray.py + metadata[name]["mode"] = preprocessing_parameters.get("mode", "lazy") + metadata[name]["prefetch_size"] = preprocessing_parameters.get("prefetch_size") + metadata[name]["reshape"] = None # paths are 1-D strings — no reshape needed + # Persist decode params so PandasDataset can reconstruct the decode fn + metadata[name]["lazy_image_params"] = { + "img_width": width, + "img_height": height, + "should_resize": should_resize, + "num_channels": num_channels, + "resize_method": preprocessing_parameters["resize_method"], + "user_specified_num_channels": user_specified_num_channels, + "standardize_image": standardize_image, + "channel_class_map": channel_class_map.tolist(), + "default_image_shape": list(default_image.shape), + } + else: + # Eager path (legacy): decode all images upfront into numpy arrays. + proc_col = backend.read_binary_files( + abs_path_column, map_fn=read_image_if_bytes_obj_and_resize, file_size=average_file_size + ) + + num_failed_image_reads = ( + proc_col.isna().sum().compute() if is_dask_series_or_df(proc_col, backend) else proc_col.isna().sum() + ) + + proc_col = backend.df_engine.map_objects( + proc_col, lambda row: default_image if not isinstance(row, np.ndarray) else row + ) + + if num_failed_image_reads > 0: + logger.warning( + f"Failed to read {num_failed_image_reads} images while preprocessing feature `{name}`. " + "Using default image for these rows in the dataset." + ) + + proc_df[feature_config[PROC_COLUMN]] = proc_col + + return proc_df + + +class ImageInputFeature(ImageFeatureMixin, InputFeature): + def __init__(self, input_feature_config: ImageInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + # set up for augmentation if it is enabled + if input_feature_config.augmentation: + # assume no image normalize is required + normalize_mean = normalize_std = None + + # determine if specified encoder is a torchvision model + if is_torchvision_encoder(self.encoder_obj): + # encoder is a torchvision model + normalize_mean = self.encoder_obj.normalize_mean + normalize_std = self.encoder_obj.normalize_std + else: + # encoder is a Ludwig encoder, determine if standardize_image is set to IMAGENET1K + if input_feature_config.preprocessing.standardize_image == IMAGENET1K: + normalize_mean = IMAGENET1K_MEAN + normalize_std = IMAGENET1K_STD + + # create augmentation pipeline object + self.augmentation_pipeline = ImageAugmentation( + input_feature_config.augmentation, + normalize_mean, + normalize_std, + ) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Image feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if inputs.dtype != torch.float32: + raise ValueError(f"Image feature inputs must be a float32 tensor, got {inputs.dtype}.") + + inputs_encoded = self.encoder_obj(inputs) + + return inputs_encoded + + @property + def input_dtype(self): + return torch.float32 + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self.encoder_obj.input_shape) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + def update_config_after_module_init(self, feature_config): + if is_torchvision_encoder(self.encoder_obj): + # update feature preprocessing parameters to reflect used in torchvision pretrained model + # Note: image height and width is determined by the encoder crop_size attribute. Source of this + # attribute is from the torchvision.transforms._presets.ImageClassification class. This class stores + # crop_size as a single element list. the single element in this list is used to set both the height + # and width of an image. + feature_config.preprocessing.height = self.encoder_obj.crop_size[0] + feature_config.preprocessing.width = self.encoder_obj.crop_size[0] + feature_config.preprocessing.num_channels = self.encoder_obj.num_channels + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + for key in ["height", "width", "num_channels", "standardize_image"]: + if hasattr(feature_config.encoder, key): + setattr(feature_config.encoder, key, feature_metadata[PREPROCESSING][key]) + + @staticmethod + def get_schema_cls(): + return ImageInputFeatureConfig + + @staticmethod + def create_preproc_module(metadata: dict[str, Any]) -> BasePreprocessingModule: + model_type = metadata["preprocessing"].get("torchvision_model_type") + model_variant = metadata["preprocessing"].get("torchvision_model_variant") + if model_variant: + torchvision_parameters = _get_torchvision_parameters(model_type, model_variant) + else: + torchvision_parameters = None + + if torchvision_parameters: + torchvision_transform, transform_metadata = _get_torchvision_transform(torchvision_parameters) + else: + torchvision_transform = None + transform_metadata = None + + return _ImagePreprocessing( + metadata, torchvision_transform=torchvision_transform, transform_metadata=transform_metadata + ) + + def get_augmentation_pipeline(self): + return self.augmentation_pipeline + + +class ImageOutputFeature(ImageFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: ImageOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + super().__init__(output_feature_config, output_features, **kwargs) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs: dict[str, torch.Tensor], target=None, **kwargs): + return self.decoder_obj(inputs, target=target) + + def metric_kwargs(self): + return {"num_outputs": self.output_shape[0]} + + def create_predict_module(self) -> PredictModule: + return _ImagePredict() + + def get_prediction_set(self): + return self.decoder_obj.get_prediction_set() + + @classmethod + def get_output_dtype(cls): + return torch.float32 + + @property + def output_shape(self) -> torch.Size: + return self.decoder_obj.output_shape + + @property + def input_shape(self) -> torch.Size: + return self.decoder_obj.input_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + for key in ["height", "width", "num_channels", "num_classes", "standardize_image"]: + if hasattr(feature_config.decoder, key): + setattr(feature_config.decoder, key, feature_metadata[PREPROCESSING][key]) + + @staticmethod + def calculate_overall_stats(predictions, targets, metadata): + # no overall stats, just return empty dictionary + return {} + + def postprocess_predictions( + self, + result, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + + if predictions_col in result: + channel_class_map = torch.ByteTensor(metadata[PREPROCESSING]["channel_class_map"]) + + if channel_class_map.shape[0]: + + def class_mask2img(row): + pred = row[predictions_col] + return get_image_from_class_mask(channel_class_map, pred) + + result[predictions_col] = result.apply(class_mask2img, axis=1) + + return result + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _ImagePostprocessing(metadata) + + @staticmethod + def get_schema_cls(): + return ImageOutputFeatureConfig diff --git a/ludwig/features/number_feature.py b/ludwig/features/number_feature.py new file mode 100644 index 0000000..d36bb01 --- /dev/null +++ b/ludwig/features/number_feature.py @@ -0,0 +1,545 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import copy +import logging +from abc import ABC, abstractmethod +from typing import Any + +import numpy as np +import pandas as pd +import torch +from torch import nn + +from ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, NUMBER, PREDICTIONS, PROC_COLUMN +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + FeaturePreprocessingMixin, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.schema.features.number_feature import NumberInputFeatureConfig, NumberOutputFeatureConfig +from ludwig.types import ( + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + + +class NumberTransformer(nn.Module, ABC): + @abstractmethod + def transform(self, x: np.ndarray) -> np.ndarray: + pass + + @abstractmethod + def inverse_transform(self, x: np.ndarray) -> np.ndarray: + pass + + @abstractmethod + def transform_inference(self, x: torch.Tensor) -> torch.Tensor: + pass + + @abstractmethod + def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor: + pass + + @staticmethod + @abstractmethod + def fit_transform_params(column: np.ndarray, backend: Any) -> dict[str, Any]: + pass + + +class ZScoreTransformer(NumberTransformer): + def __init__(self, mean: float | None = None, std: float | None = None, **kwargs: dict): + super().__init__() + self.mu = float(mean) if mean is not None else mean + self.sigma = float(std) if std is not None else std + self.feature_name = kwargs.get(NAME, "") + if self.sigma == 0: + import logging as _logging + + _logging.getLogger(__name__).warning( + "Cannot apply zscore normalization to `%s` since it has a standard deviation of 0 " + "(constant value %s). The feature will be passed through unchanged.", + self.feature_name, + self.mu, + ) + # Avoid division-by-zero; transform yields (x - mu) / 1 = 0 for all constant rows. + self.sigma = 1.0 + + def transform(self, x: np.ndarray) -> np.ndarray: + return (x - self.mu) / self.sigma + + def inverse_transform(self, x: np.ndarray) -> np.ndarray: + return x * self.sigma + self.mu + + def transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return (x - self.mu) / self.sigma + + def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return x * self.sigma + self.mu + + @staticmethod + def fit_transform_params(column: np.ndarray, backend: "Backend") -> dict[str, Any]: # noqa + compute = backend.df_engine.compute + return { + "mean": compute(column.astype(np.float32).mean()), + "std": compute(column.astype(np.float32).std()), + } + + +class MinMaxTransformer(NumberTransformer): + def __init__(self, min: float | None = None, max: float | None = None, **kwargs: dict): + super().__init__() + self.min_value = float(min) if min is not None else min + self.max_value = float(max) if max is not None else max + if self.min_value is None or self.max_value is None: + self.range = None + else: + self.range = self.max_value - self.min_value + + def transform(self, x: np.ndarray) -> np.ndarray: + return (x - self.min_value) / self.range + + def inverse_transform(self, x: np.ndarray) -> np.ndarray: + if self.range is None: + raise ValueError("Numeric transformer needs to be instantiated with min and max values.") + return x * self.range + self.min_value + + def transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return (x - self.min_value) / self.range + + def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor: + if self.range is None: + raise ValueError("Numeric transformer needs to be instantiated with min and max values.") + return x * self.range + self.min_value + + @staticmethod + def fit_transform_params(column: np.ndarray, backend: "Backend") -> dict[str, Any]: # noqa + compute = backend.df_engine.compute + return { + "min": compute(column.astype(np.float32).min()), + "max": compute(column.astype(np.float32).max()), + } + + +class InterQuartileTransformer(NumberTransformer): + def __init__(self, q1: float | None = None, q2: float | None = None, q3: float | None = None, **kwargs: dict): + super().__init__() + self.q1 = float(q1) if q1 is not None else q1 + self.q2 = float(q2) if q2 is not None else q2 + self.q3 = float(q3) if q3 is not None else q3 + if self.q1 is None or self.q3 is None: + self.interquartile_range = None + else: + self.interquartile_range = self.q3 - self.q1 + self.feature_name = kwargs.get(NAME, "") + if self.interquartile_range == 0: + raise RuntimeError( + f"Cannot apply InterQuartileNormalization to `{self.feature_name}` since" + "the interquartile range is 0, which will result in a ZeroDivisionError." + ) + + def transform(self, x: np.ndarray) -> np.ndarray: + return (x - self.q2) / self.interquartile_range + + def inverse_transform(self, x: np.ndarray) -> np.ndarray: + return x * self.interquartile_range + self.q2 + + def transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return (x - self.q2) / self.interquartile_range + + def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return x * self.interquartile_range + self.q2 + + @staticmethod + def fit_transform_params(column: np.ndarray, backend: "Backend") -> dict[str, Any]: # noqa + # backend.df_engine.compute is not used here because `percentile` is not parallelized in dask. + # We compute the percentile directly. + return { + "q1": np.percentile(column.astype(np.float32), 25), + "q2": np.percentile(column.astype(np.float32), 50), + "q3": np.percentile(column.astype(np.float32), 75), + } + + +class Log1pTransformer(NumberTransformer): + def __init__(self, **kwargs: dict): + super().__init__() + self.feature_name = kwargs.get(NAME, "") + + def transform(self, x: np.ndarray) -> np.ndarray: + if np.any(x <= 0): + raise ValueError( + f"One or more values in the `{self.feature_name}` feature are non-positive. " + "log1p normalization is defined only for positive values." + ) + return np.log1p(x) + + def inverse_transform(self, x: np.ndarray) -> np.ndarray: + return np.expm1(x) + + def transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return torch.log1p(x) + + def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return torch.expm1(x) + + @staticmethod + def fit_transform_params(column: np.ndarray, backend: "Backend") -> dict[str, Any]: # noqa + return {} + + +class IdentityTransformer(NumberTransformer): + def __init__(self, **kwargs): + super().__init__() + + def transform(self, x: np.ndarray) -> np.ndarray: + return x + + def inverse_transform(self, x: np.ndarray) -> np.ndarray: + return x + + def transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return x + + def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor: + return x + + @staticmethod + def fit_transform_params(column: np.ndarray, backend: "Backend") -> dict[str, Any]: # noqa + return {} + + +numeric_transformation_registry = { + "minmax": MinMaxTransformer, + "zscore": ZScoreTransformer, + "log1p": Log1pTransformer, + "iq": InterQuartileTransformer, + None: IdentityTransformer, +} + + +def get_transformer(metadata, preprocessing_parameters) -> NumberTransformer: + return get_from_registry( + preprocessing_parameters.get("normalization", None), + numeric_transformation_registry, + )(**metadata) + + +class _OutlierReplacer(torch.nn.Module): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.zscore_transformer = ZScoreTransformer(**metadata) + self.outlier_threshold = metadata["preprocessing"].get("outlier_threshold") + self.computed_outlier_fill_value = float(metadata["preprocessing"]["computed_outlier_fill_value"]) + + def forward(self, v: torch.Tensor) -> torch.Tensor: + outliers = self.zscore_transformer.transform_inference(v).abs().gt(self.outlier_threshold) + v_masked = torch.masked_fill(v, outliers, torch.nan) + + v = torch.nan_to_num(v_masked, nan=self.computed_outlier_fill_value) + return v.to(dtype=torch.float32) + + +class _NumberPreprocessing(BasePreprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.computed_fill_value = float(metadata["preprocessing"]["computed_fill_value"]) + self.numeric_transformer = get_transformer(metadata, metadata["preprocessing"]) + + # Optional outlier replacement + self.outlier_replacer = None + if metadata["preprocessing"].get("outlier_strategy") is not None: + self.outlier_replacer = _OutlierReplacer(metadata) + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if not torch.jit.isinstance(v, torch.Tensor): + raise ValueError(f"Unsupported input: {v}") + + v = torch.nan_to_num(v, nan=self.computed_fill_value) + v = v.to(dtype=torch.float32) + + # Handle outliers if needed + if self.outlier_replacer is not None: + v = self.outlier_replacer(v) + + return self.numeric_transformer.transform_inference(v) + + +class _NumberPostprocessing(BasePostprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.numeric_transformer = get_transformer(metadata, metadata["preprocessing"]) + self.predictions_key = PREDICTIONS + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + + return {self.predictions_key: self.numeric_transformer.inverse_transform_inference(predictions)} + + +class _NumberPredict(PredictModule): + def __init__(self, clip): + super().__init__() + self.clip = clip + + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + predictions = logits + + if self.clip is not None: + predictions = torch.clamp(logits, self.clip[0], self.clip[1]) + logger.debug(f" clipped_predictions: {predictions}") + + return {self.predictions_key: predictions, self.logits_key: logits} + + +class NumberFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return NUMBER + + @staticmethod + def cast_column(column, backend): + return backend.df_engine.df_lib.to_numeric(column, errors="coerce").astype(np.float32) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + numeric_transformer: NumberTransformer = get_from_registry( + preprocessing_parameters.get("normalization", None), + numeric_transformation_registry, + ) + + params = numeric_transformer.fit_transform_params(column, backend) + + # Ensure mean and std are computed if we're removing outliers + outlier_strategy = preprocessing_parameters.get("outlier_strategy") + if outlier_strategy is not None and ("mean" not in params or "std" not in params): + params.update(ZScoreTransformer.fit_transform_params(column, backend)) + + # Compute PLE bin edges if this is an input feature using the PLE encoder + if is_input_feature: + input_features = config.get("input_features", []) + for feat in input_features: + if isinstance(feat, dict) and feat.get("name") == config.get("_current_feature_name"): + encoder_cfg = feat.get("encoder", {}) + if isinstance(encoder_cfg, dict) and encoder_cfg.get("type") == "ple": + num_bins = encoder_cfg.get("num_bins", 64) + quantiles = np.linspace(0, 1, num_bins + 1) + col_values = column.dropna().astype(np.float32) if hasattr(column, "dropna") else column + bin_edges = np.percentile(col_values, quantiles * 100).tolist() + params["ple_bin_edges"] = bin_edges + break + + return params + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + # Had to replace normalize() function due to issue #1911 + # this comment is to provide context for the change. + # original code + # def normalize(series: pd.Series) -> pd.Series: + # series = series.copy() + # numeric_transformer = get_transformer(metadata[feature_config[NAME]], preprocessing_parameters) + # series.update(numeric_transformer.transform(series.values)) + # return series + + def normalize(series: pd.Series) -> pd.Series: + _feature_metadata = copy.deepcopy(metadata[feature_config[NAME]]) + _feature_metadata.update({NAME: feature_config[NAME]}) + + # retrieve request numeric transformer + numeric_transformer = get_transformer(_feature_metadata, preprocessing_parameters) + + # transform input numeric values with specified transformer + transformed_values = numeric_transformer.transform(series.values) + + # return transformed values with same index values as original series. + return pd.Series(transformed_values, index=series.index) + + input_series = input_df[feature_config[COLUMN]].astype(np.float32) + proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_partitions( + input_series, normalize, meta=input_series + ) + + return proc_df + + +class NumberInputFeature(NumberFeatureMixin, InputFeature): + def __init__(self, input_feature_config: NumberInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + input_feature_config.encoder.input_size = self.input_shape[-1] + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + # Set PLE bin edges from config metadata if available + if ( + hasattr(input_feature_config.encoder, "ple_bin_edges") + and input_feature_config.encoder.ple_bin_edges is not None + and hasattr(self.encoder_obj, "set_bin_edges") + ): + self.encoder_obj.set_bin_edges(input_feature_config.encoder.ple_bin_edges) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Number feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if inputs.dtype not in (torch.float32, torch.float64): + raise ValueError(f"Number feature inputs dtype must be float32 or float64, got {inputs.dtype}.") + if not (len(inputs.shape) == 1 or (len(inputs.shape) == 2 and inputs.shape[1] == 1)): + raise ValueError( + f"Number feature inputs must be 1D or 2D with shape[1]==1, got shape {tuple(inputs.shape)}." + ) + + if len(inputs.shape) == 1: + inputs = inputs[:, None] + inputs_encoded = self.encoder_obj(inputs) + + return inputs_encoded + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self.encoder_obj.output_shape) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + # Pass PLE bin edges from metadata to encoder config + if hasattr(feature_config, "encoder") and hasattr(feature_config.encoder, "type"): + if feature_config.encoder.type == "ple" and "ple_bin_edges" in feature_metadata: + feature_config.encoder.ple_bin_edges = feature_metadata["ple_bin_edges"] + + @staticmethod + def get_schema_cls(): + return NumberInputFeatureConfig + + def create_sample_input(self, batch_size: int = 2): + return torch.rand([batch_size]) + + @classmethod + def get_preproc_input_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "float32" + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _NumberPreprocessing(metadata) + + +class NumberOutputFeature(NumberFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: NumberOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self.clip = output_feature_config.clip + super().__init__(output_feature_config, output_features, **kwargs) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, **kwargs): # hidden + hidden = inputs[HIDDEN] + return self.decoder_obj(hidden) + + def create_predict_module(self) -> PredictModule: + if getattr(self, "clip", None) and not (isinstance(self.clip, (list, tuple)) and len(self.clip) == 2): + raise ValueError( + f"The clip parameter of {self.feature_name} is {self.clip}. It must be a list or a tuple of length 2." + ) + return _NumberPredict(getattr(self, "clip", None)) + + def get_prediction_set(self): + return {PREDICTIONS, LOGITS} + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.decoder_obj.config.input_size]) + + @classmethod + def get_output_dtype(cls): + return torch.float32 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([1]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + pass + + @staticmethod + def calculate_overall_stats(predictions, targets, metadata): + # no overall stats, just return empty dictionary + return {} + + def postprocess_predictions( + self, + predictions, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + if predictions_col in predictions: + # as needed convert predictions make to original value space + numeric_transformer = get_from_registry( + metadata["preprocessing"].get("normalization", None), + numeric_transformation_registry, + )(**metadata) + predictions[predictions_col] = predictions[predictions_col].map( + lambda pred: numeric_transformer.inverse_transform(pred) + ) + + return predictions + + @staticmethod + def get_schema_cls(): + return NumberOutputFeatureConfig + + @classmethod + def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str: + return "float32" + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _NumberPostprocessing(metadata) diff --git a/ludwig/features/passthrough_feature.py b/ludwig/features/passthrough_feature.py new file mode 100644 index 0000000..6de72e6 --- /dev/null +++ b/ludwig/features/passthrough_feature.py @@ -0,0 +1,104 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Passthrough input feature for embed-mode inference. + +When encoder embeddings are pre-cached during preprocessing, the ECD model +replaces the real input feature with a PassthroughInputFeature so that +``forward()`` simply returns the already-embedded tensor without re-encoding. +""" + +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.features.base_feature import InputFeature +from ludwig.schema.features.base import BaseFeatureConfig +from ludwig.types import TrainingSetMetadataDict +from ludwig.utils.types import PreprocessingInput + + +class PassthroughPreprocModule(torch.nn.Module): + """Combines preprocessing and encoding into a single module for TorchScript inference. + + For encoder outputs that were cached during preprocessing, the encoder is simply the identity function in the ECD + module. As such, we need this module to apply the encoding that would normally be done during preprocessing for + realtime inference. + """ + + def __init__(self, preproc: torch.nn.Module, encoder: torch.nn.Module): + super().__init__() + self.preproc = preproc + self.encoder = encoder + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + preproc_v = self.preproc(v) + return self.encoder(preproc_v) + + +class PassthroughInputFeature(InputFeature): + """A transparent identity-function wrapper around an input feature whose encoder was pre-cached. + + Used when encoder embeddings were computed during preprocessing (embed mode). The passthrough + delegates shape/type queries to the wrapped feature's encoder so the rest of the model sees a + consistent interface, while ``forward()`` simply returns the already-embedded tensor unchanged. + """ + + def __init__(self, config: BaseFeatureConfig, wrapped: InputFeature): + super().__init__(config) + self._wrapped = wrapped + + def forward(self, inputs, mask=None) -> dict[str, torch.Tensor]: + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"PassthroughInputFeature forward expects a torch.Tensor, got {type(inputs).__name__}.") + return {ENCODER_OUTPUT: inputs} + + @property + def input_dtype(self) -> torch.dtype: + return torch.float32 + + @property + def input_shape(self) -> torch.Size: + return self._wrapped.encoder_obj.output_shape + + @property + def output_shape(self) -> torch.Size: + return self._wrapped.encoder_obj.output_shape + + def update_config_with_metadata(self, feature_config, feature_metadata, *args, **kwargs) -> None: + return self._wrapped.update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs) + + def get_schema_cls(self) -> type: + return self._wrapped.get_schema_cls() + + def create_preproc_module(self, metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return PassthroughPreprocModule(self._wrapped.create_preproc_module(metadata), self._wrapped) + + def type(self) -> str: + return self._wrapped.type() + + def unskip(self) -> InputFeature: + return self._wrapped + + @property + def encoder_obj(self) -> torch.nn.Module: + return self._wrapped.encoder_obj + + +def create_passthrough_input_feature(feature: InputFeature, config: BaseFeatureConfig) -> PassthroughInputFeature: + """Wraps *feature* in a :class:`PassthroughInputFeature` shim. + + The shim acts as a transparent identity function — useful when encoder embeddings + were cached during preprocessing and the model should skip re-encoding. + """ + return PassthroughInputFeature(config, wrapped=feature) diff --git a/ludwig/features/sequence_feature.py b/ludwig/features/sequence_feature.py new file mode 100644 index 0000000..db970f7 --- /dev/null +++ b/ludwig/features/sequence_feature.py @@ -0,0 +1,524 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import logging +from functools import partial + +import numpy as np +import torch + +from ludwig.constants import ( + COLUMN, + LAST_PREDICTIONS, + LENGTHS, + NAME, + PREDICTIONS, + PROBABILITIES, + PROBABILITY, + PROC_COLUMN, + SEQUENCE, +) +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + FeaturePreprocessingMixin, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.features.feature_utils import compute_sequence_probability, compute_token_probabilities +from ludwig.schema.features.sequence_feature import SequenceInputFeatureConfig, SequenceOutputFeatureConfig +from ludwig.types import ( + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.math_utils import softmax +from ludwig.utils.strings_utils import ( + build_sequence_matrix, + create_vocabulary, + SpecialSymbol, + START_SYMBOL, + STOP_SYMBOL, + UNKNOWN_SYMBOL, +) +from ludwig.utils.tokenizers import get_tokenizer_from_registry +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + + +class _SequencePreprocessing(BasePreprocessingModule): + """Torchscript-enabled version of preprocessing done by SequenceFeatureMixin.add_feature_data.""" + + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.lowercase = metadata["preprocessing"]["lowercase"] + self.tokenizer_type = metadata["preprocessing"]["tokenizer"] + self.tokenizer = get_tokenizer_from_registry(self.tokenizer_type)( + pretrained_model_name_or_path=metadata["preprocessing"].get("pretrained_model_name_or_path", None) + ) + + if not isinstance(self.tokenizer, torch.nn.Module): + raise ValueError(f"tokenizer must be a torch.nn.Module, got {self.tokenizer}") + + self.padding_symbol = metadata["preprocessing"]["padding_symbol"] + self.unknown_symbol = metadata["preprocessing"]["unknown_symbol"] + self.start_symbol = START_SYMBOL + self.stop_symbol = STOP_SYMBOL + self.max_sequence_length = int(metadata["max_sequence_length"]) + self.unit_to_id = metadata["str2idx"] + self.computed_fill_value = metadata["preprocessing"]["computed_fill_value"] + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + """Takes a list of strings and returns a tensor of token ids.""" + if not torch.jit.isinstance(v, list[str]): + raise ValueError(f"Unsupported input: {v}") + + futures: list[torch.jit.Future[torch.Tensor]] = [] + for sequence in v: + futures.append( + torch.jit.fork( + self._process_sequence, + sequence, + ) + ) + + sequence_matrix = [] + for future in futures: + sequence_matrix.append(torch.jit.wait(future)) + + return torch.stack(sequence_matrix) + + def _process_sequence(self, sequence: str) -> torch.Tensor: + sequence = self.computed_fill_value if sequence == "nan" else sequence + + # If tokenizer is HF, we defer lowercase transformation to the tokenizer. + if self.lowercase and self.tokenizer_type != "hf_tokenizer": + sequence_str: str = sequence.lower() + else: + sequence_str: str = sequence + + sequence_vector = torch.full([self.max_sequence_length], self.unit_to_id[self.padding_symbol]) + + if self.tokenizer_type == "hf_tokenizer": + # Handles start, stop, and unknown symbols implicitly + unit_sequence = self.tokenizer(sequence) + assert torch.jit.isinstance(unit_sequence, list[int]) + # Ensures that the sequence lengths are aligned between the input and output tensors. + sequence_length = min(len(unit_sequence), self.max_sequence_length) + sequence_vector[:sequence_length] = torch.tensor(unit_sequence)[:sequence_length] + return sequence_vector + + # If tokenizer is not HF, we manually convert tokens to IDs and insert start, stop, and unknown symbols. + unit_sequence = self.tokenizer(sequence_str) + assert torch.jit.isinstance(unit_sequence, list[str]) + + sequence_vector[0] = self.unit_to_id[self.start_symbol] + if len(unit_sequence) + 1 < self.max_sequence_length: + sequence_length = len(unit_sequence) + sequence_vector[len(unit_sequence) + 1] = self.unit_to_id[self.stop_symbol] + else: + sequence_length = self.max_sequence_length - 1 + + for i in range(sequence_length): + curr_unit = unit_sequence[i] + if curr_unit in self.unit_to_id: + curr_id = self.unit_to_id[curr_unit] + else: + curr_id = self.unit_to_id[self.unknown_symbol] + sequence_vector[i + 1] = curr_id + return sequence_vector + + +class _SequencePostprocessing(BasePostprocessingModule): + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.max_sequence_length = int(metadata["max_sequence_length"]) + self.idx2str = metadata["idx2str"] + self.unknown_symbol = UNKNOWN_SYMBOL + self.predictions_key = PREDICTIONS + self.probabilities_key = PROBABILITIES + self.probability_key = PROBABILITY + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + pred_predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + pred_probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key) + + predictions: list[list[str]] = [] + for sequence in pred_predictions: + sequence_predictions: list[str] = [] + for i in range(self.max_sequence_length): + unit_id = int(sequence[i].item()) + if unit_id < len(self.idx2str): + unit_prediction = self.idx2str[unit_id] + else: + unit_prediction = self.unknown_symbol + sequence_predictions.append(unit_prediction) + predictions.append(sequence_predictions) + + probabilities, _ = torch.max(pred_probabilities, dim=-1) + probability = torch.sum(torch.log(probabilities.clamp(min=1e-10)), dim=-1) + + return { + self.predictions_key: predictions, + self.probabilities_key: probabilities, + self.probability_key: probability, + } + + +class _SequencePredict(PredictModule): + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + probabilities = torch.softmax(logits, -1) + predictions = torch.argmax(logits, -1) + + # predictions: [batch_size, sequence_length] + # probabilities: [batch_size, sequence_length, vocab_size] + # logits: [batch_size, sequence_length, vocab_size] + return {self.predictions_key: predictions, self.probabilities_key: probabilities, self.logits_key: logits} + + +class SequenceFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return SEQUENCE + + @staticmethod + def cast_column(column, backend): + return column.astype(str) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + vocabulary = create_vocabulary( + column, + preprocessing_parameters["tokenizer"], + lowercase=preprocessing_parameters["lowercase"], + num_most_frequent=preprocessing_parameters["most_common"], + vocab_file=preprocessing_parameters["vocab_file"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + ngram_size=preprocessing_parameters["ngram_size"], + processor=backend.df_engine, + ) + logger.info( + f"Max length of feature '{column.name}': {vocabulary.max_sequence_length} (without start and stop symbols)" + ) + + # Use sequence_length if provided, otherwise use max length found in dataset. + if preprocessing_parameters["sequence_length"] is not None: + logger.info( + f"Setting max length to sequence_length={preprocessing_parameters['sequence_length']} provided in " + f"preprocessing parameters" + ) + max_sequence_length = preprocessing_parameters["sequence_length"] + else: + max_sequence_length = vocabulary.max_sequence_length + logger.info(f"Setting max length using dataset: {max_sequence_length} (including start and stop symbols)") + + # If max_sequence_length is None, then use the max length found in the dataset. + if ( + preprocessing_parameters["max_sequence_length"] is not None + and preprocessing_parameters["max_sequence_length"] < max_sequence_length + ): + logger.info( + f"Truncating max length with max_sequence_length={preprocessing_parameters['max_sequence_length']} " + f"from preprocessing parameters" + ) + max_sequence_length = preprocessing_parameters["max_sequence_length"] + + logger.info(f"Max sequence length is {max_sequence_length} for feature '{column.name}'") + return { + "idx2str": vocabulary.vocab, + "str2idx": vocabulary.str2idx, + "str2freq": vocabulary.str2freq, + "vocab_size": len(vocabulary.vocab), + "max_sequence_length": max_sequence_length, + } + + @staticmethod + def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend): + sequence_data = build_sequence_matrix( + sequences=column, + inverse_vocabulary=metadata["str2idx"], + tokenizer_type=preprocessing_parameters["tokenizer"], + length_limit=metadata["max_sequence_length"], + padding_symbol=preprocessing_parameters["padding_symbol"], + padding=preprocessing_parameters["padding"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + lowercase=preprocessing_parameters["lowercase"], + tokenizer_vocab_file=preprocessing_parameters["vocab_file"], + processor=backend.df_engine, + ) + return sequence_data + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + sequence_data = SequenceInputFeature.feature_data( + input_df[feature_config[COLUMN]], + metadata[feature_config[NAME]], + preprocessing_parameters, + backend, + ) + proc_df[feature_config[PROC_COLUMN]] = sequence_data + return proc_df + + +class SequenceInputFeature(SequenceFeatureMixin, InputFeature): + def __init__(self, input_feature_config: SequenceInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs: torch.Tensor, mask=None): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Sequence feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if len(inputs.shape) != 2: + raise ValueError(f"Sequence feature inputs must be 2D (batch x seq_len), got shape {tuple(inputs.shape)}.") + inputs_exp = inputs.type(torch.int32) + inputs_mask = torch.not_equal(inputs, SpecialSymbol.PADDING.value) + lengths = torch.sum(inputs_mask.type(torch.int32), dim=1) + encoder_output = self.encoder_obj(inputs_exp, mask=inputs_mask) + encoder_output[LENGTHS] = lengths + return encoder_output + + @property + def input_dtype(self): + return torch.int32 + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.vocab = feature_metadata["idx2str"] + feature_config.encoder.vocab_size = len(feature_metadata["idx2str"]) + feature_config.encoder.max_sequence_length = feature_metadata["max_sequence_length"] + + @staticmethod + def get_schema_cls(): + return SequenceInputFeatureConfig + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.encoder_obj.config.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _SequencePreprocessing(metadata) + + +class SequenceOutputFeature(SequenceFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: SequenceOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + super().__init__(output_feature_config, output_features, **kwargs) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs: dict[str, torch.Tensor], target=None): + return self.decoder_obj(inputs, target=target) + + def create_predict_module(self) -> PredictModule: + return _SequencePredict() + + def get_prediction_set(self): + return self.decoder_obj.get_prediction_set() + + @classmethod + def get_output_dtype(cls): + return torch.int32 + + @property + def input_shape(self) -> torch.Size: + # Dummy implementation. + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.decoder_obj.config.max_sequence_length]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.decoder.vocab_size = feature_metadata["vocab_size"] + feature_config.decoder.max_sequence_length = feature_metadata["max_sequence_length"] + if isinstance(feature_config.loss.class_weights, (list, tuple)): + if len(feature_config.loss.class_weights) != feature_config.decoder.vocab_size: + raise ValueError( + f"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with " + f"the number of classes ({feature_config.decoder.vocab_size}) for feature {feature_config.column}. " + "Check the metadata JSON file to see the classes " + "and their order and consider there needs to be a weight " + "for the and class too." + ) + + if isinstance(feature_config.loss.class_weights, dict): + if feature_metadata["str2idx"].keys() != feature_config.loss.class_weights.keys(): + raise ValueError( + f"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with " + f"the classes ({feature_metadata['str2idx'].keys()}) of feature {feature_config.column}. " + "Check the metadata JSON file to see the classes " + "and consider there needs to be a weight " + "for the class too." + ) + else: + class_weights = feature_config.loss.class_weights + idx2str = feature_metadata["idx2str"] + class_weights_list = [class_weights[s] for s in idx2str] + feature_config.loss.class_weights = class_weights_list + + if feature_config.loss.class_similarities_temperature > 0: + if feature_config.loss.class_similarities is not None: + similarities = feature_config.loss.class_similarities + temperature = feature_config.loss.class_similarities_temperature + + curr_row = 0 + first_row_length = 0 + is_first_row = True + for row in similarities: + if is_first_row: + first_row_length = len(row) + is_first_row = False + curr_row += 1 + else: + curr_row_length = len(row) + if curr_row_length != first_row_length: + raise ValueError( + f"The length of row {curr_row} of the class_similarities " + f"of {feature_config.column} is {curr_row_length}, different from the length of " + f"the first row {first_row_length}. All rows must have " + "the same length." + ) + else: + curr_row += 1 + all_rows_length = first_row_length + + if all_rows_length != len(similarities): + raise ValueError( + f"The class_similarities matrix of {feature_config.column} has " + f"{len(similarities)} rows and {all_rows_length} columns, " + "their number must be identical." + ) + + if all_rows_length != feature_config.decoder.vocab_size: + raise ValueError( + f"The size of the class_similarities matrix of {feature_config.column} is " + f"{all_rows_length}, different from the number of classes " + f"({feature_config.decoder.vocab_size}). Check the metadata JSON file to see the classes " + "and their order and " + "consider and class too." + ) + + similarities = np.array(similarities, dtype=np.float32) + for i in range(len(similarities)): + similarities[i, :] = softmax(similarities[i, :], temperature=temperature) + feature_config.loss.class_similarities = similarities + else: + raise ValueError( + "class_similarities_temperature > 0, " + "but no class_similarities are provided " + f"for feature {feature_config.column}" + ) + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + # TODO(Justin): Add a confusion matrix, see + # https://github.com/ludwig-ai/ludwig/blob/tf-legacy/ludwig/features/sequence_feature.py#L411 + return {} + + def postprocess_predictions( + self, + result, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + lengths_col = f"{self.feature_name}_{LENGTHS}" + if predictions_col in result: + if "idx2str" in metadata: + + def idx2str(row): + pred = row[predictions_col] + length = metadata["max_sequence_length"] + return [ + metadata["idx2str"][token] if token < len(metadata["idx2str"]) else UNKNOWN_SYMBOL + for token in [pred[i] for i in range(length)] + ] + + result[predictions_col] = result.apply(idx2str, axis=1) + + last_preds_col = f"{self.feature_name}_{LAST_PREDICTIONS}" + if last_preds_col in result: + if "idx2str" in metadata: + + def last_idx2str(last_pred): + if last_pred < len(metadata["idx2str"]): + return metadata["idx2str"][last_pred] + return UNKNOWN_SYMBOL + + result[last_preds_col] = result[last_preds_col].map(last_idx2str) + + probs_col = f"{self.feature_name}_{PROBABILITIES}" + prob_col = f"{self.feature_name}_{PROBABILITY}" + if probs_col in result: + # currently does not return full probabilties because usually it is huge: + # dataset x length x classes + # TODO: add a mechanism for letting the user decide to save it + result[probs_col] = result[probs_col].map(compute_token_probabilities) + result[prob_col] = result[probs_col].map( + partial( + compute_sequence_probability, + max_sequence_length=metadata["max_sequence_length"], + return_log_prob=True, + ), + ) + + if lengths_col in result: + del result[lengths_col] + + return result + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _SequencePostprocessing(metadata) + + @staticmethod + def get_schema_cls(): + return SequenceOutputFeatureConfig diff --git a/ludwig/features/set_feature.py b/ludwig/features/set_feature.py new file mode 100644 index 0000000..cbd0b7e --- /dev/null +++ b/ludwig/features/set_feature.py @@ -0,0 +1,360 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from typing import Any + +import numpy as np +import torch + +from ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROBABILITIES, PROC_COLUMN, SET +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + FeaturePreprocessingMixin, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.features.feature_utils import set_str_to_idx +from ludwig.schema.features.set_feature import SetInputFeatureConfig, SetOutputFeatureConfig +from ludwig.types import ( + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.strings_utils import create_vocabulary, UNKNOWN_SYMBOL +from ludwig.utils.tokenizers import get_tokenizer_from_registry, TORCHSCRIPT_COMPATIBLE_TOKENIZERS +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + + +class _SetPreprocessing(BasePreprocessingModule): + """Torchscript-enabled version of preprocessing done by SetFeatureMixin.add_feature_data. + + If is_bag is true, forward returns a vector for each sample indicating counts of each token. Else, forward returns a + multi-hot vector for each sample indicating presence of each token. + """ + + def __init__(self, metadata: TrainingSetMetadataDict, is_bag: bool = False): + super().__init__() + if metadata["preprocessing"]["tokenizer"] not in TORCHSCRIPT_COMPATIBLE_TOKENIZERS: + raise ValueError( + f"{metadata['preprocessing']['tokenizer']} is not supported by torchscript. Please use " + f"one of {TORCHSCRIPT_COMPATIBLE_TOKENIZERS}." + ) + + self.lowercase = metadata["preprocessing"]["lowercase"] + self.tokenizer = get_tokenizer_from_registry(metadata["preprocessing"]["tokenizer"])() + self.vocab_size = metadata["vocab_size"] + self.unknown_symbol = UNKNOWN_SYMBOL + self.unit_to_id = metadata["str2idx"] + self.is_bag = is_bag + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + """Takes a list of strings and returns a tensor of counts for each token.""" + if not torch.jit.isinstance(v, list[str]): + raise ValueError(f"Unsupported input: {v}") + + if self.lowercase: + sequences = [sequence.lower() for sequence in v] + else: + sequences = v + + unit_sequences = self.tokenizer(sequences) + # refines type of unit_sequences from Any to List[List[str]] + assert torch.jit.isinstance(unit_sequences, list[list[str]]), "unit_sequences is not a list of lists." + + set_matrix = torch.zeros(len(unit_sequences), self.vocab_size, dtype=torch.float32) + for sample_idx, unit_sequence in enumerate(unit_sequences): + sequence_length = len(unit_sequence) + for i in range(sequence_length): + curr_unit = unit_sequence[i] + if curr_unit in self.unit_to_id: + curr_id = self.unit_to_id[curr_unit] + else: + curr_id = self.unit_to_id[self.unknown_symbol] + + if self.is_bag: + set_matrix[sample_idx][curr_id] += 1 + else: + set_matrix[sample_idx][curr_id] = 1 + + return set_matrix + + +class _SetPostprocessing(BasePostprocessingModule): + """Torchscript-enabled version of postprocessing done by SetFeatureMixin.add_feature_data.""" + + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + self.idx2str = dict(enumerate(metadata["idx2str"])) + self.predictions_key = PREDICTIONS + self.probabilities_key = PROBABILITIES + self.unk = UNKNOWN_SYMBOL + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key) + + inv_preds: list[list[str]] = [] + filtered_probs: list[torch.Tensor] = [] + for sample_idx, sample in enumerate(predictions): + sample_preds: list[str] = [] + pos_sample_idxs: list[int] = [] + pos_class_idxs: list[int] = [] + for class_idx, is_positive in enumerate(sample): + if is_positive == 1: + sample_preds.append(self.idx2str.get(class_idx, self.unk)) + pos_sample_idxs.append(sample_idx) + pos_class_idxs.append(class_idx) + inv_preds.append(sample_preds) + filtered_probs.append(probabilities[pos_sample_idxs, pos_class_idxs]) + + return { + self.predictions_key: inv_preds, + self.probabilities_key: filtered_probs, + } + + +class _SetPredict(PredictModule): + def __init__(self, threshold): + super().__init__() + self.threshold = threshold + + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + probabilities = torch.sigmoid(logits) + + predictions = torch.greater_equal(probabilities, self.threshold) + predictions = predictions.type(torch.int64) + + return {self.predictions_key: predictions, self.probabilities_key: probabilities, self.logits_key: logits} + + +class SetFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return SET + + @staticmethod + def cast_column(column, backend): + return column.astype(str) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + vocabulary = create_vocabulary( + column, + preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + add_special_symbols=False, + processor=backend.df_engine, + ) + return { + "idx2str": vocabulary.vocab, + "str2idx": vocabulary.str2idx, + "str2freq": vocabulary.str2freq, + "vocab_size": len(vocabulary.str2idx), + "max_set_size": vocabulary.max_sequence_length, + } + + @staticmethod + def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend): + def to_dense(x): + feature_vector = set_str_to_idx(x, metadata["str2idx"], preprocessing_parameters["tokenizer"]) + + set_vector = np.zeros((len(metadata["str2idx"]),)) + set_vector[feature_vector] = 1 + return set_vector.astype(np.bool_) + + return backend.df_engine.map_objects(column, to_dense) + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + proc_df[feature_config[PROC_COLUMN]] = SetFeatureMixin.feature_data( + input_df[feature_config[COLUMN]], + metadata[feature_config[NAME]], + preprocessing_parameters, + backend, + ) + return proc_df + + +class SetInputFeature(SetFeatureMixin, InputFeature): + def __init__(self, input_feature_config: SetInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Set feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + _valid_dtypes = (torch.bool, torch.int64, torch.float32) + if inputs.dtype not in _valid_dtypes: + raise ValueError(f"Set feature inputs dtype must be one of {_valid_dtypes}, got {inputs.dtype}.") + + encoder_output = self.encoder_obj(inputs) + + return encoder_output + + @property + def input_dtype(self): + return torch.bool + + @property + def input_shape(self) -> torch.Size: + return torch.Size([len(self.encoder_obj.config.vocab)]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.vocab = feature_metadata["idx2str"] + + @staticmethod + def get_schema_cls(): + return SetInputFeatureConfig + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _SetPreprocessing(metadata) + + +class SetOutputFeature(SetFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: SetOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self.threshold = output_feature_config.threshold + super().__init__(output_feature_config, output_features, **kwargs) + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, **kwargs): # hidden + hidden = inputs[HIDDEN] + return self.decoder_obj(hidden) + + def metric_kwargs(self) -> dict[str, Any]: + return {"threshold": self.threshold} + + def create_predict_module(self) -> PredictModule: + return _SetPredict(self.threshold) + + def get_prediction_set(self): + return {PREDICTIONS, PROBABILITIES, LOGITS} + + @classmethod + def get_output_dtype(cls): + return torch.bool + + @property + def input_shape(self) -> torch.Size: + return self.decoder_obj.input_shape + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.decoder_obj.config.num_classes]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.decoder.num_classes = feature_metadata["vocab_size"] + if isinstance(feature_config.loss.class_weights, (list, tuple)): + if len(feature_config.loss.class_weights) != feature_config.decoder.num_classes: + raise ValueError( + f"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with " + f"the number of classes ({feature_config.decoder.num_classes}) for feature {feature_config.name}. " + "Check the metadata JSON file to see the classes " + "and their order and consider there needs to be a weight " + "for the and class too." + ) + + if isinstance(feature_config.loss.class_weights, dict): + if feature_metadata["str2idx"].keys() != feature_config.loss.class_weights.keys(): + raise ValueError( + f"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with " + f"the classes ({feature_metadata['str2idx'].keys()}) of feature {feature_config.name}. " + "Check the metadata JSON file to see the classes " + "and consider there needs to be a weight " + "for the and class too." + ) + else: + class_weights = feature_config.loss.class_weights + idx2str = feature_metadata["idx2str"] + class_weights_list = [class_weights[s] for s in idx2str] + feature_config.loss.class_weights = class_weights_list + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + # no overall stats, just return empty dictionary + return {} + + def postprocess_predictions( + self, + result, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + if predictions_col in result: + + def idx2str(pred_set): + return [metadata["idx2str"][i] for i, pred in enumerate(pred_set) if pred] + + result[predictions_col] = result[predictions_col].map(idx2str) + + probabilities_col = f"{self.feature_name}_{PROBABILITIES}" + if probabilities_col in result: + + def get_prob(prob_set): + # Cast to float32 because empty np.array objects are np.float64, causing mismatch errors during saving. + return np.array([prob for prob in prob_set if prob >= self.threshold], dtype=np.float32) + + result[probabilities_col] = result[probabilities_col].map(get_prob) + + return result + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _SetPostprocessing(metadata) + + @staticmethod + def get_schema_cls(): + return SetOutputFeatureConfig diff --git a/ludwig/features/text_feature.py b/ludwig/features/text_feature.py new file mode 100644 index 0000000..8ddc4b8 --- /dev/null +++ b/ludwig/features/text_feature.py @@ -0,0 +1,515 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from __future__ import annotations + +import logging +from functools import partial +from typing import TYPE_CHECKING + +import numpy as np +import torch +from torch import Tensor + +if TYPE_CHECKING: + from transformers import PreTrainedTokenizer + +from ludwig.constants import ( + COLUMN, + IGNORE_INDEX_TOKEN_ID, + LAST_PREDICTIONS, + LENGTHS, + NAME, + PREDICTIONS, + PREPROCESSING, + PROBABILITIES, + PROBABILITY, + PROC_COLUMN, + RESPONSE, + TEXT, +) +from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin, OutputFeature +from ludwig.features.feature_utils import compute_sequence_probability, compute_token_probabilities +from ludwig.features.sequence_feature import ( + _SequencePostprocessing, + _SequencePreprocessing, + SequenceInputFeature, + SequenceOutputFeature, +) +from ludwig.modules.metric_registry import get_metric_tensor_input +from ludwig.schema.features.text_feature import TextInputFeatureConfig, TextOutputFeatureConfig +from ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict +from ludwig.utils.math_utils import softmax +from ludwig.utils.strings_utils import ( + build_sequence_matrix, + create_vocabulary, + get_tokenizer, + SpecialSymbol, + UNKNOWN_SYMBOL, + Vocabulary, +) + +logger = logging.getLogger(__name__) + + +def get_decoded_targets_and_predictions( + targets: Tensor, + predictions: dict[str, Tensor], + tokenizer: PreTrainedTokenizer, +) -> tuple[list[str], list[str]]: + """Returns the decoded targets and predictions, accounting for IGNORE_INDEX_TOKEN_ID.""" + # Ensure targets and predictions are on the same device + pred_tensor = predictions[PREDICTIONS] + if targets.device != pred_tensor.device: + targets = targets.to(pred_tensor.device) + sanitized_targets = torch.where(targets != IGNORE_INDEX_TOKEN_ID, targets, tokenizer.pad_token_id) + sanitized_predictions = torch.where( + targets != IGNORE_INDEX_TOKEN_ID, + pred_tensor, + tokenizer.pad_token_id, + ) + decoded_targets = tokenizer.batch_decode(sanitized_targets, skip_special_tokens=True) + decoded_predictions = tokenizer.batch_decode(sanitized_predictions, skip_special_tokens=True) + return decoded_targets, decoded_predictions + + +def _get_metadata_reconciled_max_sequence_length( + preprocessing_parameters: dict, vocabulary: Vocabulary +) -> tuple[int, int]: + """Reconciles the different ways sequence length can be specified in preprocessing parameters. + + If the max sequence length is explicitly specified, we use the minimum of the true maximum sequence length and + the explicitly specified value. If the explicitly specified value is less than the true maximum sequence length, we + log a warning. + + If the max sequence length is not specified, we use the true maximum sequence length. + + Returns: + Tuple(max_sequence_length, sequence_length_99ptile). + """ + # For sequence features with a fixed length specified by `sequence_length`, use this as the max_sequence_length. + if preprocessing_parameters["sequence_length"] is not None: + return preprocessing_parameters["sequence_length"], preprocessing_parameters["sequence_length"] + + # Max sequence length is explicitly set. Use this as the max_sequence_length. + if preprocessing_parameters["max_sequence_length"] is not None: + if preprocessing_parameters["max_sequence_length"] < vocabulary.max_sequence_length: + logger.warning( + f"The max sequence length of the data, {vocabulary.max_sequence_length}, is longer than the max " + f"sequence length set in the config, {preprocessing_parameters['max_sequence_length']}. Note that this " + "will truncate all examples to max_sequence_length=" + f"{preprocessing_parameters['max_sequence_length']}." + ) + return ( + min(vocabulary.max_sequence_length, preprocessing_parameters["max_sequence_length"]), + min(vocabulary.sequence_length_99ptile, preprocessing_parameters["max_sequence_length"]), + ) + + # Max sequence length is None. Use the max sequence length of the data. + return vocabulary.max_sequence_length, vocabulary.sequence_length_99ptile + + +class TextFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return TEXT + + @staticmethod + def cast_column(column, backend): + return column.astype(str) + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + """Returns all metadata for the given text feature. + + Raises: + ValueError, if the tokenized prompt template is longer than the max sequence length. + """ + prompt_template = config.get("prompt", {}).get("template", "") + vocabulary: Vocabulary = create_vocabulary( + column, + tokenizer_type=preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + vocab_file=preprocessing_parameters["vocab_file"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + ngram_size=preprocessing_parameters["ngram_size"], + compute_idf=preprocessing_parameters["compute_idf"], + processor=backend.df_engine, + prompt_template=prompt_template, + ) + # Note: The vocabulary's max_sequence_length includes the prompt template, which is merged into the column prior + # to computing feature metadata. + logger.info( + f"Max length of feature '{column.name}': {vocabulary.max_sequence_length} (without start and stop symbols)" + ) + + max_sequence_length, max_sequence_length_99ptile = _get_metadata_reconciled_max_sequence_length( + preprocessing_parameters, vocabulary + ) + + if is_input_feature and max_sequence_length < vocabulary.prompt_template_num_tokens: + raise ValueError( + f"The input feature's max sequence length ({max_sequence_length}) is shorter than the prompt template " + f"length ({vocabulary.prompt_template_num_tokens}). This will truncate all unique information. " + "Consider making the template shorter or increasing the input feature's max sequence length to a " + f"value >> {vocabulary.prompt_template_num_tokens}." + ) + + logger.info(f"Max sequence length is {max_sequence_length} for feature '{column.name}'") + + return { + "idx2str": vocabulary.vocab, + "str2idx": vocabulary.str2idx, + "str2freq": vocabulary.str2freq, + "str2idf": vocabulary.str2idf, + "vocab_size": len(vocabulary.vocab), + "max_sequence_length": max_sequence_length, + "max_sequence_length_99ptile": max_sequence_length_99ptile, + "pad_idx": vocabulary.pad_idx, + "padding_symbol": vocabulary.padding_symbol, + "unknown_symbol": vocabulary.unknown_symbol, + "prompt_template_num_tokens": vocabulary.prompt_template_num_tokens, + } + + @staticmethod + def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend) -> np.ndarray: + # TODO(1891): Remove backward compatibility hack once all models have been retrained with Ludwig after + # https://github.com/ludwig-ai/ludwig/pull/1859. + prefix = "" + padding_symbol_metadata_key = "padding_symbol" + unknown_symbol_metadata_key = "unknown_symbol" + if "str2idx" not in metadata: + prefix = "word_" + padding_symbol_metadata_key = "word_pad_symbol" + unknown_symbol_metadata_key = "word_unk_symbol" + + # ensure preprocessing param values match the metadata determined from dataset + preprocessing_parameters["padding_symbol"] = metadata[padding_symbol_metadata_key] + preprocessing_parameters["unknown_symbol"] = metadata[unknown_symbol_metadata_key] + if preprocessing_parameters["fill_value"] == UNKNOWN_SYMBOL: + preprocessing_parameters["fill_value"] = preprocessing_parameters["unknown_symbol"] + if ( + "computed_fill_value" in preprocessing_parameters + and preprocessing_parameters["computed_fill_value"] == UNKNOWN_SYMBOL + ): + preprocessing_parameters["computed_fill_value"] = preprocessing_parameters["unknown_symbol"] + + sequences = column + + return build_sequence_matrix( + sequences=sequences, + inverse_vocabulary=metadata[f"{prefix}str2idx"], + tokenizer_type=preprocessing_parameters[f"{prefix}tokenizer"], + length_limit=metadata[f"{prefix}max_sequence_length"], + padding_symbol=metadata[padding_symbol_metadata_key], + padding=preprocessing_parameters["padding"], + unknown_symbol=metadata[unknown_symbol_metadata_key], + lowercase=preprocessing_parameters["lowercase"], + tokenizer_vocab_file=preprocessing_parameters[f"{prefix}vocab_file"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + processor=backend.df_engine, + ) + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + proc_df[feature_config[PROC_COLUMN]] = TextFeatureMixin.feature_data( + input_df[feature_config[COLUMN]], + metadata[feature_config[NAME]], + preprocessing_parameters, + backend, + ) + return proc_df + + +class TextInputFeature(TextFeatureMixin, SequenceInputFeature): + def __init__(self, input_feature_config: TextInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, encoder_obj=encoder_obj, **kwargs) + + def forward(self, inputs, mask=None): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Text feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if len(inputs.shape) != 2: + raise ValueError(f"Text feature inputs must be 2D (batch x seq_len), got shape {tuple(inputs.shape)}.") + + inputs_mask = torch.not_equal(inputs, SpecialSymbol.PADDING.value) + + inputs_exp = inputs.type(torch.int32) + lengths = torch.sum(inputs_mask.type(torch.int32), dim=1) + encoder_output = self.encoder_obj(inputs_exp, mask=inputs_mask) + encoder_output[LENGTHS] = lengths + + return encoder_output + + @property + def input_dtype(self): + return torch.int32 + + @property + def input_shape(self): + return torch.Size([self.encoder_obj.config.max_sequence_length]) + + def update_config_after_module_init(self, feature_config): + feature_config.encoder = self.encoder_obj.config + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.vocab = feature_metadata["idx2str"] + feature_config.encoder.vocab_size = len(feature_metadata["idx2str"]) + feature_config.encoder.max_sequence_length = feature_metadata["max_sequence_length"] + feature_config.encoder.pad_idx = feature_metadata["pad_idx"] + feature_config.encoder.num_tokens = len(feature_metadata["idx2str"]) + feature_config.encoder.str2freq = feature_metadata["str2freq"] + feature_config.encoder.str2idf = feature_metadata["str2idf"] + feature_config.encoder.skip = feature_metadata[PREPROCESSING].get("cache_encoder_embeddings", False) + + @staticmethod + def get_schema_cls(): + return TextInputFeatureConfig + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _SequencePreprocessing(metadata) + + +class TextOutputFeature(TextFeatureMixin, SequenceOutputFeature): + def __init__( + self, + output_feature_config: TextOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + super().__init__(output_feature_config, output_features, **kwargs) + + @classmethod + def get_output_dtype(cls): + return torch.int32 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.decoder_obj.config.max_sequence_length]) + + def update_metrics( + self, + targets: Tensor, + predictions: dict[str, Tensor], + tokenizer: PreTrainedTokenizer | None = None, + ) -> None: + """Updates metrics with the given targets and predictions. + + If decoded_targets and decoded_predictions are provided, as through LLM model types, then additional + response-based metrics like BLEU and ROUGE are also computed. + + Args: + targets: Tensor with target values for this output feature. + predictions: Dict of tensors returned by predictions(). + """ + if tokenizer is not None: + # Decode the targets and predictions to compute response-based metrics using the initialized tokenizer. + decoded_targets, decoded_predictions = get_decoded_targets_and_predictions(targets, predictions, tokenizer) + + for metric_name, metric_fn in self._metric_functions.items(): + prediction_key = get_metric_tensor_input(metric_name) + try: + if prediction_key == RESPONSE: + if tokenizer is not None: + # RESPONSE metrics cannot be computed if decoded texts are not provided. + # Decoded texts are only provided using the LLM model type. + if decoded_targets is not None and decoded_predictions is not None: + # Move metric function to the device of the predictions. + # For CUDA, it can be computed on any of the GPUs since it uses allgather to collect + # the results from all GPUs and compute the final metric. + # We use 'predictions' as the key since it is always present in the predictions dict. + device = "cuda" if predictions["predictions"].is_cuda else "cpu" + metric_fn = metric_fn.to(device) + if metric_name == "bleu": + # BLEU takes in targets as a list. + metric_fn.update(decoded_predictions, [decoded_targets]) + else: + metric_fn.update(decoded_predictions, decoded_targets) + else: + metric_fn = metric_fn.to(predictions[prediction_key].device) + metric_fn.update(predictions[prediction_key].detach(), targets) + except Exception: + logger.warning(f"Ran into error when calculating metric {metric_name}. Skipping.", exc_info=True) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.decoder.vocab_size = feature_metadata["vocab_size"] + feature_config.decoder.max_sequence_length = feature_metadata["max_sequence_length"] + if isinstance(feature_config.loss.class_weights, (list, tuple)): + # [0, 0] for UNK and PAD + feature_config.loss.class_weights = [0, 0] + feature_config.loss.class_weights + if len(feature_config.loss.class_weights) != feature_config.decoder.vocab_size: + raise ValueError( + f"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with " + f"the number of classes ({feature_config.decoder.vocab_size})" + ) + + if isinstance(feature_config.loss.class_weights, dict): + if feature_metadata["str2idx"].keys() != feature_config.loss.class_weights.keys(): + raise ValueError( + f"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with " + f"the classes ({feature_metadata['str2idx'].keys()}) of feature {feature_config.column}. " + "Check the metadata JSON file to see the classes " + "and consider there needs to be a weight " + "for the class too." + ) + else: + class_weights = feature_config.loss.class_weights + idx2str = feature_metadata["idx2str"] + class_weights_list = [class_weights[s] for s in idx2str] + feature_config.loss.class_weights = class_weights_list + + if feature_config.loss.class_similarities_temperature > 0: + if feature_config.class_similarities: + distances = feature_config.class_similarities + temperature = feature_config.loss.class_similarities_temperature + for i in range(len(distances)): + distances[i, :] = softmax(distances[i, :], temperature=temperature) + feature_config.loss.class_similarities = distances + else: + raise ValueError( + "class_similarities_temperature > 0," + "but no class similarities are provided " + f"for feature {feature_config.column}" + ) + + @staticmethod + def calculate_overall_stats( + predictions, + targets, + train_set_metadata, + ): + return {} + + def postprocess_predictions( + self, + result, + metadata, + ): + # todo: refactor to reuse SequenceOutputFeature.postprocess_predictions + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + + tokenizer = None + if metadata["preprocessing"]["tokenizer"] == "hf_tokenizer": + tokenizer = get_tokenizer( + metadata["preprocessing"]["tokenizer"], + metadata["preprocessing"]["vocab_file"], + metadata["preprocessing"]["pretrained_model_name_or_path"], + ) + + if predictions_col in result: + token_col = result[predictions_col] + + def idx2str(pred): + if tokenizer is None: + return [ + metadata["idx2str"][token] if token < len(metadata["idx2str"]) else UNKNOWN_SYMBOL + for token in pred + ] + # Decode each token ID individually. In transformers 5.x, batch_decode + # on a 1D array treats it as a single sequence rather than individual tokens. + return [tokenizer.tokenizer.decode([int(token_id)], skip_special_tokens=True) for token_id in pred] + + result[predictions_col] = token_col.map(idx2str) + + # Add additional response column that represents the predicted text output + # as a single string instead of a list of tokens. + def idx2response(pred): + if tokenizer is None: + # This works because we treat each word as a token. + return " ".join( + [ + metadata["idx2str"][token] if token < len(metadata["idx2str"]) else UNKNOWN_SYMBOL + for token in pred + ] + ) + return tokenizer.tokenizer.decode(pred, skip_special_tokens=True) + + result[f"{self.feature_name}_response"] = token_col.map(idx2response) + + last_preds_col = f"{self.feature_name}_{LAST_PREDICTIONS}" + if last_preds_col in result: + + def last_idx2str(last_pred): + if last_pred < len(metadata["idx2str"]): + return metadata["idx2str"][last_pred] + return UNKNOWN_SYMBOL + + result[last_preds_col] = result[last_preds_col].map(last_idx2str) + + probs_col = f"{self.feature_name}_{PROBABILITIES}" + prob_col = f"{self.feature_name}_{PROBABILITY}" + + # "Summarizes" the `result`'s probability-related output: + # - result[probs_col]: + # Each row is now a list of "max" probabilities. Each element is the probability of the argmax token for + # the given time step. + # + # Note that we intentionally do not return full list of probabilties for each time step because the output + # of postprocess_predictions is saved to disk and the full probability distribution can be huge, + # especially for large vocab sizes: + # dataset_size x sequence_length x vocab_size + # + # TODO: Add a mechanism that lets the user save the full probability distribution if they want. + # - result[prob_col]: + # Each row is the overall probability of the sequence. This is the product of the max probabilities over + # all time steps. + if probs_col in result: + # result[probs_col]: From PredictModule, each row has a list of size (sequence_length) of a list of + # probabiltiies of (vocab_size). compute_token_probabilities gets the maximum probability per timestep. + result[probs_col] = result[probs_col].map(compute_token_probabilities) + result[prob_col] = result[probs_col].map( + partial( + compute_sequence_probability, + max_sequence_length=metadata["max_sequence_length"], + return_log_prob=True, + ), + ) + + lengths_col = f"{self.feature_name}_{LENGTHS}" + if lengths_col in result: + del result[lengths_col] + + return result + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _SequencePostprocessing(metadata) + + @staticmethod + def get_schema_cls(): + return TextOutputFeatureConfig diff --git a/ludwig/features/timeseries_feature.py b/ludwig/features/timeseries_feature.py new file mode 100644 index 0000000..c157ccf --- /dev/null +++ b/ludwig/features/timeseries_feature.py @@ -0,0 +1,381 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from typing import TYPE_CHECKING + +import numpy as np +import torch + +from ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROC_COLUMN, TIMESERIES +from ludwig.features.base_feature import ( + BasePreprocessingModule, + FeaturePreprocessingMixin, + OutputFeature, + PredictModule, +) +from ludwig.features.sequence_feature import SequenceInputFeature +from ludwig.features.vector_feature import _VectorPostprocessing, _VectorPredict +from ludwig.schema.features.timeseries_feature import TimeseriesInputFeatureConfig, TimeseriesOutputFeatureConfig +from ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict +from ludwig.utils.tokenizers import get_tokenizer_from_registry, TORCHSCRIPT_COMPATIBLE_TOKENIZERS +from ludwig.utils.types import PreprocessingInput, Series + +if TYPE_CHECKING: + from ludwig.backend.base import Backend + +logger = logging.getLogger(__name__) + + +def incremental_time_delay_embedding( + new_value: float, + previous_embedding: np.ndarray, + window_size: int, + padding_value: float, +) -> np.ndarray: + """Compute the next time-delay embedding incrementally given the previous one. + + The new embedding is: previous_embedding[1:] concatenated with [new_value], padded if needed. + This avoids re-running create_time_delay_embedding over the full window for a single new observation. + + Args: + new_value: The new scalar observation to append. + previous_embedding: The embedding from the previous step, shape [window_size]. + window_size: Expected length of the output embedding. + padding_value: Fill value used when previous_embedding is shorter than window_size - 1. + + Returns: + New embedding of shape [window_size] as float32. + """ + if len(previous_embedding) >= window_size - 1: + window = np.concatenate([previous_embedding[-(window_size - 1) :], [new_value]]) + else: + pad = np.full(window_size - 1 - len(previous_embedding), padding_value) + window = np.concatenate([pad, previous_embedding, [new_value]]) + return window.astype(np.float32) + + +def create_time_delay_embedding( + series: Series, window_size: int, horizon: int, padding_value: int, backend: "Backend" +) -> Series: + """Time delay embedding from: + + https://towardsdatascience.com/machine-learning-for-forecasting-transformations-and-feature-extraction-bbbea9de0ac2 + + Args: + series: Column-major timeseries data. + window_size: Size of the lookback sliding window for timeseries inputs. + horizon: Size of the forward-looking horizon for timeseries outputs. + padding_value: Value to pad out the window when there is not enough data around the observation. + + Returns: + A column of timeseries window arrays in row-major format for training. + """ + # Replace default fill value of "" with nan as we will be assuming numeric values here + series = series.replace("", np.nan) + + # Create the list of shifts we want to perform over the series. + # For backwards looking shifts, we want to include the current element, while for forward looking shifts we do not. + # Example: + # window_size=3, horizon=0 --> shift_offsets=[2, 1, 0] + # window_size=0, horizon=2 --> shift_offsets=[-1, -2] + shift_offsets = list(range(window_size - 1, -(horizon + 1), -1)) + shifts = [series.shift(i) for i in shift_offsets] + df = backend.df_engine.df_lib.concat(shifts, axis=1) + df.columns = [f"__tmp_column_{j}" for j in shift_offsets] + return df.apply(lambda x: np.nan_to_num(np.array(x.tolist()).astype(np.float32), nan=padding_value), axis=1) + + +class _TimeseriesPreprocessing(BasePreprocessingModule): + """Torchscript-enabled version of preprocessing done by TimeseriesFeatureMixin.add_feature_data.""" + + def __init__(self, metadata: TrainingSetMetadataDict): + super().__init__() + if metadata["preprocessing"]["tokenizer"] not in TORCHSCRIPT_COMPATIBLE_TOKENIZERS: + raise ValueError( + f"{metadata['preprocessing']['tokenizer']} is not supported by torchscript. Please use " + f"one of {TORCHSCRIPT_COMPATIBLE_TOKENIZERS}." + ) + self.tokenizer = get_tokenizer_from_registry(metadata["preprocessing"]["tokenizer"])() + self.padding = metadata["preprocessing"]["padding"] + self.padding_value = float(metadata["preprocessing"]["padding_value"]) + self.max_timeseries_length = int(metadata["max_timeseries_length"]) + self.computed_fill_value = metadata["preprocessing"]["computed_fill_value"] + + def _process_str_sequence(self, sequence: list[str], limit: int) -> torch.Tensor: + float_sequence = [float(s) for s in sequence[:limit]] + return torch.tensor(float_sequence) + + def _nan_to_fill_value(self, v: torch.Tensor) -> torch.Tensor: + if v.isnan().any(): + tokenized_fill_value = self.tokenizer(self.computed_fill_value) + # refines type of sequences from Any to List[str] + assert torch.jit.isinstance(tokenized_fill_value, list[str]) + return self._process_str_sequence(tokenized_fill_value, self.max_timeseries_length) + return v + + def forward_list_of_tensors(self, v: list[torch.Tensor]) -> torch.Tensor: + v = [self._nan_to_fill_value(v_i) for v_i in v] + + if self.padding == "right": + timeseries_matrix = torch.nn.utils.rnn.pad_sequence(v, batch_first=True, padding_value=self.padding_value) + timeseries_matrix = timeseries_matrix[:, : self.max_timeseries_length] + else: + reversed_timeseries = [torch.flip(v_i[: self.max_timeseries_length], dims=(0,)) for v_i in v] + reversed_timeseries_padded = torch.nn.utils.rnn.pad_sequence( + reversed_timeseries, batch_first=True, padding_value=self.padding_value + ) + timeseries_matrix = torch.flip(reversed_timeseries_padded, dims=(1,)) + return timeseries_matrix + + def forward_list_of_strs(self, v: list[str]) -> torch.Tensor: + v = [self.computed_fill_value if s == "nan" else s for s in v] + + sequences = self.tokenizer(v) + # refines type of sequences from Any to List[List[str]] + assert torch.jit.isinstance(sequences, list[list[str]]), "sequences is not a list of lists." + + timeseries_matrix = torch.full( + [len(sequences), self.max_timeseries_length], self.padding_value, dtype=torch.float32 + ) + for sample_idx, str_sequence in enumerate(sequences): + limit = min(len(str_sequence), self.max_timeseries_length) + float_sequence = self._process_str_sequence(str_sequence, limit) + if self.padding == "right": + timeseries_matrix[sample_idx][:limit] = float_sequence + else: # if self.padding == 'left + timeseries_matrix[sample_idx][self.max_timeseries_length - limit :] = float_sequence + return timeseries_matrix + + def forward(self, v: PreprocessingInput) -> torch.Tensor: + """Takes a list of float values and creates a padded torch.Tensor.""" + if torch.jit.isinstance(v, list[torch.Tensor]): + return self.forward_list_of_tensors(v) + if torch.jit.isinstance(v, list[str]): + return self.forward_list_of_strs(v) + raise ValueError(f"Unsupported input: {v}") + + +class TimeseriesFeatureMixin(FeaturePreprocessingMixin): + @staticmethod + def type(): + return TIMESERIES + + @staticmethod + def cast_column(column, backend): + return column + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + window_size = preprocessing_parameters.get("window_size", 0) or preprocessing_parameters.get("horizon", 0) + if window_size > 0: + # Column-major data + return {"max_timeseries_length": window_size} + + column = column.astype(str) + tokenizer = get_tokenizer_from_registry(preprocessing_parameters["tokenizer"])() + max_length = 0 + for timeseries in column: + processed_line = tokenizer(timeseries) + max_length = max(max_length, len(processed_line)) + max_length = min(preprocessing_parameters["timeseries_length_limit"], max_length) + + return {"max_timeseries_length": max_length} + + @staticmethod + def build_matrix(timeseries, tokenizer_name, length_limit, padding_value, padding, backend): + tokenizer = get_tokenizer_from_registry(tokenizer_name)() + + ts_vectors = backend.df_engine.map_objects( + timeseries, lambda ts: np.nan_to_num(np.array(tokenizer(ts)).astype(np.float32), nan=padding_value) + ) + + max_length = backend.df_engine.compute( + backend.df_engine.map_objects(ts_vectors, len, meta=(ts_vectors.name, int)).max() + ) + if max_length < length_limit: + logger.debug(f"max length of {tokenizer_name}: {max_length} < limit: {length_limit}") + max_length = length_limit + + def pad(vector): + padded = np.full((max_length,), padding_value, dtype=np.float32) + limit = min(vector.shape[0], max_length) + if padding == "right": + padded[:limit] = vector[:limit] + else: # if padding == 'left + padded[max_length - limit :] = vector[:limit] + return padded + + return backend.df_engine.map_objects(ts_vectors, pad) + + @staticmethod + def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend): + padding_value = preprocessing_parameters["padding_value"] + + window_size = preprocessing_parameters.get("window_size", 0) + horizon = preprocessing_parameters.get("horizon", 0) + if window_size > 0 or horizon > 0: + # Column-major data. Convert the column into the row-major embedding + return create_time_delay_embedding(column, window_size, horizon, padding_value, backend) + + timeseries_data = TimeseriesFeatureMixin.build_matrix( + column, + preprocessing_parameters["tokenizer"], + metadata["max_timeseries_length"], + padding_value, + preprocessing_parameters["padding"], + backend, + ) + return timeseries_data + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + proc_df[feature_config[PROC_COLUMN]] = TimeseriesFeatureMixin.feature_data( + input_df[feature_config[COLUMN]].astype(str), + metadata[feature_config[NAME]], + preprocessing_parameters, + backend, + ) + return proc_df + + +class TimeseriesInputFeature(TimeseriesFeatureMixin, SequenceInputFeature): + def __init__(self, input_feature_config: TimeseriesInputFeatureConfig, encoder_obj=None, **kwargs): + # add required sequence encoder parameters for time series + input_feature_config.encoder.embedding_size = 1 + input_feature_config.encoder.should_embed = False + + # SequenceInputFeauture's constructor initializes the encoder. + super().__init__(input_feature_config, encoder_obj=encoder_obj, **kwargs) + + def forward(self, inputs, mask=None): + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Timeseries feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + _valid_dtypes = (torch.float16, torch.float32, torch.float64) + if inputs.dtype not in _valid_dtypes: + raise ValueError(f"Timeseries feature inputs dtype must be a float type, got {inputs.dtype}.") + if len(inputs.shape) != 2: + raise ValueError(f"Timeseries feature inputs must be 2D, got shape {tuple(inputs.shape)}.") + + inputs_exp = inputs.type(torch.float32) + encoder_output = self.encoder_obj(inputs_exp, mask=mask) + + return encoder_output + + @property + def input_shape(self) -> torch.Size: + return self.encoder_obj.input_shape + + @property + def input_dtype(self): + return torch.float32 + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.input_size = feature_metadata["max_timeseries_length"] + feature_config.encoder.max_sequence_length = feature_metadata["max_timeseries_length"] + + @staticmethod + def get_schema_cls(): + return TimeseriesInputFeatureConfig + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _TimeseriesPreprocessing(metadata) + + +class TimeseriesOutputFeature(TimeseriesFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: TimeseriesOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self.horizon = output_feature_config.horizon + super().__init__(output_feature_config, output_features, **kwargs) + output_feature_config.decoder.output_size = self.horizon + + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, **kwargs): # hidden + hidden = inputs[HIDDEN] + return self.decoder_obj(hidden) + + def loss_kwargs(self): + return self.loss.to_dict() + + def metric_kwargs(self): + return {"num_outputs": self.output_shape[0]} + + def create_predict_module(self) -> PredictModule: + return _VectorPredict() + + def get_prediction_set(self): + return {PREDICTIONS, LOGITS} + + @classmethod + def get_output_dtype(cls): + return torch.float32 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.horizon]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.horizon = feature_metadata["max_timeseries_length"] + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + # no overall stats, just return empty dictionary + return {} + + def postprocess_predictions( + self, + result, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + if predictions_col in result: + result[predictions_col] = result[predictions_col].map(lambda pred: pred.tolist()) + return result + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _VectorPostprocessing() + + @staticmethod + def get_schema_cls(): + return TimeseriesOutputFeatureConfig diff --git a/ludwig/features/transforms.py b/ludwig/features/transforms.py new file mode 100644 index 0000000..9434eed --- /dev/null +++ b/ludwig/features/transforms.py @@ -0,0 +1,50 @@ +"""Feature transform protocol for Ludwig's lazy preprocessing pipeline. + +All feature-specific data transforms should extend ``FeatureTransform`` so they +are composable, testable, and can be moved to GPU when that becomes beneficial. +""" + +from __future__ import annotations + +from abc import abstractmethod + +import torch +import torch.nn as nn + + +class FeatureTransform(nn.Module): + """Base class for all per-sample feature transforms. + + Subclasses implement ``forward(x: Tensor) -> Tensor``. They are + ``nn.Module`` subclasses so they can be: + + * Composed with ``nn.Sequential`` + * Saved / loaded with ``torch.save`` / ``torch.load`` + * Moved to GPU with ``.to(device)`` + * JIT-compiled with ``torch.jit.script`` (where supported) + """ + + @abstractmethod + def forward(self, x: torch.Tensor) -> torch.Tensor: + raise NotImplementedError + + +class IdentityTransform(FeatureTransform): + """Pass-through — useful as a placeholder or in tests.""" + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x + + +class NormalizationTransform(FeatureTransform): + """Subtract mean, divide by std. Both are registered as buffers so the + transform travels correctly through ``torch.save`` / ``.to(device)``.""" + + def __init__(self, mean: float, std: float) -> None: + super().__init__() + self.register_buffer("mean", torch.tensor(mean, dtype=torch.float32)) + self.register_buffer("std", torch.tensor(std, dtype=torch.float32)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + std = self.std.clamp(min=1e-8) + return (x - self.mean) / std diff --git a/ludwig/features/vector_feature.py b/ludwig/features/vector_feature.py new file mode 100644 index 0000000..80b0c67 --- /dev/null +++ b/ludwig/features/vector_feature.py @@ -0,0 +1,259 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import numpy as np +import torch + +from ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROC_COLUMN, VECTOR +from ludwig.features.base_feature import ( + BasePostprocessingModule, + BasePreprocessingModule, + InputFeature, + OutputFeature, + PredictModule, +) +from ludwig.schema.features.vector_feature import VectorInputFeatureConfig, VectorOutputFeatureConfig +from ludwig.types import ( + FeatureMetadataDict, + FeaturePostProcessingOutputDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils import output_feature_utils +from ludwig.utils.types import PreprocessingInput + +logger = logging.getLogger(__name__) + + +class _VectorPreprocessing(BasePreprocessingModule): + def forward(self, v: PreprocessingInput) -> torch.Tensor: + if torch.jit.isinstance(v, torch.Tensor): + out = v + elif torch.jit.isinstance(v, list[torch.Tensor]): + out = torch.stack(v) + elif torch.jit.isinstance(v, list[str]): + vectors = [] + for sample in v: + vector = torch.tensor([float(x) for x in sample.split()], dtype=torch.float32) + vectors.append(vector) + out = torch.stack(vectors) + else: + raise ValueError(f"Unsupported input: {v}") + + if out.isnan().any(): + raise ValueError("Scripted NaN handling not implemented for Vector feature") + return out + + +class _VectorPostprocessing(BasePostprocessingModule): + def __init__(self): + super().__init__() + self.predictions_key = PREDICTIONS + self.logits_key = LOGITS + + def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict: + predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key) + logits = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.logits_key) + + return {self.predictions_key: predictions, self.logits_key: logits} + + +class _VectorPredict(PredictModule): + def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]: + logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key) + + return {self.predictions_key: logits, self.logits_key: logits} + + +class VectorFeatureMixin: + @staticmethod + def type(): + return VECTOR + + @staticmethod + def cast_column(column, backend): + return column + + @staticmethod + def get_feature_meta( + config: ModelConfigDict, + column, + preprocessing_parameters: PreprocessingConfigDict, + backend, + is_input_feature: bool, + ) -> FeatureMetadataDict: + return {"preprocessing": preprocessing_parameters} + + @staticmethod + def add_feature_data( + feature_config, + input_df, + proc_df, + metadata, + preprocessing_parameters: PreprocessingConfigDict, + backend, + skip_save_processed_input, + ): + """Expects all the vectors to be of the same size. + + The vectors need to be whitespace delimited strings. Missing values are not handled. + """ + if len(input_df[feature_config[COLUMN]]) == 0: + raise ValueError("There are no vectors in the dataset provided") + + # Convert the string of features into a numpy array + try: + proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_objects( + input_df[feature_config[COLUMN]], lambda x: np.array(x.split(), dtype=np.float32) + ) + except ValueError: + logger.error( + "Unable to read the vector data. Make sure that all the vectors" + " are of the same size and do not have missing/null values." + ) + raise + + # Determine vector size + _col = proc_df[feature_config[PROC_COLUMN]] + vector_size = backend.df_engine.compute(backend.df_engine.map_objects(_col, len, meta=(_col.name, int)).max()) + vector_size_param = preprocessing_parameters.get("vector_size") + if vector_size_param is not None: + # TODO(travis): do we even need a user param for vector size if we're going to auto-infer it in all + # cases? Is this only useful as a sanity check for the user to make sure their data conforms to + # expectations? + if vector_size != vector_size_param: + raise ValueError( + f"The user provided value for vector size ({preprocessing_parameters}) does not " + f"match the value observed in the data: {vector_size}" + ) + else: + logger.debug(f"Detected vector size: {vector_size}") + + metadata[feature_config[NAME]]["vector_size"] = vector_size + return proc_df + + +class VectorInputFeature(VectorFeatureMixin, InputFeature): + def __init__(self, input_feature_config: VectorInputFeatureConfig, encoder_obj=None, **kwargs): + super().__init__(input_feature_config, **kwargs) + + # input_feature_config.encoder.input_size = input_feature_config.encoder.vector_size + if encoder_obj: + self.encoder_obj = encoder_obj + else: + self.encoder_obj = self.initialize_encoder(input_feature_config.encoder) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + if not isinstance(inputs, torch.Tensor): + raise TypeError(f"Vector feature forward expects a torch.Tensor, got {type(inputs).__name__}.") + if inputs.dtype not in (torch.float32, torch.float64): + raise ValueError(f"Vector feature inputs dtype must be float32 or float64, got {inputs.dtype}.") + if len(inputs.shape) != 2: + raise ValueError(f"Vector feature inputs must be 2D, got shape {tuple(inputs.shape)}.") + + inputs_encoded = self.encoder_obj(inputs) + + return inputs_encoded + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.encoder_obj.config.input_size]) + + @property + def output_shape(self) -> torch.Size: + return self.encoder_obj.output_shape + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.encoder.input_size = feature_metadata["vector_size"] + + @staticmethod + def create_preproc_module(metadata: TrainingSetMetadataDict) -> BasePreprocessingModule: + return _VectorPreprocessing() + + @staticmethod + def get_schema_cls(): + return VectorInputFeatureConfig + + +class VectorOutputFeature(VectorFeatureMixin, OutputFeature): + def __init__( + self, + output_feature_config: VectorOutputFeatureConfig | dict, + output_features: dict[str, OutputFeature], + **kwargs, + ): + self.vector_size = output_feature_config.vector_size + super().__init__(output_feature_config, output_features, **kwargs) + output_feature_config.decoder.output_size = self.vector_size + + self.decoder_obj = self.initialize_decoder(output_feature_config.decoder) + self._setup_loss() + self._setup_metrics() + + def logits(self, inputs, **kwargs): # hidden + hidden = inputs[HIDDEN] + return self.decoder_obj(hidden) + + def metric_kwargs(self): + return {"num_outputs": self.output_shape[0]} + + def create_predict_module(self) -> PredictModule: + return _VectorPredict() + + def get_prediction_set(self): + return {PREDICTIONS, LOGITS} + + @classmethod + def get_output_dtype(cls): + return torch.float32 + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.vector_size]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @staticmethod + def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs): + feature_config.vector_size = feature_metadata["vector_size"] + + @staticmethod + def calculate_overall_stats(predictions, targets, train_set_metadata): + # no overall stats, just return empty dictionary + return {} + + def postprocess_predictions( + self, + result, + metadata, + ): + predictions_col = f"{self.feature_name}_{PREDICTIONS}" + if predictions_col in result: + result[predictions_col] = result[predictions_col].map(lambda pred: pred.tolist()) + return result + + @staticmethod + def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module: + return _VectorPostprocessing() + + @staticmethod + def get_schema_cls(): + return VectorOutputFeatureConfig diff --git a/ludwig/forecast.py b/ludwig/forecast.py new file mode 100644 index 0000000..415e87e --- /dev/null +++ b/ludwig/forecast.py @@ -0,0 +1,157 @@ +import argparse +import logging +import sys + +import pandas as pd + +from ludwig.api import LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + + +def forecast_cli( + model_path: str, + dataset: str | dict | pd.DataFrame = None, + data_format: str | None = None, + horizon: int = 1, + output_directory: str | None = None, + output_format: str = "parquet", + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + logging_level: int = logging.INFO, + **kwargs, +) -> None: + """Loads pre-trained model to forecast on the provided dataset. + + Args: + model_path: Filepath to pre-trained model. + dataset: Source containing the entire dataset to be used in the prediction. + data_format: Format to interpret data sources. Will be inferred automatically if not specified. + horizon: How many samples into the future to forecast. + output_directory: The directory that will contain the forecasted values. + output_format: Format of the output dataset. + callbacks: A list of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline. + backend: Backend or string name of backend to use to execute preprocessing / training steps. + logging_level: Log level that will be sent to stderr. + """ + model = LudwigModel.load( + model_path, + logging_level=logging_level, + backend=backend, + callbacks=callbacks, + ) + model.forecast( + dataset=dataset, + data_format=data_format, + horizon=horizon, + output_directory=output_directory, + output_format=output_format, + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script loads a pretrained model and uses it to forecast", + prog="ludwig forecast", + usage="%(prog)s [options]", + ) + + parser.add_argument( + "-n", "--horizon", help="horizon, or number of steps in the future to forecast", type=int, default=1 + ) + + # --------------- + # Data parameters + # --------------- + parser.add_argument("--dataset", help="input data file path", required=True) + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "html", + "tables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=True) + + # ------------------------- + # Output results parameters + # ------------------------- + parser.add_argument( + "-od", "--output_directory", type=str, default="results", help="directory that contains the results" + ) + + parser.add_argument( + "-of", + "--output_format", + help="format to write the output dataset", + default="parquet", + choices=[ + "csv", + "parquet", + ], + ) + + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("forecast", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.forecast") + + args.backend = initialize_backend(args.backend) + if args.backend.is_coordinator(): + print_ludwig("Forecast", LUDWIG_VERSION) + logger.info(f"Dataset path: {args.dataset}") + logger.info(f"Model path: {args.model_path}") + logger.info("") + + forecast_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/globals.py b/ludwig/globals.py new file mode 100644 index 0000000..77484c7 --- /dev/null +++ b/ludwig/globals.py @@ -0,0 +1,59 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +LUDWIG_VERSION = "0.17.7" + +MODEL_FILE_NAME = "model" +MODEL_WEIGHTS_FILE_NAME = "model_weights" # legacy pickle format +MODEL_WEIGHTS_SAFETENSORS_FILE_NAME = "model_weights.safetensors" +MODEL_HYPERPARAMETERS_FILE_NAME = "model_hyperparameters.json" +TRAIN_SET_METADATA_FILE_NAME = "training_set_metadata.json" +TRAINING_PROGRESS_TRACKER_FILE_NAME = "training_progress.json" +TRAINING_CHECKPOINTS_DIR_PATH = "training_checkpoints" + +TEST_STATISTICS_FILE_NAME = "test_statistics.json" + +DESCRIPTION_FILE_NAME = "description.json" + +PREDICTIONS_PARQUET_FILE_NAME = "predictions.parquet" +PREDICTIONS_SHAPES_FILE_NAME = "predictions.shapes.json" + +TRAINING_PREPROC_FILE_NAME = "training.parquet" + +HYPEROPT_STATISTICS_FILE_NAME = "hyperopt_statistics.json" + +CONFIG_YAML = "config.yaml" + + +def model_weights_exist(directory): + """Check if model weights exist in either SafeTensors or legacy pickle format.""" + import os + + return os.path.exists(os.path.join(directory, MODEL_WEIGHTS_SAFETENSORS_FILE_NAME)) or os.path.exists( + os.path.join(directory, MODEL_WEIGHTS_FILE_NAME) + ) + + +DISABLE_PROGRESSBAR = False + + +def set_disable_progressbar(value): + global DISABLE_PROGRESSBAR + DISABLE_PROGRESSBAR = value + + +def is_progressbar_disabled(): + return DISABLE_PROGRESSBAR diff --git a/ludwig/hyperopt/__init__.py b/ludwig/hyperopt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/hyperopt/execution.py b/ludwig/hyperopt/execution.py new file mode 100644 index 0000000..ea9e3a3 --- /dev/null +++ b/ludwig/hyperopt/execution.py @@ -0,0 +1,1225 @@ +import contextlib +import copy +import datetime +import glob +import json +import logging +import os +import shutil +import sys +import tempfile +import threading +import time +import traceback +import uuid +from collections.abc import Callable +from functools import lru_cache +from inspect import signature +from pathlib import Path +from typing import Any + +try: + import ray + from ray import tune + from ray.tune import ExperimentAnalysis, PlacementGroupFactory, register_trainable, Stopper + from ray.tune.schedulers.resource_changing_scheduler import DistributeResources, ResourceChangingScheduler + from ray.tune.search import BasicVariantGenerator, ConcurrencyLimiter, SEARCH_ALG_IMPORT + from ray.tune.utils import wait_for_gpu + from ray.util.queue import Queue as RayQueue + + _RAY_AVAILABLE = True +except ImportError: + _RAY_AVAILABLE = False + ray = None # type: ignore[assignment] + +from ludwig.api import LudwigModel +from ludwig.backend import initialize_backend, RAY + +try: + from ludwig.backend.ray import initialize_ray +except ImportError: + initialize_ray = None # type: ignore[assignment] +from ludwig.callbacks import Callback +from ludwig.constants import MAXIMIZE, TEST, TRAINER, TRAINING, TYPE, VALIDATION +from ludwig.hyperopt.results import HyperoptResults, TrialResults +from ludwig.hyperopt.search_algos import get_search_algorithm +from ludwig.hyperopt.utils import load_json_values, substitute_parameters +from ludwig.modules.metric_modules import get_best_function +from ludwig.schema.model_types.utils import merge_with_defaults +from ludwig.utils import metric_utils +from ludwig.utils.data_utils import hash_dict, NumpyEncoder +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import has_remote_protocol, safe_move_file +from ludwig.utils.misc_utils import get_from_registry + +logger = logging.getLogger(__name__) + + +def _patch_bohb_configspace_conversion(): + """Monkey-patch TuneBOHB.convert_search_space for ConfigSpace 1.x compatibility. + + ConfigSpace 1.x removed the `q` (quantization) parameter from hyperparameter classes. + Ray Tune's BOHB integration still passes `q=...`, so we patch the converter to drop it. + """ + try: + # Check if ConfigSpace 1.x (no 'q' parameter) + import inspect + import math + + import ConfigSpace + from ray.tune.search.bohb.bohb_search import TuneBOHB + from ray.tune.search.sample import Categorical, Float, Integer, LogUniform, Normal, Quantized, Uniform + from ray.tune.search.variant_generator import parse_spec_vars + from ray.tune.utils import flatten_dict + + sig = inspect.signature(ConfigSpace.UniformFloatHyperparameter.__init__) + if "q" in sig.parameters: + return # Old ConfigSpace, no patching needed + + @staticmethod + def convert_search_space(spec): + resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec) + if grid_vars: + raise ValueError("Grid search parameters cannot be automatically converted to a TuneBOHB search space.") + spec = flatten_dict(spec, prevent_delimiter=True) + resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec) + + def resolve_value(par, domain): + quantize = None + sampler = domain.get_sampler() + if isinstance(sampler, Quantized): + quantize = sampler.q + sampler = sampler.sampler + + if isinstance(domain, Float): + if isinstance(sampler, LogUniform): + lower = domain.lower + upper = domain.upper + if quantize: + lower = math.ceil(domain.lower / quantize) * quantize + upper = math.floor(domain.upper / quantize) * quantize + return ConfigSpace.UniformFloatHyperparameter(par, lower=lower, upper=upper, log=True) + elif isinstance(sampler, Uniform): + lower = domain.lower + upper = domain.upper + if quantize: + lower = math.ceil(domain.lower / quantize) * quantize + upper = math.floor(domain.upper / quantize) * quantize + return ConfigSpace.UniformFloatHyperparameter(par, lower=lower, upper=upper, log=False) + elif isinstance(sampler, Normal): + return ConfigSpace.hyperparameters.NormalFloatHyperparameter( + par, mu=sampler.mean, sigma=sampler.sd, log=False + ) + elif isinstance(domain, Integer): + if isinstance(sampler, LogUniform): + lower = domain.lower + upper = domain.upper + if quantize: + lower = math.ceil(domain.lower / quantize) * quantize + upper = math.floor(domain.upper / quantize) * quantize + else: + upper -= 1 + return ConfigSpace.UniformIntegerHyperparameter(par, lower=lower, upper=upper, log=True) + elif isinstance(sampler, Uniform): + lower = domain.lower + upper = domain.upper + if quantize: + lower = math.ceil(domain.lower / quantize) * quantize + upper = math.floor(domain.upper / quantize) * quantize + else: + upper -= 1 + return ConfigSpace.UniformIntegerHyperparameter(par, lower=lower, upper=upper, log=False) + elif isinstance(domain, Categorical): + if isinstance(sampler, Uniform): + return ConfigSpace.CategoricalHyperparameter(par, choices=domain.categories) + + raise ValueError( + "TuneBOHB does not support parameters of type " + f"`{type(domain).__name__}` with samplers of type `{type(domain.sampler).__name__}`" + ) + + cs = ConfigSpace.ConfigurationSpace() + for path, domain in domain_vars: + par = "/".join(str(p) for p in path) + value = resolve_value(par, domain) + cs.add_hyperparameter(value) + return cs + + TuneBOHB.convert_search_space = convert_search_space + logger.info("Patched TuneBOHB.convert_search_space for ConfigSpace 1.x compatibility") + + except ImportError: + pass # BOHB not installed + + +_patch_bohb_configspace_conversion() + + +try: + from ludwig.backend.ray import RayBackend + + # TODO: refactor this into an interface + def _is_ray_backend(backend) -> bool: + if isinstance(backend, str): + return backend == RAY + return isinstance(backend, RayBackend) + +except ImportError as e: + logger.warning( + f"ImportError (execution.py) failed to import RayBackend with error: \n\t{e}. " + "The LocalBackend will be used instead. If you want to use the RayBackend, please install ludwig[distributed]." + ) + + class RayBackend: + pass + + def _is_ray_backend(backend) -> bool: + return False + + +def identity(x): + return x + + +def _get_relative_checkpoints_dir_parts(path: Path): + return path.parts[-2:] + + +# Follwing disabled at the moment, expect to be re-enabled pending https://github.com/ludwig-ai/ludwig/issues/2039 +def ray_resource_allocation_function( + trial_runner: "trial_runner.TrialRunner", # noqa + trial: "Trial", # noqa + result: dict[str, Any], + scheduler: "ResourceChangingScheduler", +): + """Determine resources to allocate to running trials.""" + pgf = DistributeResources(trial_runner, trial, result, scheduler) + # restore original base trial resources + + # create bundles + if scheduler.base_trial_resources.required_resources.get("GPU", 0): + bundles = [{"CPU": 1, "GPU": 1}] * int(pgf.required_resources["GPU"]) + else: + bundles = [{"CPU": 1}] * (int(pgf.required_resources["CPU"] - 0.001)) + # we can't set Trial actor's CPUs to 0 so we just go very low + bundles = [{"CPU": 0.001}] + bundles + pgf = PlacementGroupFactory(bundles) + return pgf + + +def _create_tune_checkpoint(save_path): + """Create a Ray Tune Checkpoint from a model save path.""" + + def ignore_dot_files(src, files): + return [f for f in files if f.startswith(".")] + + tmpdir = tempfile.mkdtemp() + checkpoint_model = os.path.join(tmpdir, "model") + if os.path.exists(save_path): + copy_id = uuid.uuid4() + tmp_dst = f"{checkpoint_model}.{copy_id}.tmp" + shutil.copytree(save_path, tmp_dst, ignore=ignore_dot_files) + try: + os.rename(tmp_dst, checkpoint_model) + except OSError: + shutil.rmtree(tmp_dst) + + return tune.Checkpoint.from_directory(tmpdir) + + +class RayTuneExecutor: + def __init__( + self, + parameters: dict, + output_feature: str, + metric: str, + goal: str, + split: str, + search_alg: dict | None = None, + cpu_resources_per_trial: int | None = None, + gpu_resources_per_trial: int | None = None, + kubernetes_namespace: str | None = None, + time_budget_s: int | float | datetime.timedelta | None = None, + max_concurrent_trials: int | None = None, + num_samples: int = 1, + scheduler: dict | None = None, + **kwargs, + ) -> None: + if ray is None: + raise ImportError("ray module is not installed. To install it, try running pip install ray") + self.output_feature = output_feature + self.metric = metric + self.split = split + initialize_ray() + self.search_space, self.decode_ctx = self._get_search_space(parameters) + self.num_samples = num_samples + self.goal = goal + self.search_algorithm = get_search_algorithm(search_alg) + self.scheduler = None if scheduler is None else tune.create_scheduler(scheduler[TYPE], **scheduler) + self.output_feature = output_feature + self.metric = metric + self.split = split + self.trial_id = 0 + self.cpu_resources_per_trial = cpu_resources_per_trial + self.gpu_resources_per_trial = gpu_resources_per_trial + self.kubernetes_namespace = kubernetes_namespace + self.time_budget_s = time_budget_s + self.max_concurrent_trials = max_concurrent_trials + self.sync_config = None + self.sync_client = None + # Head node is the node to which all checkpoints are synced if running on a K8s cluster. + self.head_node_ip = ray.util.get_node_ip_address() + + def _get_search_space(self, parameters: dict) -> tuple[dict, dict]: + """Encode search space parameters as JSON with context for decoding.""" + config = {} + ctx = {} + for param, values in parameters.items(): + # Encode list and dict types as JSON encoded strings to + # workaround type limitations of the underlying frameworks + values = self.encode_values(param, values, ctx) + + param_search_type = values["space"].lower() + if hasattr(tune, param_search_type): + param_search_space = getattr(tune, param_search_type) + else: + raise ValueError(f"'{param_search_type}' is not a supported Ray Tune search space") + + param_search_input_args = {} + param_search_space_sig = signature(param_search_space) + for arg in param_search_space_sig.parameters.values(): + if arg.name in values: + param_search_input_args[arg.name] = values[arg.name] + else: + if arg.default is arg.empty: + raise ValueError(f"Parameter '{arg}' not defined for {param}") + config[param] = param_search_space(**param_search_input_args) + return config, ctx + + @staticmethod + def encode_values(param: str, values: dict, ctx: dict) -> dict: + """JSON encodes any search spaces whose values are lists / dicts. + + Only applies to grid search and choice options. See here for details: + + https://docs.ray.io/en/master/tune/api_docs/search_space.html#random-distributions-api + """ + values = values.copy() + for key in ["values", "categories"]: + if key in values and not isinstance(values[key][0], (int, float)): + values[key] = [json.dumps(v) for v in values[key]] + ctx[param] = json.loads + return values + + @staticmethod + def decode_values(config: dict, ctx: dict) -> dict: + """Decode config values with the decode function in the context. + + Uses the identity function if no encoding is needed. + """ + return {key: ctx.get(key, identity)(value) for key, value in config.items()} + + def _has_metric(self, stats, split): + if not stats: + return False + + if split is not None: + if split not in stats: + return False + stats = stats[split] + + if self.output_feature not in stats: + return False + stats = stats[self.output_feature] + + if self.metric not in stats: + return False + stats = stats[self.metric] + return len(stats) > 0 + + def _has_eval_metric(self, stats): + if stats is None: + return False + + if self.output_feature not in stats: + return False + stats = stats[self.output_feature] + + for metric_part in self.metric.split("."): + if not isinstance(stats, dict) or metric_part not in stats: + return False + stats = stats[metric_part] + return isinstance(stats, float) + + def get_metric_score(self, train_stats) -> float: + if self._has_metric(train_stats, VALIDATION): + logger.info("Returning metric score from training (validation) statistics") + return self.get_metric_score_from_train_stats(train_stats, VALIDATION) + elif self._has_metric(train_stats, TRAINING): + logger.info("Returning metric score from training split statistics, as no validation was given") + return self.get_metric_score_from_train_stats(train_stats, TRAINING) + else: + raise RuntimeError("Unable to obtain metric score from missing training (validation) statistics") + + def get_metric_score_from_eval_stats(self, eval_stats) -> float | list: + stats = eval_stats[self.output_feature] + for metric_part in self.metric.split("."): + if isinstance(stats, dict): + if metric_part in stats: + stats = stats[metric_part] + else: + raise ValueError(f"Evaluation statistics do not contain the metric {self.metric}") + else: + raise ValueError(f"Evaluation statistics do not contain the metric {self.metric}") + + if not isinstance(stats, float): + raise ValueError(f"The metric {self.metric} in evaluation statistics is not a numerical value: {stats}") + return stats + + def get_metric_score_from_train_stats(self, train_stats, select_split=None) -> float: + select_split = select_split or VALIDATION + + # grab the results of the model with highest validation test performance + train_valiset_stats = train_stats[select_split] + + validation_field_result = train_valiset_stats[self.output_feature] + best_function = get_best_function(self.metric) + + # results of the model with highest validation test performance + epoch_best_validation_metric, best_validation_metric = best_function( + enumerate(validation_field_result[self.metric]), key=lambda pair: pair[1] + ) + + return best_validation_metric + + def sort_hyperopt_results(self, hyperopt_results): + return sorted( + hyperopt_results, key=lambda hp_res: hp_res.metric_score, reverse=self.hyperopt_sampler.goal == MAXIMIZE + ) + + @property + def _cpu_resources_per_trial_non_none(self): + return self.cpu_resources_per_trial if self.cpu_resources_per_trial is not None else 1 + + @property + def _gpu_resources_per_trial_non_none(self): + return self.gpu_resources_per_trial if self.gpu_resources_per_trial is not None else 0 + + def _get_remote_checkpoint_dir(self, trial_dir: Path) -> str | tuple[str, str] | None: + """Get the path to remote checkpoint directory.""" + if self.sync_config is None: + return None + + if self.sync_config.upload_dir is not None: + # Cloud storage sync config + remote_checkpoint_dir = os.path.join( + self.sync_config.upload_dir, *_get_relative_checkpoints_dir_parts(trial_dir) + ) + return remote_checkpoint_dir + elif self.kubernetes_namespace is not None: + # Kubernetes sync config. Returns driver node name and path. + # When running on kubernetes, each trial is rsynced to the node running the main process. + node_name = self._get_kubernetes_node_address_by_ip()(self.head_node_ip) + return (node_name, trial_dir) + else: + logger.warning( + "Checkpoint syncing disabled as syncing is only supported to remote cloud storage or on Kubernetes " + "clusters is supported. To use syncing, set the kubernetes_namespace in the config or use a cloud URI " + "as the output directory." + ) + return None + + @lru_cache(maxsize=1) + def _get_kubernetes_node_address_by_ip(self) -> Callable: + """Returns a method to get the node name by IP address within a K8s cluster.""" + if self.kubernetes_namespace is None: + raise ValueError( + "kubernetes_namespace is required for Kubernetes-based hyperopt syncing.\n" + "Fix: set kubernetes_namespace in your hyperopt backend config." + ) + from ray.tune.integration.kubernetes import KubernetesSyncer + + # Initialized with null local and remote directories as we only need to use get_node_address_by_ip. + kubernetes_syncer = KubernetesSyncer(None, None) + + return kubernetes_syncer.get_node_address_by_ip + + # For specified [stopped] trial, remove checkpoint marker on any partial checkpoints + @staticmethod + def _remove_partial_checkpoints(trial_path: str): + marker_paths = glob.glob(os.path.join(glob.escape(trial_path), "checkpoint_*/.is_checkpoint")) + for marker_path in marker_paths: + chkpt_dir = os.path.dirname(marker_path) + metadata_file = glob.glob(os.path.join(glob.escape(chkpt_dir), "*.tune_metadata")) + # glob.glob: filenames starting with a dot are special cases + # that are not matched by '*' and '?' patterns. + metadata_file += glob.glob(os.path.join(glob.escape(chkpt_dir), ".tune_metadata")) + metadata_file = list(set(metadata_file)) # avoid duplication + if len(metadata_file) < 1: + # Remove checkpoint marker on incomplete directory + os.remove(marker_path) + + @contextlib.contextmanager + def _get_best_model_path(self, trial_or_path, analysis: ExperimentAnalysis) -> str: + # Accept either a Trial object or a path string + from ray.tune.experiment.trial import Trial + + if isinstance(trial_or_path, str): + trial_path = trial_or_path + else: + trial_path = trial_or_path.local_path + + remote_checkpoint_dir = self._get_remote_checkpoint_dir(Path(trial_path)) + if remote_checkpoint_dir is not None and self.sync_client is not None: + self.sync_client.sync_down(remote_checkpoint_dir, trial_path) + self.sync_client.wait_or_retry() + self._remove_partial_checkpoints(trial_path) # needed by get_best_checkpoint + + # get_best_checkpoint requires a Trial object in Ray 2.x + if isinstance(trial_or_path, Trial): + trial = trial_or_path + else: + # Try to find the trial by matching its path + trial = None + for t in analysis.trials: + if t.local_path and t.local_path.rstrip("/") == trial_path.rstrip("/"): + trial = t + break + + try: + if trial is not None: + checkpoint = analysis.get_best_checkpoint(trial) + else: + checkpoint = None + except Exception: + logger.warning( + f"Cannot get best model path for {trial_path} due to exception below:\n{traceback.format_exc()}" + ) + yield None + return + + if checkpoint is not None: + with checkpoint.as_directory() as path: + yield path + else: + yield checkpoint + + @staticmethod + def _evaluate_best_model( + trial, + trial_path, + best_model_path, + dataset, + data_format, + skip_save_unprocessed_output, + skip_save_predictions, + skip_save_eval_stats, + gpus, + gpu_memory_limit, + allow_parallel_threads, + backend, + debug, + ): + model_path = os.path.join(best_model_path, "model") + if not os.path.isdir(model_path): + logger.warning( + f"Best model path {model_path} does not exist or is incomplete. " + "This can happen when time budget expires mid-checkpoint. Skipping evaluation." + ) + return + best_model = LudwigModel.load( + model_path, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + from_checkpoint=True, + ) + if best_model.config[TRAINER]["eval_batch_size"]: + batch_size = best_model.config[TRAINER]["eval_batch_size"] + else: + batch_size = best_model.config[TRAINER]["batch_size"] + try: + eval_stats, _, _ = best_model.evaluate( + dataset=dataset, + data_format=data_format, + batch_size=batch_size, + output_directory=trial_path, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + collect_predictions=False, + collect_overall_stats=True, + return_type="dict", + debug=debug, + ) + trial["eval_stats"] = json.dumps(eval_stats, cls=NumpyEncoder) + except NotImplementedError: + logger.warning( + "Skipping evaluation as the necessary methods are not " + "supported. Full exception below:\n" + f"{traceback.format_exc()}" + ) + + def _run_experiment( + self, + config, + checkpoint_dir, + hyperopt_dict, + decode_ctx, + is_using_ray_backend=False, + ): + # Ray Tune redirects stdout/stderr through a Tee object that may not + # implement isatty(), which ray.data's progress bar code requires. + # Patch it to avoid AttributeError. + for stream in (sys.stdout, sys.stderr): + if not hasattr(stream, "isatty"): + stream.isatty = lambda: False + + for gpu_id in ray.get_gpu_ids(): + # Previous trial may not have freed its memory yet, so wait to avoid OOM + wait_for_gpu(gpu_id) + + # Some config values may be JSON encoded as strings, so decode them here + config = self.decode_values(config, decode_ctx) + + # Remove mlflow injected config parameters: https://github.com/ludwig-ai/ludwig/issues/2288 + if "mlflow" in config: + del config["mlflow"] + + trial_id = tune.get_context().get_trial_id() + trial_dir = Path(tune.get_context().get_trial_dir()) + + modified_config = substitute_parameters(copy.deepcopy(hyperopt_dict["config"]), config) + + modified_config = merge_with_defaults(modified_config) + + hyperopt_dict["config"] = modified_config + hyperopt_dict["experiment_name "] = f"{hyperopt_dict['experiment_name']}_{trial_id}" + hyperopt_dict["output_directory"] = str(trial_dir) + + tune_executor = self + if is_using_ray_backend: + ray_queue = RayQueue(actor_options={"num_cpus": 0}) + else: + ray_queue = None + + def report(progress_tracker, save_path=None): + # The progress tracker's metrics are nested dictionaries of TrainerMetrics: feature_name -> metric_name -> + # List[TrainerMetric], with one entry per training checkpoint, according to steps_per_checkpoint. + # We reduce the dictionary of TrainerMetrics to a simple list of floats for interfacing with Ray Tune. + train_stats = { + TRAINING: metric_utils.reduce_trainer_metrics_dict(progress_tracker.train_metrics), + VALIDATION: metric_utils.reduce_trainer_metrics_dict(progress_tracker.validation_metrics), + TEST: metric_utils.reduce_trainer_metrics_dict(progress_tracker.test_metrics), + } + + metric_score = tune_executor.get_metric_score(train_stats) + report_kwargs = { + "metrics": { + "parameters": json.dumps(config, cls=NumpyEncoder), + "metric_score": metric_score, + "training_stats": json.dumps(train_stats, cls=NumpyEncoder), + "eval_stats": "{}", + "trial_id": tune.get_context().get_trial_id(), + "trial_dir": str(tune.get_context().get_trial_dir()), + } + } + if save_path is not None: + report_kwargs["checkpoint"] = _create_tune_checkpoint(save_path) + tune.report(**report_kwargs) + + class RayTuneReportCallback(Callback): + def __init__(self): + super().__init__() + self.last_steps = 0 + self.resume_ckpt_dir = None + + def _get_remote_checkpoint_dir(self) -> str | tuple[str, str] | None: + # sync client has to be recreated to avoid issues with serialization + return tune_executor._get_remote_checkpoint_dir(trial_dir) + + def _checkpoint_progress(self, trainer, progress_tracker, save_path) -> None: + """Checkpoints the progress tracker.""" + if is_using_ray_backend: + # Pass the save_path directly through the queue. On single-node clusters, + # the trial driver and training workers share the same filesystem. + # For multi-node, the checkpoint should be on shared storage. + ray_queue.put((progress_tracker, save_path)) + return + # For non-Ray backend, report metrics + checkpoint together + report(progress_tracker, save_path=save_path) + + def on_train_start(self, model, config: dict[str, Any], config_fp: str | None): + if is_using_ray_backend and checkpoint_dir: + # Store the checkpoint directory path for syncing to the trainer worker. + self.resume_ckpt_dir = checkpoint_dir + + def on_trainer_train_setup(self, trainer, save_path, is_coordinator): + # Check local rank before manipulating files, as otherwise there will be a race condition + # between multiple workers running on the same node. + if self.resume_ckpt_dir is not None and trainer.local_rank == 0: + # Resume from a previous checkpoint by syncing files from the checkpoint + # directory to the save_path. + ckpt_path = self.resume_ckpt_dir + # Attempt an atomic move from the ckpt_path to the save_path + # This may first require removing the existing save_path + tmp_path = save_path + ".tmp" + if os.path.exists(save_path): + os.rename(save_path, tmp_path) + + try: + model_path = os.path.join(ckpt_path, "model") + if os.path.exists(model_path): + safe_move_file(model_path, save_path) + elif os.path.exists(ckpt_path): + safe_move_file(ckpt_path, save_path) + except OSError: + # Rollback from partial changes. Remove the save_path + # and move the original save_path back. + if os.path.exists(save_path): + shutil.rmtree(save_path) + if os.path.exists(tmp_path): + os.rename(tmp_path, save_path) + raise + + # Cleanup the backup save_path as it's no longer needed + if os.path.exists(tmp_path): + shutil.rmtree(tmp_path) + + # Sync all workers here before continuing to training + trainer.barrier() + + def on_eval_end(self, trainer, progress_tracker, save_path): + progress_tracker.tune_checkpoint_num += 1 + self.last_steps = progress_tracker.steps + self._checkpoint_progress(trainer, progress_tracker, save_path) + + def on_trainer_train_teardown(self, trainer, progress_tracker, save_path, is_coordinator): + if is_coordinator and progress_tracker.steps > self.last_steps: + # Note: Calling tune.report in both on_eval_end() and here can cause multiprocessing issues + # for some ray samplers if not steps have happened since the last eval. + self._checkpoint_progress(trainer, progress_tracker, save_path) + + callbacks = hyperopt_dict.get("callbacks") or [] + hyperopt_dict["callbacks"] = callbacks + [RayTuneReportCallback()] + + # set tune resources + if is_using_ray_backend: + resources = tune.get_context().get_trial_resources() + # check if we are using at least 1 gpu per trial + use_gpu = bool(self._gpu_resources_per_trial_non_none) + # get the resources assigned to the current trial + num_gpus = resources.required_resources.get("GPU", 0) + num_cpus = resources.required_resources.get("CPU", 1) if num_gpus == 0 else 0 + + distributed_kwargs = { + "num_workers": int(num_gpus) if use_gpu else 1, + "use_gpu": use_gpu, + "resources_per_worker": { + "CPU": num_cpus, + "GPU": 1 if use_gpu else 0, + }, + } + hyperopt_dict["backend"].set_distributed_kwargs(**distributed_kwargs) + + logger.debug(f"Trial distributed kwargs: {distributed_kwargs}") + + stats = [] + thread_error = [None] # Use list to allow mutation from nested function + + def _run(): + try: + train_stats, eval_stats = run_experiment( + **hyperopt_dict, + model_resume_path=checkpoint_dir, + parameters=config, + ) + stats.append((train_stats, eval_stats)) + except Exception as e: + thread_error[0] = e + logger.error(f"Error in hyperopt trial thread: {e}") + + if is_using_ray_backend: + # We have to pull the results to the trial actor + # from worker actors, as the Tune session is running + # only on the trial actor + thread = threading.Thread(target=_run) + thread.daemon = True + thread.start() + + def check_queue(): + qsize = ray_queue.qsize() + if qsize: + results = ray_queue.get_nowait_batch(qsize) + for progress_tracker, save_path in results: + report(progress_tracker, save_path=save_path) + + while thread.is_alive(): + thread.join(timeout=0) + check_queue() + time.sleep(0.1) + thread.join() + check_queue() + else: + # remove threading overhead + _run() + + if thread_error[0] is not None: + raise RuntimeError(f"Experiment failed: {thread_error[0]}") from thread_error[0] + if not stats: + raise RuntimeError( + "Hyperopt trial did not produce any results — the experiment did not complete.\n" + "Check the trial logs for errors. This can happen if the trial was killed by the scheduler " + "or ran out of time before reporting any metrics." + ) + train_stats, eval_stats = stats.pop() + + metric_score = self.get_metric_score(train_stats) + tune.report( + metrics={ + "parameters": json.dumps(config, cls=NumpyEncoder), + "metric_score": metric_score, + "training_stats": json.dumps(train_stats, cls=NumpyEncoder), + "eval_stats": json.dumps(eval_stats, cls=NumpyEncoder), + "trial_id": tune.get_context().get_trial_id(), + "trial_dir": str(tune.get_context().get_trial_dir()), + } + ) + + def execute( + self, + config, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + data_format=None, + experiment_name="hyperopt", + model_name="run", + resume=None, + skip_save_training_description=False, + skip_save_training_statistics=False, + skip_save_model=False, + skip_save_progress=False, + skip_save_log=False, + skip_save_processed_input=True, + skip_save_unprocessed_output=False, + skip_save_predictions=False, + skip_save_eval_stats=False, + output_directory="results", + gpus=None, + gpu_memory_limit=None, + allow_parallel_threads=True, + callbacks=None, + tune_callbacks=None, + backend=None, + random_seed=default_random_seed, + debug=False, + hyperopt_log_verbosity=3, + **kwargs, + ) -> HyperoptResults: + if isinstance(dataset, str) and not has_remote_protocol(dataset) and not os.path.isabs(dataset): + dataset = os.path.abspath(dataset) + + # Ray Tune / PyArrow requires absolute paths or URIs for storage_path + if not has_remote_protocol(output_directory) and not os.path.isabs(output_directory): + output_directory = os.path.abspath(output_directory) + + if isinstance(backend, str): + backend = initialize_backend(backend) + + if gpus is not None: + raise ValueError( + "Parameter `gpus` is not supported when using Ray Tune. " + "Configure GPU resources with Ray and set `gpu_resources_per_trial` in your " + "hyperopt config." + ) + + if gpu_memory_limit is None and 0 < self._gpu_resources_per_trial_non_none < 1: + # Enforce fractional GPU utilization + gpu_memory_limit = self.gpu_resources_per_trial + + hyperopt_dict = { + "config": config, + "dataset": dataset, + "training_set": training_set, + "validation_set": validation_set, + "test_set": test_set, + "training_set_metadata": training_set_metadata, + "data_format": data_format, + "experiment_name": experiment_name, + "model_name": model_name, + "eval_split": self.split, + "skip_save_training_description": skip_save_training_description, + "skip_save_training_statistics": skip_save_training_statistics, + "skip_save_model": skip_save_model, + "skip_save_progress": skip_save_progress, + "skip_save_log": skip_save_log, + "skip_save_processed_input": skip_save_processed_input, + "skip_save_unprocessed_output": skip_save_unprocessed_output, + "skip_save_predictions": skip_save_predictions, + "skip_save_eval_stats": skip_save_eval_stats, + "output_directory": output_directory, + "gpus": gpus, + "gpu_memory_limit": gpu_memory_limit, + "allow_parallel_threads": allow_parallel_threads, + "callbacks": callbacks, + "backend": backend, + "random_seed": random_seed, + "debug": debug, + } + + mode = "min" if self.goal != MAXIMIZE else "max" + metric = "metric_score" + # if random seed not set, use Ludwig seed + self.search_algorithm.check_for_random_seed(random_seed) + if self.search_algorithm.search_alg_dict is not None: + if TYPE not in self.search_algorithm.search_alg_dict: + candiate_search_algs = list(SEARCH_ALG_IMPORT.keys()) + logger.warning( + "WARNING: search_alg type parameter missing, using 'variant_generator' as default. " + f"These are possible values for the type parameter: {candiate_search_algs}." + ) + search_alg = None + else: + search_alg_type = self.search_algorithm.search_alg_dict[TYPE] + search_alg = tune.create_searcher( + search_alg_type, metric=metric, mode=mode, **self.search_algorithm.search_alg_dict + ) + else: + search_alg = None + + if self.max_concurrent_trials: + if self.max_concurrent_trials <= 0: + raise ValueError( + f"`max_concurrent_trials` must be greater than 0, got {self.max_concurrent_trials}.\n" + f"Fix: set max_concurrent_trials to a positive integer." + ) + if isinstance(search_alg, BasicVariantGenerator) or search_alg is None: + search_alg = BasicVariantGenerator(max_concurrent=self.max_concurrent_trials) + elif isinstance(search_alg, ConcurrencyLimiter): + raise ValueError( + "You have specified `max_concurrent_trials`, but the search " + "algorithm is already a `ConcurrencyLimiter`. FIX THIS " + "by setting `max_concurrent_trials=None`." + ) + else: + search_alg = ConcurrencyLimiter(search_alg, max_concurrent=self.max_concurrent_trials) + + resources_per_trial = { + "cpu": self._cpu_resources_per_trial_non_none, + "gpu": self._gpu_resources_per_trial_non_none, + } + + def run_experiment_trial(config, local_hyperopt_dict, checkpoint_dir=None): + return self._run_experiment( + config, + checkpoint_dir, + local_hyperopt_dict, + self.decode_ctx, + _is_ray_backend(backend), + ) + + tune_config = {} + _tune_callbacks = list(tune_callbacks or []) + for callback in callbacks or []: + run_experiment_trial, tune_config = callback.prepare_ray_tune( + run_experiment_trial, + tune_config, + _tune_callbacks, + ) + tune_callbacks = _tune_callbacks + + if _is_ray_backend(backend): + # for now, we do not do distributed training on cpu (until spread scheduling is implemented for Ray Train) + # but we do want to enable it when GPUs are specified + resources_per_trial = PlacementGroupFactory( + [{}] + ([{"CPU": 0, "GPU": 1}] * self._gpu_resources_per_trial_non_none) + if self._gpu_resources_per_trial_non_none + else [{}] + [{"CPU": self._cpu_resources_per_trial_non_none}] + ) + + if has_remote_protocol(output_directory): + # In Ray 2.x, remote storage is handled via RunConfig storage_path + self.sync_config = tune.SyncConfig() + self.sync_client = None + # output_directory will be used as storage_path + elif self.kubernetes_namespace: + logger.warning( + "Kubernetes-specific syncing is no longer supported in Ray 2.x. " + "Use cloud storage (S3, GCS) as the output directory instead." + ) + + run_experiment_trial_params = tune.with_parameters(run_experiment_trial, local_hyperopt_dict=hyperopt_dict) + + @ray.remote + def _register(name, trainable): + register_trainable(name, trainable) + + ray.get(_register.remote(f"trainable_func_f{hash_dict(config).decode('ascii')}", run_experiment_trial_params)) + + # Note that resume="AUTO" will attempt to resume the experiment if possible, and + # otherwise will start a new experiment: + # https://docs.ray.io/en/latest/tune/tutorials/tune-stopping.html + should_resume = "AUTO" if resume is None else resume + + # If the output directory is an S3 path and AWS_ENDPOINT_URL is set, + # configure a custom S3 filesystem for Ray Tune. We use fsspec's s3fs + # wrapped in PyArrow's FSSpecHandler because PyArrow's native S3 C++ + # client doesn't read AWS_ENDPOINT_URL and its chunked transfer encoding + # is incompatible with some S3-compatible stores (e.g. MinIO). + storage_filesystem = None + if output_directory and str(output_directory).startswith("s3://"): + endpoint_url = os.environ.get("AWS_ENDPOINT_URL") + if endpoint_url: + import pyarrow.fs + import s3fs + + s3 = s3fs.S3FileSystem( + endpoint_url=endpoint_url, + key=os.environ.get("AWS_ACCESS_KEY_ID"), + secret=os.environ.get("AWS_SECRET_ACCESS_KEY"), + ) + storage_filesystem = pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(s3)) + # When storage_filesystem is set, storage_path must be a plain + # path (bucket/key...), not a URI (s3://bucket/key...). + output_directory = str(output_directory).removeprefix("s3://") + + try: + analysis = tune.run( + f"trainable_func_f{hash_dict(config).decode('ascii')}", + name=experiment_name, + config={ + **self.search_space, + **tune_config, + }, + scheduler=self.scheduler, + search_alg=search_alg, + num_samples=self.num_samples, + checkpoint_config=tune.CheckpointConfig(num_to_keep=1), + max_failures=1, # retry a trial failure once + resources_per_trial=resources_per_trial, + time_budget_s=self.time_budget_s, + sync_config=self.sync_config, + storage_path=output_directory, + storage_filesystem=storage_filesystem, + metric=metric, + mode=mode, + trial_name_creator=lambda trial: f"trial_{trial.trial_id}", + trial_dirname_creator=lambda trial: f"trial_{trial.trial_id}", + callbacks=tune_callbacks, + stop=CallbackStopper(callbacks), + verbose=hyperopt_log_verbosity, + resume=should_resume, + log_to_file=True, + ) + except Exception as e: + # Explicitly raise a RuntimeError if an error is encountered during a Ray trial. + # NOTE: Cascading the exception with "raise _ from e" still results in hanging. + raise RuntimeError(f"Encountered Ray Tune error: {e}") + + if "metric_score" in analysis.results_df.columns: + ordered_trials = analysis.results_df.sort_values("metric_score", ascending=self.goal != MAXIMIZE) + + # Catch nans in edge case where the trial doesn't complete + temp_ordered_trials = [] + for kwargs in ordered_trials.to_dict(orient="records"): + for key in ["parameters", "training_stats", "eval_stats"]: + if isinstance(kwargs[key], float): + kwargs[key] = {} + temp_ordered_trials.append(kwargs) + + # Trials with empty eval_stats but non-empty training_stats were terminated before + # post-train evaluation (e.g., time budget or early stopping). Evaluate the best + # checkpoint on the validation split so results are recorded in hyperopt_statistics.json. + for trial in temp_ordered_trials: + if trial["eval_stats"] != "{}" or trial["training_stats"] == "{}": + continue + if validation_set is None or validation_set.size == 0: + logger.warning("Skipping evaluation as no validation set was provided") + continue + trial_path = trial["trial_dir"] + with self._get_best_model_path(trial_path, analysis) as best_model_path: + if best_model_path is None: + logger.warning("Skipping evaluation as no model checkpoints were available") + continue + try: + self._evaluate_best_model( + trial, + trial_path, + best_model_path, + validation_set, + data_format, + skip_save_unprocessed_output, + skip_save_predictions, + skip_save_eval_stats, + gpus, + gpu_memory_limit, + allow_parallel_threads, + backend, + debug, + ) + except Exception: + logger.warning( + f"Failed to evaluate best model for trial {trial_path}. " + "This can happen with incomplete checkpoints from early stopping. " + f"Full exception:\n{traceback.format_exc()}" + ) + + ordered_trials = [TrialResults.from_dict(load_json_values(kwargs)) for kwargs in temp_ordered_trials] + else: + logger.warning("No trials reported results; check if time budget lower than epoch latency") + ordered_trials = [] + + return HyperoptResults(ordered_trials=ordered_trials, experiment_analysis=analysis) + + +_StopperBase = Stopper if _RAY_AVAILABLE else object # type: ignore[misc] + + +class CallbackStopper(_StopperBase): + """Ray Tune Stopper that triggers the entire job to stop if one callback returns True.""" + + def __init__(self, callbacks: list[Callback] | None): + self.callbacks = callbacks or [] + + def __call__(self, trial_id, result): + return False + + def stop_all(self): + for callback in self.callbacks: + if callback.should_stop_hyperopt(): + return True + return False + + +def get_build_hyperopt_executor(executor_type): + return get_from_registry(executor_type, executor_registry) + + +def _get_optuna_executor(): + from ludwig.hyperopt.optuna_executor import OptunaExecutor + + return OptunaExecutor + + +class _LazyRegistry(dict): + """Registry that lazily imports executor classes to avoid import errors when optional deps are missing.""" + + def __init__(self, eager, lazy): + super().__init__(eager) + self._lazy = lazy + + def __getitem__(self, key): + if key in self._lazy: + cls = self._lazy[key]() + self[key] = cls + del self._lazy[key] + return cls + return super().__getitem__(key) + + def __contains__(self, key): + return key in self._lazy or super().__contains__(key) + + def keys(self): + return list(super().keys()) + list(self._lazy.keys()) + + +executor_registry = _LazyRegistry({"ray": RayTuneExecutor}, {"optuna": _get_optuna_executor}) + + +def set_values(params: dict[str, Any], model_dict: dict[str, Any]): + for key, value in params.items(): + if isinstance(value, dict): + for sub_key, sub_value in value.items(): + if key not in model_dict: + model_dict[key] = {} + model_dict[key][sub_key] = sub_value + else: + model_dict[key] = value + + +def run_experiment( + config, + parameters=None, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + data_format=None, + experiment_name="hyperopt", + model_name="run", + model_resume_path=None, + eval_split=VALIDATION, + skip_save_training_description=False, + skip_save_training_statistics=False, + skip_save_model=False, + skip_save_progress=False, + skip_save_log=False, + skip_save_processed_input=False, + skip_save_unprocessed_output=False, + skip_save_predictions=False, + skip_save_eval_stats=False, + output_directory="results", + gpus=None, + gpu_memory_limit=None, + allow_parallel_threads=True, + callbacks=None, + backend=None, + random_seed=default_random_seed, + debug=False, + **kwargs, +): + for callback in callbacks or []: + callback.on_hyperopt_trial_start(parameters) + + # Collect training and validation losses and metrics + # & append it to `results` + model = LudwigModel( + config=config, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + + eval_stats, train_stats, _, _ = model.experiment( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + model_resume_path=model_resume_path, + eval_split=eval_split, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + output_directory=output_directory, + skip_collect_predictions=True, + skip_collect_overall_stats=False, + random_seed=random_seed, + debug=debug, + ) + + for callback in callbacks or []: + callback.on_hyperopt_trial_end(parameters) + + return train_stats, eval_stats + + +def _run_experiment_unary(kwargs): + """Unary function is needed by Fiber to map a list of args.""" + return run_experiment(**kwargs) diff --git a/ludwig/hyperopt/optuna_executor.py b/ludwig/hyperopt/optuna_executor.py new file mode 100644 index 0000000..255e18d --- /dev/null +++ b/ludwig/hyperopt/optuna_executor.py @@ -0,0 +1,296 @@ +"""Native Optuna hyperparameter optimization executor. + +Provides direct Optuna integration without requiring Ray Tune as an intermediary. +Runs trials sequentially on the local machine using Ludwig's standard training API. + +Supports AutoSampler (auto-selects best algorithm), GPSampler (Bayesian optimization), +TPE, CMA-ES, and other Optuna samplers. + +For distributed execution, use the Ray executor with OptunaSearch instead. + +Usage in Ludwig config: + hyperopt: + executor: + type: optuna + num_samples: 50 + sampler: auto # auto, gp, tpe, cmaes, random +""" + +import copy +import logging +import os +import traceback +from typing import Any + +from ludwig.api import LudwigModel +from ludwig.constants import MAXIMIZE, TEST, TRAINING, VALIDATION +from ludwig.hyperopt.results import HyperoptResults, TrialResults +from ludwig.hyperopt.utils import substitute_parameters +from ludwig.utils.defaults import default_random_seed + +logger = logging.getLogger(__name__) + + +def _create_sampler(sampler_type: str): + """Create an Optuna sampler from type string.""" + import optuna + + if sampler_type == "auto": + try: + return optuna.samplers.AutoSampler() + except AttributeError: + logger.info("AutoSampler not available, falling back to TPE") + return optuna.samplers.TPESampler() + elif sampler_type == "gp": + try: + return optuna.samplers.GPSampler() + except AttributeError: + logger.info("GPSampler not available, falling back to TPE") + return optuna.samplers.TPESampler() + elif sampler_type == "tpe": + return optuna.samplers.TPESampler() + elif sampler_type == "cmaes": + return optuna.samplers.CmaEsSampler() + elif sampler_type == "random": + return optuna.samplers.RandomSampler() + else: + raise ValueError(f"Unknown sampler: {sampler_type}. Options: auto, gp, tpe, cmaes, random") + + +def _suggest_params(trial, parameters: dict) -> dict[str, Any]: + """Suggest parameter values for a trial based on the search space definition.""" + params = {} + for param_name, space_def in parameters.items(): + space_type = space_def.get("space", "uniform") + if space_type == "uniform": + params[param_name] = trial.suggest_float(param_name, space_def["lower"], space_def["upper"]) + elif space_type == "loguniform": + params[param_name] = trial.suggest_float(param_name, space_def["lower"], space_def["upper"], log=True) + elif space_type in ("int", "randint", "qrandint"): + params[param_name] = trial.suggest_int(param_name, int(space_def["lower"]), int(space_def["upper"])) + elif space_type in ("choice", "categorical"): + params[param_name] = trial.suggest_categorical(param_name, space_def["categories"]) + elif space_type == "grid_search": + params[param_name] = trial.suggest_categorical(param_name, space_def["values"]) + else: + raise ValueError(f"Unknown search space type: {space_type} for parameter {param_name}") + return params + + +class OptunaExecutor: + """Native Optuna hyperparameter optimization executor. + + Runs trials sequentially on the local machine. Each trial trains a full Ludwig model with parameters suggested by + Optuna, then reports the validation metric back. + """ + + def __init__( + self, + parameters: dict, + output_feature: str, + metric: str, + goal: str, + split: str, + search_alg: dict | None = None, + num_samples: int = 10, + sampler: str = "auto", + pruner: str | None = None, + study_name: str | None = None, + storage: str | None = None, + **kwargs, + ) -> None: + try: + import optuna # noqa: F401 + except ImportError: + raise ImportError("Optuna is required for the optuna executor. Install with: pip install optuna") + + self.parameters = parameters + self.output_feature = output_feature + self.metric = metric + self.goal = goal + self.split = split + self.num_samples = num_samples + self.sampler_type = sampler + self.pruner_type = pruner + self.study_name = study_name or "ludwig_hyperopt" + self.storage = storage + + def execute( + self, + config, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + training_set_metadata=None, + data_format=None, + experiment_name="hyperopt", + model_name="run", + resume=None, + skip_save_training_description=False, + skip_save_training_statistics=False, + skip_save_model=False, + skip_save_progress=False, + skip_save_log=False, + skip_save_processed_input=True, + skip_save_unprocessed_output=False, + skip_save_predictions=False, + skip_save_eval_stats=False, + output_directory="results", + gpus=None, + gpu_memory_limit=None, + allow_parallel_threads=True, + callbacks=None, + tune_callbacks=None, + backend=None, + random_seed=default_random_seed, + debug=False, + hyperopt_log_verbosity=3, + **kwargs, + ) -> HyperoptResults: + import optuna + + sampler_obj = _create_sampler(self.sampler_type) + + pruner_obj = None + if self.pruner_type == "median": + pruner_obj = optuna.pruners.MedianPruner() + elif self.pruner_type == "hyperband": + pruner_obj = optuna.pruners.HyperbandPruner() + + direction = "minimize" if self.goal != MAXIMIZE else "maximize" + study = optuna.create_study( + study_name=self.study_name, + direction=direction, + sampler=sampler_obj, + pruner=pruner_obj, + storage=self.storage, + load_if_exists=True, + ) + + trial_results = [] + + output_dir = os.path.join(output_directory, experiment_name) + os.makedirs(output_dir, exist_ok=True) + + def objective(trial): + sampled_params = _suggest_params(trial, self.parameters) + + for cb in callbacks or []: + cb.on_hyperopt_trial_start(sampled_params) + + # Substitute sampled parameters into config + trial_config = copy.deepcopy(config) + substitute_parameters(trial_config, sampled_params) + + trial_dir = os.path.join(output_dir, f"trial_{trial.number}") + os.makedirs(trial_dir, exist_ok=True) + + try: + model = LudwigModel( + config=trial_config, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + + train_result = model.train( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=f"trial_{trial.number}", + model_name=model_name, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + output_directory=trial_dir, + random_seed=random_seed + trial.number, + ) + train_stats = train_result.train_stats + preprocessed_data = train_result.preprocessed_data + + # Evaluate on the target split + eval_split = self.split + eval_dataset = None + if eval_split == TRAINING: + eval_dataset = preprocessed_data.training_set + elif eval_split == VALIDATION: + eval_dataset = preprocessed_data.validation_set + elif eval_split == TEST: + eval_dataset = preprocessed_data.test_set + + eval_stats = {} + if eval_dataset is not None: + eval_stats_list, _, _ = model.evaluate( + dataset=eval_dataset, + skip_save_unprocessed_output=True, + skip_save_predictions=True, + skip_save_eval_stats=True, + callbacks=callbacks, + ) + eval_stats = eval_stats_list + + # Extract the target metric + metric_value = None + if self.output_feature in eval_stats: + feature_stats = eval_stats[self.output_feature] + if self.metric in feature_stats: + metric_value = feature_stats[self.metric] + elif "combined" in eval_stats and self.metric in eval_stats["combined"]: + metric_value = eval_stats["combined"][self.metric] + + if metric_value is None: + raise ValueError( + f"Could not find metric '{self.metric}' for output feature " + f"'{self.output_feature}' in evaluation stats: {list(eval_stats.keys())}" + ) + + trial_results.append( + TrialResults( + parameters=sampled_params, + metric_score=metric_value, + training_stats=train_stats, + eval_stats=eval_stats, + ) + ) + + for cb in callbacks or []: + cb.on_hyperopt_trial_end(sampled_params) + + logger.info( + f"Trial {trial.number}: {self.output_feature}.{self.metric} = {metric_value:.6f} " + f"(params: {sampled_params})" + ) + + return metric_value + + except Exception as e: + logger.error(f"Trial {trial.number} failed: {e}\n{traceback.format_exc()}") + for cb in callbacks or []: + cb.on_hyperopt_trial_end(sampled_params) + raise optuna.TrialPruned(f"Trial failed: {e}") + + logger.info( + f"Starting Optuna hyperopt: {self.num_samples} trials, " + f"{self.goal} {self.output_feature}.{self.metric}, sampler={self.sampler_type}" + ) + + study.optimize(objective, n_trials=self.num_samples) + + # Sort results by metric score + trial_results.sort(key=lambda t: t.metric_score, reverse=(self.goal == MAXIMIZE)) + + logger.info( + f"Optuna hyperopt complete. Best {self.output_feature}.{self.metric}: " + f"{study.best_value:.6f}, params: {study.best_params}" + ) + + return HyperoptResults(ordered_trials=trial_results, experiment_analysis=study) diff --git a/ludwig/hyperopt/results.py b/ludwig/hyperopt/results.py new file mode 100644 index 0000000..736aadb --- /dev/null +++ b/ludwig/hyperopt/results.py @@ -0,0 +1,26 @@ +# !/usr/bin/env python + +from dataclasses import dataclass +from typing import Any + +from dataclasses_json import dataclass_json + +try: + from ray.tune import ExperimentAnalysis +except ImportError: + ExperimentAnalysis = Any + + +@dataclass_json +@dataclass +class TrialResults: + parameters: dict + metric_score: float + training_stats: dict + eval_stats: dict + + +@dataclass +class HyperoptResults: + ordered_trials: list[TrialResults] + experiment_analysis: ExperimentAnalysis diff --git a/ludwig/hyperopt/run.py b/ludwig/hyperopt/run.py new file mode 100644 index 0000000..c5728da --- /dev/null +++ b/ludwig/hyperopt/run.py @@ -0,0 +1,372 @@ +import copy +import logging +import os +from pprint import pformat + +import pandas as pd +import yaml +from tabulate import tabulate + +from ludwig.api import LudwigModel +from ludwig.backend import Backend, initialize_backend, LocalBackend +from ludwig.callbacks import Callback +from ludwig.constants import ( + AUTO, + COMBINED, + EXECUTOR, + GOAL, + HYPEROPT, + LOSS, + MAX_CONCURRENT_TRIALS, + METRIC, + NAME, + OUTPUT_FEATURES, + PARAMETERS, + PREPROCESSING, + SEARCH_ALG, + SPLIT, + TEST, + TRAINING, + TYPE, + VALIDATION, +) +from ludwig.data.split import get_splitter +from ludwig.hyperopt.results import HyperoptResults +from ludwig.hyperopt.utils import ( + log_warning_if_all_grid_type_parameters, + print_hyperopt_results, + save_hyperopt_stats, + should_tune_preprocessing, + update_hyperopt_params_with_defaults, +) +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version +from ludwig.utils.dataset_utils import generate_dataset_statistics +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import makedirs, open_file + +try: + from ray.tune import Callback as TuneCallback + + from ludwig.backend.ray import RayBackend +except ImportError: + TuneCallback = object + + class RayBackend: + pass + + +logger = logging.getLogger(__name__) + + +def hyperopt( + config: str | dict, + dataset: str | dict | pd.DataFrame = None, + training_set: str | dict | pd.DataFrame = None, + validation_set: str | dict | pd.DataFrame = None, + test_set: str | dict | pd.DataFrame = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + experiment_name: str = "hyperopt", + model_name: str = "run", + resume: bool | None = None, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = True, + skip_save_unprocessed_output: bool = False, + skip_save_predictions: bool = False, + skip_save_eval_stats: bool = False, + skip_save_hyperopt_statistics: bool = False, + output_directory: str = "results", + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + tune_callbacks: list[TuneCallback] | None = None, + backend: Backend | str = None, + random_seed: int = default_random_seed, + hyperopt_log_verbosity: int = 3, + **kwargs, +) -> HyperoptResults: + """Run hyperparameter optimization. + + Args: + config: Config dict or path to a YAML config file. + dataset: Source containing the entire dataset. If it has a split + column, it will be used for splitting (0: train, 1: validation, + 2: test); otherwise the dataset will be randomly split. + training_set: Source containing training data. + validation_set: Source containing validation data. + test_set: Source containing test data. + training_set_metadata: Metadata JSON file or loaded metadata dict. + Intermediate preprocessed structure containing feature mappings + created the first time an input file is used. + data_format: Format to interpret data sources. Inferred automatically + if not specified. Valid values: ``'auto'``, ``'csv'``, + ``'excel'``, ``'feather'``, ``'fwf'``, ``'hdf5'``, + ``'html'``, ``'json'``, ``'jsonl'``, ``'parquet'``, + ``'pickle'``, ``'sas'``, ``'spss'``, ``'stata'``, ``'tsv'``. + experiment_name: Name for the experiment. + model_name: Name of the model being used. + resume: If ``True``, resume from the previous run in ``output_directory`` + with the same experiment name. If ``False``, create new trials + ignoring any prior state. Defaults to resuming when a matching + experiment exists, creating new trials otherwise. + skip_save_training_description: Disable saving the description JSON + file. + skip_save_training_statistics: Disable saving training statistics + JSON file. + skip_save_model: Disable saving model weights after each epoch the + validation metric improves. The returned model will have weights + from the final epoch rather than the best epoch. + skip_save_progress: Disable saving weights and stats after each epoch + (disables training resumption). + skip_save_log: Disable saving TensorBoard logs. + skip_save_processed_input: Disable caching preprocessed input as + HDF5/JSON files. + skip_save_unprocessed_output: If ``True``, skip saving raw numpy + output files; only postprocessed CSV files are saved. + skip_save_predictions: Disable saving test prediction CSV files. + skip_save_eval_stats: Disable saving test statistics JSON file. + skip_save_hyperopt_statistics: Disable saving hyperopt stats file. + output_directory: Directory that will contain training statistics, + TensorBoard logs, the saved model, and training progress files. + gpus: List of GPUs available for training. + gpu_memory_limit: Maximum memory fraction ``[0, 1]`` allowed to + allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading + parallelism (improves performance at the cost of determinism). + callbacks: List of ``Callback`` objects providing hooks into the + Ludwig pipeline. + tune_callbacks: Additional Ray Tune callbacks. + backend: Backend or string name of the backend to use for + preprocessing and training. + random_seed: Random seed for weights initialization, splits, and + shuffling. + hyperopt_log_verbosity: Verbosity of Ray Tune log messages. + 0 = silent, 1 = status only, 2 = status + brief results, + 3 = status + detailed results. + + Returns: + Trial results ordered by descending performance on the target metric. + """ + from ludwig.hyperopt.execution import get_build_hyperopt_executor, RayTuneExecutor + + # check if config is a path or a dict + if isinstance(config, str): # assume path + with open_file(config, "r") as def_file: + config_dict = yaml.safe_load(def_file) + else: + config_dict = config + + if HYPEROPT not in config_dict: + raise ValueError("Hyperopt Section not present in config") + + # backwards compatibility + upgraded_config = upgrade_config_dict_to_latest_version(config_dict) + + # Initialize config object + config_obj = ModelConfig.from_dict(upgraded_config) + + # Retain pre-merged config for hyperopt schema generation + premerged_config = copy.deepcopy(upgraded_config) + + # Get full config with defaults + full_config = config_obj.to_dict() # TODO (Connor): Refactor to use config object + + hyperopt_config = full_config[HYPEROPT] + + # Explicitly default to a local backend to avoid picking up Ray + # backend from the environment. + backend = backend or config_dict.get("backend") or "local" + backend = initialize_backend(backend) + + update_hyperopt_params_with_defaults(hyperopt_config) + + # Check if all features are grid type parameters and log UserWarning if needed + log_warning_if_all_grid_type_parameters(hyperopt_config) + + # Infer max concurrent trials + if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO: + hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config) + logger.info(f"Setting max_concurrent_trials to {hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS]}") + + # Print hyperopt config + logger.info("Hyperopt Config") + logger.info(pformat(hyperopt_config, indent=4)) + logger.info("\n") + + search_alg = hyperopt_config[SEARCH_ALG] + executor = hyperopt_config[EXECUTOR] + parameters = hyperopt_config[PARAMETERS] + split = hyperopt_config[SPLIT] + output_feature = hyperopt_config["output_feature"] + metric = hyperopt_config[METRIC] + goal = hyperopt_config[GOAL] + + ###################### + # check validity of output_feature / metric/ split combination + ###################### + splitter = get_splitter(**full_config[PREPROCESSING]["split"]) + if split == TRAINING: + if training_set is None and not splitter.has_split(0): + raise ValueError( + f'The data for the specified split for hyperopt "{split}" ' + "was not provided, " + "or the split amount specified in the preprocessing section " + "of the config is not greater than 0" + ) + elif split == VALIDATION: + if validation_set is None and not splitter.has_split(1): + raise ValueError( + f'The data for the specified split for hyperopt "{split}" ' + "was not provided, " + "or the split amount specified in the preprocessing section " + "of the config is not greater than 0" + ) + elif split == TEST: + if test_set is None and not splitter.has_split(2): + raise ValueError( + f'The data for the specified split for hyperopt "{split}" ' + "was not provided, " + "or the split amount specified in the preprocessing section " + "of the config is not greater than 0" + ) + else: + raise ValueError( + f'unrecognized hyperopt split "{split}". Please provide one of: { ({TRAINING, VALIDATION, TEST}) }' + ) + if output_feature == COMBINED: + if metric != LOSS: + raise ValueError('The only valid metric for "combined" output feature is "loss"') + else: + output_feature_names = {of[NAME] for of in full_config[OUTPUT_FEATURES]} + if output_feature not in output_feature_names: + raise ValueError( + f'The output feature specified for hyperopt "{output_feature}" ' + "cannot be found in the config. " + f'Available ones are: {output_feature_names} and "combined"' + ) + + hyperopt_executor = get_build_hyperopt_executor(executor[TYPE])( + parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor + ) + + # Explicitly default to a local backend to avoid picking up Ray + # backend from the environment. + backend = backend or config_dict.get("backend") or "local" + backend = initialize_backend(backend) + from ludwig.hyperopt.optuna_executor import OptunaExecutor + + if not ( + isinstance(backend, LocalBackend) + or isinstance(hyperopt_executor, OptunaExecutor) + or (isinstance(hyperopt_executor, RayTuneExecutor) and isinstance(backend, RayBackend)) + ): + raise ValueError( + "Hyperopt requires using a `local` backend at this time, or " + "`ray` backend with `ray` executor, or `optuna` executor." + ) + + for callback in callbacks or []: + callback.on_hyperopt_init(experiment_name) + + if not should_tune_preprocessing(full_config): + # preprocessing is not being tuned, so generate it once before starting trials + for callback in callbacks or []: + callback.on_hyperopt_preprocessing_start(experiment_name) + + model = LudwigModel( + config=full_config, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + + preprocessed = model.preprocess( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + skip_save_processed_input=skip_save_processed_input, + random_seed=random_seed, + ) + training_set = preprocessed.training_set + validation_set = preprocessed.validation_set + test_set = preprocessed.test_set + training_set_metadata = preprocessed.training_set_metadata + dataset = None + + dataset_statistics = generate_dataset_statistics(training_set, validation_set, test_set) + + logger.info("\nDataset Statistics") + logger.info(tabulate(dataset_statistics, headers="firstrow", tablefmt="fancy_grid")) + + for callback in callbacks or []: + callback.on_hyperopt_preprocessing_end(experiment_name) + + for callback in callbacks or []: + callback.on_hyperopt_start(experiment_name) + + hyperopt_results = hyperopt_executor.execute( + premerged_config, + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + resume=resume, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + output_directory=output_directory, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + tune_callbacks=tune_callbacks, + backend=backend, + random_seed=random_seed, + hyperopt_log_verbosity=hyperopt_log_verbosity, + **kwargs, + ) + + if backend.is_coordinator(): + print_hyperopt_results(hyperopt_results) + + if not skip_save_hyperopt_statistics: + with backend.storage.artifacts.use_credentials(): + results_directory = os.path.join(output_directory, experiment_name) + makedirs(results_directory, exist_ok=True) + + hyperopt_stats = { + "hyperopt_config": hyperopt_config, + "hyperopt_results": [t.to_dict() for t in hyperopt_results.ordered_trials], + } + + save_hyperopt_stats(hyperopt_stats, results_directory) + logger.info(f"Hyperopt stats saved to: {results_directory}") + + for callback in callbacks or []: + callback.on_hyperopt_end(experiment_name) + callback.on_hyperopt_finish(experiment_name) + + logger.info("Finished hyperopt") + + return hyperopt_results diff --git a/ludwig/hyperopt/search_algos.py b/ludwig/hyperopt/search_algos.py new file mode 100644 index 0000000..cd54d35 --- /dev/null +++ b/ludwig/hyperopt/search_algos.py @@ -0,0 +1,170 @@ +import logging +from abc import ABC +from importlib import import_module + +from ludwig.constants import TYPE +from ludwig.utils.misc_utils import get_from_registry + +logger = logging.getLogger(__name__) + + +def _is_package_installed(package_name: str, search_algo_name: str) -> bool: + try: + import_module(package_name) + return True + except ImportError: + raise ImportError( + f"Search algorithm {search_algo_name} requires package {package_name}, however package is not installed." + " Please refer to Ray Tune documentation for packages required for this search algorithm." + ) + + +class SearchAlgorithm(ABC): + def __init__(self, search_alg_dict: dict) -> None: + self.search_alg_dict = search_alg_dict + self.random_seed_attribute_name = None + + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + if self.random_seed_attribute_name not in self.search_alg_dict: + self.search_alg_dict[self.random_seed_attribute_name] = ludwig_random_seed + + +class BasicVariantSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "random_state" + + +class HyperoptSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("hyperopt", "hyperopt") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "random_state_seed" + + +class BOHBSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("hpbandster", "bohb") + _is_package_installed("ConfigSpace", "bohb") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "seed" + + +class AxSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("sqlalchemy", "ax") + _is_package_installed("ax", "ax") + super().__init__(search_alg_dict) + + # override parent method, this search algorithm does not support + # setting random seed + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + pass + + +class BayesOptSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("bayes_opt", "bayesopt") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "random_state" + + +class BlendsearchSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("flaml", "blendsearch") + super().__init__(search_alg_dict) + + # override parent method, this search algorithm does not support + # setting random seed + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + pass + + +class CFOSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("flaml", "cfo") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "seed" + + # override parent method, this search algorithm does not support + # setting random seed + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + pass + + +class DragonflySA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("dragonfly", "dragonfly") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "random_state_seed" + + +class HEBOSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("hebo", "hebo") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "random_state_seed" + + +class SkoptSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("skopt", "skopt") + super().__init__(search_alg_dict) + + # override parent method, this search algorithm does not support + # setting random seed + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + pass + + +class NevergradSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("nevergrad", "nevergrad") + super().__init__(search_alg_dict) + + # override parent method, this search algorithm does not support + # setting random seed + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + pass + + +class OptunaSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("optuna", "optuna") + super().__init__(search_alg_dict) + self.random_seed_attribute_name = "seed" + + +class ZooptSA(SearchAlgorithm): + def __init__(self, search_alg_dict: dict) -> None: + _is_package_installed("zoopt", "zoopt") + super().__init__(search_alg_dict) + + # override parent method, this search algorithm does not support + # setting random seed + def check_for_random_seed(self, ludwig_random_seed: int) -> None: + pass + + +def get_search_algorithm(search_algo): + search_algo_name = search_algo.get(TYPE, None) + return get_from_registry(search_algo_name, search_algo_registry)(search_algo) + + +search_algo_registry = { + None: BasicVariantSA, + "variant_generator": BasicVariantSA, + "random": BasicVariantSA, + "hyperopt": HyperoptSA, + "bohb": BOHBSA, + "ax": AxSA, + "bayesopt": BayesOptSA, + "blendsearch": BlendsearchSA, + "cfo": CFOSA, + "dragonfly": DragonflySA, + "hebo": HEBOSA, + "skopt": SkoptSA, + "nevergrad": NevergradSA, + "optuna": OptunaSA, + "zoopt": ZooptSA, +} diff --git a/ludwig/hyperopt/search_space_generator.py b/ludwig/hyperopt/search_space_generator.py new file mode 100644 index 0000000..5ce6f57 --- /dev/null +++ b/ludwig/hyperopt/search_space_generator.py @@ -0,0 +1,131 @@ +"""Auto-generate Optuna search spaces from Ludwig Pydantic config field constraints. + +Inspects a Ludwig config schema (Pydantic model) and generates an Optuna-compatible +search space dict based on field types, defaults, and constraints. + +Usage: + from ludwig.hyperopt.search_space_generator import generate_search_space + + space = generate_search_space(ECDTrainerConfig, fields=["learning_rate", "batch_size", "dropout"]) + # Returns: {"trainer.learning_rate": {"space": "loguniform", "lower": 1e-5, "upper": 0.1}, ...} +""" + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def generate_search_space( + config_class, + fields: list[str] | None = None, + prefix: str = "", +) -> dict[str, dict[str, Any]]: + """Generate Optuna search space from Pydantic config field constraints. + + Inspects field types, defaults, and metadata to create appropriate search spaces: + - float fields with range constraints -> uniform or loguniform + - int fields with range constraints -> int (randint) + - str fields with options -> categorical + - bool fields -> categorical [True, False] + + Args: + config_class: A Ludwig config class (LudwigBaseConfig subclass). + fields: List of field names to include. None for all tunable fields. + prefix: Prefix for parameter names (e.g., "trainer." for trainer params). + + Returns: + Dict mapping parameter paths to Optuna search space definitions. + """ + space = {} + + for field_name, field_info in config_class.model_fields.items(): + if fields is not None and field_name not in fields: + continue + + param_name = f"{prefix}{field_name}" if prefix else field_name + field_type = field_info.annotation + default = field_info.default + + # Skip non-tunable fields + if field_name in ("type", "model_type"): + continue + + # Extract constraints from metadata + metadata = field_info.metadata or [] + min_val = None + max_val = None + + for meta in metadata: + if hasattr(meta, "ge"): + min_val = meta.ge + if hasattr(meta, "gt"): + min_val = meta.gt + if hasattr(meta, "le"): + max_val = meta.le + if hasattr(meta, "lt"): + max_val = meta.lt + + # Determine search space type from field type and constraints + type_str = str(field_type) if field_type else "" + + if "float" in type_str: + if default and isinstance(default, (int, float)) and default > 0 and default < 0.01: + # Small float default suggests log-uniform (learning rates, etc.) + space[param_name] = { + "space": "loguniform", + "lower": (min_val or default / 10) if default else 1e-6, + "upper": (max_val or default * 10) if default else 1.0, + } + elif min_val is not None or max_val is not None: + space[param_name] = { + "space": "uniform", + "lower": min_val if min_val is not None else 0.0, + "upper": max_val if max_val is not None else 1.0, + } + elif "int" in type_str: + if min_val is not None or max_val is not None: + space[param_name] = { + "space": "int", + "lower": int(min_val) if min_val is not None else 1, + "upper": int(max_val) if max_val is not None else (int(default * 4) if default else 100), + } + elif "bool" in type_str: + space[param_name] = { + "space": "categorical", + "categories": [True, False], + } + + return space + + +def generate_trainer_search_space( + model_type: str = "ecd", + tunable_fields: list[str] | None = None, +) -> dict[str, dict[str, Any]]: + """Generate a default search space for trainer hyperparameters. + + If no fields are specified, generates search spaces for commonly tuned parameters. + + Args: + model_type: "ecd" or "llm". + tunable_fields: Specific fields to tune. None for sensible defaults. + + Returns: + Optuna-compatible search space dict. + """ + if tunable_fields is None: + tunable_fields = ["learning_rate", "batch_size", "dropout"] + + # Common sensible defaults that work regardless of schema inspection + defaults = { + "trainer.learning_rate": {"space": "loguniform", "lower": 1e-5, "upper": 0.01}, + "trainer.batch_size": {"space": "int", "lower": 16, "upper": 512}, + "trainer.dropout": {"space": "uniform", "lower": 0.0, "upper": 0.5}, + "combiner.num_layers": {"space": "int", "lower": 1, "upper": 6}, + "combiner.hidden_size": {"space": "categorical", "categories": [64, 128, 256, 512]}, + "combiner.num_heads": {"space": "categorical", "categories": [2, 4, 8]}, + "combiner.dropout": {"space": "uniform", "lower": 0.0, "upper": 0.5}, + } + + return {k: v for k, v in defaults.items() if any(f in k for f in tunable_fields)} diff --git a/ludwig/hyperopt/utils.py b/ludwig/hyperopt/utils.py new file mode 100644 index 0000000..72e2499 --- /dev/null +++ b/ludwig/hyperopt/utils.py @@ -0,0 +1,235 @@ +import copy +import dataclasses +import json +import logging +import os +import warnings +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + AUTO, + COMBINED, + EXECUTOR, + GOAL, + GRID_SEARCH, + HYPEROPT, + INPUT_FEATURES, + LOSS, + MAX_CONCURRENT_TRIALS, + METRIC, + MINIMIZE, + NAME, + NUM_SAMPLES, + OUTPUT_FEATURES, + PARAMETERS, + PREPROCESSING, + RAY, + SPACE, + SPLIT, + TYPE, + VALIDATION, +) +from ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME +from ludwig.hyperopt.results import HyperoptResults, TrialResults +from ludwig.types import HyperoptConfigDict, ModelConfigDict +from ludwig.utils.data_utils import save_json +from ludwig.utils.misc_utils import ( + get_class_attributes, + get_from_registry, + merge_dict, + set_default_value, + set_default_values, +) +from ludwig.utils.print_utils import print_boxed + +logger = logging.getLogger(__name__) + + +def print_hyperopt_results(hyperopt_results: HyperoptResults): + print_boxed("HYPEROPT RESULTS", print_fun=logger.info) + for trial_results in hyperopt_results.ordered_trials: + if not isinstance(trial_results.metric_score, str): + logger.info(f"score: {trial_results.metric_score:.6f} | parameters: {trial_results.parameters}") + logger.info("") + + +def save_hyperopt_stats(hyperopt_stats, hyperopt_dir_name): + hyperopt_stats_fn = os.path.join(hyperopt_dir_name, HYPEROPT_STATISTICS_FILE_NAME) + save_json(hyperopt_stats_fn, hyperopt_stats) + + +def load_json_value(v): + try: + return json.loads(v) + except Exception as e: + logger.warning(f"While loading json, encountered exception: {e}") + return v + + +# define set containing names to return for TrialResults +TRIAL_RESULTS_NAMES_SET = {f.name for f in dataclasses.fields(TrialResults)} + + +def load_json_values(d): + # ensure metric_score is a string for the json load to eliminate extraneous exception message + d["metric_score"] = str(d["metric_score"]) + + # load only data required for TrialResults + return {k: load_json_value(v) for k, v in d.items() if k in TRIAL_RESULTS_NAMES_SET} + + +def should_tune_preprocessing(config): + parameters = config[HYPEROPT][PARAMETERS] + for param_name in parameters: + if f"{PREPROCESSING}." in param_name: + return True + return False + + +def parameter_to_dict(name, value): + if name == ".": + # Parameter name ".", means top-level config + return value + + parameter_dict = {} + curr_dict = parameter_dict + name_list = name.split(".") + for i, name_elem in enumerate(name_list): + if i == len(name_list) - 1: + curr_dict[name_elem] = value + else: + name_dict = curr_dict.get(name_elem, {}) + curr_dict[name_elem] = name_dict + curr_dict = name_dict + return parameter_dict + + +def feature_list_to_dict(config: ModelConfigDict) -> ModelConfigDict: + input_features_dict = {} + for feature in config[INPUT_FEATURES]: + input_features_dict[feature[NAME]] = feature + + output_features_dict = {} + for feature in config[OUTPUT_FEATURES]: + output_features_dict[feature[NAME]] = feature + + config = copy.copy(config) + config[INPUT_FEATURES] = input_features_dict + config[OUTPUT_FEATURES] = output_features_dict + return config + + +def feature_dict_to_list(config: ModelConfigDict) -> ModelConfigDict: + # This works because Python dicts are order-preserving, so we do not need to + # do anything special to map from a key in the dict to an index in a list + input_features_list = [] + for feature in config[INPUT_FEATURES].values(): + input_features_list.append(feature) + + output_features_list = [] + for feature in config[OUTPUT_FEATURES].values(): + output_features_list.append(feature) + + config = copy.copy(config) + config[INPUT_FEATURES] = input_features_list + config[OUTPUT_FEATURES] = output_features_list + return config + + +def substitute_parameters( + config: ModelConfigDict, + parameters: dict[str, Any], +): + """Update Ludwig config with parameters sampled from the Hyperopt sampler.""" + + # Collect the sets of names for each feature grouping so we can map feature names to + # groups + input_feature_names = {feature[NAME] for feature in config[INPUT_FEATURES]} + output_feature_names = {feature[NAME] for feature in config[OUTPUT_FEATURES]} + + # Features in the user config are provided as a list, but in hyperopt we reference + # features by name, so convert temporarily to a dict to simplify the mergep process. + config = feature_list_to_dict(config) + + # Merge parameters into the user configuration in order. As such, if there are conflicting + # params, the later params will take precedence. + for name, value in parameters.items(): + # User params are provided as ., but we group input / output features + # together during the merge to make it easier and unambiguous to convert back and forth + # TODO(travis): we should revisit the user format here, as it silently breaks situations + # where the user has a feature named "trainer", "combiner", etc. + prefix = name.split(".")[0] + if prefix in input_feature_names: + name = f"{INPUT_FEATURES}.{name}" + elif prefix in output_feature_names: + name = f"{OUTPUT_FEATURES}.{name}" + + param_dict = parameter_to_dict(name, value) + config = merge_dict(config, param_dict) + + # Now that all features have been merged, convert back to the original list format. + config = feature_dict_to_list(config) + + return config + + +@DeveloperAPI +def get_num_duplicate_trials(hyperopt_config: HyperoptConfigDict) -> int: + """Returns the number of duplicate trials that will be created. + + Duplicate trials are only created when there are grid type parameters and num_samples > 1. + """ + num_samples = hyperopt_config[EXECUTOR].get(NUM_SAMPLES, 1) + if num_samples == 1: + return 0 + + total_grid_search_trials = 1 + for _, param_info in hyperopt_config[PARAMETERS].items(): + if param_info.get(SPACE, None) == GRID_SEARCH: + total_grid_search_trials *= len(param_info.get("values", [])) + + num_duplicate_trials = (total_grid_search_trials * num_samples) - total_grid_search_trials + return num_duplicate_trials + + +def log_warning_if_all_grid_type_parameters(hyperopt_config: HyperoptConfigDict) -> None: + """Logs warning if all parameters have a grid type search space and num_samples > 1 since this will result in + duplicate trials being created.""" + num_duplicate_trials = get_num_duplicate_trials(hyperopt_config) + if num_duplicate_trials == 0: + return + + num_samples = hyperopt_config[EXECUTOR].get(NUM_SAMPLES, 1) + warnings.warn( + "All hyperopt parameters in Ludwig config are using grid_search space, but number of samples " + f"({num_samples}) is greater than 1. This will result in {num_duplicate_trials} duplicate trials being " + "created. Consider setting `num_samples` to 1 in the hyperopt executor to prevent trial duplication.", + RuntimeWarning, + ) + + +def update_hyperopt_params_with_defaults(hyperopt_params: HyperoptConfigDict) -> None: + """Updates user's Ludwig config with default hyperopt parameters.""" + from ludwig.hyperopt.execution import executor_registry + + set_default_value(hyperopt_params, EXECUTOR, {}) + set_default_value(hyperopt_params, SPLIT, VALIDATION) + set_default_value(hyperopt_params, "output_feature", COMBINED) + set_default_value(hyperopt_params, METRIC, LOSS) + set_default_value(hyperopt_params, GOAL, MINIMIZE) + + set_default_values( + hyperopt_params[EXECUTOR], + {TYPE: RAY, NUM_SAMPLES: 1, MAX_CONCURRENT_TRIALS: AUTO}, + ) + + if hyperopt_params[EXECUTOR].get("trial_driver_resources") is None: + hyperopt_params[EXECUTOR]["trial_driver_resources"] = {"CPU": 1, "GPU": 0} + + executor = get_from_registry(hyperopt_params[EXECUTOR][TYPE], executor_registry) + executor_defaults = {k: v for k, v in executor.__dict__.items() if k in get_class_attributes(executor)} + set_default_values( + hyperopt_params[EXECUTOR], + executor_defaults, + ) diff --git a/ludwig/hyperopt_cli.py b/ludwig/hyperopt_cli.py new file mode 100644 index 0000000..c177210 --- /dev/null +++ b/ludwig/hyperopt_cli.py @@ -0,0 +1,364 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import sys + +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.hyperopt.run import hyperopt +from ludwig.utils.data_utils import load_config_from_str, load_yaml +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + + +def hyperopt_cli( + config: str | dict, + dataset: str | None = None, + training_set: str | None = None, + validation_set: str | None = None, + test_set: str | None = None, + training_set_metadata: str | None = None, + data_format: str | None = None, + experiment_name: str = "experiment", + model_name: str = "run", + # model_load_path=None, + # model_resume_path=None, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = False, + skip_save_unprocessed_output: bool = False, + skip_save_predictions: bool = False, + skip_save_eval_stats: bool = False, + skip_save_hyperopt_statistics: bool = False, + output_directory: str = "results", + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + random_seed: int = default_random_seed, + hyperopt_log_verbosity: int = 3, + **kwargs, +): + """Search for optimal hyperparameters via the CLI entry point. + + Args: + config: In-memory config dict or path to a YAML config file. + dataset: Source containing the entire dataset. If it has a split + column, it will be used for splitting (0: train, 1: validation, + 2: test); otherwise the dataset will be randomly split. + training_set: Source containing training data. + validation_set: Source containing validation data. + test_set: Source containing test data. + training_set_metadata: Metadata JSON file or loaded metadata dict. + data_format: Format to interpret data sources. Inferred automatically + if not specified. + experiment_name: Name for the experiment. + model_name: Name of the model being used. + skip_save_training_description: Disable saving the description JSON + file. + skip_save_training_statistics: Disable saving training statistics + JSON file. + skip_save_model: Disable saving model weights after each epoch the + validation metric improves. + skip_save_progress: Disable saving weights and stats after each epoch. + skip_save_log: Disable saving TensorBoard logs. + skip_save_processed_input: Disable caching preprocessed input as + HDF5/JSON files. + skip_save_unprocessed_output: If ``True``, skip saving raw numpy + output files. + skip_save_predictions: Disable saving test prediction CSV files. + skip_save_eval_stats: Disable saving test statistics JSON file. + skip_save_hyperopt_statistics: Disable saving hyperopt stats file. + output_directory: Directory that will contain all results. + gpus: List of GPUs available for training. + gpu_memory_limit: Maximum memory fraction ``[0, 1]`` allowed to + allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading + parallelism. + callbacks: List of ``Callback`` objects providing hooks into the + Ludwig pipeline. + backend: Backend or string name of the backend to use. + random_seed: Random seed for weights initialization, splits, and + shuffling. + hyperopt_log_verbosity: Verbosity of Ray Tune log messages. + 0 = silent, 1 = status only, 2 = status + brief results, + 3 = status + detailed results. + """ + return hyperopt( + config=config, + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + # model_load_path=model_load_path, + # model_resume_path=model_resume_path, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + skip_save_hyperopt_statistics=skip_save_hyperopt_statistics, + output_directory=output_directory, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + backend=backend, + random_seed=random_seed, + hyperopt_log_verbosity=hyperopt_log_verbosity, + **kwargs, + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script searches for optimal Hyperparameters", + prog="ludwig hyperopt", + usage="%(prog)s [options]", + ) + + # ------------------- + # Hyperopt parameters + # ------------------- + parser.add_argument( + "-sshs", + "--skip_save_hyperopt_statistics", + help="skips saving hyperopt statistics file", + action="store_true", + default=False, + ) + + # ---------------------------- + # Experiment naming parameters + # ---------------------------- + parser.add_argument( + "--output_directory", + type=str, + default="results", + help="directory that contains the results", + ) + parser.add_argument("--experiment_name", type=str, default="hyperopt", help="experiment name") + parser.add_argument("--model_name", type=str, default="run", help="name for the model") + + # --------------- + # Data parameters + # --------------- + parser.add_argument( + "--dataset", + help="input data file path. " + "If it has a split column, it will be used for splitting " + "(0: train, 1: validation, 2: test), " + "otherwise the dataset will be randomly split", + ) + parser.add_argument("--training_set", help="input train data file path") + parser.add_argument("--validation_set", help="input validation data file path") + parser.add_argument("--test_set", help="input test data file path") + + parser.add_argument( + "--training_set_metadata", + help="input metadata JSON file path. An intermediate preprocessed file " + "containing the mappings of the input file created " + "the first time a file is used, in the same directory " + "with the same name and a .json extension", + ) + + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "htmltables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + + parser.add_argument( + "-sspi", + "--skip_save_processed_input", + help="skips saving intermediate HDF5 and JSON files", + action="store_true", + default=False, + ) + + # ---------------- + # Model parameters + # ---------------- + config = parser.add_mutually_exclusive_group(required=True) + config.add_argument( + "-c", + "--config", + type=load_yaml, + help="Path to the YAML file containing the model configuration", + ) + config.add_argument( + "-cs", + "--config_str", + dest="config", + type=load_config_from_str, + help="JSON or YAML serialized string of the model configuration", + ) + + parser.add_argument( + "-mlp", + "--model_load_path", + help="path of a pretrained model to load as initialization", + ) + parser.add_argument( + "-mrp", + "--model_resume_path", + help="path of the model directory to resume training of", + ) + parser.add_argument( + "-sstd", + "--skip_save_training_description", + action="store_true", + default=False, + help="disables saving the description JSON file", + ) + parser.add_argument( + "-ssts", + "--skip_save_training_statistics", + action="store_true", + default=False, + help="disables saving training statistics JSON file", + ) + parser.add_argument( + "-ssm", + "--skip_save_model", + action="store_true", + default=False, + help="disables saving weights each time the model improves. " + "By default Ludwig saves weights after each epoch " + "the validation metric (improves, but if the model is really big " + "that can be time consuming. If you do not want to keep " + "the weights and just find out what performance a model can get " + "with a set of hyperparameters, use this parameter to skip it", + ) + parser.add_argument( + "-ssp", + "--skip_save_progress", + action="store_true", + default=False, + help="disables saving weights after each epoch. By default ludwig saves " + "weights after each epoch for enabling resuming of training, but " + "if the model is really big that can be time consuming and will " + "save twice as much space, use this parameter to skip it", + ) + parser.add_argument( + "-ssl", + "--skip_save_log", + action="store_true", + default=False, + help="disables saving TensorBoard logs. By default Ludwig saves " + "logs for the TensorBoard, but if it is not needed turning it off " + "can slightly increase the overall speed", + ) + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument( + "-rs", + "--random_seed", + type=int, + default=42, + help="a random seed that is going to be used anywhere there is a call " + "to a random number generator: data splitting, parameter " + "initialization and training set shuffling", + ) + parser.add_argument( + "-hlv", + "--hyperopt_log_verbosity", + type=int, + default=3, + choices=[0, 1, 2, 3], + help="Controls verbosity of ray tune log messages. Valid values: " + "0 = silent, 1 = only status updates, 2 = status and brief trial " + "results, 3 = status and detailed trial results.", + ) + parser.add_argument("-g", "--gpus", nargs="+", type=int, default=None, help="list of gpus to use") + parser.add_argument( + "-gml", + "--gpu_memory_limit", + type=float, + default=None, + help="maximum memory fraction [0, 1] allowed to allocate per GPU device", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("hyperopt", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.hyperopt") + + args.backend = initialize_backend(args.backend or args.config.get("backend")) + if args.backend.is_coordinator(): + print_ludwig("Hyperopt", LUDWIG_VERSION) + + hyperopt_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/inspect_model.py b/ludwig/inspect_model.py new file mode 100644 index 0000000..f8f0460 --- /dev/null +++ b/ludwig/inspect_model.py @@ -0,0 +1,89 @@ +"""CLI for model inspection -- ``ludwig inspect``.""" + +import argparse +import json +import logging + +from ludwig.api import LudwigModel + +logger = logging.getLogger(__name__) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="Inspect a trained Ludwig model", + prog="ludwig inspect", + ) + parser.add_argument("-m", "--model_path", required=True, help="Path to the trained model directory") + parser.add_argument( + "--weights", + action="store_true", + help="Show detailed weight tensor info", + ) + parser.add_argument( + "--importance", + action="store_true", + help="Show approximate feature importance from encoder weights", + ) + parser.add_argument( + "--json", + action="store_true", + dest="output_json", + help="Output as JSON instead of formatted text", + ) + + args = parser.parse_args(sys_argv) + + model = LudwigModel.load(args.model_path) + + from ludwig.model_inspector import ModelInspector + + inspector = ModelInspector( + model=model.model, + config=model.config, + training_set_metadata=model.training_set_metadata, + ) + + summary = inspector.model_summary() + + if args.output_json: + output = {"summary": summary} + if args.weights: + output["weights"] = inspector.collect_weights() + if args.importance: + output["feature_importance"] = inspector.feature_importance_proxy() + print(json.dumps(output, indent=2)) + else: + print("\nModel Summary") + print("=" * 50) + print(f" Model type: {summary['model_type']}") + print(f" Combiner: {summary['combiner_type']}") + print(f" Input features: {summary['num_input_features']}") + print(f" Output features: {summary['num_output_features']}") + print(f" Total parameters: {summary['total_parameters']:,}") + print(f" Trainable parameters: {summary['trainable_parameters']:,}") + print(f" Frozen parameters: {summary['frozen_parameters']:,}") + print(f" Model size: {summary['model_size_mb']:.2f} MB") + print() + + if args.weights: + weights = inspector.collect_weights() + print(f"Weights ({len(weights)} tensors)") + print("=" * 50) + for w in weights: + grad = "trainable" if w["requires_grad"] else "frozen" + print(f" {w['name']}: {w['shape']} ({w['num_elements']:,} params, {grad})") + print() + + if args.importance: + importance = inspector.feature_importance_proxy() + if importance: + print("Feature Importance (approximate)") + print("=" * 50) + sorted_imp = sorted(importance.items(), key=lambda x: x[1], reverse=True) + for name, score in sorted_imp: + bar = "#" * int(score * 30) + print(f" {name:30s} {score:.4f} {bar}") + else: + print(" No input features found for importance estimation") + print() diff --git a/ludwig/model_export/base_model_exporter.py b/ludwig/model_export/base_model_exporter.py new file mode 100644 index 0000000..3003f5e --- /dev/null +++ b/ludwig/model_export/base_model_exporter.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod + +import torch + + +class LudwigTorchWrapper(torch.nn.Module): + """Base class that establishes the contract for exporting to different file formats.""" + + def __init__(self, model): + super().__init__() + self.model = model + + def forward(self, x): + return self.model({"image_path": x}) + + +class BaseModelExporter(ABC): + @abstractmethod + def export(self, model_path, export_path, export_args_override): + pass + + @abstractmethod + def check_model_export(self, path): + pass diff --git a/ludwig/model_export/onnx_exporter.py b/ludwig/model_export/onnx_exporter.py new file mode 100644 index 0000000..bca5ee7 --- /dev/null +++ b/ludwig/model_export/onnx_exporter.py @@ -0,0 +1,50 @@ +import os + +import torch + +from ludwig.api import LudwigModel +from ludwig.model_export.base_model_exporter import BaseModelExporter, LudwigTorchWrapper + + +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +class OnnxExporter(BaseModelExporter): + """Class that abstracts the convertion of torch to onnx.""" + + def export(self, model_path, export_path, output_model_name): + ludwig_model = LudwigModel.load(model_path) + model = LudwigTorchWrapper(ludwig_model.model) # Wrap the model + model.eval() # inference mode, is this needed.. I think onnx export does this for us + + width = ludwig_model.config["input_features"][0]["preprocessing"]["width"] + height = ludwig_model.config["input_features"][0]["preprocessing"]["height"] + example_input = torch.randn(1, 3, width, height, requires_grad=True) + + torch.onnx.export( + model, + example_input, + os.path.join(export_path, output_model_name), + opset_version=18, + export_params=True, + do_constant_folding=True, + input_names=["input"], + output_names=["combiner_hidden_1", "output", "combiner_hidden_2"], + ) + + def check_model_export(self, path): + import onnx + + onnx_model = onnx.load(path) + onnx.checker.check_model(onnx_model) diff --git a/ludwig/model_inspector.py b/ludwig/model_inspector.py new file mode 100644 index 0000000..1d6619a --- /dev/null +++ b/ludwig/model_inspector.py @@ -0,0 +1,103 @@ +"""Model introspection utilities. + +Extracted from LudwigModel to reduce the god object. Provides: +- Weight collection +- Activation collection +- Schema generation +- Model summary +""" + +import logging +from typing import Any + +import torch + +logger = logging.getLogger(__name__) + + +class ModelInspector: + """Inspect and analyze a trained Ludwig model.""" + + def __init__(self, model: torch.nn.Module, config: dict, training_set_metadata: dict): + self.model = model + self.config = config + self.training_set_metadata = training_set_metadata + + def collect_weights(self, tensor_names: list[str] | None = None) -> list[dict[str, Any]]: + """Collect model weight tensors. + + Args: + tensor_names: Specific parameter names to collect. None for all. + + Returns: + List of dicts with 'name', 'shape', 'dtype', 'values' keys. + """ + results = [] + for name, param in self.model.named_parameters(): + if tensor_names is None or name in tensor_names: + results.append( + { + "name": name, + "shape": list(param.shape), + "dtype": str(param.dtype), + "requires_grad": param.requires_grad, + "num_elements": param.numel(), + } + ) + return results + + def model_summary(self) -> dict[str, Any]: + """Generate a summary of the model architecture. + + Returns: + Dict with parameter counts, layer info, and feature details. + """ + total_params = sum(p.numel() for p in self.model.parameters()) + trainable_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad) + frozen_params = total_params - trainable_params + + # Model size in MB + model_size_mb = sum(p.numel() * p.element_size() for p in self.model.parameters()) / (1024 * 1024) + + # Count layers by type + layer_counts = {} + for module in self.model.modules(): + class_name = type(module).__name__ + layer_counts[class_name] = layer_counts.get(class_name, 0) + 1 + + return { + "total_parameters": total_params, + "trainable_parameters": trainable_params, + "frozen_parameters": frozen_params, + "model_size_mb": round(model_size_mb, 2), + "layer_counts": layer_counts, + "model_type": self.config.get("model_type", "ecd"), + "combiner_type": self.config.get("combiner", {}).get("type", "concat"), + "num_input_features": len(self.config.get("input_features", [])), + "num_output_features": len(self.config.get("output_features", [])), + } + + def feature_importance_proxy(self) -> dict[str, float]: + """Estimate feature importance from encoder weight magnitudes. + + This is a rough proxy, not a rigorous importance measure. For proper + feature importance, use SHAP or Captum via Ludwig's explain module. + + Returns: + Dict mapping feature names to relative importance scores. + """ + importance = {} + if hasattr(self.model, "input_features"): + for name, feature in self.model.input_features.items(): + total_weight_magnitude = 0.0 + for param in feature.parameters(): + total_weight_magnitude += param.abs().mean().item() + importance[name] = total_weight_magnitude + + # Normalize to 0-1 range + if importance: + max_imp = max(importance.values()) + if max_imp > 0: + importance = {k: v / max_imp for k, v in importance.items()} + + return importance diff --git a/ludwig/models/__init__.py b/ludwig/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/models/base.py b/ludwig/models/base.py new file mode 100644 index 0000000..87e92f4 --- /dev/null +++ b/ludwig/models/base.py @@ -0,0 +1,350 @@ +import contextlib +import logging +from abc import ABCMeta, abstractmethod +from collections import OrderedDict +from typing import Any + +import numpy as np +import torch +import torchmetrics + +from ludwig.combiners.combiners import Combiner +from ludwig.constants import COMBINED, LOSS, NAME +from ludwig.encoders.base import Encoder +from ludwig.features.base_feature import InputFeature, NonPropertyModuleWrapper, OutputFeature +from ludwig.features.feature_registries import get_input_type_registry, get_output_type_registry +from ludwig.features.feature_utils import LudwigFeatureDict +from ludwig.features.passthrough_feature import create_passthrough_input_feature +from ludwig.modules.metric_modules import LudwigMetric +from ludwig.modules.training_hooks import TrainingHook +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig, FeatureCollection +from ludwig.utils.algorithms_utils import topological_sort_feature_dependencies +from ludwig.utils.metric_utils import get_scalar_from_ludwig_metric +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.torch_utils import LudwigModule, reg_loss + +logger = logging.getLogger(__name__) + + +class BaseModel(LudwigModule, metaclass=ABCMeta): + """Base model for use in LudwigModule. + + Implementations of this class should implement the following methods: + - type() + - forward() + """ + + @staticmethod + @abstractmethod + def type() -> str: + """Returns the model type.""" + + def __init__(self, random_seed: int | None = None): + self._random_seed = random_seed + + # Ensures model weight initialization is deterministic even though set_random_seed() + # is called later in the trainer. Required for test_api::test_api_training_determinism. + if random_seed is not None: + torch.random.manual_seed(random_seed) + + super().__init__() + + self.input_features = self.create_feature_dict() + self.output_features = self.create_feature_dict() + + # ================ Combined loss metric ================ + self._eval_loss_metric = NonPropertyModuleWrapper(torchmetrics.MeanMetric()) + self._eval_additional_losses_metrics = NonPropertyModuleWrapper(torchmetrics.MeanMetric()) + + # ================ Training Hook Handles ================ + self._forward_hook_handles: list[TrainingHook] = [] + + def create_feature_dict(self) -> LudwigFeatureDict: + """Creates and returns a LudwigFeatureDict.""" + return LudwigFeatureDict() + + def to_device(self, device): + return self.to(device) + + def metrics_to_device(self, device: str): + self._eval_loss_metric.module = self._eval_loss_metric.module.to(device) + self._eval_additional_losses_metrics.module = self._eval_additional_losses_metrics.module.to(device) + for feature in self.output_features.values(): + feature._eval_loss_metric.module = feature._eval_loss_metric.module.to(device) + + @classmethod + def build_inputs(cls, input_feature_configs: FeatureCollection[BaseInputFeatureConfig]) -> dict[str, InputFeature]: + """Builds and returns input features in topological order.""" + input_features = OrderedDict() + input_features_def = topological_sort_feature_dependencies(input_feature_configs.to_list()) + for input_feature_def in input_features_def: + input_features[input_feature_def[NAME]] = cls.build_single_input( + getattr(input_feature_configs, input_feature_def[NAME]), input_features + ) + return input_features + + @staticmethod + def build_single_input( + feature_config: BaseInputFeatureConfig, other_input_features: dict[str, InputFeature] | None + ) -> InputFeature: + """Builds a single input feature from the input feature definition.""" + logger.debug(f"Input {feature_config.type} feature {feature_config.name}") + + encoder_obj = None + if feature_config.tied is not None: + tied_input_feature_name = feature_config.tied + if tied_input_feature_name in other_input_features: + encoder_obj = other_input_features[tied_input_feature_name].encoder_obj + + return create_input_feature(feature_config, encoder_obj) + + @classmethod + def build_outputs( + cls, output_feature_configs: FeatureCollection[BaseOutputFeatureConfig], combiner: Combiner + ) -> dict[str, OutputFeature]: + """Builds and returns output features in topological order.""" + output_features_def = topological_sort_feature_dependencies(output_feature_configs.to_list()) + output_features = {} + + for output_feature_def in output_features_def: + # TODO(Justin): Check that the semantics of input_size align with what the combiner's output shape returns + # for seq2seq. + getattr(output_feature_configs, output_feature_def[NAME]).input_size = combiner.output_shape[-1] + output_features[output_feature_def[NAME]] = cls.build_single_output( + getattr(output_feature_configs, output_feature_def[NAME]), output_features + ) + return output_features + + @staticmethod + def build_single_output( + feature_config: BaseOutputFeatureConfig, output_features: dict[str, OutputFeature] | None + ) -> OutputFeature: + """Builds a single output feature from the output feature definition.""" + logger.debug(f"Output {feature_config.type} feature {feature_config.name}") + output_feature_class = get_from_registry(feature_config.type, get_output_type_registry()) + output_feature_obj = output_feature_class(feature_config, output_features=output_features) + return output_feature_obj + + def get_model_inputs(self): + """Returns a dict of feature name -> sample model input.""" + device = next(self.parameters()).device + inputs = { + input_feature_name: input_feature.create_sample_input().to(device) + for input_feature_name, input_feature in self.input_features.items() + } + return inputs + + def get_model_size(self) -> int: + """Returns total number of parameters in model.""" + model_tensors = self.collect_weights() + total_size = 0 + for tnsr in model_tensors: + total_size += tnsr[1].detach().cpu().numpy().size + return total_size + + @property + def input_shape(self) -> torch.Size: + """Returns the shape of a single model input (excluding batch dimension). + + Subclasses should override this to return a meaningful shape. + The default is a (1,) scalar — sufficient for models that don't rely + on input_shape for decoder sizing. + """ + return torch.Size([1]) + + @abstractmethod + def forward( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + mask=None, + ) -> dict[str, torch.Tensor]: + """Forward pass of the model. + + Args: + inputs: Inputs to the model. Can be a dictionary of input names to + input tensors or a tuple of (inputs, targets) where inputs is + a dictionary of input names to input tensors and targets is a + dictionary of target names to target tensors. + mask: A mask for the inputs. + + Returns: + A dictionary of output {feature name}::{tensor_name} -> output tensor. + """ + + def predictions(self, inputs): + """Returns the model's predictions for the given inputs.""" + outputs = self(inputs) + return self.outputs_to_predictions(outputs) + + def outputs_to_predictions(self, outputs: dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]: + """Returns the model's predictions given the raw model outputs.""" + predictions = {} + for of_name in self.output_features: + predictions[of_name] = self.output_features.get(of_name).predictions(outputs, of_name) + return predictions + + def evaluation_step(self, inputs, targets): + """Predict the inputs and update evaluation metrics.""" + predictions = self.predictions(inputs) + self.update_metrics(targets, predictions) + return predictions + + def predict_step(self, inputs): + """Predict the inputs.""" + return self.predictions(inputs) + + def train_loss( + self, + targets, + predictions, + regularization_type: str | None = None, + regularization_lambda: float | None = None, + ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + """Computes the training loss for the model. + + Args: + targets: A dictionary of target names to target tensors. + predictions: A dictionary of output names to output tensors. + regularization_type: One of 'l1', 'l2', 'l1_l2', or None. + regularization_lambda: The regularization lambda. + + Returns: + A tuple of the loss tensor and a dictionary of loss for every + output feature. + """ + of_train_losses = {} + of_weights = {} + for of_name, of_obj in self.output_features.items(): + of_train_loss = of_obj.train_loss(targets[of_name], predictions, of_name) + of_train_losses[of_name] = of_train_loss + of_weights[of_name] = of_obj.loss.weight + + # Use loss balancer if available, otherwise static weighted sum + if hasattr(self, "loss_balancer") and self.loss_balancer is not None: + train_loss = self.loss_balancer(of_train_losses, of_weights) + else: + train_loss = sum(of_weights[k] * of_train_losses[k] for k in of_train_losses) + + additional_losses = self.losses() + if additional_losses: + train_loss += torch.sum(torch.stack(additional_losses)) # other losses + + # Add regularization loss + if regularization_type is not None and regularization_lambda != 0: + train_loss += reg_loss(self, regularization_type, l1=regularization_lambda, l2=regularization_lambda) + + return train_loss, of_train_losses + + def eval_loss(self, targets, predictions): + """Computes all evaluation losses for the model given targets and predictions. + + Args: + targets: A dictionary of target names to target tensors. + predictions: A dictionary of output names to output tensors. + + Returns: + A tuple of loss values for eval losses and additional losses. + """ + eval_loss = 0 + for of_name, of_obj in self.output_features.items(): + of_eval_loss = of_obj.eval_loss(targets[of_name], predictions[of_name]) + eval_loss += of_obj.loss.weight * of_eval_loss + + additional_loss = 0 + additional_losses = self.losses() + if additional_losses: + additional_loss = torch.sum(torch.stack(additional_losses)) # other losses + + return eval_loss, additional_loss + + def update_metrics(self, targets, predictions): + """Updates the model's metrics given targets and predictions.""" + for of_name, of_obj in self.output_features.items(): + of_obj.update_metrics(targets[of_name], predictions[of_name]) + + eval_loss, additional_losses = self.eval_loss(targets, predictions) + self.eval_loss_metric.update(eval_loss) + self.eval_additional_losses_metrics.update(additional_losses) + + @property + def eval_loss_metric(self) -> LudwigMetric: + return self._eval_loss_metric.module + + @eval_loss_metric.setter + def eval_loss_metric(self, value: LudwigMetric) -> None: + self._eval_loss_metric.module = value + + @property + def eval_additional_losses_metrics(self) -> LudwigMetric: + return self._eval_additional_losses_metrics.module + + def get_metrics(self) -> dict[str, dict[str, float]]: + """Returns a dictionary of metrics for each output feature of the model.""" + all_of_metrics = {} + for of_name, of_obj in self.output_features.items(): + all_of_metrics[of_name] = of_obj.get_metrics() + all_of_metrics[COMBINED] = { + LOSS: get_scalar_from_ludwig_metric(self.eval_loss_metric) + + get_scalar_from_ludwig_metric(self.eval_additional_losses_metrics) + } + return all_of_metrics + + def reset_metrics(self): + """Resets the model's metrics.""" + for of_obj in self.output_features.values(): + of_obj.reset_metrics() + self.eval_loss_metric.reset() + + def collect_weights(self, tensor_names=None, **kwargs): + """Returns named parameters filtered against `tensor_names` if not None.""" + if not tensor_names: + return self.named_parameters() + + # Check for bad tensor names. + weight_names = {name for name, _ in self.named_parameters()} + for name in tensor_names: + if name not in weight_names: + raise ValueError(f'Requested tensor name filter "{name}" not present in the model graph') + + # Apply filter. + tensor_set = set(tensor_names) + return [named_param for named_param in self.named_parameters() if named_param[0] in tensor_set] + + def unskip(self): + """Converts all skipped features into their fully encoded versions.""" + + @abstractmethod + def save(self, save_path: str): + """Saves the model to the given path.""" + + @abstractmethod + def load(self, save_path: str): + """Loads the model from the given path.""" + + @abstractmethod + def get_args(self): + """Returns init arguments for constructing this model.""" + + @contextlib.contextmanager + def use_generation_config(self, generation_config: dict[str, Any]): + if generation_config is not None: + raise NotImplementedError(f"{self.__class__.__name__} does not support generation_config. ") + yield + + def _activate_forward_hooks(self): + """Activates/registers forward hooks for the model.""" + + def _deactivate_forward_hooks(self) -> None: + """Deactivates/de-registers forward hooks for the model (if needed).""" + for handle in self._forward_hook_handles: + handle.deactivate_hook() + + +def create_input_feature(feature_config: BaseInputFeatureConfig, encoder_obj: Encoder | None) -> InputFeature: + input_feature_cls = get_from_registry(feature_config.type, get_input_type_registry()) + input_feature = input_feature_cls(feature_config, encoder_obj=encoder_obj) + if not feature_config.encoder.skip: + return input_feature + return create_passthrough_input_feature(input_feature, feature_config) diff --git a/ludwig/models/calibrator.py b/ludwig/models/calibrator.py new file mode 100644 index 0000000..9093970 --- /dev/null +++ b/ludwig/models/calibrator.py @@ -0,0 +1,60 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import numpy as np + +from ludwig.backend import Backend +from ludwig.models.ecd import ECD + + +class Calibrator: + """Calibrator calibrates the output probabilities of a model.""" + + def __init__(self, model: ECD, backend: Backend, batch_size: int = 128): + self.model = model + self.backend = backend + self.batch_size = batch_size + + def calibration_enabled(self): + """Calibration is enabled if the config requests calibration for any output feature. + + If no output features have calibration enabled, the calibration phase should be skipped. + """ + return any(o.calibration_module is not None for o in self.model.output_features.values()) + + def train_calibration(self, dataset, dataset_name: str): + """Calibrates model output probabilities on validation set after training. + + This works well for most datasets, though it may fail for some difficult or extremely imbalanced datasets. + """ + if not self.calibration_enabled(): + # Early out if no output features have calibration enabled. + return + with self.backend.create_predictor(self.model, batch_size=self.batch_size) as predictor: + metrics, predictions = predictor.batch_evaluation( + dataset, collect_predictions=True, collect_logits=True, dataset_name=dataset_name + ) + + dataset_df = dataset.to_df() + for output_feature in self.model.output_features.values(): + if output_feature.calibration_module is not None: + feature_logits_key = f"{output_feature.feature_name}_logits" + if feature_logits_key in predictions: + feature_logits = self.backend.df_engine.compute(predictions[feature_logits_key]) + feature_labels = self.backend.df_engine.compute(dataset_df[output_feature.proc_column]) + output_feature.calibration_module.train_calibration( + np.stack(feature_logits.values, axis=0), np.stack(feature_labels.values, axis=0) + ) diff --git a/ludwig/models/ecd.py b/ludwig/models/ecd.py new file mode 100644 index 0000000..fe745b9 --- /dev/null +++ b/ludwig/models/ecd.py @@ -0,0 +1,282 @@ +import logging +import os + +import numpy as np +import torch + +from ludwig.accounting.used_tokens import get_used_tokens_for_ecd +from ludwig.combiners.combiners import create_combiner +from ludwig.constants import MODEL_ECD, MODEL_LLM, USED_TOKENS +from ludwig.globals import MODEL_WEIGHTS_FILE_NAME, MODEL_WEIGHTS_SAFETENSORS_FILE_NAME +from ludwig.models.base import BaseModel +from ludwig.schema.model_types.ecd import ECDModelConfig +from ludwig.utils import output_feature_utils +from ludwig.utils.augmentation_utils import AugmentationPipelines +from ludwig.utils.data_utils import clear_data_cache +from ludwig.utils.fs_utils import open_file +from ludwig.utils.state_dict_backward_compatibility import update_state_dict +from ludwig.utils.torch_utils import get_torch_device + +logger = logging.getLogger(__name__) + + +class ECD(BaseModel): + @staticmethod + def type() -> str: + return MODEL_ECD + + def __init__( + self, + config_obj: ECDModelConfig, + random_seed=None, + **_kwargs, + ): + self.config_obj = config_obj + self._random_seed = random_seed + + super().__init__(random_seed=self._random_seed) + + # ================ Inputs ================ + try: + self.input_features.update(self.build_inputs(input_feature_configs=self.config_obj.input_features)) + except KeyError as e: + raise KeyError( + f"An input feature has a name that conflicts with a class attribute of torch's ModuleDict: {e}" + ) from e + + # ================ Combiner ================ + logger.debug(f"Combiner {self.config_obj.combiner.type}") + self.combiner = create_combiner(self.config_obj.combiner, input_features=self.input_features) + + # ================ Outputs ================ + self.output_features.update( + self.build_outputs(output_feature_configs=self.config_obj.output_features, combiner=self.combiner) + ) + + # ================ Loss Balancing ================ + loss_balancing = getattr(config_obj.trainer, "loss_balancing", "none") + if loss_balancing and loss_balancing != "none": + from ludwig.modules.loss_balancing import create_loss_balancer + + output_feature_names = [f.name for f in config_obj.output_features] + alpha = getattr(config_obj.trainer, "loss_balancing_alpha", 1.5) + lr = getattr(config_obj.trainer, "loss_balancing_lr", 0.01) + preference_vector = getattr(config_obj.trainer, "loss_balancing_preference_vector", None) + tchebycheff_weight = getattr(config_obj.trainer, "loss_balancing_tchebycheff_weight", 0.5) + self.loss_balancer = create_loss_balancer( + loss_balancing, + output_feature_names, + alpha=alpha, + lr=lr, + preference_vector=preference_vector, + tchebycheff_weight=tchebycheff_weight, + ) + else: + self.loss_balancer = None + + # ================ Modality Dropout ================ + modality_dropout_prob = getattr(config_obj.trainer, "modality_dropout", 0.0) + if modality_dropout_prob > 0: + from ludwig.modules.modality_dropout import ModalityDropout + + feature_shapes = {name: feat.output_shape for name, feat in self.input_features.items()} + self.modality_dropout = ModalityDropout(feature_shapes, modality_dropout_prob) + else: + self.modality_dropout = None + + # After constructing all layers, clear the cache to free up memory + clear_data_cache() + + def prepare_for_training(self): + # 1/10/23: For parity with how the LLM model type sets up adapters and quantization, LLM encoders should call + # `prepare_for_training` at training time rather than at initialization. This loop searches for input features + # using the LLM encoder and calls `prepare_for_training` on those encoders only. No other changes should be + # made to the ECD model itself or any other encoders. + for feature in self.config_obj.input_features: + encoder_type = feature.encoder.type + if encoder_type == MODEL_LLM: + feature_name = feature.name + encoder = self.input_features.get(feature_name) + encoder.prepare_for_training() + + def encode( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + ): + # Convert inputs to tensors. + for input_feature_name, input_values in inputs.items(): + if not isinstance(input_values, torch.Tensor): + inputs[input_feature_name] = torch.from_numpy(input_values) + else: + inputs[input_feature_name] = input_values + + encoder_outputs = {} + for input_feature_name, input_values in inputs.items(): + encoder = self.input_features.get(input_feature_name) + encoder_output = encoder(input_values) + encoder_outputs[input_feature_name] = encoder_output + + return encoder_outputs + + def combine(self, encoder_outputs): + return self.combiner(encoder_outputs) + + def decode(self, combiner_outputs, targets, mask): + # Invoke output features. + output_logits = {} + output_last_hidden = {} + for output_feature_name, output_feature in self.output_features.items(): + # Use the presence or absence of targets to signal training or prediction. + target = targets[output_feature_name] if targets is not None else None + decoder_outputs = output_feature(combiner_outputs, output_last_hidden, mask=mask, target=target) + + # Add decoder outputs to overall output dictionary. + for decoder_output_name, tensor in decoder_outputs.items(): + output_feature_utils.set_output_feature_tensor( + output_logits, output_feature_name, decoder_output_name, tensor + ) + + # Save the hidden state of the output feature (for feature dependencies). + output_last_hidden[output_feature_name] = decoder_outputs["last_hidden"] + return output_logits + + def forward( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + mask=None, + ) -> dict[str, torch.Tensor]: + """Forward pass of the model. + + Args: + inputs: Inputs to the model. Can be a dictionary of input names to + input tensors or a tuple of (inputs, targets) where inputs is + a dictionary of input names to input tensors and targets is a + dictionary of target names to target tensors. + mask: A mask for the inputs. + + Returns: + A dictionary of output {feature name}::{tensor_name} -> output tensor. + """ + + if isinstance(inputs, tuple): + inputs, targets = inputs + # Convert targets to tensors. + for target_feature_name, target_value in targets.items(): + if not isinstance(target_value, torch.Tensor): + targets[target_feature_name] = torch.from_numpy(target_value) + else: + targets[target_feature_name] = target_value + else: + targets = None + + if list(inputs.keys()) != list(self.input_features.keys()): + raise ValueError( + f"Input feature keys don't match model's expected features.\n" + f"Expected: {list(self.input_features.keys())}\n" + f"Got: {list(inputs.keys())}" + ) + + encoder_outputs = self.encode(inputs) + if self.modality_dropout is not None: + encoder_outputs = self.modality_dropout(encoder_outputs) + combiner_outputs = self.combine(encoder_outputs) + decoder_outputs = self.decode(combiner_outputs, targets, mask) + + # Compute the number of used tokens. + decoder_outputs[USED_TOKENS] = get_used_tokens_for_ecd(inputs, targets) + return decoder_outputs + + def unskip(self): + for k in self.input_features: + self.input_features.set(k, self.input_features.get(k).unskip()) + + def save(self, save_path): + """Saves the model to the given path using SafeTensors format. + + PyTorch RNN/GRU/LSTM layers pack weights into a contiguous flat buffer after flatten_parameters() is called + during the forward pass on CUDA. Each individual weight (weight_ih_l0, etc.) becomes a view into that buffer. + No single weight covers the entire storage, so safetensors _remove_duplicate_names fails with "None is + covering the entire storage." We clone such partial-view tensors to give each weight independent storage. + + Tensors that cover their full storage — including genuinely tied weights like embedding↔projection tying in + the sequence decoder — are left untouched so save_model() can record the tie relationship in metadata. + """ + from safetensors.torch import save_model + + class _FlatWeightsCleaned(torch.nn.Module): + """Thin wrapper that clones RNN flat-buffer views in state_dict() so save_model() can proceed.""" + + def __init__(self, model): + super().__init__() + self._model = model + + def state_dict(self, *args, **kwargs): + sd = self._model.state_dict(*args, **kwargs) + return {k: v.clone() if v.untyped_storage().nbytes() > v.nbytes else v for k, v in sd.items()} + + weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_SAFETENSORS_FILE_NAME) + save_model(_FlatWeightsCleaned(self), weights_save_path) + # Ensure the file is fully flushed to disk before any other process reads it + with open(weights_save_path, "rb") as f: + os.fsync(f.fileno()) + + def load(self, save_path): + """Loads the model from the given path. + + Tries SafeTensors first (using load_model to restore tied weights), falls back to legacy pickle. + """ + safetensors_path = os.path.join(save_path, MODEL_WEIGHTS_SAFETENSORS_FILE_NAME) + legacy_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME) + device = str(torch.device(get_torch_device())) + + if os.path.exists(safetensors_path): + from safetensors.torch import load_model + + load_model(self, safetensors_path, device=device) + elif os.path.exists(legacy_path): + logger.info("Loading legacy pickle checkpoint (no SafeTensors file found)") + with open_file(legacy_path, "rb") as f: + state_dict = torch.load(f, map_location=device, weights_only=True) + self.load_state_dict(update_state_dict(state_dict)) + else: + # Try open_file for remote paths (fsspec) + try: + with open_file(safetensors_path, "rb") as f: + from safetensors.torch import load as safetensors_load + + # Remote path: load bytes, then load_state_dict (ties may not be restored) + state_dict = safetensors_load(f.read()) + self.load_state_dict(update_state_dict(state_dict)) + except FileNotFoundError: + with open_file(legacy_path, "rb") as f: + state_dict = torch.load(f, map_location=device, weights_only=True) + self.load_state_dict(update_state_dict(state_dict)) + + def get_args(self): + """Returns init arguments for constructing this model.""" + return ( + self.config_obj.input_features.to_list(), + self.config_obj.combiner.to_dict(), + self.config_obj.output_features.to_list(), + self._random_seed, + ) + + def get_augmentation_pipelines(self) -> AugmentationPipelines: + """Returns the augmentation pipeline for this model.""" + # dictionary to hold any augmentation pipeline + augmentation_pipelines = {} + + # loop through all input features and add their augmentation pipeline to the dictionary + for input_feature in self.config_obj.input_features: + # if augmentation was specified for this input feature, add AugmentationPipeline to dictionary + if input_feature.has_augmentation(): + # use input feature proc_column as key because that is what is used in the Batcher + augmentation_pipelines[input_feature.proc_column] = self.input_features.get( + input_feature.name + ).get_augmentation_pipeline() + + return AugmentationPipelines(augmentation_pipelines) diff --git a/ludwig/models/embedder.py b/ludwig/models/embedder.py new file mode 100644 index 0000000..68c2f84 --- /dev/null +++ b/ludwig/models/embedder.py @@ -0,0 +1,135 @@ +from collections.abc import Callable + +import numpy as np +import pandas as pd +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT, MODEL_ECD, NAME, PROC_COLUMN, TYPE +from ludwig.features.feature_registries import get_input_type_registry +from ludwig.features.feature_utils import LudwigFeatureDict +from ludwig.models.base import BaseModel +from ludwig.schema.features.base import BaseInputFeatureConfig, FeatureCollection +from ludwig.schema.features.utils import get_input_feature_cls +from ludwig.types import FeatureConfigDict, TrainingSetMetadataDict +from ludwig.utils.batch_size_tuner import BatchSizeEvaluator +from ludwig.utils.dataframe_utils import from_numpy_dataset +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.torch_utils import get_torch_device, LudwigModule + + +@DeveloperAPI +class Embedder(LudwigModule): + def __init__(self, feature_configs: list[FeatureConfigDict], metadata: TrainingSetMetadataDict): + super().__init__() + + self.input_features = LudwigFeatureDict() + + input_feature_configs = [] + for feature in feature_configs: + feature_cls = get_from_registry(feature[TYPE], get_input_type_registry()) + + # TODO(travis): this assumes ECD is the selected model type. The best solution is to change the + # input params from FeatureConfigDict types to BaseInputFeatureConfig types, which will require a + # refactor of preprocessing to use the schema, not the dict types. + feature_obj = get_input_feature_cls(MODEL_ECD, feature[TYPE]).from_dict(feature) + feature_cls.update_config_with_metadata(feature_obj, metadata[feature[NAME]]) + + # When running prediction or eval, we need the preprocessing to use the original pretrained + # weights, which requires unsetting this field. In the future, we could avoid this by plumbing + # through the saved weights and loading them dynamically after building the model. + feature_obj.encoder.saved_weights_in_checkpoint = False + + input_feature_configs.append(feature_obj) + + feature_collection = FeatureCollection[BaseInputFeatureConfig](input_feature_configs) + try: + self.input_features.update(BaseModel.build_inputs(input_feature_configs=feature_collection)) + except KeyError as e: + raise KeyError( + f"An input feature has a name that conflicts with a class attribute of torch's ModuleDict: {e}" + ) + + def forward(self, inputs: dict[str, torch.Tensor]): + encoder_outputs = {} + for input_feature_name, input_values in inputs.items(): + encoder = self.input_features.get(input_feature_name) + encoder_output = encoder(input_values) + encoder_outputs[input_feature_name] = encoder_output[ENCODER_OUTPUT] + return encoder_outputs + + +@DeveloperAPI +def create_embed_batch_size_evaluator( + features_to_encode: list[FeatureConfigDict], metadata: TrainingSetMetadataDict +) -> BatchSizeEvaluator: + class _EmbedBatchSizeEvaluator(BatchSizeEvaluator): + def __init__(self): + embedder = Embedder(features_to_encode, metadata) + self.device = get_torch_device() + self.embedder = embedder.to(self.device) + self.embedder.eval() + + def step(self, batch_size: int, global_max_sequence_length: int | None = None): + inputs = { + input_feature_name: input_feature.create_sample_input(batch_size=batch_size).to(self.device) + for input_feature_name, input_feature in self.embedder.input_features.items() + } + with torch.no_grad(): + self.embedder(inputs) + + return _EmbedBatchSizeEvaluator + + +@DeveloperAPI +def create_embed_transform_fn( + features_to_encode: list[FeatureConfigDict], metadata: TrainingSetMetadataDict +) -> Callable: + class EmbedTransformFn: + def __init__(self): + embedder = Embedder(features_to_encode, metadata) + self.device = get_torch_device() + self.embedder = embedder.to(self.device) + self.embedder.eval() + + def __call__(self, df: pd.DataFrame) -> pd.DataFrame: + batch = _prepare_batch(df, features_to_encode, metadata) + name_to_proc = {i_feat.feature_name: i_feat.proc_column for i_feat in self.embedder.input_features.values()} + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(self.device) + for i_feat in self.embedder.input_features.values() + } + with torch.no_grad(): + encoder_outputs = self.embedder(inputs) + + encoded = {name_to_proc[k]: v.detach().cpu().float().numpy() for k, v in encoder_outputs.items()} + output_df = from_numpy_dataset(encoded) + + for c in output_df.columns: + df[c] = output_df[c] + + return df + + return EmbedTransformFn + + +# TODO(travis): consolidate with implementation in data/ray.py +def _prepare_batch( + df: pd.DataFrame, features: list[FeatureConfigDict], metadata: TrainingSetMetadataDict +) -> dict[str, np.ndarray]: + batch = {} + for feature in features: + c = feature[PROC_COLUMN] + if df[c].values.dtype == "object": + # Ensure columns stacked instead of turned into np.array([np.array, ...], dtype=object) objects + batch[c] = np.stack(df[c].values) + else: + batch[c] = df[c].to_numpy() + + for feature in features: + c = feature[PROC_COLUMN] + reshape = metadata.get(feature[NAME], {}).get("reshape") + if reshape is not None: + batch[c] = batch[c].reshape((-1, *reshape)) + + return batch diff --git a/ludwig/models/llm.py b/ludwig/models/llm.py new file mode 100644 index 0000000..3b576c4 --- /dev/null +++ b/ludwig/models/llm.py @@ -0,0 +1,901 @@ +import contextlib +import logging +import os +from typing import Any + +import numpy as np +import torch +from transformers import AutoConfig, GenerationConfig + +from ludwig.accounting.used_tokens import get_used_tokens_for_llm +from ludwig.constants import IGNORE_INDEX_TOKEN_ID, LOGITS, MODEL_LLM, PREDICTIONS, TEXT, USED_TOKENS +from ludwig.features.base_feature import NonPropertyModuleWrapper, OutputFeature +from ludwig.features.text_feature import TextOutputFeature +from ludwig.globals import MODEL_WEIGHTS_FILE_NAME +from ludwig.models.base import BaseModel +from ludwig.modules.training_hooks import NEFTuneHook +from ludwig.schema.features.base import BaseOutputFeatureConfig, FeatureCollection +from ludwig.schema.model_types.llm import LLMModelConfig +from ludwig.utils.augmentation_utils import AugmentationPipelines +from ludwig.utils.data_utils import clear_data_cache +from ludwig.utils.llm_quantization_utils import convert_quantized_linear_to_linear +from ludwig.utils.llm_utils import ( + add_left_padding, + generate_merged_ids, + get_context_len, + get_realigned_target_and_prediction_tensors_for_inference, + initialize_adapter, + load_pretrained_from_config, + pad_target_tensor_for_fine_tuning, + remove_left_padding, + to_device, +) +from ludwig.utils.logging_utils import log_once +from ludwig.utils.output_feature_utils import set_output_feature_tensor +from ludwig.utils.tokenizers import HFTokenizer +from ludwig.utils.torch_utils import reg_loss + +logger = logging.getLogger(__name__) + + +class LLM(BaseModel): + @staticmethod + def type() -> str: + return MODEL_LLM + + def __init__( + self, + config_obj: LLMModelConfig, + random_seed=None, + _device=None, + **_kwargs, + ): + super().__init__(random_seed=random_seed) + + self.config_obj = config_obj + self._random_seed = random_seed + self._adapter_initialized = False + + self.model_name = self.config_obj.base_model + self.model_config = AutoConfig.from_pretrained( + self.config_obj.base_model, + trust_remote_code=self.config_obj.trust_remote_code, + ) + + self.model = load_pretrained_from_config(self.config_obj, model_config=self.model_config) + self.curr_device = next(self.model.parameters()).device + logger.info("Done.") + + self.context_len = get_context_len(self.model_config) + + # TODO(Arnav): This needs be more flexible to account for RoPE Scaling + # When merging input IDs and target IDs for LLM fine-tuning, we want to make sure that the merged tensor is + # not longer than the global maximum sequence length. This is provided in the preprocessing config. We never + # want to exceed the maximum possible context length so we also check for that. + if self.config_obj.preprocessing.global_max_sequence_length: + global_max_sequence_length = self.config_obj.preprocessing.global_max_sequence_length + self.global_max_sequence_length = ( + global_max_sequence_length if global_max_sequence_length <= self.context_len else self.context_len + ) + else: + self.global_max_sequence_length = self.context_len + + # Initialize tokenizer + self.tokenizer = HFTokenizer( + self.config_obj.base_model, + trust_remote_code=self.config_obj.trust_remote_code, + ).tokenizer + + self._set_generation_config(self.config_obj.generation.to_dict()) + + # ================ Inputs ================ + try: + self.input_features.update(self.build_inputs(input_feature_configs=self.config_obj.input_features)) + except KeyError as e: + raise KeyError( + f"An input feature has a name that conflicts with a class attribute of torch's ModuleDict: {e}" + ) from e + + # This is used to store the model inputs during the forward pass when fine-tuning LLMs. This allows us to have + # access to the joint model inputs (input_ids and target_ids) when computing metrics. In particular, the target + # ids are needed to correctly compute next token softmax cross entropy loss. + self.model_inputs = None + + # ================ Outputs ================ + self.output_feature_type = self.config_obj.output_features[0].type + + self.output_features.update( + self.build_outputs( + output_feature_configs=self.config_obj.output_features, + # Set the input size to the model vocab size instead of the tokenizer vocab size + # because the model has additional "head" layers that are used to predict the next + # token in the sequence. These head layers can add additional dimensions to the + # logits tensor, beyond the vocab_size dimension. + input_size=self.input_shape[-1] if self.output_feature_type == TEXT else self.model_config.vocab_size, + ) + ) + + # Extract the decoder object for the forward pass + self._output_feature_decoder = NonPropertyModuleWrapper(next(iter(self.output_features.values()))) + + self.attention_masks = None + + clear_data_cache() + + @property + def input_shape(self) -> torch.Size: + """Returns (context_len, hidden_size) — the shape of a single LLM input sequence's hidden states.""" + hidden_size = getattr(self.model_config, "hidden_size", getattr(self.model_config, "d_model", 1)) + return torch.Size([self.context_len, hidden_size]) + + def create_feature_dict(self) -> dict: + """Returns a plain dict instead of LudwigFeatureDict to avoid exposing input/output features as nn.Module + submodules of the LLM. + + This prevents systems like DeepSpeed from picking them up as trainable modules. + """ + return {} + + @contextlib.contextmanager + def use_generation_config(self, generation_config_dict: dict[str, Any] | None = None): + """Sets the generation config for the model.""" + # Save the original generation config so that we can reset it if/when we change it when self.generation gets is + # dynamically mutated during 1-off predict calls after fine-tuning. + original_generation_config_dict = self.generation.to_dict() + try: + # no-op if generation_config is None + if generation_config_dict is not None: + # unwrap the original generation config, update it with the new generation config + new_generation_config_dict = {**original_generation_config_dict, **generation_config_dict} + self._set_generation_config(new_generation_config_dict) + yield + finally: + self._set_generation_config(original_generation_config_dict) + + def _set_generation_config(self, new_generation_config_dict: dict[str, Any]): + self.generation = GenerationConfig(**new_generation_config_dict) + # We need to manually set the pad_token_id to the tokenizer's pad_token_id for certain models like GPT and + # CodeLlama to avoid getting an error. This workaround can be found here: + # (https://github.com/huggingface/transformers/issues/25353#issuecomment-1669339754) + self.generation.pad_token_id = self.tokenizer.pad_token_id + self.max_new_tokens = self.generation.max_new_tokens + # max input length value copied from FastChat + # https://github.com/lm-sys/FastChat/blob/0e958b852a14f4bef5f0e9d7a5e7373477329cf2/fastchat/serve/inference.py#L183 + self.max_input_length = self.context_len - self.max_new_tokens - 8 + + @property + def output_feature_decoder(self) -> OutputFeature: + return self._output_feature_decoder.module + + def initialize_adapter(self): + """If an adapter config is provided, wrap the model with a PEFT model for fine-tuning. + + Handles both the singular ``config_obj.adapter`` and the multi-adapter + ``config_obj.adapters`` forms — the two fields are schema-level mutually exclusive. + Guarded by _adapter_initialized to prevent double-wrapping when called multiple + times (e.g. prepare_for_training is called on every Trainer construction, + including on resume). + """ + has_adapter = bool(self.config_obj.adapter) + has_adapters = getattr(self.config_obj, "adapters", None) is not None + + if (has_adapter or has_adapters) and not self._adapter_initialized: + # Finetune is required for any adapter path unless we're loading pretrained + # adapter weights on the single-adapter fast path. + if self.config_obj.trainer.type != "finetune": + if has_adapter and self.config_obj.adapter.pretrained_adapter_weights: + pass # allowed: pretrained adapter inference + else: + raise ValueError( + "Adapter config was provided, but trainer type is not set to `finetune`. " + "Either set the trainer to `finetune` or remove the adapter config." + ) + + self.model = initialize_adapter(self.model, self.config_obj) + + logger.info("==================================================") + logger.info("Trainable Parameter Summary For Fine-Tuning") + if has_adapter: + logger.info(f"Fine-tuning with adapter: {self.config_obj.adapter.type}") + else: + adapter_types = [ + cfg.get("type") if isinstance(cfg, dict) else cfg.type + for cfg in self.config_obj.adapters.adapters.values() + ] + logger.info( + "Fine-tuning with %d named adapters: %s", + len(adapter_types), + list(zip(self.config_obj.adapters.adapters.keys(), adapter_types)), + ) + self.model.print_trainable_parameters() + logger.info("==================================================") + + self._adapter_initialized = True + + def prepare_for_training(self): + """Prepare the model for training by setting up quantization and adapters. + + Safe to call multiple times (e.g. when resuming from a checkpoint). For the bitsandbytes backend, quantization + is applied at model-load time in __init__ via load_pretrained_from_config, so prepare_model_for_kbit_training + only needs to cast non-quantized layers to float32 and freeze the quantized base — both operations are + idempotent. For the torchao backend, quantization / QAT preparation happens here after load. Adapter + initialization is guarded by _adapter_initialized so the PEFT wrapper is only created once; on resume the saved + adapter weights are subsequently loaded by the trainer via load_state_dict. + """ + quantization = self.config_obj.quantization + backend = getattr(quantization, "backend", "bitsandbytes") if quantization else None + if quantization: + if backend == "bitsandbytes": + self.prepare_for_quantized_training() + elif backend == "torchao": + self._prepare_for_torchao_training() + self.initialize_adapter() + + def prepare_for_quantized_training(self): + from peft import prepare_model_for_kbit_training + + self.model = prepare_model_for_kbit_training(self.model, use_gradient_checkpointing=False) + + def _prepare_for_torchao_training(self): + """Insert torchao fake-quant observers for QAT, or defer PTQ until save for non-QAT. + + * QAT (``quantization.qat: true``): insert fake-quant observers *before* training so + the model is trained in the target low-precision regime. Guarded by + ``_torchao_qat_prepared`` so repeated prepare_for_training calls do not re-wrap. + * PTQ (``quantization.qat: false``): do nothing here — we quantize at save time so + gradients flow through the trainable fp32 weights during fine-tuning. For inference-only + LLM configs this still gives quantized weights at export via ``save``. + """ + quantization = self.config_obj.quantization + if not quantization.qat: + return + if getattr(self, "_torchao_qat_prepared", False): + return + + from ludwig.utils.quantization import prepare_qat_model + + logger.info("Preparing LLM for torchao QAT (mode=%s)", quantization.mode) + self.model = prepare_qat_model(self.model, quantization.mode) + self._torchao_qat_prepared = True + + def to_device(self, device): + # Always refresh curr_device from actual parameter location, since + # nn.Module.to() can move parameters without updating curr_device. + self.curr_device = next(self.model.parameters()).device + self.model, device = to_device(self.model, device, self.config_obj, self.curr_device) + self.curr_device = device + return self + + @classmethod + def build_outputs( + cls, output_feature_configs: FeatureCollection[BaseOutputFeatureConfig], input_size: int + ) -> dict[str, OutputFeature]: + """Builds and returns output feature.""" + # TODO: only single task currently + if len(output_feature_configs) > 1: + raise ValueError("The LLM model type only supports a single output feature.") + + output_feature_config = output_feature_configs[0] + output_feature_config.input_size = input_size + + output_features = {} + output_feature = cls.build_single_output(output_feature_config, output_features) + output_features[output_feature_config.name] = output_feature + + return output_features + + def forward( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + mask=None, + ) -> dict[str, torch.Tensor]: + """Produces logits tensor for finetuning the model. + + Args: + inputs: Inputs to the model. Can be a dictionary of input names to + input tensors or a tuple of (inputs, targets) where inputs is + a dictionary of input names to input tensors and targets is a + dictionary of target names to target tensors. + mask: A mask for the inputs. + + Returns: + A dictionary of output {feature name}::{tensor_name} -> output tensor. + """ + input_ids, target_ids = self._unpack_inputs(inputs) + + # Generate merged input_id, target_id pairs for the model, and create corresponding attention masks. + # If packing is enabled, multiple sequences are packed into each batch entry with block-diagonal + # attention masks preventing cross-sequence attention (2-4x throughput improvement). + packing_enabled = ( + hasattr(self.config_obj, "trainer") + and hasattr(self.config_obj.trainer, "packing") + and self.config_obj.trainer.packing + and self.training + and target_ids is not None + ) + if packing_enabled: + from ludwig.utils.llm_utils import generate_merged_ids_packed + + max_per_pack = getattr(self.config_obj.trainer, "packing_max_sequences_per_pack", 8) + self.model_inputs, self.attention_masks = generate_merged_ids_packed( + input_ids, target_ids, self.tokenizer, self.global_max_sequence_length, max_per_pack + ) + else: + self.model_inputs, self.attention_masks = generate_merged_ids( + input_ids, target_ids, self.tokenizer, self.global_max_sequence_length + ) + + # TODO (jeffkinnison): Determine why the 8-bit `SCB` and `CB` matrices are deleted in the forward pass + model_outputs = self.model(input_ids=self.model_inputs, attention_mask=self.attention_masks).get(LOGITS) + + if self.output_feature_type != TEXT: + # Pass generated tokens through decoder after averaging the token probabilities + # This is required for the classification head for the classifier decoder + model_outputs = torch.mean(model_outputs, dim=1) + + if self.output_feature_type == TEXT: + decoder_outputs = model_outputs + else: + decoder_outputs = self.output_feature_decoder.decoder_obj(model_outputs) + + # Set the output feature tensor to the decoder outputs (logits) + outputs = {} + of_name = self.config_obj.output_features[0].name + set_output_feature_tensor(outputs, of_name, LOGITS, decoder_outputs) + + # Get predictions, probabilities and logits tensor from the output feature's predictions function + outputs = self.output_features.get(of_name).predictions(outputs, of_name) + + # Cast to float32 for metric computation incase we're using deespeed with + # reduced precision such as bfloat16. + for prediction_key, prediction_tensor in outputs.items(): + if prediction_key != PREDICTIONS: + # Skipping casting it to float32 since the predictions are tokens and they should be int64 + # (which is already the case) + outputs[prediction_key] = prediction_tensor.type(torch.float32) + + # Add token usage. + outputs[USED_TOKENS] = get_used_tokens_for_llm(self.model_inputs, self.tokenizer) + return outputs + + def generate( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + mask=None, + ) -> dict[str, torch.Tensor]: + """Generate tokens using the model and extract structured predictions. + + For each sample the method strips left-padding from the prompt, calls + ``model.generate`` with ``output_scores=True`` to obtain per-step + vocabulary logits, and passes both generated sequences and scores to + the decoder so real logits/probs can be computed. An optional + constrained-decoding ``LogitsProcessorList`` is retrieved from the + decoder (used when ``constrain_to_vocabulary=True``). + """ + log_once(f"For generating text, using: {self.generation}") + input_ids, _ = self._unpack_inputs(inputs) + + # Retrieve an optional logits processor from the decoder (e.g. for + # constrained category generation). Returns None for text decoders. + decoder_obj = self.output_feature_decoder.decoder_obj + logits_processor = None + if hasattr(decoder_obj, "get_logits_processor"): + logits_processor = decoder_obj.get_logits_processor() + + with torch.no_grad(): + input_lengths = [] + sequences_list = [] + # Per-sample generation scores: list of tuples of (vocab_size,) tensors. + generation_scores_list: list[tuple[torch.Tensor, ...]] = [] + + for input_ids_sample in input_ids: + input_ids_sample_no_padding = remove_left_padding(input_ids_sample, self.tokenizer) + + if input_ids_sample_no_padding.shape[1] > self.max_input_length: + logger.warning( + f"Input length {input_ids_sample_no_padding.shape[1]} is " + f"greater than max input length {self.max_input_length}. Truncating." + ) + input_ids_sample_no_padding = input_ids_sample_no_padding[:, -self.max_input_length :] + + input_lengths.append(input_ids_sample_no_padding.shape[1]) + + # Ensure input_ids are on the same device as the model + model_device = next(self.model.parameters()).device + input_ids_sample_no_padding = input_ids_sample_no_padding.to(model_device) + + generate_kwargs = { + "input_ids": input_ids_sample_no_padding, + "attention_mask": mask, + "generation_config": self.generation, + "return_dict_in_generate": True, + "output_scores": True, + } + if logits_processor is not None: + generate_kwargs["logits_processor"] = logits_processor + + # Generate text using the model + model_outputs = self.model.generate(**generate_kwargs) + + sequences_list.append(model_outputs.sequences[0]) + + # model_outputs.scores is a tuple of (1, vocab_size) tensors. + # Squeeze out the batch dim so each entry is (vocab_size,). + if model_outputs.scores is not None: + sample_scores = tuple(s.squeeze(0) for s in model_outputs.scores) + else: + sample_scores = () + generation_scores_list.append(sample_scores) + + # Extract predictions, probabilities and logits through the decoder. + # Pass generation scores so real logits/probs can be computed. + outputs = self.output_feature_decoder.decoder_obj.forward( + sequences_list, + input_lengths, + self.max_new_tokens, + generation_scores=generation_scores_list, + ) + + return outputs + + def is_merge_and_unload_set(self) -> bool: + """Check if the "adapter" configuration section exists and, if affirmative, that it contains the + "postprocessor" subsection and the "merge_adapter_into_base_model" and "progressbar" directives. + + # Return + + :return (bool): whether merge_and_unload should be done. + """ + return ( + self.config_obj.adapter is not None + and self.config_obj.adapter.postprocessor is not None + and self.config_obj.adapter.postprocessor.merge_adapter_into_base_model + ) + + def merge_and_unload(self, progressbar: bool = False) -> None: + """This method merges the LoRa layers into the base model. This is needed if someone wants to use the base + model as a standalone model. The implementation calls merge_and_unload() of the underlying LoraModel class + (in peft). + + Args: + progressbar (bool): whether to show a progressbar indicating the unload and merge process + """ + from peft import LoraModel + + if isinstance(self.model.base_model, LoraModel): + self.model.base_model.merge_and_unload(progressbar=progressbar) + else: + raise ValueError("This operation requires an LLM model trained with a LoRA adapter.") + + def _unpack_inputs( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + ) -> tuple[torch.Tensor, torch.Tensor | None]: + """Converts input tensors to input ids.""" + if isinstance(inputs, tuple): + inputs, targets = inputs + # Convert targets to tensors. + for target_feature_name, target_value in targets.items(): + if not isinstance(target_value, torch.Tensor): + targets[target_feature_name] = torch.from_numpy(target_value) + else: + targets[target_feature_name] = target_value + else: + targets = None + + if list(inputs.keys()) != list(self.input_features.keys()): + raise ValueError( + f"Input feature keys don't match model's expected features.\n" + f"Expected: {list(self.input_features.keys())}\n" + f"Got: {list(inputs.keys())}" + ) + + input_ids = self.get_input_ids(inputs) + target_ids = self.get_target_ids(targets) if targets else None + + return input_ids, target_ids + + def get_input_ids( + self, + inputs: ( + dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]] + ), + ) -> torch.Tensor: + """Returns the input ids for the text feature input.""" + return inputs[self.config_obj.input_features[0].name].type(torch.int32) + + def get_target_ids(self, outputs: dict[str, torch.Tensor]) -> torch.Tensor: + """Returns the output ids for the text feature output.""" + return outputs[self.config_obj.output_features[0].name].type(torch.int32) + + def update_metrics(self, targets, predictions): + """Updates the model's metrics given targets and predictions for zero- shot/few-shot.""" + for of_name, of_obj in self.output_features.items(): + if isinstance(of_obj, TextOutputFeature): + # Align the target length with the predictions length to enable text metric evaluation. + _targets, _predictions = get_realigned_target_and_prediction_tensors_for_inference( + targets, predictions, of_name, self.tokenizer + ) + of_obj.update_metrics(_targets[of_name], _predictions[of_name], self.tokenizer) + else: + of_obj.update_metrics(targets[of_name], predictions[of_name]) + + # HACK (Tim): get the device of the targets to transfer self.eval_loss_metric to the same device + target_device = list(targets.values())[0].device + + eval_loss, additional_losses = self.eval_loss(targets, predictions) + self.eval_loss_metric = self.eval_loss_metric.to(target_device) + self.eval_loss_metric.update(eval_loss) + self.eval_additional_losses_metrics.update(additional_losses) + + def update_metrics_finetune_llm(self, targets, predictions): + """Updates the model's metrics given targets and predictions for fine- tuning.""" + _targets, _predictions = targets, predictions + for of_name, of_obj in self.output_features.items(): + if isinstance(of_obj, TextOutputFeature): + # Update the target tensor to enable text metric evaluation. This pads the target tensor with -100s + # to match the prediction length and depends on how much of the target tensor was included in the + # forward pass. + _targets = self._update_target_tensor_for_finetuning(_targets, _predictions, of_name) + if isinstance(of_obj, TextOutputFeature): + of_obj.update_metrics(_targets[of_name], _predictions[of_name], self.tokenizer) + else: + of_obj.update_metrics(_targets[of_name], _predictions[of_name]) + continue + + of_obj.update_metrics(_targets[of_name], _predictions[of_name]) + + eval_loss, additional_losses = self.eval_loss(_targets, _predictions) + self.eval_loss_metric.update(eval_loss) + self.eval_additional_losses_metrics.update(additional_losses) + + def train_loss( + self, + targets, + predictions, + regularization_type: str | None = None, + regularization_lambda: float | None = None, + ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]: + """Computes the training loss for the model. + + Args: + targets: A dictionary of target names to target tensors. + predictions: A dictionary of output names to output tensors. + regularization_type: One of 'l1', 'l2', 'l1_l2', or None. + regularization_lambda: The regularization lambda. + + Returns: + A tuple of the loss tensor and a dictionary of loss for every + output feature. + """ + train_loss = 0 + of_train_losses = {} + for of_name, of_obj in self.output_features.items(): + _targets, _predictions = targets, predictions + if isinstance(of_obj, TextOutputFeature): + _predictions = {of_name: _predictions} + + # Update the target tensor to enable text metric evaluation. This pads the target tensor with -100s + # to match the prediction length and depends on how much of the target tensor was included in the + # forward pass. + _targets = self._update_target_tensor_for_finetuning(_targets, _predictions, of_name) + + # TODO(Arnav): Seems like doing this again and going between these format types in unnecessary, but + # refactor so that we don't have to do this at a later point. + predictions = {} + for key, _ in _predictions[of_name].items(): + set_output_feature_tensor(predictions, of_name, key, _predictions[of_name][key]) + _predictions = predictions + + of_train_loss = of_obj.train_loss(_targets[of_name], _predictions, of_name) + train_loss += of_obj.loss.weight * of_train_loss + of_train_losses[of_name] = of_train_loss + + additional_losses = self.losses() + if additional_losses: + train_loss += torch.sum(torch.stack(additional_losses)) # other losses + + # Add regularization loss + if regularization_type is not None and regularization_lambda != 0: + train_loss += reg_loss(self, regularization_type, l1=regularization_lambda, l2=regularization_lambda) + + return train_loss, of_train_losses + + def eval_loss(self, targets, predictions): + """Computes all evaluation losses for the model given targets and predictions. + + Args: + targets: A dictionary of target names to target tensors. + predictions: A dictionary of output names to output tensors. + + Returns: + A tuple of loss values for eval losses and additional losses. + """ + eval_loss = 0 + for of_name, of_obj in self.output_features.items(): + if isinstance(of_obj, TextOutputFeature): + # Align the target length with the predictions length to enable text metric evaluation. + _targets, _predictions = get_realigned_target_and_prediction_tensors_for_inference( + targets, predictions, of_name, self.tokenizer + ) + of_eval_loss = of_obj.eval_loss(_targets[of_name], _predictions[of_name]) + else: + # HACK(geoffrey): we need a non-empty loss, so we just fill it with zeros + of_eval_loss = torch.tensor(0.0).to(predictions[of_name][LOGITS].device) + + eval_loss += of_obj.loss.weight * of_eval_loss + + additional_loss = 0 + additional_losses = self.losses() + if additional_losses: + additional_loss = torch.sum(torch.stack(additional_losses)) # other losses + + return eval_loss, additional_loss + + def outputs_to_predictions(self, outputs: dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]: + """Returns the model's predictions for each output feature.""" + predictions = {} + for of_name in self.output_features: + # TODO(travis): this will need to change when we support multiple output features + predictions[of_name] = outputs + return predictions + + def save(self, save_path): + """Saves the model to the given path.""" + # TODO(travis): use the implementation of trainer itself to decide whether to save the model, to + # avoid this hack + if self.config_obj.trainer.type != "none": + weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME) + self._apply_torchao_save_time_quantization() + # We initialize the model's generation configuration; otherwise, we get a validation error. + self.model.generation_config = self.generation + self.model.save_pretrained(weights_save_path) + else: + logger.info("Skipped saving LLM without weight adjustments.") + + def _apply_torchao_save_time_quantization(self): + """For the torchao backend, quantize (or convert QAT -> quantized) at save time. + + * PTQ (``qat: false``): apply torchao PTQ now so the saved weights are quantized. + * QAT (``qat: true``): convert the QAT-prepared model (fake-quant observers) to real + quantized weights via ``convert_qat_model``. + + Idempotent via ``_torchao_quantized``: if training checkpoints call save() multiple + times we don't double-quantize. + """ + quantization = self.config_obj.quantization + if not quantization or getattr(quantization, "backend", "bitsandbytes") != "torchao": + return + if getattr(self, "_torchao_quantized", False): + return + + from ludwig.utils.quantization import convert_qat_model, quantize_model + + if quantization.qat: + logger.info("Converting QAT-prepared LLM to %s quantized weights for save", quantization.mode) + self.model = convert_qat_model(self.model, quantization.mode) + else: + logger.info("Applying torchao PTQ (%s) before saving LLM", quantization.mode) + self.model = quantize_model(self.model, quantization.mode) + self._torchao_quantized = True + + def save_base_model(self, save_path): + """Saves the base LLM model to the given path.""" + # TODO: see the "TODO" statement from "LLM.save()" in this module. + if self.config_obj.trainer.type != "none": + weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME) + self.model.base_model.save_pretrained(weights_save_path) + # While this class initializes the tokenizer (from the base_model) automatically, and hence does not + # need to be saved if inference is to be done using LudwigModel.predict(), the rationale for saving the + # tokenizer to HuggingFace Hub is to provide access to models fine-tuned and persisted to HuggingFace Hub + # using Ludwig at a later time, with the ability to perform inference, independently of Ludwig itself. + self.tokenizer.save_pretrained(weights_save_path) + else: + logger.info("Skipped saving LLM without weight adjustments.") + + def save_dequantized_base_model(self, save_path: str) -> None: + """Upscales quantized weights of a model to fp16 and saves the result in a folder specified by save_path. + + Args: + save_path (str): The path to the folder where the upscaled model weights will be saved. + + Returns: + None + """ + from peft import PeftModel + + if isinstance(self.model, PeftModel): + # Get the base model back by removing all the adapter modules without merging. + logger.warning( + "LLM model is currently wrapped in a PeftModel. Removing the adapter layers and saving the base model." + "Reload the model via LudwigModel.load() to use your trained adapter layers for inference." + ) + self.model = self.model.unload() + + # Dequantize the model weights and cast them to fp16 - replace quantized layers with appropriate + # linear layers in-place. + logger.info("Upscaling quantized weights to fp16...") + convert_quantized_linear_to_linear(self.model) + logger.info("Done.") + + # Remove the quantization configuration from the model + # The reason we can't delete the quantization config is because it is a property of the model and + # HF does some weird serialization of the config that causes an error when trying to access `self.model.config` + # after you try and delete a key from the config: TypeError: Object of type dtype is not JSON serializable. + self.model.config.quantization_config = {} + + # Override properties of the model to indicate that it is no longer quantized. + # This is also necessary to ensure that the model can be saved, otherwise it will raise an error like + # "You are calling `save_pretrained` on a 4-bit converted model. This is currently not supported" + # See: https://github.com/huggingface/transformers/blob/0ad4e7e6dad670a7151aaceb1af3c272a3bf73a8/src/transformers/modeling_utils.py#L2054 + self.model.is_loaded_in_4bit = False + self.model.is_loaded_in_8bit = False + + # Save the model + logger.info(f"Saving upscaled model to {save_path}") + self.model.save_pretrained(save_path) + logger.info("Done.") + + # Save the tokenizer + logger.info(f"Saving tokenizer to {save_path}") + self.tokenizer.save_pretrained(save_path) + logger.info("Done.") + + def load(self, save_path): + """Loads the model from the given path.""" + weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME) + if getattr(self.config_obj, "adapters", None) is not None: + self._load_multi_adapters(weights_save_path) + elif self.config_obj.adapter: + # Check if the saved weights are merged (no adapter_config.json) or adapter-only + adapter_config_path = os.path.join(weights_save_path, "adapter_config.json") + if os.path.exists(adapter_config_path): + from peft import PeftModel + + if isinstance(self.model, PeftModel): + # Unwrap and reload PeftModel + self.model = self.model.base_model + + self.model = PeftModel.from_pretrained(self.model, weights_save_path) + else: + # Weights were already merged (merge_and_unload was done before save), + # so load as a regular pretrained model. + logger.info("Loading merged LoRA weights (no adapter_config.json found).") + self.model = load_pretrained_from_config( + self.config_obj, model_config=self.model_config, weights_save_path=weights_save_path + ) + elif self.config_obj.trainer.type != "none": + self.model = load_pretrained_from_config( + self.config_obj, model_config=self.model_config, weights_save_path=weights_save_path + ) + else: + logger.info("Skipped loading LLM without weight adjustments.") + + def _load_multi_adapters(self, weights_save_path: str) -> None: + """Reload a multi-adapter PeftModel saved via `save_pretrained`. + + PEFT stores each named adapter under a subdirectory at `weights_save_path//`. We load the first adapter + via `PeftModel.from_pretrained(base, path, adapter_name=name)` and every additional adapter via + `peft_model.load_adapter(path, adapter_name=name)`. The merged adapter (if configured) was saved just like any + other named adapter — it is loaded the same way, no special case needed. Finally we activate whichever adapter + the config declares via `set_adapter(active)`. + """ + from peft import PeftModel + + adapters_cfg = self.config_obj.adapters + names = list(adapters_cfg.adapters.keys()) + if adapters_cfg.merge is not None: + names = names + [adapters_cfg.merge.name] + + if isinstance(self.model, PeftModel): + # Already wrapped (e.g. resuming from an in-memory init); peel back to the base. + self.model = self.model.base_model + + first_name = names[0] + first_adapter_dir = os.path.join(weights_save_path, first_name) + if os.path.exists(first_adapter_dir): + self.model = PeftModel.from_pretrained(self.model, first_adapter_dir, adapter_name=first_name) + else: + # Fallback: PEFT may have saved adapters directly at `weights_save_path` rather + # than in per-name subdirectories (old save layout). Use the original path. + self.model = PeftModel.from_pretrained(self.model, weights_save_path, adapter_name=first_name) + + for name in names[1:]: + adapter_dir = os.path.join(weights_save_path, name) + load_path = adapter_dir if os.path.exists(adapter_dir) else weights_save_path + self.model.load_adapter(load_path, adapter_name=name) + + active = adapters_cfg.active or names[0] + self.model.set_adapter(active) + logger.info("Reloaded adapters: %s (active=%s)", names, active) + + def get_args(self): + """Returns init arguments for constructing this model.""" + return ( + self.config_obj.input_features.to_list(), + self.config_obj.output_features.to_list(), + self._random_seed, + ) + + def _update_target_tensor_for_finetuning( + self, targets: dict[str, torch.Tensor], predictions: dict[str, torch.Tensor], of_name: str + ) -> dict[str, torch.Tensor]: + """Update target tensor for fine-tuning. + + This method removes left padding from target tensors, adds a eos token to the end of the target tensors, + and pads the target tensors with -100 to ensure equal length for loss computation. It then realigns the + target tensors with the prediction tensors. + + Args: + targets (Dict[str, torch.Tensor]): A dictionary containing the target tensors. + predictions (Dict[str, torch.Tensor]): A dictionary containing the predicted tensors. + of_name (str): The name of the target tensor. + + Returns: + Dict[str, torch.Tensor]: A dictionary containing the updated target tensors aligned with predictions. + """ + # Remove left padding from target tensors since we also do this for the model's forward pass when we + # concatenate the input_ids with the target_ids. We also need to add the pad token to the end of the + # target tensors. + targets_without_padding = [] + lengths = [] + + eos_token_tensor = torch.tensor([self.tokenizer.eos_token_id]) + for target in targets[of_name]: + target = remove_left_padding(target, self.tokenizer)[0] + target = torch.cat([target, eos_token_tensor.to(device=target.device)], dim=-1).unsqueeze(0) + targets_without_padding.append(target) + lengths.append(target.shape[1]) + + # We need all target tensors to have the same length for the loss computation. We pad the target + # tensors with -100 since we want to negate all tokens that are not target_ids during the softmax + # cross entropy loss computation. This ensures that the loss is computed only for the target tokens. + max_length = max(lengths) + for i, target in enumerate(targets_without_padding): + targets_without_padding[i] = add_left_padding( + targets_without_padding[i][0], + max_length, + IGNORE_INDEX_TOKEN_ID, + ) + + targets[of_name] = torch.stack(targets_without_padding, dim=0).to( + dtype=targets[of_name].dtype, + device=targets[of_name].device, + ) + + # Re-align target tensors without padding to have equal length before realigning with the prediction + # tensors. Padding left with -100 to match the length of the target tensor masks the input ids during + # softmax cross entropy loss computation. This ensures that the loss is computed only for the target + # token IDs. Examples: + # BERTLMHead: https://github.com/huggingface/transformers/blob/v4.29.1/src/transformers/models/bert/modeling_bert.py#L1216-L1219 + # GPTNeoForCausalLM: https://github.com/huggingface/transformers/blob/v4.29.1/src/transformers/models/gpt_neo/modeling_gpt_neo.py#L736 + _targets = pad_target_tensor_for_fine_tuning(targets, predictions, self.model_inputs, of_name) + + return _targets + + def _activate_forward_hooks(self): + """Activates/registers forward hooks for the model.""" + if not self.config_obj.model_parameters: + return + + # Initialize forward hook handles + if self.config_obj.model_parameters.neftune_noise_alpha: + self._forward_hook_handles.append( + NEFTuneHook(neftune_noise_alpha=self.config_obj.model_parameters.neftune_noise_alpha) + ) + + # Activate forward hooks iteratively + for hook in self._forward_hook_handles: + # Update the model with the forward hooks in place + self.model = hook.activate_hook(self.model) + + @staticmethod + def get_augmentation_pipelines() -> AugmentationPipelines: + """Returns the augmentation pipeline for this model.""" + return AugmentationPipelines({}) diff --git a/ludwig/models/predictor.py b/ludwig/models/predictor.py new file mode 100644 index 0000000..25b94b8 --- /dev/null +++ b/ludwig/models/predictor.py @@ -0,0 +1,571 @@ +import logging +import os +import sys +import tempfile +from abc import ABC, abstractmethod +from collections import defaultdict, OrderedDict +from pprint import pformat + +import numpy as np +import pandas as pd +import psutil +import torch +from torch import nn + +from ludwig.constants import COMBINED, LAST_HIDDEN, LOGITS, MODEL_ECD, MODEL_LLM +from ludwig.data.dataset.base import Dataset +from ludwig.data.utils import convert_to_dict +from ludwig.distributed.base import DistributedStrategy, LocalStrategy +from ludwig.globals import is_progressbar_disabled, PREDICTIONS_PARQUET_FILE_NAME, TEST_STATISTICS_FILE_NAME +from ludwig.models.base import BaseModel +from ludwig.progress_bar import LudwigProgressBar +from ludwig.utils.data_utils import save_csv, save_json +from ludwig.utils.dataframe_utils import from_numpy_dataset +from ludwig.utils.print_utils import repr_ordered_dict +from ludwig.utils.registry import Registry +from ludwig.utils.strings_utils import make_safe_filename +from ludwig.utils.torch_utils import get_torch_device + +EXCLUDE_PRED_SET = {LOGITS, LAST_HIDDEN} +SKIP_EVAL_METRICS = {"confusion_matrix", "roc_curve"} +STATS_SAMPLE_SIZE = 10000 + +logger = logging.getLogger(__name__) + + +class BasePredictor(ABC): + @abstractmethod + def batch_predict(self, dataset, dataset_name=None): + raise NotImplementedError() + + @abstractmethod + def predict_single(self, batch): + raise NotImplementedError() + + @abstractmethod + def batch_evaluation(self, dataset, collect_predictions=False, collect_logits=False, dataset_name=None): + raise NotImplementedError() + + @abstractmethod + def batch_collect_activations(self, layer_names, dataset, bucketing_field=None): + raise NotImplementedError() + + # Remote implementations may override this + def shutdown(self): + pass + + # Functions needed to treat Trainer as a context manager + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown() + + +_predictor_registry = Registry[BasePredictor]() + + +def register_predictor(model_types: list[str]): + def wrap(cls): + for model_type in model_types: + _predictor_registry[model_type] = cls + return cls + + return wrap + + +def get_predictor_cls(model_type: str) -> type[BasePredictor]: + return _predictor_registry[model_type] + + +@register_predictor([MODEL_ECD]) +class Predictor(BasePredictor): + """Predictor is a class that uses a model to predict and evaluate.""" + + def __init__( + self, + dist_model: nn.Module, + batch_size: int = 128, + distributed: DistributedStrategy = None, + report_tqdm_to_ray: bool = False, + model: BaseModel | None = None, + remote: bool = False, + **kwargs, + ): + """ + Args: + dist_model: model to use for prediction, post-wrap for distributed training. + batch_size: batch size to use for prediction. + distributed: distributed strategy to use for prediction. + report_tqdm_to_ray: whether to report tqdm progress to Ray. + model: Ludwig BaseModel before being wrapped for distributed training. Used to call Ludwig helper + functions. + """ + model = model or dist_model + if not isinstance(model, BaseModel): + raise TypeError( + f"model must be a BaseModel instance, got {type(model).__name__}.\n" + f"Fix: pass a Ludwig BaseModel (ECD or LLM) as the model argument." + ) + + self._batch_size = batch_size + self._distributed = distributed if distributed is not None else LocalStrategy() + self.report_tqdm_to_ray = report_tqdm_to_ray + + device = get_torch_device() + self.device = device + self.dist_model = dist_model + self.model = model + self.model.metrics_to_device(device) + + if remote: + # Only return results from rank 0 to reduce network overhead + self.batch_predict = self._distributed.return_first(self.batch_predict) + self.batch_evaluation = self._distributed.return_first(self.batch_evaluation) + + def batch_predict(self, dataset: Dataset, dataset_name: str | None = None, collect_logits: bool = False): + self.dist_model = self._distributed.to_device(self.dist_model) + prev_model_training_mode = self.dist_model.training # store previous model training mode + self.dist_model.eval() # set model to eval mode + + with torch.no_grad(): + with dataset.initialize_batcher(self._batch_size, should_shuffle=False) as batcher: + progress_bar_config = { + "desc": "Prediction" if dataset_name is None else f"Prediction {dataset_name: <5.5}", + "total": batcher.steps_per_epoch, + "file": sys.stdout, + "disable": is_progressbar_disabled(), + } + progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator()) + predictions = defaultdict(list) + while not batcher.last_batch(): + batch = batcher.next_batch() + preds = self._predict(batch) + self._accumulate_preds( + preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET + ) + progress_bar.update(1) + + progress_bar.close() + + # consolidate predictions from each batch to a single tensor + self._concat_preds(predictions) + + self.dist_model.train(prev_model_training_mode) + + return from_numpy_dataset(predictions) + + def predict_single(self, batch, collect_logits: bool = False): + prev_model_training_mode = self.dist_model.training # store previous model training mode + self.dist_model.eval() # set model to eval mode + + with torch.no_grad(): + predictions = defaultdict(list) + preds = self._predict(batch) + self._accumulate_preds( + preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET + ) + self._concat_preds(predictions) + + # reset model to its original training mode + self.dist_model.train(prev_model_training_mode) + return from_numpy_dataset(predictions) + + def _predict(self, batch: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + """Predict a batch of data. + + Params: + model: BaseModel model + batch: batch of data + + Returns: + predictions: dictionary of predictions + """ + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(self.device) + for i_feat in self.model.input_features.values() + } + + outputs = self._predict_on_inputs(inputs) + return self.model.outputs_to_predictions(outputs) + + def _accumulate_preds(self, preds, predictions, exclude_pred_set=EXCLUDE_PRED_SET): + # accumulate predictions from batch for each output feature + for of_name, of_preds in preds.items(): + for pred_name, pred_values in of_preds.items(): + if pred_name not in exclude_pred_set: + key = f"{of_name}_{pred_name}" + predictions[key].append(pred_values.detach().cpu()) + + def _concat_preds(self, predictions): + for key, pred_value_list in predictions.items(): + # Without detaching, a runtime error is raised since pred_value_list + # is a tensor that requires grad. + predictions[key] = torch.cat(pred_value_list, dim=0).numpy() + + def batch_evaluation(self, dataset, collect_predictions=False, collect_logits=False, dataset_name=None): + """Batch evaluate model on dataset. + + Params: + dataset (Union[str, dict, pandas.DataFrame]): source containing the entire dataset to be evaluated. + collect_predictions: Return model predictions. + collect_logits: Return model logits and final layer activations. + + Returns: + Tuple of dictionaries of (metrics, predictions). The keys of metrics are determined by the metrics in the + model config. The keys of the predictions dictionary depend on which values are requested by the caller: + collect_predictions, collect_logits. + """ + self.dist_model = self._distributed.to_device(self.dist_model) + prev_model_training_mode = self.dist_model.training # store previous model training mode + self.dist_model.eval() # set model to eval mode + + with torch.no_grad(): + with dataset.initialize_batcher( + self._batch_size, should_shuffle=False, distributed=self._distributed + ) as batcher: + progress_bar_config = { + "desc": "Evaluation" if dataset_name is None else f"Evaluation {dataset_name: <5.5}", + "total": batcher.steps_per_epoch, + "file": sys.stdout, + "disable": is_progressbar_disabled(), + "position": 0, # Necessary to disable extra new line artifacts in training logs. + } + progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator()) + + predictions = defaultdict(list) + eval_steps = ( + self.dist_model.config_obj.trainer.eval_steps + if hasattr(self.dist_model, "config_obj") + and hasattr(self.dist_model.config_obj.trainer, "eval_steps") + else None + ) + eval_steps_counter = 0 + while not batcher.last_batch(): + if eval_steps and eval_steps_counter >= eval_steps: + logger.info(f"Reached evaluation step {eval_steps}. Ending evaluation.") + break + batch = batcher.next_batch() + logger.debug( + f"evaluation for {dataset_name}: obtained next batch " + f"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB" + ) + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to( + self.device + ) + for i_feat in self.model.input_features.values() + } + targets = { + o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to( + self.device + ) + for o_feat in self.model.output_features.values() + } + + outputs = self._predict_on_inputs(inputs) + preds = self.model.outputs_to_predictions(outputs) + self.model.update_metrics(targets, preds) + + # accumulate predictions from batch for each output feature + if collect_predictions: + self._accumulate_preds( + preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET + ) + + progress_bar.update(1) + eval_steps_counter += 1 + if self.is_coordinator(): + logger.debug( + f"evaluation for {dataset_name}: completed batch {progress_bar.total_steps} " + f"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB" + ) + progress_bar.close() + + # consolidate predictions from each batch to a single tensor + if collect_predictions: + self._concat_preds(predictions) + + metrics = self.model.get_metrics() + self.model.reset_metrics() + + self.dist_model.train(prev_model_training_mode) # Restores previous model training mode. + + return metrics, from_numpy_dataset(predictions) + + def batch_collect_activations(self, layer_names, dataset, bucketing_field=None): + """Collect activations from the model for the given dataset. + + Uses disk offloading to avoid OOM on large datasets: each batch's activations + are written to a temporary .npy file and the final result is assembled by + loading them one at a time. Peak RAM is one batch at a time rather than the + full dataset. + """ + if bucketing_field: + raise ValueError("BucketedBatcher is not supported yet") + + self.dist_model = self._distributed.to_device(self.dist_model) + prev_model_training_mode = self.dist_model.training # store previous model training mode + self.dist_model.eval() # set model to eval mode + + with tempfile.TemporaryDirectory() as tmp_dir: + # Maps layer name -> list of per-batch .npy file paths + batch_files: dict[str, list[str]] = {} + + with torch.no_grad(): + with dataset.initialize_batcher( + self._batch_size, should_shuffle=False, distributed=self._distributed + ) as batcher: + progress_bar_config = { + "desc": "Collecting Tensors", + "total": batcher.steps_per_epoch, + "file": sys.stdout, + "disable": is_progressbar_disabled(), + } + progress_bar = LudwigProgressBar( + self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator() + ) + + batch_idx = 0 + while not batcher.last_batch(): + batch = batcher.next_batch() + + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to( + self.device + ) + for i_feat in self.model.input_features.values() + } + outputs = self._predict_on_inputs(inputs) + for name, tensor in outputs.items(): + if name not in batch_files: + batch_files[name] = [] + if isinstance(tensor, torch.Tensor): + path = os.path.join(tmp_dir, f"{make_safe_filename(name)}_{batch_idx}.npy") + np.save(path, tensor.detach().cpu().numpy()) + batch_files[name].append(path) + else: + # Non-tensor (e.g., used_tokens list): accumulate normally. + # These are small metadata values, not large activation tensors. + if name not in batch_files: + batch_files[name] = [] + batch_files[name].append(tensor) + batch_idx += 1 + progress_bar.update(1) + + progress_bar.close() + + self.dist_model.train(prev_model_training_mode) + + # Assemble results: load batch files one at a time to cap peak RAM usage. + collected_tensors = [] + for name, items in batch_files.items(): + if items and isinstance(items[0], str): + # Disk-offloaded tensors: load and concatenate. + arrays = [np.load(f) for f in items] + combined = np.concatenate(arrays, axis=0) + collected_tensors.append((name, torch.from_numpy(combined))) + else: + # Non-tensor metadata: flatten list of batch items. + flat = [x for batch in items for x in (batch if isinstance(batch, list) else [batch])] + collected_tensors.append((name, flat)) + + return collected_tensors + + def _predict_on_inputs(self, inputs: dict) -> dict: + return self.dist_model(inputs) + + def is_coordinator(self): + return self._distributed.rank() == 0 + + +@register_predictor([MODEL_LLM]) +class LlmPredictor(Predictor): + def _predict_on_inputs(self, inputs: dict) -> dict: + return self.dist_model.generate(inputs) + + +class LlmFineTunePredictor(Predictor): + def batch_evaluation(self, dataset, collect_predictions=False, collect_logits=False, dataset_name=None): + """Batch evaluate model on dataset. + + Params: + dataset (Union[str, dict, pandas.DataFrame]): source containing the entire dataset to be evaluated. + collect_predictions: Return model predictions. + collect_logits: Return model logits and final layer activations. + + Returns: + Tuple of dictionaries of (metrics, predictions, input/target/output dictionary). The keys of metrics are + determined by the metrics in the model config. The keys of the predictions dictionary depend on which values + are requested by the caller: collect_predictions, collect_logits. The keys of the input/target/output + dictionary are "inputs", "targets", and "outputs". The values of each of these keys are dictionaries of + feature names to lists of tensors. The tensors are the inputs, targets, and outputs for each batch. + """ + prev_model_training_mode = self.dist_model.training # store previous model training mode + self.dist_model.eval() # set model to eval mode + example_inputs = defaultdict(list) + example_targets = defaultdict(list) + example_outputs = defaultdict(list) + with torch.no_grad(): + with dataset.initialize_batcher( + self._batch_size, should_shuffle=False, distributed=self._distributed + ) as batcher: + progress_bar_config = { + "desc": "Evaluation" if dataset_name is None else f"Evaluation {dataset_name: <5.5}", + "total": batcher.steps_per_epoch, + "file": sys.stdout, + "disable": is_progressbar_disabled(), + "position": 0, # Necessary to disable extra new line artifacts in training logs. + } + progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator()) + + predictions = defaultdict(list) + eval_steps = ( + self.dist_model.config_obj.trainer.eval_steps + if hasattr(self.dist_model, "config_obj") + and hasattr(self.dist_model.config_obj.trainer, "eval_steps") + else None + ) + eval_steps_counter = 0 + while not batcher.last_batch(): + if eval_steps and eval_steps_counter >= eval_steps: + logger.info(f"Reached evaluation step {eval_steps}. Ending evaluation.") + break + batch = batcher.next_batch() + logger.debug( + f"evaluation for {dataset_name}: obtained next batch " + f"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB" + ) + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to( + self.device + ) + for i_feat in self.model.input_features.values() + } + targets = { + o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to( + self.device + ) + for o_feat in self.model.output_features.values() + } + + outputs = self._predict_on_inputs((inputs, targets)) + preds = self.model.outputs_to_predictions(outputs) + + for key in inputs: + example_inputs[key].extend(inputs[key]) + for key in targets: + example_targets[key].extend(targets[key]) + for key in preds: + example_outputs[key].extend(preds[key]["predictions"]) + + # Need to pass through a custom fine-tune metric function because we need to transform + # the targets into the right format for loss calculation (requires padding with -100s to the left) + # and other tensor alignment. + self.model.update_metrics_finetune_llm(targets, preds) + + # accumulate predictions from batch for each output feature + if collect_predictions: + self._accumulate_preds( + preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET + ) + + progress_bar.update(1) + eval_steps_counter += 1 + if self.is_coordinator(): + logger.debug( + f"evaluation for {dataset_name}: completed batch {progress_bar.total_steps} " + f"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB" + ) + + progress_bar.close() + + # consolidate predictions from each batch to a single tensor + if collect_predictions: + for key, pred_value_list in predictions.items(): + predictions[key] = torch.cat(pred_value_list, dim=0).detach().cpu().numpy() + + metrics = self.model.get_metrics() + self.model.reset_metrics() + + input_target_output_dict = { + "inputs": example_inputs, + "targets": example_targets, + "outputs": example_outputs, + } + + self.dist_model.train(prev_model_training_mode) # Restores previous model training mode. + return metrics, from_numpy_dataset(predictions), input_target_output_dict + + +def calculate_overall_stats(output_features, predictions, dataset, training_set_metadata): + overall_stats = {} + for of_name, output_feature in output_features.items(): + feature_metadata = training_set_metadata[output_feature.feature_name] + feature_metadata.update(training_set_metadata[output_feature.feature_name]) + + feature_df = predictions.loc[:, [c for c in predictions.columns if str(c).startswith(of_name)]] + feature_df = feature_df.rename(columns=lambda c: c[len(of_name) + 1 :]) + + target = dataset.loc[:, output_feature.proc_column] + + if not isinstance(feature_df, pd.DataFrame): + logger.warning( + "Full computation of stats only supported for pandas dataframes. " + "Sampling the first 10000 rows of the feature and target dataframes for computing overall stats." + ) + feature_df = feature_df.head(n=STATS_SAMPLE_SIZE, npartitions=-1, compute=True) + target = target.head(n=STATS_SAMPLE_SIZE, npartitions=-1, compute=True) + + overall_stats[of_name] = output_feature.calculate_overall_stats( + feature_df, # predictions + target, + feature_metadata, # output feature metadata + ) + return overall_stats + + +def save_prediction_outputs( + postprocessed_output, + output_features, + output_directory, + backend, +): + backend.df_engine.write_predictions( + postprocessed_output, os.path.join(output_directory, PREDICTIONS_PARQUET_FILE_NAME) + ) + if not backend.df_engine.partitioned: + # csv can only be written out for unpartitioned df format (i.e., pandas) + postprocessed_dict = convert_to_dict(postprocessed_output, output_features) + csv_filename = os.path.join(output_directory, "{}_{}.csv") + for output_field, outputs in postprocessed_dict.items(): + for output_name, values in outputs.items(): + save_csv(csv_filename.format(output_field, make_safe_filename(output_name)), values) + + +def save_evaluation_stats(test_stats, output_directory): + test_stats_fn = os.path.join(output_directory, TEST_STATISTICS_FILE_NAME) + save_json(test_stats_fn, test_stats) + + +def print_evaluation_stats(test_stats): + for output_field, result in test_stats.items(): + if output_field != COMBINED or (output_field == COMBINED and len(test_stats) > 2): + logger.info(f"\n===== {output_field} =====") + for metric in sorted(list(result)): + if metric not in SKIP_EVAL_METRICS: + value = result[metric] + if isinstance(value, OrderedDict): + value_repr = repr_ordered_dict(value) + else: + value_repr = pformat(result[metric], indent=2) + logger.info(f"{metric}: {value_repr}") + + +def get_output_columns(output_features, include_logits: bool = False): + output_columns = [] + for of_name, feature in output_features.items(): + for pred in feature.get_prediction_set(): + if pred not in EXCLUDE_PRED_SET or (pred == LOGITS and include_logits): + output_columns.append(f"{of_name}_{pred}") + return output_columns diff --git a/ludwig/models/registry.py b/ludwig/models/registry.py new file mode 100644 index 0000000..8bd7d4c --- /dev/null +++ b/ludwig/models/registry.py @@ -0,0 +1,13 @@ +import logging + +from ludwig.constants import MODEL_ECD, MODEL_LLM +from ludwig.models.ecd import ECD +from ludwig.models.llm import LLM + +logger = logging.getLogger(__name__) + + +model_type_registry = { + MODEL_ECD: ECD, + MODEL_LLM: LLM, +} diff --git a/ludwig/models/retrieval.py b/ludwig/models/retrieval.py new file mode 100644 index 0000000..edc153e --- /dev/null +++ b/ludwig/models/retrieval.py @@ -0,0 +1,222 @@ +import hashlib +import json +import os +from abc import ABC, abstractmethod +from collections.abc import Callable +from typing import Any, TYPE_CHECKING + +import numpy as np +import pandas as pd +from tqdm import tqdm + +from ludwig.vector_index import FAISS, get_vector_index_cls +from ludwig.vector_index.base import VectorIndex + +if TYPE_CHECKING: + from sentence_transformers import SentenceTransformer + + from ludwig.backend.base import Backend + +from ludwig.utils.batch_size_tuner import BatchSizeEvaluator +from ludwig.utils.torch_utils import get_torch_device + + +def df_checksum(df: pd.DataFrame) -> str: + return hashlib.sha1(pd.util.hash_pandas_object(df).values).hexdigest() + + +def df_to_row_strs(df: pd.DataFrame) -> list[str]: + rows = df.to_dict(orient="records") + row_strs = [json.dumps(r) for r in rows] + return row_strs + + +class RetrievalModel(ABC): + @abstractmethod + def create_dataset_index(self, df: pd.DataFrame, backend: "Backend", columns_to_index: list[str] | None = None): + """Creates an index for the dataset. + + If `columns_to_index` is None, all columns are indexed. Otherwise, only the columns in `columns_to_index` are + used for indexing, but all columns in `df` are returned in the search results. + """ + + @abstractmethod + def search( + self, df, backend: "Backend", k: int = 10, return_data: bool = False + ) -> list[int] | list[dict[str, Any]]: + """Retrieve the top k results for the given query. + + If `return_data` is True, returns the data associated with the indices. Otherwise, returns the indices. + """ + + @abstractmethod + def save_index(self, name: str, cache_directory: str): + """Saves the index to the cache directory.""" + + @abstractmethod + def load_index(self, name: str, cache_directory: str): + """Loads the index from the cache directory.""" + + +class RandomRetrieval(RetrievalModel): + """Random retrieval model. + + Gets k random indices from the dataset regardless of the query. + """ + + def __init__(self, **kwargs): + self.index = None + self.index_data = None + + def create_dataset_index(self, df: pd.DataFrame, backend: "Backend", columns_to_index: list[str] | None = None): + self.index = np.array(range(len(df))) + self.index_data = df + + def search( + self, df, backend: "Backend", k: int = 10, return_data: bool = False + ) -> list[int] | list[dict[str, Any]]: + results = [] + for _ in tqdm(range(len(df))): + indices = np.random.choice(self.index, k, replace=False) + + if return_data: + result = self.index_data.iloc[indices].to_dict(orient="records") + else: + result = indices + results.append(result) + return results + + def save_index(self, name: str, cache_directory: str): + index_file_path = os.path.join(cache_directory, name + ".index") + # open file to prevent using the .npy extension + # https://numpy.org/doc/stable/reference/generated/numpy.save.html + with open(index_file_path, "wb") as f: + np.save(f, self.index) + + index_data_file_path = os.path.join(cache_directory, name + "_data.csv") + self.index_data.to_csv(index_data_file_path, index=False) + + def load_index(self, name: str, cache_directory: str): + index_file_path = os.path.join(cache_directory, name + ".index") + self.index = np.load(index_file_path) + + index_data_file_path = os.path.join(cache_directory, name + "_data.csv") + self.index_data = pd.read_csv(index_data_file_path) + + +class SemanticRetrieval(RetrievalModel): + """Semantic retrieval model. + + Uses a sentence transformer model to encode the dataset and retrieve the top k most similar results to the query. + """ + + def __init__(self, model_name, **kwargs): + self.model_name = model_name + self.model = get_semantic_retrieval_model(self.model_name) + self.index: VectorIndex = None + self.index_data: pd.DataFrame = None + + # best batch size computed during the encoding step + self.best_batch_size = None + + def create_dataset_index(self, df: pd.DataFrame, backend: "Backend", columns_to_index: list[str] | None = None): + if columns_to_index is None: + columns_to_index = df.columns + df_to_index = df[columns_to_index] + row_strs = df_to_row_strs(df_to_index) + + embeddings = self._encode(row_strs, backend) + self.index = get_vector_index_cls(FAISS).from_embeddings(embeddings) + # Save the entire df so we can return the full row when searching + self.index_data = df + + def _encode(self, row_strs: list[str], backend: "Backend") -> np.ndarray: + # only do this step once + if self.best_batch_size is None: + self.best_batch_size = backend.tune_batch_size( + create_semantic_retrieval_model_evaluator(self.model, row_strs), len(row_strs) + ) + + transform_fn = create_semantic_retrieval_model_fn(self.model, self.best_batch_size) + df = backend.df_engine.from_pandas(pd.DataFrame({"data": row_strs})) + df = backend.batch_transform(df, self.best_batch_size, transform_fn) + df = backend.df_engine.compute(df) + embeddings = np.stack(df["data"].values).astype(np.float32) + return embeddings + + def search( + self, df: pd.DataFrame, backend: "Backend", k: int = 10, return_data: bool = False + ) -> list[int] | list[dict[str, Any]]: + row_strs = df_to_row_strs(df) + + query_vectors = self._encode(row_strs, backend) + results = [] + # TODO(geoffrey): figure out why self.index.search segfaults with larger batch sizes + for query_vector in tqdm(query_vectors, total=query_vectors.shape[0]): + indices = self.index.search(query_vector.reshape(1, -1), k) + if return_data: + result = self.index_data.iloc[indices].to_dict(orient="records") + else: + result = indices + results.append(result) + return results + + def save_index(self, name: str, cache_directory: str): + index_file_path = os.path.join(cache_directory, name + ".index") + self.index.save(index_file_path) + + index_data_file_path = os.path.join(cache_directory, name + "_data.csv") + self.index_data.to_csv(index_data_file_path, index=False) + + def load_index(self, name: str, cache_directory: str): + index_file_path = os.path.join(cache_directory, name + ".index") + self.index = get_vector_index_cls(FAISS).from_path(index_file_path) + + index_data_file_path = os.path.join(cache_directory, name + "_data.csv") + self.index_data = pd.read_csv(index_data_file_path) + + +def create_semantic_retrieval_model_evaluator( + model: "SentenceTransformer", samples: list[str] +) -> type[BatchSizeEvaluator]: + class _RetrievalModelEvaluator(BatchSizeEvaluator): + def __init__(self): + self.model = model.to(get_torch_device()) + self.samples = samples + + def step(self, batch_size: int, global_max_sequence_length: int | None = None): + self.model.encode(self.samples[:batch_size], batch_size=batch_size, show_progress_bar=False) + + return _RetrievalModelEvaluator + + +def create_semantic_retrieval_model_fn( + model: "SentenceTransformer", batch_size: int +) -> Callable[[pd.DataFrame], np.ndarray]: + class _RetrievalModelFn: + def __init__(self): + self.model = model.to(get_torch_device()) + self.batch_size = batch_size + + def __call__(self, df: pd.DataFrame) -> np.ndarray: + row_strs = df["data"].tolist() + result = self.model.encode(row_strs, batch_size=self.batch_size, show_progress_bar=False) + df["data"] = result.tolist() + return df + + return _RetrievalModelFn + + +def get_semantic_retrieval_model(model_name: str) -> "SentenceTransformer": + from sentence_transformers import SentenceTransformer + + return SentenceTransformer(model_name, device=get_torch_device()) + + +def get_retrieval_model(type: str, **kwargs) -> RetrievalModel: + if type == "random": + return RandomRetrieval(**kwargs) + elif type == "semantic": + return SemanticRetrieval(**kwargs) + else: + raise ValueError(f"Unsupported retrieval model type: {type}") diff --git a/ludwig/modules/__init__.py b/ludwig/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/modules/attention_modules.py b/ludwig/modules/attention_modules.py new file mode 100644 index 0000000..ad16e58 --- /dev/null +++ b/ludwig/modules/attention_modules.py @@ -0,0 +1,234 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch +from torch import nn +from torch.nn import functional as F + +from ludwig.utils.torch_utils import get_activation, LudwigModule + +logger = logging.getLogger(__name__) + + +def _rotate_half(x): + """Rotate half of the hidden dims of x.""" + x1, x2 = x.chunk(2, dim=-1) + return torch.cat((-x2, x1), dim=-1) + + +class RotaryEmbedding(nn.Module): + """Rotary Position Embedding (Su et al., 2024). + + Encodes position by rotating query and key vectors in 2D subspaces. Used by LLaMA, Mistral, and other modern + transformers. + """ + + def __init__(self, dim, max_seq_len=8192, base=10000.0): + super().__init__() + inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim)) + self.register_buffer("inv_freq", inv_freq, persistent=False) + self._max_seq_len = max_seq_len + self._cos_cached = None + self._sin_cached = None + + def _build_cache(self, seq_len, device): + if self._cos_cached is not None and seq_len <= self._cos_cached.shape[2]: + return + t = torch.arange(seq_len, device=device, dtype=torch.float32) + freqs = torch.outer(t, self.inv_freq.to(device)) + emb = torch.cat([freqs, freqs], dim=-1) + self._cos_cached = emb.cos()[None, None, :, :] # [1, 1, seq, dim] + self._sin_cached = emb.sin()[None, None, :, :] + + def forward(self, q, k): + seq_len = q.shape[-2] + self._build_cache(seq_len, q.device) + cos = self._cos_cached[:, :, :seq_len, :].to(q.dtype) + sin = self._sin_cached[:, :, :seq_len, :].to(q.dtype) + q_rot = (q * cos) + (_rotate_half(q) * sin) + k_rot = (k * cos) + (_rotate_half(k) * sin) + return q_rot, k_rot + + +class FeedForwardAttentionReducer(LudwigModule): + def __init__(self, input_size, hidden_size=256, activation="tanh"): + super().__init__() + self.fc_layer1 = nn.Linear(input_size, hidden_size) + self.fc_layer1_activation = get_activation(activation) + self.fc_layer2 = nn.Linear(hidden_size, 1, bias=False) + self.input_shape_var = None + self.output_shape_var = None + + def forward(self, inputs, mask=None): + # current_inputs shape [b, s, h] + self.input_shape_var = inputs.size()[1:] + hidden = self.fc_layer1(inputs) # [b, s, h'] + hidden = self.fc_layer1_activation(hidden) + hidden = self.fc_layer2(hidden) # [b, s, 1] + attention = F.softmax(hidden, dim=1) + gated_inputs = torch.sum(attention * inputs, dim=1) + self.output_shape_var = gated_inputs.size()[1:] + return gated_inputs # [b, h] + + @property + def input_shape(self) -> torch.Size: + return self.input_shape_var + + @property + def output_shape(self) -> torch.Size: + return self.output_shape_var + + +class MultiHeadSelfAttention(LudwigModule): + def __init__(self, input_size, hidden_size, num_heads=8, use_rope=False): + super().__init__() + self.embedding_size = hidden_size + self.num_heads = num_heads + if hidden_size % num_heads != 0: + raise ValueError( + f"When using multi-head attention, `hidden_size` ({hidden_size}), should be divisible by " + f"`num_heads` ({num_heads}). Please update the `transformer` section of the model config." + ) + self.projection_dim = hidden_size // num_heads + self.query_dense = nn.Linear(input_size, hidden_size) + self.key_dense = nn.Linear(input_size, hidden_size) + self.value_dense = nn.Linear(input_size, hidden_size) + self.combine_heads = nn.Linear(hidden_size, hidden_size) + self.use_rope = use_rope + if use_rope: + self.rotary_emb = RotaryEmbedding(self.projection_dim) + + def separate_heads(self, inputs, batch_size): + inputs = torch.reshape(inputs, (batch_size, -1, self.num_heads, self.projection_dim)) + return torch.permute(inputs, (0, 2, 1, 3)) + + def forward(self, inputs: torch.Tensor, mask=None): + # inputs.shape = [batch_size, seq_len, embedding_dim] + batch_size = inputs.shape[0] + query = self.query_dense(inputs) # (batch_size, seq_len, h) + key = self.key_dense(inputs) # (batch_size, seq_len, h) + value = self.value_dense(inputs) # (batch_size, seq_len, h) + query = self.separate_heads(query, batch_size) # (batch_size, num_heads, seq_len, projection_dim) + key = self.separate_heads(key, batch_size) # (batch_size, num_heads, seq_len, projection_dim) + value = self.separate_heads(value, batch_size) # (batch_size, num_heads, seq_len, projection_dim) + if self.use_rope: + query, key = self.rotary_emb(query, key) + attn_mask = mask if mask is not None else None + outputs = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask) + outputs = torch.permute(outputs, (0, 2, 1, 3)) # (batch_size, seq_len, num_heads, projection_dim) + concat_outputs = torch.reshape(outputs, (batch_size, -1, self.embedding_size)) # (batch_size, seq_len, h) + projected_outputs = self.combine_heads(concat_outputs) # (batch_size, seq_len, h) + return projected_outputs + + @property + def output_shape(self): + return torch.Size([self.embedding_size]) + + +class TransformerBlock(LudwigModule): + def __init__( + self, + input_size: int, + max_sequence_length: int, + hidden_size: int, + num_heads: int, + output_size: int, + dropout: float = 0.1, + use_rope: bool = False, + ): + super().__init__() + self.input_size = input_size + self.max_sequence_length = max_sequence_length + self.hidden_size = hidden_size + + self.self_attention = MultiHeadSelfAttention(input_size, hidden_size, num_heads=num_heads, use_rope=use_rope) + self.dropout1 = nn.Dropout(dropout) + self.layernorm1 = nn.LayerNorm(hidden_size, eps=1e-6) + self.fully_connected = nn.Sequential( + nn.Linear(input_size, output_size), get_activation("relu"), nn.Linear(output_size, hidden_size) + ) + self.dropout2 = nn.Dropout(dropout) + self.layernorm2 = nn.LayerNorm(hidden_size, eps=1e-6) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length, self.input_size]) + + def forward(self, inputs, mask=None): + # inputs [b, s, h] + attn_output = self.self_attention(inputs) # [b, s, h] + attn_output = self.dropout1(attn_output) # [b, s, h] + ln1_output = self.layernorm1(inputs + attn_output) # [b, s, h] + fc_output = self.fully_connected(ln1_output) # [b, s, h] + fc_output = self.dropout2(fc_output) # [b, s, h] + return self.layernorm2(ln1_output + fc_output) # [b, s, h] + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length, self.hidden_size]) + + +class TransformerStack(LudwigModule): + def __init__( + self, + input_size: int, + max_sequence_length: int, + hidden_size: int = 256, + num_heads: int = 8, + output_size: int = 256, + num_layers: int = 1, + dropout: float = 0.1, + use_rope: bool = False, + **kwargs, + ): + super().__init__() + self.supports_masking = True + self.max_sequence_length = max_sequence_length + self.input_size = input_size + self.hidden_size = hidden_size + + self.layers = nn.ModuleList() + + prior_input_size = input_size + for i in range(num_layers): + layer = TransformerBlock( + input_size=prior_input_size, + max_sequence_length=max_sequence_length, + hidden_size=hidden_size, + num_heads=num_heads, + output_size=output_size, + dropout=dropout, + use_rope=use_rope, + ) + self.layers.append(layer) + prior_input_size = self.layers[i].output_shape[-1] + + for layer in self.layers: + logger.debug(f" {layer._get_name()}") + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length, self.input_size]) + + def forward(self, inputs, mask=None): + hidden = inputs + for layer in self.layers: + hidden = layer(hidden, mask=mask) + return hidden + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length, self.hidden_size]) diff --git a/ludwig/modules/batch_ensemble.py b/ludwig/modules/batch_ensemble.py new file mode 100644 index 0000000..09f4148 --- /dev/null +++ b/ludwig/modules/batch_ensemble.py @@ -0,0 +1,76 @@ +"""TabM-style BatchEnsemble for parameter-efficient ensembling. + +Implements the BatchEnsemble technique from Wen et al. (ICLR 2020) adapted for +tabular deep learning as described in TabM (Gorishniy et al., ICLR 2025). + +A single MLP efficiently imitates an ensemble by sharing a backbone and using +per-member affine transforms (non-shared scaling vectors). This provides +ensemble-level performance at single-model inference cost. + +Usage: + from ludwig.modules.batch_ensemble import BatchEnsembleLinear + + # Replace nn.Linear with BatchEnsembleLinear + layer = BatchEnsembleLinear(in_features=128, out_features=64, num_members=4) +""" + +import torch +import torch.nn as nn + + +class BatchEnsembleLinear(nn.Module): + """Linear layer with BatchEnsemble for parameter-efficient ensembling. + + Shares the main weight matrix across ensemble members, but each member has its own rank-1 scaling factors (r_i and + s_i): output_i = (s_i * (W @ (r_i * x))) + b + + This adds only O(in + out) parameters per member instead of O(in * out). + """ + + def __init__(self, in_features: int, out_features: int, num_members: int = 4, bias: bool = True): + super().__init__() + self.in_features = in_features + self.out_features = out_features + self.num_members = num_members + + # Shared backbone + self.weight = nn.Parameter(torch.randn(out_features, in_features) / in_features**0.5) + if bias: + self.bias = nn.Parameter(torch.zeros(out_features)) + else: + self.bias = None + + # Per-member scaling vectors (rank-1 perturbations) + self.r = nn.Parameter(torch.ones(num_members, in_features)) # input scaling + self.s = nn.Parameter(torch.ones(num_members, out_features)) # output scaling + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Forward pass with implicit ensemble. + + During training, randomly selects an ensemble member per sample. + During eval, averages predictions across all members. + + Args: + x: [batch, in_features] + + Returns: + [batch, out_features] + """ + if self.training: + # Random member assignment per sample + member_idx = torch.randint(0, self.num_members, (x.shape[0],), device=x.device) + r = self.r[member_idx] # [batch, in_features] + s = self.s[member_idx] # [batch, out_features] + + # Apply: s * (W @ (r * x)) + b + x_scaled = x * r + out = torch.nn.functional.linear(x_scaled, self.weight, self.bias) + return out * s + else: + # Average over all members at eval time + outputs = [] + for i in range(self.num_members): + x_scaled = x * self.r[i] + out = torch.nn.functional.linear(x_scaled, self.weight, self.bias) + outputs.append(out * self.s[i]) + return torch.stack(outputs).mean(dim=0) diff --git a/ludwig/modules/contrastive_alignment.py b/ludwig/modules/contrastive_alignment.py new file mode 100644 index 0000000..3724031 --- /dev/null +++ b/ludwig/modules/contrastive_alignment.py @@ -0,0 +1,112 @@ +"""Contrastive pre-alignment between encoders and combiner (Phase 6.4.2). + +Before end-to-end training, a brief contrastive pre-training phase can align the output +spaces of different per-feature encoders so the combiner sees already-comparable +representations. This mirrors the contrastive stage used in multimodal works like +CLIP (Radford et al., ICML 2021) and HyperFusion (Mansour & Shkolnisky, 2024), but +adapted to Ludwig's multi-encoder ECD architecture where every input feature has its +own encoder. + +The module here is small and model-agnostic: given a dict of per-feature embeddings +``{feature_name: (batch, dim)}`` it projects each into a shared aligned space and +computes a symmetric multi-view InfoNCE loss across every pair of features in the +batch. The aligned space is learnable (one linear projection per feature) and is +discarded after pre-training — only the updated encoder weights carry forward. +""" + +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class ContrastiveAlignmentLoss(nn.Module): + """Symmetric multi-view InfoNCE loss over per-feature encoder outputs. + + Args: + feature_dims: mapping ``{feature_name: encoder_output_dim}`` — each feature's + unreduced embedding width. A linear projection into the shared alignment + space is created per feature. + projection_dim: width of the shared alignment space. + temperature: InfoNCE temperature. Lower values sharpen the softmax. CLIP + uses a learnable log-temperature initialised to ``log(1/0.07)``; we follow + that convention and expose the initial value as a constructor arg. + learnable_temperature: when True, the (log) temperature is a trainable parameter; + otherwise it's fixed. + + The forward returns a scalar loss summed over all ordered pairs + ``(feature_i, feature_j)`` with ``i != j``. Each pair contributes a symmetric + InfoNCE term (row-wise + column-wise cross entropy), so permuting features + leaves the loss value unchanged. + """ + + def __init__( + self, + feature_dims: dict[str, int], + projection_dim: int = 128, + temperature: float = 0.07, + learnable_temperature: bool = True, + ) -> None: + super().__init__() + if len(feature_dims) < 2: + raise ValueError(f"ContrastiveAlignmentLoss requires at least 2 input features, got {len(feature_dims)}") + self.feature_names = list(feature_dims.keys()) + self.projections = nn.ModuleDict({name: nn.Linear(dim, projection_dim) for name, dim in feature_dims.items()}) + init_log_t = math.log(1.0 / temperature) + if learnable_temperature: + self.log_temperature = nn.Parameter(torch.tensor(init_log_t, dtype=torch.float32)) + else: + self.register_buffer("log_temperature", torch.tensor(init_log_t, dtype=torch.float32)) + + def _project(self, embeddings: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + projected = {} + for name in self.feature_names: + if name not in embeddings: + raise KeyError( + f"ContrastiveAlignmentLoss expected feature {name!r} in batch; got {list(embeddings.keys())}" + ) + z = self.projections[name](embeddings[name]) + projected[name] = F.normalize(z, dim=-1) + return projected + + def forward(self, embeddings: dict[str, torch.Tensor]) -> torch.Tensor: + """Compute the pairwise symmetric InfoNCE loss. + + Args: + embeddings: ``{feature_name: (batch, dim)}``. Every key in + ``self.feature_names`` must be present; batch size must match across + features. + + Returns: + Scalar loss tensor. + """ + projected = self._project(embeddings) + # All features must share the same batch size; mismatched sizes produce a + # non-square logits matrix where logits.T has the wrong number of rows for + # the targets tensor. + batch_sizes = {name: projected[name].shape[0] for name in self.feature_names} + if len(set(batch_sizes.values())) != 1: + raise ValueError(f"ContrastiveAlignmentLoss: batch size must match across all features, got {batch_sizes}") + # ``log_temperature`` stores log(1/T) following the CLIP convention, so + # exp(log_temperature) is the *inverse* temperature / logit scale. Multiplying + # the cosine similarity by this scale therefore sharpens the softmax when T is + # small (and log_temperature is large positive). + logit_scale = torch.exp(self.log_temperature).clamp(max=100.0) + + loss = projected[self.feature_names[0]].new_zeros(()) + num_pairs = 0 + for i in range(len(self.feature_names)): + for j in range(i + 1, len(self.feature_names)): + z_i = projected[self.feature_names[i]] + z_j = projected[self.feature_names[j]] + batch = z_i.shape[0] + logits = (z_i @ z_j.T) * logit_scale + targets = torch.arange(batch, device=logits.device) + # Symmetric InfoNCE: each example should identify its positive in both directions. + loss = loss + 0.5 * (F.cross_entropy(logits, targets) + F.cross_entropy(logits.T, targets)) + num_pairs += 1 + + return loss / max(num_pairs, 1) diff --git a/ludwig/modules/convolutional_modules.py b/ludwig/modules/convolutional_modules.py new file mode 100644 index 0000000..5e6f204 --- /dev/null +++ b/ludwig/modules/convolutional_modules.py @@ -0,0 +1,1502 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from functools import partial +from typing import Any + +import torch +import torch.nn as nn + +from ludwig.utils.image_utils import get_img_output_shape +from ludwig.utils.torch_utils import get_activation, LudwigModule + +logger = logging.getLogger(__name__) + + +class Conv1DLayer(LudwigModule): + def __init__( + self, + in_channels=1, + out_channels=256, + max_sequence_length=None, + kernel_size=3, + strides=1, + padding="same", + dilation=1, + groups=1, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + norm=None, + norm_params=None, + activation="relu", + dropout=0, + pool_function="max", + pool_size=2, + pool_strides=None, + pool_padding="valid", + ): + super().__init__() + + self.in_channels = in_channels + self.out_channels = out_channels + self.max_sequence_length = max_sequence_length + self.kernel_size = kernel_size + self.stride = strides + self.padding = padding + self.dilation = dilation + self.groups = groups + self.pool_size = pool_size + if pool_strides is None: + self.pool_strides = pool_size + else: + self.pool_strides = pool_strides + if pool_padding == "same" and pool_size is not None: + self.pool_padding = (self.pool_size - 1) // 2 + else: + self.pool_padding = 0 + + self.layers = nn.ModuleList() + + self.layers.append( + nn.Conv1d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=(kernel_size,), + stride=(strides,), + padding=padding, + dilation=(dilation,), + ) + ) + + if norm and norm_params is None: + norm_params = {} + if norm == "batch": + self.layers.append(nn.BatchNorm1d(num_features=out_channels, **norm_params)) + elif norm == "layer": + self.layers.append(nn.LayerNorm(normalized_shape=[out_channels, self.max_sequence_length], **norm_params)) + + self.layers.append(get_activation(activation)) + + if dropout > 0: + self.layers.append(nn.Dropout(dropout)) + + if pool_size is not None: + pool = nn.MaxPool1d + if pool_function in {"average", "avg", "mean"}: + pool = nn.AvgPool1d + self.layers.append(pool(kernel_size=self.pool_size, stride=self.pool_strides, padding=self.pool_padding)) + + for layer in self.layers: + logger.debug(f" {layer._get_name()}") + + @property + def input_shape(self): + """Returns the size of the input tensor without the batch dimension.""" + return torch.Size([self.max_sequence_length, self.in_channels]) + + def forward(self, inputs, training=None, mask=None): + # inputs: [batch_size, seq_size, in_channels] + # in Torch nomenclature (N, L, C) + hidden = inputs + + # put in torch compatible form [batch_size, in_channels, seq_size] + hidden = hidden.transpose(1, 2) + + for layer in self.layers: + hidden = layer(hidden) + + # revert back to normal form [batch_size, seq_size, out_channels] + hidden = hidden.transpose(1, 2) + + return hidden # (batch_size, seq_size, out_channels) + + +class Conv1DStack(LudwigModule): + def __init__( + self, + in_channels=1, + max_sequence_length=None, + layers=None, + num_layers=None, + default_num_filters=256, + default_filter_size=3, + default_strides=1, + default_padding="same", + default_dilation_rate=1, + default_use_bias=True, + default_weights_initializer="xavier_uniform", + default_bias_initializer="zeros", + default_norm=None, + default_norm_params=None, + default_activation="relu", + default_dropout=0, + default_pool_function="max", + default_pool_size=2, + default_pool_strides=None, + default_pool_padding="same", + **kwargs, + ): + super().__init__() + + self.max_sequence_length = max_sequence_length + self.in_channels = in_channels + + if layers is None: + if num_layers is None: + self.layers = [ + {"filter_size": 7, "pool_size": 3}, + {"filter_size": 7, "pool_size": 3}, + {"filter_size": 3, "pool_size": None}, + {"filter_size": 3, "pool_size": None}, + {"filter_size": 3, "pool_size": None}, + {"filter_size": 3, "pool_size": 3}, + ] + else: + self.layers = [] + for i in range(num_layers): + self.layers.append( + { + "filter_size": default_filter_size, + "num_filters": default_num_filters, + "pool_size": default_pool_size, + "pool_strides": default_pool_strides, + } + ) + else: + self.layers = layers + + for layer in self.layers: + if "num_filters" not in layer: + layer["num_filters"] = default_num_filters + if "filter_size" not in layer: + layer["filter_size"] = default_filter_size + if "strides" not in layer: + layer["strides"] = default_strides + if "padding" not in layer: + layer["padding"] = default_padding + if "dilation_rate" not in layer: + layer["dilation_rate"] = default_dilation_rate + if "use_bias" not in layer: + layer["use_bias"] = default_use_bias + if "weights_initializer" not in layer: + layer["weights_initializer"] = default_weights_initializer + if "bias_initializer" not in layer: + layer["bias_initializer"] = default_bias_initializer + if "norm" not in layer: + layer["norm"] = default_norm + if "norm_params" not in layer: + layer["norm_params"] = default_norm_params + if "activation" not in layer: + layer["activation"] = default_activation + if "dropout" not in layer: + layer["dropout"] = default_dropout + if "pool_function" not in layer: + layer["pool_function"] = default_pool_function + if "pool_size" not in layer: + layer["pool_size"] = default_pool_size + if "pool_strides" not in layer: + layer["pool_strides"] = default_pool_strides + if "pool_padding" not in layer: + layer["pool_padding"] = default_pool_padding + + self.stack = nn.ModuleList() + + prior_layer_channels = in_channels + l_in = self.max_sequence_length # torch L_in + for i, layer in enumerate(self.layers): + logger.debug(f" stack layer {i}") + self.stack.append( + Conv1DLayer( + in_channels=prior_layer_channels, + out_channels=layer["num_filters"], + max_sequence_length=l_in, + kernel_size=layer["filter_size"], + strides=layer["strides"], + padding=layer["padding"], + dilation=layer["dilation_rate"], + use_bias=layer["use_bias"], + weights_initializer=layer["weights_initializer"], + bias_initializer=layer["bias_initializer"], + norm=layer["norm"], + norm_params=layer["norm_params"], + activation=layer["activation"], + dropout=layer["dropout"], + pool_function=layer["pool_function"], + pool_size=layer["pool_size"], + pool_strides=layer["pool_strides"], + pool_padding=layer["pool_padding"], + ) + ) + + # retrieve number of channels from prior layer + input_shape = self.stack[i].input_shape + output_shape = self.stack[i].output_shape + + logger.debug(f"{self.__class__.__name__}: input_shape {input_shape}, output shape {output_shape}") + + # pass along shape for the input to the next layer + l_in, prior_layer_channels = output_shape + + @property + def input_shape(self): + """Returns the size of the input tensor without the batch dimension.""" + return torch.Size([self.max_sequence_length, self.in_channels]) + + def forward(self, inputs, mask=None): + hidden = inputs + + # todo: enumerate for debugging, remove after testing + for _i, layer in enumerate(self.stack): + hidden = layer(hidden) + + if hidden.shape[1] == 0: + raise ValueError( + "The output of the conv stack has the second dimension " + "(length of the sequence) equal to 0. " + "This means that the combination of filter_size, padding, " + "stride, pool_size, pool_padding and pool_stride reduces " + "the sequence length more than is possible. " + 'Try using "same" padding and reducing or eliminating stride ' + "and pool." + ) + + return hidden + + +class ParallelConv1D(LudwigModule): + def __init__( + self, + in_channels=1, + max_sequence_length=None, + layers=None, + default_num_filters=256, + default_filter_size=3, + default_strides=1, + default_padding="same", + default_dilation_rate=1, + default_use_bias=True, + default_weights_initializer="xavier_uniform", + default_bias_initializer="zeros", + default_norm=None, + default_norm_params=None, + default_activation="relu", + default_dropout=0, + default_pool_function="max", + default_pool_size=None, + default_pool_strides=None, + default_pool_padding="valid", + **kwargs, + ): + super().__init__() + + self.in_channels = in_channels + self.max_sequence_length = max_sequence_length + + if layers is None: + self.layers = [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}] + else: + self.layers = layers + + for layer in self.layers: + if "num_filters" not in layer: + layer["num_filters"] = default_num_filters + if "filter_size" not in layer: + layer["filter_size"] = default_filter_size + if "strides" not in layer: + layer["strides"] = default_strides + if "padding" not in layer: + layer["padding"] = default_padding + if "dilation_rate" not in layer: + layer["dilation_rate"] = default_dilation_rate + if "use_bias" not in layer: + layer["use_bias"] = default_use_bias + if "weights_initializer" not in layer: + layer["weights_initializer"] = default_weights_initializer + if "bias_initializer" not in layer: + layer["bias_initializer"] = default_bias_initializer + if "norm" not in layer: + layer["norm"] = default_norm + if "norm_params" not in layer: + layer["norm_params"] = default_norm_params + if "activation" not in layer: + layer["activation"] = default_activation + if "dropout" not in layer: + layer["dropout"] = default_dropout + if "pool_function" not in layer: + layer["pool_function"] = default_pool_function + if "pool_size" not in layer: + layer["pool_size"] = default_pool_size + if "pool_strides" not in layer: + layer["pool_strides"] = default_pool_strides + if "pool_padding" not in layer: + layer["pool_padding"] = default_pool_padding + + self.parallel_layers = nn.ModuleList() + + for i, layer in enumerate(self.layers): + logger.debug(f" parallel layer {i}") + self.parallel_layers.append( + Conv1DLayer( + in_channels=self.in_channels, + out_channels=layer["num_filters"], + max_sequence_length=self.max_sequence_length, + kernel_size=layer["filter_size"], + strides=layer["strides"], + padding=layer["padding"], + dilation=layer["dilation_rate"], + use_bias=layer["use_bias"], + weights_initializer=layer["weights_initializer"], + bias_initializer=layer["bias_initializer"], + norm=layer["norm"], + norm_params=layer["norm_params"], + activation=layer["activation"], + dropout=layer["dropout"], + pool_function=layer["pool_function"], + pool_size=layer["pool_size"], + pool_strides=layer["pool_strides"], + pool_padding=layer["pool_padding"], + ) + ) + + logger.debug( + f"{self.__class__.__name__} layer {i}, input shape " + f"{self.parallel_layers[i].input_shape}, output shape " + f"{self.parallel_layers[i].output_shape}" + ) + + @property + def input_shape(self) -> torch.Size: + """Returns the size of the input tensor without the batch dimension.""" + return torch.Size([self.max_sequence_length, self.in_channels]) + + def forward(self, inputs, mask=None): + # inputs: [batch_size, seq_size, in_channels) + + hidden = inputs + hiddens = [] + + for layer in self.parallel_layers: + hiddens.append(layer(hidden)) + hidden = torch.cat(hiddens, 2) + + if hidden.shape[1] == 0: + raise ValueError( + "The output of the conv stack has the second dimension " + "(length of the sequence) equal to 0. " + "This means that the combination of filter_size, padding, " + "stride, pool_size, pool_padding and pool_stride reduces " + "the sequence length more than is possible. " + 'Try using "same" padding and reducing or eliminating stride ' + "and pool." + ) + + # (batch_size, seq_size, len(parallel_layers) * out_channels) + return hidden + + +class ParallelConv1DStack(LudwigModule): + def __init__( + self, + in_channels=None, + stacked_layers=None, + max_sequence_length=None, + default_num_filters=64, + default_filter_size=3, + default_strides=1, + default_padding="same", + default_dilation_rate=1, + default_use_bias=True, + default_weights_initializer="xavier_uniform", + default_bias_initializer="zeros", + default_norm=None, + default_norm_params=None, + default_activation="relu", + default_dropout=0, + default_pool_function="max", + default_pool_size=None, + default_pool_strides=None, + default_pool_padding="valid", + **kwargs, + ): + super().__init__() + + self.max_sequence_length = max_sequence_length + self.in_channels = in_channels + + if stacked_layers is None: + self.stacked_parallel_layers = [ + [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}], + [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}], + [{"filter_size": 2}, {"filter_size": 3}, {"filter_size": 4}, {"filter_size": 5}], + ] + + else: + self.stacked_parallel_layers = stacked_layers + + for i, parallel_layers in enumerate(self.stacked_parallel_layers): + for j in range(len(parallel_layers)): + layer = parallel_layers[j] + if "num_filters" not in layer: + layer["num_filters"] = default_num_filters + if "filter_size" not in layer: + layer["filter_size"] = default_filter_size + if "strides" not in layer: + layer["strides"] = default_strides + if "padding" not in layer: + layer["padding"] = default_padding + if "dilation_rate" not in layer: + layer["dilation_rate"] = default_dilation_rate + if "use_bias" not in layer: + layer["use_bias"] = default_use_bias + if "weights_initializer" not in layer: + layer["weights_initializer"] = default_weights_initializer + if "bias_initializer" not in layer: + layer["bias_initializer"] = default_bias_initializer + if "norm" not in layer: + layer["norm"] = default_norm + if "norm_params" not in layer: + layer["norm_params"] = default_norm_params + if "activation" not in layer: + layer["activation"] = default_activation + if "dropout" not in layer: + layer["dropout"] = default_dropout + if "pool_function" not in layer: + layer["pool_function"] = default_pool_function + if "pool_size" not in layer: + if i == len(self.stacked_parallel_layers) - 1: + layer["pool_size"] = default_pool_size + else: + layer["pool_size"] = None + if "pool_strides" not in layer: + layer["pool_strides"] = default_pool_strides + if "pool_padding" not in layer: + layer["pool_padding"] = default_pool_padding + + self.stack = nn.ModuleList() + num_channels = self.in_channels + sequence_length = self.max_sequence_length + for i, parallel_layers in enumerate(self.stacked_parallel_layers): + logger.debug(f" stack layer {i}") + self.stack.append(ParallelConv1D(num_channels, sequence_length, layers=parallel_layers)) + + logger.debug( + f"{self.__class__.__name__} layer {i}, input shape " + f"{self.stack[i].input_shape}, output shape " + f"{self.stack[i].output_shape}" + ) + + # set input specification for the layer + num_channels = self.stack[i].output_shape[1] + sequence_length = self.stack[i].output_shape[0] + + @property + def input_shape(self): + """Returns the size of the input tensor without the batch dimension.""" + return torch.Size([self.max_sequence_length, self.in_channels]) + + def forward(self, inputs, mask=None): + hidden = inputs + + for layer in self.stack: + hidden = layer(hidden) + + if hidden.shape[2] == 0: + raise ValueError( + "The output of the conv stack has the second dimension " + "(length of the sequence) equal to 0. " + "This means that the combination of filter_size, padding, " + "stride, pool_size, pool_padding and pool_stride is reduces " + "the sequence length more than is possible. " + 'Try using "same" padding and reducing or eliminating stride ' + "and pool." + ) + + return hidden + + +class Conv2DLayer(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + in_channels: int, + out_channels: int = 256, + kernel_size: int | tuple[int] = 3, + stride: int | tuple[int] = 1, + padding: int | tuple[int] | str = "valid", + dilation: int | tuple[int] = 1, + groups: int = 1, + use_bias: bool = True, + padding_mode: str = "zeros", + norm: str | None = None, + norm_params: dict[str, Any] | None = None, + activation: str = "relu", + dropout: float = 0, + pool_function: int = "max", + pool_kernel_size: int | tuple[int] | None = None, + pool_stride: int | None = None, + pool_padding: int | tuple[int] = 0, + pool_dilation: int | tuple[int] = 1, + ): + super().__init__() + + self.layers = torch.nn.ModuleList() + + self._input_shape = (in_channels, img_height, img_width) + pool_stride = pool_stride or pool_kernel_size + + self.layers.append( + nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + bias=use_bias, + padding_mode=padding_mode, + ) + ) + out_height, out_width = get_img_output_shape(img_height, img_width, kernel_size, stride, padding, dilation) + + if norm and norm_params is None: + norm_params = {} + if norm == "batch": + # Batch norm over channels + self.layers.append(nn.BatchNorm2d(num_features=out_channels, **norm_params)) + elif norm == "layer": + # Layer norm over image height and width + self.layers.append(nn.LayerNorm(normalized_shape=(out_height, out_width), **norm_params)) + + self.layers.append(get_activation(activation)) + + if dropout > 0: + self.layers.append(nn.Dropout(dropout)) + + if pool_kernel_size is not None: + pool = partial(nn.MaxPool2d, dilation=pool_dilation) + if pool_function in {"average", "avg", "mean"}: + pool = nn.AvgPool2d + self.layers.append(pool(kernel_size=pool_kernel_size, stride=pool_stride, padding=pool_padding)) + out_height, out_width = get_img_output_shape( + img_height=out_height, + img_width=out_width, + kernel_size=pool_kernel_size, + stride=pool_stride, + padding=pool_padding, + dilation=pool_dilation, + ) + + for layer in self.layers: + logger.debug(f" {layer._get_name()}") + + self._output_shape = (out_channels, out_height, out_width) + + def forward(self, inputs): + hidden = inputs + + for layer in self.layers: + hidden = layer(hidden) + + return hidden + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +class Conv2DStack(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + layers: list[dict] | None = None, + num_layers: int | None = None, + first_in_channels: int | None = None, + default_out_channels: int = 256, + default_kernel_size: int | tuple[int] = 3, + default_stride: int | tuple[int] = 1, + default_padding: int | tuple[int] | str = "valid", + default_dilation: int | tuple[int] = 1, + default_groups: int = 1, + default_use_bias: bool = True, + default_padding_mode: str = "zeros", + default_norm: str | None = None, + default_norm_params: dict[str, Any] | None = None, + default_activation: str = "relu", + default_dropout: int = 0, + default_pool_function: int = "max", + default_pool_kernel_size: int | tuple[int] = 2, + default_pool_stride: int | tuple[int] | None = None, + default_pool_padding: int | tuple[int] = 0, + default_pool_dilation: int | tuple[int] = 1, + ): + super().__init__() + + # Confirm that all inputs are consistent + first_in_channels = self._check_in_channels(first_in_channels, layers) + default_pool_stride = default_pool_stride or default_pool_kernel_size + if layers is not None and num_layers is not None: + raise Warning("Both layers and num_layers are not None.Default to using layers.") + if ( + first_in_channels is not None + and layers is not None + and len(layers) > 0 + and "in_channels" in layers[0] + and layers[0]["in_channels"] != first_in_channels + ): + raise Warning( + "Input channels is set via layers[0]['in_channels'] and first_in_channels." + "Default to using first_in_channels." + ) + + self._input_shape = (first_in_channels, img_height, img_width) + + if layers is None: + if num_layers is None: + self.layers = [ + {"out_channels": 32}, + {"out_channels": 64}, + ] + else: + self.layers = [] + for i in range(num_layers): + self.layers.append( + { + "kernel_size": default_kernel_size, + "out_channels": default_out_channels, + "pool_kernel_size": default_pool_kernel_size, + } + ) + else: + self.layers = layers + + for layer in self.layers: + if "out_channels" not in layer: + layer["out_channels"] = default_out_channels + if "kernel_size" not in layer: + layer["kernel_size"] = default_kernel_size + if "stride" not in layer: + layer["stride"] = default_stride + if "padding" not in layer: + layer["padding"] = default_padding + if "dilation" not in layer: + layer["dilation"] = default_dilation + if "groups" not in layer: + layer["groups"] = default_groups + if "use_bias" not in layer: + layer["use_bias"] = default_use_bias + if "padding_mode" not in layer: + layer["padding_mode"] = default_padding_mode + if "norm" not in layer: + layer["norm"] = default_norm + if "norm_params" not in layer: + layer["norm_params"] = default_norm_params + if "activation" not in layer: + layer["activation"] = default_activation + if "dropout" not in layer: + layer["dropout"] = default_dropout + if "pool_function" not in layer: + layer["pool_function"] = default_pool_function + if "pool_kernel_size" not in layer: + layer["pool_kernel_size"] = default_pool_kernel_size + if "pool_stride" not in layer: + layer["pool_stride"] = default_pool_stride + if "pool_padding" not in layer: + layer["pool_padding"] = default_pool_padding + if "pool_dilation" not in layer: + layer["pool_dilation"] = default_pool_dilation + + self.stack = torch.nn.ModuleList() + + in_channels = first_in_channels + for i, layer in enumerate(self.layers): + logger.debug(f" stack layer {i}") + self.stack.append( + Conv2DLayer( + img_height=img_height, + img_width=img_width, + in_channels=in_channels, + out_channels=layer["out_channels"], + kernel_size=layer["kernel_size"], + stride=layer["stride"], + padding=layer["padding"], + dilation=layer["dilation"], + groups=layer["groups"], + use_bias=layer["use_bias"], + padding_mode=layer["padding_mode"], + norm=layer["norm"], + norm_params=layer["norm_params"], + activation=layer["activation"], + dropout=layer["dropout"], + pool_function=layer["pool_function"], + pool_kernel_size=layer["pool_kernel_size"], + pool_stride=layer["pool_stride"], + pool_padding=layer["pool_padding"], + pool_dilation=layer["pool_dilation"], + ) + ) + in_channels, img_height, img_width = self.stack[-1].output_shape + + self._output_shape = (in_channels, img_height, img_width) + + def forward(self, inputs): + hidden = inputs + + for layer in self.stack: + hidden = layer(hidden) + + return hidden + + def _check_in_channels(self, first_in_channels: int | None, layers: list[dict] | None) -> None: + """Confirms that in_channels for first layer of the stack exists.""" + + if first_in_channels is not None: + return first_in_channels + elif layers is not None and len(layers) > 0 and "in_channels" in layers[0]: + return layers[0]["in_channels"] + raise ValueError( + "In_channels for first layer should be specified either via `first_in_channels` or `layers` arguments." + ) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.size(self._input_shape) + + +class Conv2DLayerFixedPadding(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + in_channels: int, + out_channels=256, + kernel_size=3, + stride=1, + dilation=1, + groups=1, + use_bias=False, + ): + super().__init__() + + self.layers = torch.nn.ModuleList() + self._input_shape = (in_channels, img_height, img_width) + + padding = "same" + if stride > 1: + padding = (kernel_size - 1) // 2 + + self.layers.append( + nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups, + bias=use_bias, + ) + ) + img_height, img_width = get_img_output_shape( + img_height=img_height, + img_width=img_width, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + ) + + for layer in self.layers: + logger.debug(f" {layer._get_name()}") + + self._output_shape = (out_channels, img_height, img_width) + + def forward(self, inputs): + hidden = inputs + + for layer in self.layers: + hidden = layer(hidden) + + return hidden + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + +class ResNetBlock(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + first_in_channels: int, + out_channels: int, + stride: int = 1, + batch_norm_momentum: float = 0.1, + batch_norm_epsilon: float = 0.001, + projection_shortcut: LudwigModule | None = None, + ): + """Resnet blocks used for ResNet34 and smaller. + + stride: A single int specifying the stride of the first convolution. + The last convolution will have stride of 1. + """ + super().__init__() + self._input_shape = (first_in_channels, img_height, img_width) + + self.conv1 = Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=first_in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + ) + in_channels, img_height, img_width = self.conv1.output_shape + self.norm1 = nn.BatchNorm2d(num_features=in_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum) + self.relu1 = get_activation("relu") + + self.conv2 = Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=out_channels, + out_channels=out_channels, + kernel_size=3, + stride=1, + ) + self.norm2 = nn.BatchNorm2d(num_features=out_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum) + self.relu2 = get_activation("relu") + + for layer in [self.conv1, self.norm1, self.relu1, self.conv2, self.norm2, self.relu2]: + logger.debug(f" {layer._get_name()}") + + self._output_shape = self.conv2.output_shape + + self.projection_shortcut = projection_shortcut + if self.projection_shortcut is not None and self.projection_shortcut.output_shape != self._output_shape: + raise ValueError( + f"Output shapes of ResnetBlock and projection_shortcut should " + f"match but are {self._output_shape} and " + f"{self.projection_shortcut.output_shape} respectively." + ) + if self.projection_shortcut is None and self._input_shape != self._output_shape: + self.projection_shortcut = Conv2DLayer( + img_height=self._input_shape[1], + img_width=self._input_shape[2], + in_channels=first_in_channels, + out_channels=out_channels, + kernel_size=1, + stride=stride, + ) + + def forward(self, inputs): + shortcut = inputs + + if self.projection_shortcut is not None: + shortcut = self.projection_shortcut(shortcut) + + hidden = self.conv1(inputs) + hidden = self.norm1(hidden) + hidden = self.relu1(hidden) + hidden = self.conv2(hidden) + hidden = self.norm2(hidden) + + return self.relu2(hidden + shortcut) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + +# TODO(shreya): Combine with ResNetBlock by adding a flag. +class ResNetBottleneckBlock(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + first_in_channels: int, + out_channels: int, + stride: int = 1, + batch_norm_momentum: float = 0.1, + batch_norm_epsilon: float = 0.001, + projection_shortcut: LudwigModule | None = None, + ): + """Resnet bottleneck blocks used for ResNet50 and larger. + + stride: A single int specifying the stride of the middle convolution. + The first and last convolution will have stride of 1. + """ + super().__init__() + + self._input_shape = (first_in_channels, img_height, img_width) + + self.conv1 = Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=first_in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + ) + in_channels, img_height, img_width = self.conv1.output_shape + self.norm1 = nn.BatchNorm2d(num_features=in_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum) + self.relu1 = get_activation("relu") + + self.conv2 = Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=in_channels, + out_channels=out_channels, + kernel_size=3, + stride=stride, + ) + in_channels, img_height, img_width = self.conv2.output_shape + self.norm2 = nn.BatchNorm2d(num_features=in_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum) + self.relu2 = get_activation("relu") + + self.conv3 = Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=in_channels, + out_channels=4 * out_channels, + kernel_size=1, + stride=1, + ) + self.norm3 = nn.BatchNorm2d(num_features=4 * out_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum) + self.relu3 = get_activation("relu") + + for layer in [ + self.conv1, + self.norm1, + self.relu1, + self.conv2, + self.norm2, + self.relu2, + self.conv3, + self.norm3, + self.relu3, + ]: + logger.debug(f" {layer._get_name()}") + + self._output_shape = self.conv3.output_shape + + self.projection_shortcut = projection_shortcut + if self.projection_shortcut is not None and self.projection_shortcut.output_shape != self._output_shape: + raise ValueError( + f"Output shapes of ResnetBlock and projection_shortcut should " + f"match but are {self._output_shape} and " + f"{self.projection_shortcut.output_shape} respectively." + ) + if self.projection_shortcut is None and self._input_shape != self._output_shape: + self.projection_shortcut = Conv2DLayer( + img_height=self._input_shape[1], + img_width=self._input_shape[2], + in_channels=first_in_channels, + out_channels=4 * out_channels, + kernel_size=1, + stride=stride, + ) + + def forward(self, inputs): + shortcut = inputs + + if self.projection_shortcut is not None: + shortcut = self.projection_shortcut(shortcut) + + hidden = self.conv1(inputs) + hidden = self.norm1(hidden) + hidden = self.relu1(hidden) + hidden = self.conv2(hidden) + hidden = self.norm2(hidden) + hidden = self.relu2(hidden) + hidden = self.conv3(hidden) + hidden = self.norm3(hidden) + + return self.relu3(hidden + shortcut) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +class ResNetBlockLayer(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + first_in_channels: int, + out_channels: int, + is_bottleneck: bool, + block_fn: ResNetBlock | ResNetBottleneckBlock, + num_blocks: int, + stride: int | tuple[int] = 1, + batch_norm_momentum: float = 0.1, + batch_norm_epsilon: float = 0.001, + ): + super().__init__() + + self._input_shape = (first_in_channels, img_height, img_width) + + # Bottleneck blocks end with 4x the number of channels as they start with + projection_out_channels = out_channels * 4 if is_bottleneck else out_channels + projection_shortcut = Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=first_in_channels, + out_channels=projection_out_channels, + kernel_size=1, + stride=stride, + ) + + self.layers = torch.nn.ModuleList( + [ + block_fn( + img_height, + img_width, + first_in_channels, + out_channels, + stride, + batch_norm_momentum, + batch_norm_epsilon, + projection_shortcut, + ) + ] + ) + in_channels, img_height, img_width = self.layers[-1].output_shape + + for _ in range(1, num_blocks): + self.layers.append( + block_fn( + img_height=img_height, + img_width=img_width, + first_in_channels=in_channels, + out_channels=out_channels, + stride=1, + batch_norm_momentum=batch_norm_momentum, + batch_norm_epsilon=batch_norm_epsilon, + ) + ) + in_channels, img_height, img_width = self.layers[-1].output_shape + + for layer in self.layers: + logger.debug(f" {layer._get_name()}") + + self._output_shape = (in_channels, img_height, img_width) + + def forward(self, inputs): + hidden = inputs + for layer in self.layers: + hidden = layer(hidden) + return hidden + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +class ResNet(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + first_in_channels: int, + out_channels: int, + resnet_size: int = 34, + kernel_size: int | tuple[int] = 7, + conv_stride: int | tuple[int] = 2, + first_pool_kernel_size: int | tuple[int] = 3, + first_pool_stride: int | tuple[int] = 2, + block_sizes: list[int] | None = None, + block_strides: list[int | tuple[int]] | None = None, + batch_norm_momentum: float = 0.1, + batch_norm_epsilon: float = 0.001, + ): + """Creates a model obtaining an image representation. + + Implements ResNet v2: + Identity Mappings in Deep Residual Networks + https://arxiv.org/pdf/1603.05027.pdf + by Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun, Jul 2016. + + Args: + resnet_size: A single integer for the size of the ResNet model. + is_bottleneck: Use regular blocks or bottleneck blocks. + out_channels: The number of filters to use for the first block layer + of the model. This number is then doubled for each subsequent block + layer. + kernel_size: The kernel size to use for convolution. + conv_stride: stride size for the initial convolutional layer + first_pool_kernel_size: Pool size to be used for the first pooling layer. + If none, the first pooling layer is skipped. + first_pool_stride: stride size for the first pooling layer. Not used + if first_pool_kernel_size is None. + block_sizes: A list containing n values, where n is the number of sets of + block layers desired. Each value should be the number of blocks in the + i-th set. + block_strides: List of integers representing the desired stride size for + each of the sets of block layers. Should be same length as block_sizes. + Raises: + ValueError: if invalid version is selected. + """ + super().__init__() + + self._input_shape = (first_in_channels, img_height, img_width) + + is_bottleneck = self.get_is_bottleneck(resnet_size, block_sizes) + block_class = self.get_block_fn(is_bottleneck) + block_sizes, block_strides = self.get_blocks(resnet_size, block_sizes, block_strides) + + self.layers = torch.nn.ModuleList() + self.layers.append( + Conv2DLayerFixedPadding( + img_height=img_height, + img_width=img_width, + in_channels=first_in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=conv_stride, + ) + ) + in_channels, img_height, img_width = self.layers[-1].output_shape + self.layers.append( + nn.BatchNorm2d(num_features=out_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum) + ) + self.layers.append(get_activation("relu")) + + if first_pool_kernel_size: + self.layers.append(nn.MaxPool2d(kernel_size=first_pool_kernel_size, stride=first_pool_stride, padding=1)) + img_height, img_width = get_img_output_shape( + img_height=img_height, + img_width=img_width, + kernel_size=first_pool_kernel_size, + stride=first_pool_stride, + padding=1, + dilation=1, + ) + + for i, num_blocks in enumerate(block_sizes): + self.layers.append( + ResNetBlockLayer( + img_height=img_height, + img_width=img_width, + first_in_channels=in_channels, + out_channels=out_channels, + is_bottleneck=is_bottleneck, + block_fn=block_class, + num_blocks=num_blocks, + stride=block_strides[i], + batch_norm_momentum=batch_norm_momentum, + batch_norm_epsilon=batch_norm_epsilon, + ) + ) + out_channels *= 2 + in_channels, img_height, img_width = self.layers[-1].output_shape + + for layer in self.layers: + logger.debug(f" {layer._get_name()}") + + self._output_shape = (in_channels, img_height, img_width) + + def get_is_bottleneck(self, resnet_size: int, block_sizes: list[int]) -> bool: + if (resnet_size is not None and resnet_size >= 50) or (block_sizes is not None and sum(block_sizes) >= 16): + return True + return False + + def get_block_fn(self, is_bottleneck: bool) -> ResNetBlock | ResNetBottleneckBlock: + if is_bottleneck: + return ResNetBottleneckBlock + return ResNetBlock + + def get_blocks(self, resnet_size: int, block_sizes: list[int], block_strides: list[int]) -> tuple[list[int]]: + if block_sizes is None: + block_sizes = get_resnet_block_sizes(resnet_size) + if block_strides is None: + block_strides = [1] + [2 for _ in range(len(block_sizes) - 1)] + return block_sizes, block_strides + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + hidden = inputs + for layer in self.layers: + hidden = layer(hidden) + + return hidden + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +################################################################################ +# The following code for ResNet is adapted from the TensorFlow implementation +# https://github.com/tensorflow/models/blob/master/official/resnet/resnet_model.py +################################################################################ + +################################################################################ +# Convenience functions for building the ResNet model. +################################################################################ +resnet_choices = { + 8: [1, 2, 2], + 14: [1, 2, 2], + 18: [2, 2, 2, 2], + 34: [3, 4, 6, 3], + 50: [3, 4, 6, 3], + 101: [3, 4, 23, 3], + 152: [3, 8, 36, 3], + 200: [3, 24, 36, 3], +} + + +def get_resnet_block_sizes(resnet_size): + """Retrieve the size of each block_layer in the ResNet model. + + The number of block layers used for the Resnet model varies according + to the size of the model. This helper grabs the layer set we want, throwing + an error if a non-standard size has been selected. + Args: + resnet_size: The number of convolutional layers needed in the model. + Returns: + A list of block sizes to use in building the model. + Raises: + KeyError: if invalid resnet_size is received. + """ + try: + return resnet_choices[resnet_size] + except KeyError: + err = ( + "Could not find layers for selected Resnet size.\n" + f"Size received: {resnet_size}; sizes allowed: {resnet_choices.keys()}." + ) + raise ValueError(err) + + +class UNetDoubleConvLayer(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + in_channels: int, + out_channels: int, + norm: str | None = None, + ): + """Two Conv2d layers, each followed by a ReLU, used for U-Net. + + Args: + img_height: the input image height + img_width: the input image width + in_channels: the number of input channels + out_channels: the number of output channels + norm: the normalization to be applied + """ + super().__init__() + + self.layers = nn.ModuleList() + + self.layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)) + if norm == "batch": + self.layers.append(nn.BatchNorm2d(out_channels)) + self.layers.append(nn.ReLU()) + + self.layers.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)) + if norm == "batch": + self.layers.append(nn.BatchNorm2d(out_channels)) + self.layers.append(nn.ReLU()) + + self._input_shape = (in_channels, img_height, img_width) + self._output_shape = (out_channels, img_height, img_width) + + def forward(self, inputs): + hidden = inputs + + for layer in self.layers: + hidden = layer(hidden) + + return hidden + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +class UNetDownStack(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + in_channels: int, + norm: str | None = None, + stack_depth: int = 4, + ): + """Creates the contracting downsampling path of a U-Net stack. + + Implements + U-Net: Convolutional Networks for Biomedical Image Segmentation + https://arxiv.org/abs/1505.04597 + by Olaf Ronneberger, Philipp Fischer, Thomas Brox, May 2015. + + Args: + img_height: the input image height + img_width: the input image width + in_channels: the number of input channels + norm: the normalization to be applied + stack_depth: the depth of the unet stack + """ + super().__init__() + + self.conv_layers = nn.ModuleList() + self.down_layers = nn.ModuleList() + + height = img_height + width = img_width + in_c = in_channels + out_c = 64 + + self._input_shape = (in_c, height, width) + + for _i in range(stack_depth): + self.conv_layers.append(UNetDoubleConvLayer(height, width, in_c, out_c, norm)) + in_c = out_c + out_c = out_c * 2 + + self.down_layers.append(nn.MaxPool2d(kernel_size=2, stride=2)) + height = height // 2 + width = width // 2 + + self.bottleneck = UNetDoubleConvLayer(height, width, in_c, out_c, norm) + + self._output_shape = (out_c, height, width) + + def forward(self, inputs): + skips = [] # skip connections + hidden = inputs + + for conv_layer, down_layer in zip(self.conv_layers, self.down_layers): + hidden = conv_layer(hidden) + skips.append(hidden) + hidden = down_layer(hidden) + + hidden = self.bottleneck(hidden) + return hidden, skips + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + +class UNetUpStack(LudwigModule): + def __init__( + self, + img_height: int, + img_width: int, + out_channels: int, + norm: str | None = None, + stack_depth: int = 4, + ): + """Creates the expansive upsampling path of a U-Net stack. + + Implements + U-Net: Convolutional Networks for Biomedical Image Segmentation + https://arxiv.org/abs/1505.04597 + by Olaf Ronneberger, Philipp Fischer, Thomas Brox, May 2015. + + Args: + img_height: the output image height + img_width: the output image width + out_channels: the number of output classes + norm: the normalization to be applied + stack_depth: the depth of the unet stack + """ + super().__init__() + + self.conv_layers = nn.ModuleList() + self.up_layers = nn.ModuleList() + + height = img_height >> stack_depth + width = img_width >> stack_depth + in_c = 64 << stack_depth + out_c = in_c // 2 + + self._input_shape = (in_c, height, width) + + for _i in range(stack_depth): + self.up_layers.append(nn.ConvTranspose2d(in_c, out_c, kernel_size=2, stride=2)) + height = height * 2 + width = width * 2 + + self.conv_layers.append(UNetDoubleConvLayer(height, width, out_c * 2, out_c, norm)) + in_c = out_c + out_c = out_c // 2 + + self.last_conv = nn.Conv2d(in_c, out_channels, kernel_size=1, padding=0) + + self._output_shape = (out_channels, img_height, img_width) + + def forward(self, inputs, skips): + hidden = inputs + + for conv_layer, up_layer in zip(self.conv_layers, self.up_layers): + hidden = up_layer(hidden) + skip = skips.pop() + hidden = torch.cat([hidden, skip], axis=1) + hidden = conv_layer(hidden) + + hidden = self.last_conv(hidden) + return hidden + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) diff --git a/ludwig/modules/dpo_loss.py b/ludwig/modules/dpo_loss.py new file mode 100644 index 0000000..5ddd95a --- /dev/null +++ b/ludwig/modules/dpo_loss.py @@ -0,0 +1,108 @@ +"""Direct Preference Optimization (DPO) loss function. + +Implements the DPO loss from Rafailov et al., "Direct Preference Optimization: +Your Language Model is Secretly a Reward Model", NeurIPS 2023. + +DPO trains a language model to prefer "chosen" completions over "rejected" ones +by optimizing: + L_DPO = -log(sigmoid(beta * (log_ratio_chosen - log_ratio_rejected))) + +where log_ratio = log(pi(y|x)) - log(pi_ref(y|x)) for policy pi and reference pi_ref. +""" + +import torch +import torch.nn.functional as F + + +def compute_token_log_probs(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor: + """Compute per-token log probabilities from logits and labels. + + Args: + logits: [batch, seq_len, vocab_size] + labels: [batch, seq_len] with -100 for tokens to ignore + + Returns: + Per-example sum of log probabilities (only for non-ignored tokens). + Shape: [batch] + """ + # Shift for next-token prediction: logits predict next token + shift_logits = logits[:, :-1, :].contiguous() + shift_labels = labels[:, 1:].contiguous() + + # Per-token log probs + log_probs = F.log_softmax(shift_logits, dim=-1) + per_token_log_probs = log_probs.gather(dim=-1, index=shift_labels.clamp(min=0).unsqueeze(-1)).squeeze(-1) + + # Mask out ignored tokens (-100) + mask = shift_labels != -100 + per_token_log_probs = per_token_log_probs * mask + + # Sum log probs per example + return per_token_log_probs.sum(dim=-1) + + +def dpo_loss( + policy_chosen_logits: torch.Tensor, + policy_rejected_logits: torch.Tensor, + chosen_labels: torch.Tensor, + rejected_labels: torch.Tensor, + reference_chosen_log_probs: torch.Tensor | None = None, + reference_rejected_log_probs: torch.Tensor | None = None, + beta: float = 0.1, + label_smoothing: float = 0.0, + loss_type: str = "sigmoid", +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Compute DPO loss. + + When reference log probs are None, uses the implicit reference model + (the model's own log probs before training, approximated as 0). + + Args: + policy_chosen_logits: [batch, seq_len, vocab_size] from policy model on chosen text + policy_rejected_logits: [batch, seq_len, vocab_size] from policy model on rejected text + chosen_labels: [batch, seq_len] token IDs for chosen, -100 for masked + rejected_labels: [batch, seq_len] token IDs for rejected, -100 for masked + reference_chosen_log_probs: [batch] pre-computed reference model log probs (optional) + reference_rejected_log_probs: [batch] pre-computed reference model log probs (optional) + beta: Temperature parameter controlling deviation from reference (typical: 0.1-0.5) + label_smoothing: Smoothing factor for label targets (0 = no smoothing) + loss_type: "sigmoid" (standard DPO) or "ipo" (identity preference optimization) + + Returns: + (loss, chosen_rewards, rejected_rewards) + """ + policy_chosen_log_probs = compute_token_log_probs(policy_chosen_logits, chosen_labels) + policy_rejected_log_probs = compute_token_log_probs(policy_rejected_logits, rejected_labels) + + # Log ratios: log(pi(y|x)) - log(pi_ref(y|x)) + if reference_chosen_log_probs is not None: + chosen_log_ratios = policy_chosen_log_probs - reference_chosen_log_probs + else: + chosen_log_ratios = policy_chosen_log_probs + + if reference_rejected_log_probs is not None: + rejected_log_ratios = policy_rejected_log_probs - reference_rejected_log_probs + else: + rejected_log_ratios = policy_rejected_log_probs + + # Preference margin + logits_diff = beta * (chosen_log_ratios - rejected_log_ratios) + + if loss_type == "sigmoid": + # Standard DPO: -log(sigmoid(beta * (log_ratio_chosen - log_ratio_rejected))) + if label_smoothing > 0: + # Soft labels: interpolate between 1 and 0.5 + losses = -F.logsigmoid(logits_diff) * (1 - label_smoothing) - F.logsigmoid(-logits_diff) * label_smoothing + else: + losses = -F.logsigmoid(logits_diff) + elif loss_type == "ipo": + # Identity Preference Optimization: (logits_diff - 1/(2*beta))^2 + losses = (logits_diff - 1 / (2 * beta)) ** 2 + else: + raise ValueError(f"Unknown DPO loss type: {loss_type}. Use 'sigmoid' or 'ipo'.") + + # Rewards for logging + chosen_rewards = beta * chosen_log_ratios.detach() + rejected_rewards = beta * rejected_log_ratios.detach() + + return losses.mean(), chosen_rewards.mean(), rejected_rewards.mean() diff --git a/ludwig/modules/embedding_modules.py b/ludwig/modules/embedding_modules.py new file mode 100644 index 0000000..3e991b8 --- /dev/null +++ b/ludwig/modules/embedding_modules.py @@ -0,0 +1,408 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch +from torch import nn + +from ludwig.constants import TYPE +from ludwig.modules.initializer_modules import get_initializer +from ludwig.utils.data_utils import load_pretrained_embeddings +from ludwig.utils.torch_utils import get_torch_device, LudwigModule + +logger = logging.getLogger(__name__) + +DEVICE = get_torch_device() + + +def embedding_matrix( + vocab: list[str], + embedding_size: int, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embedding_initializer: str | dict | None = None, +) -> tuple[nn.Module, int]: + """Returns initialized torch.nn.Embedding module and embedding size.""" + + vocab_size = len(vocab) + if representation == "dense": + if pretrained_embeddings: + embeddings_matrix = load_pretrained_embeddings(pretrained_embeddings, vocab) + if embeddings_matrix.shape[-1] != embedding_size: + if not force_embedding_size: + embedding_size = embeddings_matrix.shape[-1] + logger.info(f"Setting embedding size to be equal to {embeddings_matrix.shape[-1]}.") + else: + raise ValueError( + f"The size of the pretrained embeddings is " + f"{embeddings_matrix.shape[-1]}, but the specified " + f"embedding_size is {embedding_size}. Please change " + f"the embedding_size accordingly." + ) + embedding_initializer_obj = torch.tensor(embeddings_matrix, dtype=torch.float32) + + else: + if vocab_size < embedding_size and not force_embedding_size: + logger.info( + f" embedding_size ({embedding_size}) is greater than " + f"vocab_size ({vocab_size}). Setting embedding size to be " + f"equal to vocab_size." + ) + embedding_size = vocab_size + + if embedding_initializer is not None: + embedding_initializer_obj_ref = get_initializer(embedding_initializer) + else: + embedding_initializer_obj_ref = get_initializer({TYPE: "uniform", "a": -1.0, "b": 1.0}) + embedding_initializer_obj = embedding_initializer_obj_ref([vocab_size, embedding_size]) + + embeddings = embedding_initializer_obj + + elif representation == "sparse": + embedding_size = vocab_size + embeddings = get_initializer("identity")([vocab_size, embedding_size]) + embeddings.requires_grad = False + else: + raise Exception(f"Embedding representation {representation} not supported.") + + embeddings = nn.Embedding.from_pretrained(embeddings, freeze=not embeddings_trainable) + return embeddings, embedding_size + + +def embedding_matrix_on_device( + vocab: list[str], + embedding_size: int, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embeddings_on_cpu: bool = False, + embedding_initializer: str | None = None, +) -> tuple[nn.Module, int]: + embeddings, embedding_size = embedding_matrix( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embedding_initializer=embedding_initializer, + ) + if embeddings_on_cpu: + embeddings.to("cpu") + elif not embeddings_on_cpu and torch.cuda.is_available(): + embeddings.to(device="cuda") + + return embeddings, embedding_size + + +class Embed(LudwigModule): + """Module to embed Category, Date, and H3 data types.""" + + def __init__( + self, + vocab: list[str], + embedding_size: int, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embeddings_on_cpu: bool = False, + dropout: float = 0.0, + embedding_initializer: str | dict | None = None, + ): + super().__init__() + self.supports_masking = True + + self.vocab_size = len(vocab) + self.embeddings, self.embedding_size = embedding_matrix_on_device( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + embedding_initializer=embedding_initializer, + ) + + if dropout > 0: + self.dropout = torch.nn.Dropout(p=dropout) + else: + self.dropout = None + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: + if inputs.ndim != 2 or inputs.shape[1] != 1: + raise RuntimeError( + f"Embed only takes inputs of shape [batch x 1]. Received inputs with size: {inputs.size()}" + ) + embedded = self.embeddings(inputs.long()) + embedded = torch.squeeze(embedded, dim=1) + if self.dropout: + embedded = self.dropout(embedded) + return embedded + + @property + def input_shape(self) -> torch.Size: + return torch.Size([1]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.embedding_size]) + + +class EmbedSet(LudwigModule): + """Module to embed Set data types, works on multi-hot encoded input.""" + + def __init__( + self, + vocab: list[str], + embedding_size: int, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embeddings_on_cpu: bool = False, + dropout: float = 0.0, + embedding_initializer: str | dict | None = None, + aggregation_function: str = "sum", + ): + super().__init__() + self.supports_masking = True + + self.vocab_size = len(vocab) + self.embeddings, self.embedding_size = embedding_matrix_on_device( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + embedding_initializer=embedding_initializer, + ) + + if dropout > 0: + self.dropout = torch.nn.Dropout(p=dropout) + else: + self.dropout = None + + if aggregation_function == "sum": + self.aggregation_function = torch.sum + elif aggregation_function == "avg": + self.aggregation_function = torch.mean + else: + raise ValueError(f"Unsupported aggregation function {aggregation_function}") + + self.register_buffer("vocab_indices", torch.arange(self.vocab_size)) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: + """ + Params: + inputs: Boolean multi-hot tensor of size [batch x vocab_size], where + inputs[b, i] indicates that token i is present in sample b. + """ + # Convert multi-hot input to input of indices + inputs = inputs.int() * self.vocab_indices + embedded = self.embeddings(inputs.long()) + # Mask out the 0th embedding + mask = torch.unsqueeze(inputs, -1) + embedded = embedded * mask + # Sum over all positive tokens + embedded = self.aggregation_function(embedded, dim=1) + if self.dropout: + embedded = self.dropout(embedded) + return embedded + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.vocab_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.embedding_size]) + + @property + def input_dtype(self): + return torch.bool + + +class EmbedWeighted(LudwigModule): + """Module to embed Bag data type, works on input of token frequencies.""" + + def __init__( + self, + vocab: list[str], + embedding_size: int, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embeddings_on_cpu: bool = False, + dropout: float = 0.0, + embedding_initializer: str | None = None, + ): + super().__init__() + + self.embeddings, self.embedding_size = embedding_matrix_on_device( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + embedding_initializer=embedding_initializer, + ) + self.vocab_size = len(vocab) + + if dropout > 0: + self.dropout = nn.Dropout(dropout) + else: + self.dropout = None + + self.register_buffer("vocab_indices", torch.arange(self.vocab_size, dtype=torch.int32)) + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: + """ + Params: + inputs: Tensor of frequencies, where inputs[b, i] represents + frequency of token i in sample b of batch. + """ + # Convert to multi-hot input + signed_input = (inputs != 0).type(torch.int32) + multiple_hot_indexes = signed_input * self.vocab_indices + embedded = self.embeddings(multiple_hot_indexes) + # Mask out the 0th embedding + mask = torch.unsqueeze(inputs, -1) + weighted_embedded = embedded * mask + # Sum over the all the positive indices + embedded_reduced = torch.sum(weighted_embedded, dim=1) + if self.dropout: + embedded_reduced = self.dropout(embedded_reduced) + return embedded_reduced + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.vocab_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.embedding_size]) + + +class EmbedSequence(LudwigModule): + def __init__( + self, + vocab: list[str], + embedding_size: int, + max_sequence_length: int, + representation: str = "dense", + embeddings_trainable: bool = True, + pretrained_embeddings: str | None = None, + force_embedding_size: bool = False, + embeddings_on_cpu: bool = False, + dropout: float = 0.0, + embedding_initializer: str | None = None, + ): + super().__init__() + self.supports_masking = True + + self.vocab_size = len(vocab) + self.max_sequence_length = max_sequence_length + self.embeddings, self.embedding_size = embedding_matrix_on_device( + vocab, + embedding_size, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + embedding_initializer=embedding_initializer, + ) + + if dropout > 0: + self.dropout = nn.Dropout(dropout) + else: + self.dropout = None + + def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None): + if inputs.dtype not in [torch.int, torch.long]: + raise RuntimeError( + f"Expected tensor of type torch.int or torch.long as input.Received {inputs.dtype} instead." + ) + + embedded = self.embeddings(inputs) + if self.dropout: + embedded = self.dropout(embedded) + return embedded + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length, self.embedding_size]) + + +class TokenAndPositionEmbedding(LudwigModule): + def __init__( + self, + max_sequence_length, + vocab, + embedding_size, + representation="dense", + embeddings_trainable=True, + pretrained_embeddings=None, + force_embedding_size=False, + embeddings_on_cpu=False, + dropout=0.0, + embedding_initializer=None, + ): + super().__init__() + self.max_sequence_length = max_sequence_length + self.embedding_size = embedding_size + self.token_embed = EmbedSequence( + vocab=vocab, + embedding_size=embedding_size, + max_sequence_length=max_sequence_length, + representation=representation, + embeddings_trainable=embeddings_trainable, + pretrained_embeddings=pretrained_embeddings, + force_embedding_size=force_embedding_size, + embeddings_on_cpu=embeddings_on_cpu, + dropout=dropout, + embedding_initializer=embedding_initializer, + ) + self.position_embed = nn.Embedding( + num_embeddings=max_sequence_length, embedding_dim=self.token_embed.embedding_size + ) + self.register_buffer("positions", torch.arange(0, max_sequence_length)) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.max_sequence_length]) + + @property + def output_shape(self) -> torch.Size: + return self.token_embed.output_shape + + def forward(self, inputs, mask: torch.Tensor | None = None): + positions_hidden = self.position_embed(self.positions) + token_hidden = self.token_embed(inputs) + return token_hidden + positions_hidden diff --git a/ludwig/modules/fully_connected_modules.py b/ludwig/modules/fully_connected_modules.py new file mode 100644 index 0000000..964b3f7 --- /dev/null +++ b/ludwig/modules/fully_connected_modules.py @@ -0,0 +1,220 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from copy import deepcopy + +import torch +from torch.nn import Dropout, Linear, ModuleList + +from ludwig.modules.normalization_modules import create_norm_layer +from ludwig.utils.torch_utils import activations, initializer_registry, LudwigModule + +logger = logging.getLogger(__name__) + + +class FCLayer(LudwigModule): + """A torch.nn.Linear wrapper that declares input and output shapes, and enables the customization of: + + 1. how weights and biases are initialized + 2. normalization (layer and batch) + 3. activations + 4. dropout + """ + + def __init__( + self, + input_size: int, + input_rank: int = 2, + output_size: int = 256, + use_bias: bool = True, + weights_initializer: str = "xavier_uniform", + bias_initializer: str = "zeros", + norm: str | None = None, + norm_params: dict | None = None, + activation: str = "relu", + dropout: float = 0, + ): + super().__init__() + + self.layers = ModuleList() + self.input_size = input_size + self.output_size = output_size + + fc = Linear(in_features=input_size, out_features=output_size, bias=use_bias) + self.layers.append(fc) + + weights_initializer = initializer_registry[weights_initializer] + weights_initializer(fc.weight) + + if use_bias: + bias_initializer = initializer_registry[bias_initializer] + bias_initializer(fc.bias) + + if norm is not None: + norm_params = norm_params or {} + self.layers.append(create_norm_layer(norm, input_rank, output_size, **norm_params)) + + # Dict for activation objects in pytorch? + self.layers.append(activations[activation]()) + + if dropout > 0: + self.layers.append(Dropout(dropout)) + + def forward(self, inputs, mask=None): + hidden = inputs + for layer in self.layers: + hidden = layer(hidden) + + return hidden + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.output_size]) + + +class FCStack(LudwigModule): + """A stack of FCLayers. + + The specification of each FCLayer is specified by the `layers` dictionary parameter, whose keys correspond with an + FCLayer's constructor arguments, i.e. + + [ + {"input_size": 2, "output_size": 4}, + {"output_size": 4, "use_bias": False}, + ] + + `default_*` parameters dictate default values to use for each FCLayer, if not specified by `layers`. If `layers` is + `None`, then a stack of size `num_layers` of `FCLayer`s configured with all of the `default_*` parameters is used. + + If `layers` is None and `num_layers` is 0, then there are no fully connected layers and this module serves as a + trivial passthrough. + """ + + def __init__( + self, + first_layer_input_size: int, + layers: list[dict] | None = None, + num_layers: int = 1, + default_input_rank: int = 2, + default_output_size: int = 256, + default_use_bias: bool = True, + default_weights_initializer: str = "xavier_uniform", + default_bias_initializer: str = "zeros", + default_norm: str | None = None, + default_norm_params: dict | None = None, + default_activation: str = "relu", + default_dropout: float = 0, + residual: bool = False, + **kwargs, + ): + super().__init__() + self.input_size = first_layer_input_size + + self.norm_layer = None + if default_norm is not None: + norm_params = default_norm_params or {} + self.norm_layer = create_norm_layer(default_norm, default_input_rank, self.input_size, **norm_params) + + self.dropout = None + if default_dropout > 0: + self.dropout = torch.nn.Dropout(default_dropout) + + if layers is None: + self.layers = [] + for i in range(num_layers): + self.layers.append({}) + else: + # deep copy the layer definitions so that we don't modify the original + self.layers = deepcopy(layers) + + if len(self.layers) > 0 and "input_size" not in self.layers[0]: + self.layers[0]["input_size"] = first_layer_input_size + for i, layer in enumerate(self.layers): + if i != 0: + layer["input_size"] = self.layers[i - 1]["output_size"] + if "input_rank" not in layer: + layer["input_rank"] = default_input_rank + if "output_size" not in layer: + layer["output_size"] = default_output_size + if "use_bias" not in layer: + layer["use_bias"] = default_use_bias + if "weights_initializer" not in layer: + layer["weights_initializer"] = default_weights_initializer + if "bias_initializer" not in layer: + layer["bias_initializer"] = default_bias_initializer + if "norm" not in layer: + layer["norm"] = default_norm + if "norm_params" not in layer: + layer["norm_params"] = default_norm_params + if "activation" not in layer: + layer["activation"] = default_activation + if "dropout" not in layer: + layer["dropout"] = default_dropout + + self.stack = ModuleList() + + for i, layer in enumerate(self.layers): + self.stack.append( + FCLayer( + input_size=layer["input_size"], + input_rank=layer["input_rank"], + output_size=layer["output_size"], + use_bias=layer["use_bias"], + weights_initializer=layer["weights_initializer"], + bias_initializer=layer["bias_initializer"], + norm=layer["norm"], + norm_params=layer["norm_params"], + activation=layer["activation"], + dropout=layer["dropout"], + ) + ) + self.residual = residual + + def forward(self, inputs, mask=None): + hidden = inputs + + if self.norm_layer is not None: + hidden = self.norm_layer(hidden) + + if self.dropout is not None: + hidden = self.dropout(hidden) + + prev_fc_layer_size = self.input_size + for layer in self.stack: + out = layer(hidden) + if self.residual and layer.output_size == prev_fc_layer_size: + hidden = hidden + out + else: + hidden = out + prev_fc_layer_size = layer.layers[0].out_features + return hidden + + @property + def num_layers(self) -> int: + return len(self.layers) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + if len(self.stack) > 0: + return self.stack[-1].output_shape + return torch.Size([self.input_size]) diff --git a/ludwig/modules/gated_cross_attention.py b/ludwig/modules/gated_cross_attention.py new file mode 100644 index 0000000..6486b38 --- /dev/null +++ b/ludwig/modules/gated_cross_attention.py @@ -0,0 +1,105 @@ +"""Gated cross-attention module for vision-language fusion (Flamingo-style). + +Based on the Flamingo paper (Alayrac et al., NeurIPS 2022). A gated cross-attention block +conditions a text (or other query) representation on visual (or other key/value) tokens. +The gate is initialised to zero so that the block is an identity at the start of training: +the pretrained language model's behaviour is preserved until the gate learns to attend to +the visual features. Stable fine-tuning of VLMs on new modalities typically requires this +zero-init gating, otherwise the random cross-attention outputs wreck the LM head's calibration +on the first step. +""" + +from __future__ import annotations + +import torch +import torch.nn as nn + + +class GatedCrossAttention(nn.Module): + """Flamingo-style gated cross-attention block. + + Args: + d_model: hidden size of the query stream (usually the LM hidden size). + num_heads: number of attention heads. + kv_dim: hidden size of the key/value stream (usually the vision encoder output). + Defaults to ``d_model``. + ffn_size: feed-forward network width. Defaults to ``4 * d_model``. + dropout: dropout probability applied after attention and FFN. + tanh_gate: if True, wrap the gates in ``tanh`` so they're bounded in ``(-1, 1)``. + + The module owns two learnable scalar gates, ``attn_gate`` and ``ffn_gate``, both + initialised to zero. A forward pass over ``(x, kv)`` computes: + + .. code:: text + + x = x + tanh(attn_gate) * CrossAttn(x, kv) + x = x + tanh(ffn_gate) * FFN(x) + + so at step 0 the module is exactly the identity. + """ + + def __init__( + self, + d_model: int, + num_heads: int = 8, + kv_dim: int | None = None, + ffn_size: int | None = None, + dropout: float = 0.0, + tanh_gate: bool = True, + ) -> None: + super().__init__() + kv_dim = kv_dim if kv_dim is not None else d_model + ffn_size = ffn_size if ffn_size is not None else 4 * d_model + + self.q_norm = nn.LayerNorm(d_model) + self.kv_norm = nn.LayerNorm(kv_dim) + self.kv_proj = nn.Linear(kv_dim, d_model) if kv_dim != d_model else nn.Identity() + + self.cross_attn = nn.MultiheadAttention( + embed_dim=d_model, + num_heads=num_heads, + dropout=dropout, + batch_first=True, + ) + + self.ffn_norm = nn.LayerNorm(d_model) + self.ffn = nn.Sequential( + nn.Linear(d_model, ffn_size), + nn.GELU(), + nn.Dropout(dropout), + nn.Linear(ffn_size, d_model), + ) + + self.attn_gate = nn.Parameter(torch.zeros(1)) + self.ffn_gate = nn.Parameter(torch.zeros(1)) + self.tanh_gate = tanh_gate + + def _gate(self, g: torch.Tensor) -> torch.Tensor: + return torch.tanh(g) if self.tanh_gate else g + + def forward( + self, + x: torch.Tensor, + kv: torch.Tensor, + *, + key_padding_mask: torch.Tensor | None = None, + ) -> torch.Tensor: + """Apply gated cross-attention. + + Args: + x: query stream, shape ``(batch, seq_len_q, d_model)``. + kv: key/value stream, shape ``(batch, seq_len_kv, kv_dim)``. + key_padding_mask: optional bool mask with ``True`` at padded positions in ``kv``, + shape ``(batch, seq_len_kv)``. + + Returns: + Updated query stream, same shape as ``x``. + """ + kv_proj = self.kv_proj(self.kv_norm(kv)) + q_norm = self.q_norm(x) + attn_out, _ = self.cross_attn(q_norm, kv_proj, kv_proj, key_padding_mask=key_padding_mask, need_weights=False) + x = x + self._gate(self.attn_gate) * attn_out + + ffn_out = self.ffn(self.ffn_norm(x)) + x = x + self._gate(self.ffn_gate) * ffn_out + return x diff --git a/ludwig/modules/initializer_modules.py b/ludwig/modules/initializer_modules.py new file mode 100644 index 0000000..17d5bdc --- /dev/null +++ b/ludwig/modules/initializer_modules.py @@ -0,0 +1,45 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import torch + +from ludwig.constants import TYPE +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.torch_utils import initializer_registry + + +def _create_and_init(init_fn, init_kwargs, *args, **kwargs): + t = torch.empty(*args, **kwargs) + init_fn(t, **init_kwargs) + return t + + +def get_initializer(parameters): + if parameters is None: + return lambda *args, **kwargs: _create_and_init(initializer_registry[parameters], {}, *args, **kwargs) + elif isinstance(parameters, str): + initializer_fun = get_from_registry(parameters, initializer_registry) + return lambda *args, **kwargs: _create_and_init(initializer_fun, {}, *args, **kwargs) + elif isinstance(parameters, dict): + initializer_fun = get_from_registry(parameters[TYPE], initializer_registry) + init_kwargs = parameters.copy() + del init_kwargs[TYPE] + return lambda *args, **kwargs: _create_and_init(initializer_fun, init_kwargs, *args, **kwargs) + else: + raise ValueError( + f"Initializers parameters should be either strings or dictionaries, " + f"but the provided parameters are a {type(parameters)}. " + f"Parameters values: {parameters}" + ) diff --git a/ludwig/modules/loss_balancing.py b/ludwig/modules/loss_balancing.py new file mode 100644 index 0000000..86c0c47 --- /dev/null +++ b/ludwig/modules/loss_balancing.py @@ -0,0 +1,318 @@ +"""Multi-task loss balancing strategies. + +Replaces the static weighted sum in BaseModel.train_loss() with pluggable strategies: +- none: Static weighted sum (current behavior) +- log_transform: log(1 + loss) compression (DB-MTL, 2024) +- uncertainty: Homoscedastic uncertainty weighting (Kendall et al., CVPR 2018) +- famo: Fast Adaptive Multitask Optimization (Liu et al., NeurIPS 2023) +- gradnorm: Gradient normalization (Chen et al., ICML 2018) +- nash_mtl: Nash bargaining for multi-task learning (Navon et al., ICML 2022) +- pareto_mtl: Preference-conditioned Pareto scalarisation (Mahapatra & Rajan, ICML 2020) +""" + +import logging +from abc import ABC, abstractmethod + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from ludwig.utils.torch_utils import LudwigModule + +logger = logging.getLogger(__name__) + + +class LossBalancer(LudwigModule, ABC): + """Base class for multi-task loss balancing strategies.""" + + def __init__(self, output_feature_names: list[str]): + super().__init__() + self.output_feature_names = output_feature_names + + @abstractmethod + def forward(self, per_task_losses: dict[str, torch.Tensor], per_task_weights: dict[str, float]) -> torch.Tensor: + """Compute the balanced total loss from individual task losses. + + Args: + per_task_losses: Dict mapping output feature name to scalar loss tensor. + per_task_weights: Dict mapping output feature name to static weight from config. + + Returns: + Scalar total loss tensor. + """ + ... + + def post_step(self, per_task_losses: dict[str, torch.Tensor]): + """Hook called after optimizer step. + + Override for strategies needing EMA updates. + """ + pass + + @property + def input_shape(self) -> torch.Size: + return torch.Size([len(self.output_feature_names)]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([1]) + + +class NoneLossBalancer(LossBalancer): + """Static weighted sum — reproduces the original Ludwig behavior.""" + + def forward(self, per_task_losses, per_task_weights): + return sum(per_task_weights[k] * per_task_losses[k] for k in per_task_losses) + + +class LogTransformLossBalancer(LossBalancer): + """Log-transform loss compression (DB-MTL, 2024). + + Applies log(1 + loss) to compress loss scales before weighting, preventing large-scale tasks from dominating. + """ + + def forward(self, per_task_losses, per_task_weights): + return sum(per_task_weights[k] * torch.log1p(per_task_losses[k]) for k in per_task_losses) + + +class UncertaintyLossBalancer(LossBalancer): + """Homoscedastic uncertainty weighting (Kendall et al., CVPR 2018). + + Learns a log-variance parameter per task. Loss becomes: precision * task_loss + 0.5 * log_var, where precision = + exp(-log_var). No hyperparameters needed. + """ + + def __init__(self, output_feature_names: list[str]): + super().__init__(output_feature_names) + self.log_vars = nn.ParameterDict({name: nn.Parameter(torch.zeros(1)) for name in output_feature_names}) + + def forward(self, per_task_losses, per_task_weights): + total = next(iter(per_task_losses.values())).new_zeros(1).squeeze() + for name, loss in per_task_losses.items(): + # Clamp log_vars to prevent exp overflow / underflow. + log_var = self.log_vars[name].clamp(min=-20.0, max=20.0) + precision = torch.exp(-log_var) + total = total + per_task_weights[name] * (precision * loss + 0.5 * log_var) + return total + + +class FAMOLossBalancer(LossBalancer): + """Fast Adaptive Multitask Optimization (Liu et al., NeurIPS 2023). + + Maintains learnable softmax weights updated via EMA of loss ratios. O(1) overhead, no gradient hooks needed. + """ + + def __init__(self, output_feature_names: list[str], alpha: float = 0.1, lr: float = 0.01): + super().__init__(output_feature_names) + self.alpha = alpha + self.lr = lr + self.log_weights = nn.ParameterDict({name: nn.Parameter(torch.zeros(1)) for name in output_feature_names}) + self._prev_losses: dict[str, float] = {} + + def forward(self, per_task_losses, per_task_weights): + log_w = torch.stack([self.log_weights[name] for name in self.output_feature_names]) + weights = F.softmax(log_w, dim=0) + + total = next(iter(per_task_losses.values())).new_zeros(1).squeeze() + for i, name in enumerate(self.output_feature_names): + total = total + per_task_weights[name] * weights[i] * per_task_losses[name] + return total + + @torch.no_grad() + def post_step(self, per_task_losses): + if self._prev_losses: + for name in self.output_feature_names: + curr = per_task_losses[name].detach().item() + prev = self._prev_losses.get(name, curr) + if prev > 0: + ratio = curr / (prev + 1e-8) + self.log_weights[name].data += self.lr * (ratio - 1.0) + + self._prev_losses = {name: loss.detach().item() for name, loss in per_task_losses.items()} + + +class GradNormLossBalancer(LossBalancer): + """Gradient normalization (Chen et al., ICML 2018). + + Dynamically adjusts task weights to normalize gradient magnitudes across tasks, preventing any single task from + dominating the shared representation. Requires gradient computation w.r.t. shared layer. + """ + + def __init__(self, output_feature_names: list[str], alpha: float = 1.5, **kwargs): + super().__init__(output_feature_names) + self.alpha = alpha + self.task_weights = nn.ParameterDict({name: nn.Parameter(torch.ones(1)) for name in output_feature_names}) + self._initial_losses: dict[str, float] = {} + + def forward(self, per_task_losses, per_task_weights): + total = next(iter(per_task_losses.values())).new_zeros(1).squeeze() + for name, loss in per_task_losses.items(): + total = total + torch.abs(self.task_weights[name]) * per_task_weights[name] * loss + return total + + @torch.no_grad() + def post_step(self, per_task_losses): + if not self._initial_losses: + self._initial_losses = {name: loss.detach().item() for name, loss in per_task_losses.items()} + return + + loss_ratios = {} + for name in self.output_feature_names: + initial = self._initial_losses.get(name, 1.0) + current = per_task_losses[name].detach().item() + loss_ratios[name] = current / (initial + 1e-8) + + num_tasks = len(self.output_feature_names) + mean_ratio = sum(loss_ratios.values()) / num_tasks + + for name in self.output_feature_names: + relative_rate = loss_ratios[name] / (mean_ratio + 1e-8) + target_weight = relative_rate**self.alpha + self.task_weights[name].data = 0.9 * self.task_weights[name].data + 0.1 * target_weight + + # Renormalize weights to sum to num_tasks + total_weight = sum(torch.abs(self.task_weights[name]).item() for name in self.output_feature_names) + for name in self.output_feature_names: + self.task_weights[name].data *= num_tasks / (total_weight + 1e-8) + + +class NashMTLLossBalancer(LossBalancer): + """Nash-MTL: Nash bargaining for multi-task learning (Navon et al., ICML 2022). + + Finds the Nash bargaining solution for task weight allocation by solving a + cooperative game where each task is a player. More principled than heuristic + methods but computationally more expensive (requires per-task gradients). + + For most use cases, FAMO or Uncertainty weighting are sufficient. + Nash-MTL is for power users with many conflicting output features. + """ + + def __init__(self, output_feature_names: list[str], update_rate: float = 0.1, **kwargs): + super().__init__(output_feature_names) + self.update_rate = update_rate + n = len(output_feature_names) + self.task_weights = nn.ParameterDict({name: nn.Parameter(torch.ones(1) / n) for name in output_feature_names}) + + def forward(self, per_task_losses, per_task_weights): + total = next(iter(per_task_losses.values())).new_zeros(1).squeeze() + for name, loss in per_task_losses.items(): + total = total + torch.abs(self.task_weights[name]) * per_task_weights[name] * loss + return total + + @torch.no_grad() + def post_step(self, per_task_losses): + loss_values = torch.stack([per_task_losses[name].detach() for name in self.output_feature_names]) + inv_losses = 1.0 / (loss_values + 1e-8) + target_weights = inv_losses / inv_losses.sum() + + for i, name in enumerate(self.output_feature_names): + self.task_weights[name].data = (1 - self.update_rate) * self.task_weights[ + name + ].data + self.update_rate * target_weights[i] + + +class ParetoMTLLossBalancer(LossBalancer): + """Preference-vector-conditioned multi-task loss balancer. + + Implements exact-Pareto-optimal (EPO / PE-LGD style) scalarisation: + given a user preference vector ``lambda = (lambda_1, ..., lambda_T)`` with + ``sum(lambda) == 1``, training steers the loss tuple along the Pareto front + toward the point where losses are inversely proportional to ``lambda``. + + Concretely, this balancer combines two scalarisation schemes: + + * a *linear* component ``sum(lambda_i * L_i)`` — keeps training grounded in a + reasonable direction from step 0; + * a *Tchebycheff* component ``max_i (lambda_i * L_i)`` — drives convergence + toward the Pareto-optimal solution that matches the preference vector. + + The two are blended via ``tchebycheff_weight`` in ``[0, 1]``. A pure + Tchebycheff balancer (``tchebycheff_weight=1``) gives exact preference + adherence but is rough to train; a pure linear mix (``0``) trains smoothly but + doesn't match the preference as exactly. The default of ``0.5`` is the + "mixed-exact" scalarisation from Mahapatra & Rajan, ICML 2020. + + References: + * Mahapatra & Rajan, "Multi-Task Learning with User Preferences: Gradient + Descent with Controlled Ascent in Pareto Optimization", ICML 2020. + * Lin et al., "Pareto Multi-Task Learning", NeurIPS 2019. + """ + + def __init__( + self, + output_feature_names: list[str], + preference_vector: list[float] | None = None, + tchebycheff_weight: float = 0.5, + **kwargs, + ) -> None: + super().__init__(output_feature_names) + n = len(output_feature_names) + if preference_vector is None: + preference_vector = [1.0 / n] * n + if len(preference_vector) != n: + raise ValueError( + f"preference_vector has {len(preference_vector)} entries, expected one per output feature ({n})" + ) + if any(p < 0 for p in preference_vector): + raise ValueError("preference_vector entries must be non-negative") + total = float(sum(preference_vector)) + if total <= 0: + raise ValueError("preference_vector must sum to a positive value") + if not (0.0 <= tchebycheff_weight <= 1.0): + raise ValueError(f"tchebycheff_weight must be in [0, 1], got {tchebycheff_weight}") + + normalised = [p / total for p in preference_vector] + self.register_buffer( + "preference_vector", + torch.tensor(normalised, dtype=torch.float32), + ) + self.tchebycheff_weight = tchebycheff_weight + self._index = {name: i for i, name in enumerate(output_feature_names)} + + def forward(self, per_task_losses, per_task_weights): + device = next(iter(per_task_losses.values())).device + losses = torch.stack([per_task_losses[name] * per_task_weights[name] for name in self.output_feature_names]) + lam = self.preference_vector.to(device) + + linear_term = (lam * losses).sum() + tcheb_term = (lam * losses).max() + return (1.0 - self.tchebycheff_weight) * linear_term + self.tchebycheff_weight * tcheb_term + + +LOSS_BALANCER_REGISTRY: dict[str, type[LossBalancer]] = { + "none": NoneLossBalancer, + "log_transform": LogTransformLossBalancer, + "uncertainty": UncertaintyLossBalancer, + "famo": FAMOLossBalancer, + "gradnorm": GradNormLossBalancer, + "nash_mtl": NashMTLLossBalancer, + "pareto_mtl": ParetoMTLLossBalancer, +} + + +def create_loss_balancer( + strategy: str, + output_feature_names: list[str], + alpha: float = 1.5, + lr: float = 0.01, + preference_vector: list[float] | None = None, + tchebycheff_weight: float = 0.5, +) -> LossBalancer: + """Create a loss balancer from strategy name.""" + if strategy not in LOSS_BALANCER_REGISTRY: + valid = sorted(LOSS_BALANCER_REGISTRY) + raise ValueError(f"Unknown loss balancing strategy {strategy!r}. Valid options: {valid}") + + cls = LOSS_BALANCER_REGISTRY[strategy] + if strategy == "famo": + return cls(output_feature_names, alpha=alpha, lr=lr) + elif strategy == "gradnorm": + return cls(output_feature_names, alpha=alpha) + elif strategy == "pareto_mtl": + return cls( + output_feature_names, + preference_vector=preference_vector, + tchebycheff_weight=tchebycheff_weight, + ) + else: + return cls(output_feature_names) diff --git a/ludwig/modules/loss_implementations/__init__.py b/ludwig/modules/loss_implementations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/modules/loss_implementations/corn.py b/ludwig/modules/loss_implementations/corn.py new file mode 100644 index 0000000..9aa304a --- /dev/null +++ b/ludwig/modules/loss_implementations/corn.py @@ -0,0 +1,73 @@ +# Source: https://github.com/Raschka-research-group/coral-pytorch/blob/main/coral_pytorch/losses.py +# Sebastian Raschka 2020-2021 +# coral_pytorch +# Author: Sebastian Raschka +# +# License: MIT + +import torch +import torch.nn.functional as F + + +def corn_loss(logits, y_train, num_classes): + """Computes the CORN loss described in our forthcoming 'Deep Neural Networks for Rank Consistent Ordinal + Regression based on Conditional Probabilities' manuscript. + + Parameters + ---------- + logits : torch.tensor, shape=(num_examples, num_classes-1) + Outputs of the CORN layer. + + y_train : torch.tensor, shape=(num_examples) + Torch tensor containing the class labels. + + num_classes : int + Number of unique class labels (class labels should start at 0). + + Returns + ---------- + loss : torch.tensor + A torch.tensor containing a single loss value. + + Examples + ---------- + >>> # Consider 8 training examples + >>> _ = torch.manual_seed(123) + >>> X_train = torch.rand(8, 99) + >>> y_train = torch.tensor([0, 1, 2, 2, 2, 3, 4, 4]) + >>> NUM_CLASSES = 5 + >>> # + >>> # + >>> # def __init__(self): + >>> corn_net = torch.nn.Linear(99, NUM_CLASSES-1) + >>> # + >>> # + >>> # def forward(self, X_train): + >>> logits = corn_net(X_train) + >>> logits.shape + torch.Size([8, 4]) + >>> corn_loss(logits, y_train, NUM_CLASSES) + tensor(0.7127, grad_fn=) + """ + sets = [] + for i in range(num_classes - 1): + label_mask = y_train > i - 1 + label_tensor = (y_train[label_mask] > i).to(torch.int64) + sets.append((label_mask, label_tensor)) + + num_examples = 0 + losses = 0.0 + for task_index, s in enumerate(sets): + train_examples = s[0] + train_labels = s[1] + + if len(train_labels) < 1: + continue + + num_examples += len(train_labels) + pred = logits[train_examples, task_index] + + loss = -torch.sum(F.logsigmoid(pred) * train_labels + (F.logsigmoid(pred) - pred) * (1 - train_labels)) + losses += loss + + return losses / num_examples diff --git a/ludwig/modules/loss_modules.py b/ludwig/modules/loss_modules.py new file mode 100644 index 0000000..9b70745 --- /dev/null +++ b/ludwig/modules/loss_modules.py @@ -0,0 +1,766 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import torch +import torch.nn.functional as F +from torch import nn, Tensor +from torch.nn import HuberLoss as _HuberLoss +from torch.nn import L1Loss +from torch.nn import MSELoss as _MSELoss +from torchmetrics.functional import mean_absolute_percentage_error + +import ludwig.utils.loss_utils as utils +from ludwig.constants import LOGITS +from ludwig.modules.loss_implementations.corn import corn_loss +from ludwig.schema.features.loss.loss import ( + BaseLossConfig, + BWCEWLossConfig, + CORNLossConfig, + DeepSADLossConfig, + DeepSVDDLossConfig, + DiceLossConfig, + DROCCLossConfig, + Entmax15LossConfig, + EntropicOpenSetLossConfig, + FocalLossConfig, + HuberLossConfig, + LovaszSoftmaxLossConfig, + MAELossConfig, + MAPELossConfig, + MSELossConfig, + NextTokenSoftmaxCrossEntropyLossConfig, + NTXentLossConfig, + ObjectosphereLossConfig, + PolyLossConfig, + RMSELossConfig, + RMSPELossConfig, + SequenceSoftmaxCrossEntropyLossConfig, + SigmoidCrossEntropyLossConfig, + SoftmaxCrossEntropyLossConfig, + SparsemaxLossConfig, +) +from ludwig.utils import strings_utils +from ludwig.utils.entmax.losses import Entmax15Loss as _Entmax15Loss +from ludwig.utils.entmax.losses import SparsemaxLoss as _SparsemaxLoss +from ludwig.utils.registry import Registry + +# used for Laplace smoothing for candidate samplers +EPSILON = 1.0e-10 + +loss_impl_registry = Registry[type[nn.Module]]() + + +def register_loss(config_cls: type[BaseLossConfig]): + def wrap(cls: type[nn.Module]): + loss_impl_registry[config_cls] = cls + return cls + + return wrap + + +def create_loss(config: BaseLossConfig) -> nn.Module: + return loss_impl_registry[type(config)](config) + + +class LogitsInputsMixin: + @classmethod + def get_loss_inputs(cls): + """Maps loss to the desired predicted input type.""" + return LOGITS + + +@register_loss(MSELossConfig) +class MSELoss(_MSELoss, LogitsInputsMixin): + """Mean squared error.""" + + def __init__(self, config: MSELossConfig): + super().__init__() + + +@register_loss(MAELossConfig) +class MAELoss(L1Loss, LogitsInputsMixin): + """Mean absolute error.""" + + def __init__(self, config: MAELossConfig): + super().__init__() + + +@register_loss(MAPELossConfig) +class MAPELoss(nn.Module, LogitsInputsMixin): + """Mean absolute error.""" + + def __init__(self, config: MAPELossConfig): + super().__init__() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + return mean_absolute_percentage_error(preds, target) + + +@register_loss(RMSELossConfig) +class RMSELoss(nn.Module, LogitsInputsMixin): + """Root mean square error.""" + + def __init__(self, config: RMSELossConfig): + super().__init__() + self.mse = nn.MSELoss() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + return torch.sqrt(self.mse(preds, target)) + + +@register_loss(RMSPELossConfig) +class RMSPELoss(nn.Module, LogitsInputsMixin): + """Root mean square percentage error.""" + + def __init__(self, config: RMSPELossConfig): + super().__init__() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + loss = utils.rmspe_loss(target, preds) + return loss + + +@register_loss(BWCEWLossConfig) +class BWCEWLoss(nn.Module, LogitsInputsMixin): + """Binary weighted cross entropy loss.""" + + def __init__(self, config: BWCEWLossConfig): + super().__init__() + if config.positive_class_weight: + self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([config.positive_class_weight])) + else: + self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=config.positive_class_weight) + self.robust_lambda = config.robust_lambda + self.confidence_penalty = config.confidence_penalty + + def forward(self, preds: torch.Tensor, target: torch.Tensor): + train_loss = self.loss_fn(preds, target.float()) + # robust lambda + if self.robust_lambda > 0: + train_loss = (1 - self.robust_lambda) * train_loss + self.robust_lambda / 2 + + train_mean_loss = torch.mean(train_loss) + + # confidence penalty + if self.confidence_penalty > 0: + probabilities = torch.sigmoid(preds) + mean_penalty = utils.mean_confidence_penalty(probabilities, 2) + train_mean_loss += self.confidence_penalty * mean_penalty + + return train_mean_loss + + +@register_loss(SoftmaxCrossEntropyLossConfig) +class SoftmaxCrossEntropyLoss(nn.Module, LogitsInputsMixin): + def __init__(self, config: SoftmaxCrossEntropyLossConfig): + """ + Params: + class_weights: List or 1D tensor of length equal to number of classes. + """ + super().__init__() + if config.class_weights: + self.loss_fn = nn.CrossEntropyLoss(weight=torch.Tensor(config.class_weights)) + else: + self.loss_fn = nn.CrossEntropyLoss() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Tensor of shape [batch x num_classes] + or shape [batch x num_classes x H x W] + target: Tensor of shape [batch], where each element is integral + between 0 and num_classes. + or shape [batch x H x W], where each element is integral + between 0 and num_classes. + """ + if len(target.shape) == 1 or len(target.shape) == 3: + # Assumes we are providing the target as a single class, rather than a distribution + # The target shape can be a 3D tensor [batch x H x W], for image segmentation + target = target.long() + return self.loss_fn(preds, target) + + +@register_loss(SequenceSoftmaxCrossEntropyLossConfig) +class SequenceSoftmaxCrossEntropyLoss(nn.Module, LogitsInputsMixin): + def __init__(self, config: SequenceSoftmaxCrossEntropyLossConfig): + """ + Params: + class_weights: List or 1D tensor of length equal to number of classes. + """ + super().__init__() + if config.class_weights: + self.loss_fn = nn.CrossEntropyLoss( + weight=torch.Tensor(config.class_weights), ignore_index=strings_utils.SpecialSymbol.PADDING.value + ) + else: + self.loss_fn = nn.CrossEntropyLoss(ignore_index=strings_utils.SpecialSymbol.PADDING.value) + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Tensor of shape [batch x sequence_length x vocab_size] + target: Tensor of shape [batch x sequence_length], where each element is integral between 0 and vocab_size. + """ + target = target.long() + return self.loss_fn(preds[1:].view(-1, preds.size(-1)), target[1:].view(-1)) + + +@register_loss(NextTokenSoftmaxCrossEntropyLossConfig) +class NextTokenSoftmaxCrossEntropyLoss(nn.Module, LogitsInputsMixin): + def __init__(self, config: NextTokenSoftmaxCrossEntropyLossConfig): + super().__init__() + self.loss_fn = nn.CrossEntropyLoss() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Tensor of shape [batch x sequence_length x vocab_size] + target: Tensor of shape [batch x sequence_length], where each element is integral between 0 and vocab_size. + + Reference implementation: + https://github.com/huggingface/transformers/blob/v4.29.1/src/transformers/models/bert/modeling_bert.py#LL1253C1-L1260C1 # noqa + """ + target = target.long() + _, _, vocab_size = preds.shape + # logits for all tensors except n+1 since each logit tensor at position i represents the log probabilities for + # the next token i+1 if we were to do argmax on the logits ensor at position i. + shifted_predictions = preds[:, :-1, :] + # Shift by 1 since the logits at position 0 in predictions represent the log likelihood of target token 1 + shifted_targets = target[:, 1:] + return self.loss_fn(shifted_predictions.reshape(-1, vocab_size), shifted_targets.reshape(-1)) + + +@register_loss(SigmoidCrossEntropyLossConfig) +class SigmoidCrossEntropyLoss(nn.Module, LogitsInputsMixin): + def __init__(self, config: SigmoidCrossEntropyLossConfig): + """ + Params: + class_weights: List or 1D tensor of length equal to number of classes. + """ + super().__init__() + if config.class_weights: + self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor(config.class_weights)) + else: + self.loss_fn = nn.BCEWithLogitsLoss() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + if preds.ndim != 2: + raise RuntimeError("SigmoidCrossEntropyLoss currently only supported for 2D tensors.") + + return self.loss_fn(preds.type(torch.float32), target.type(torch.float32)) + + +@register_loss(HuberLossConfig) +class HuberLoss(_HuberLoss, LogitsInputsMixin): + """Huber loss.""" + + def __init__(self, config: HuberLossConfig): + super().__init__(delta=config.delta) + + +@register_loss(CORNLossConfig) +class CORNLoss(nn.Module, LogitsInputsMixin): + """CORN loss.""" + + def __init__(self, config: CORNLossConfig): + super().__init__() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + num_classes = preds.shape[1] + return corn_loss(preds, target, num_classes=num_classes) + + +class AnomalyScoreInputsMixin: + """Mixin for anomaly detection losses: consume anomaly_score tensor (not logits).""" + + @classmethod + def get_loss_inputs(cls): + from ludwig.constants import ANOMALY_SCORE + + return ANOMALY_SCORE + + +@register_loss(DeepSVDDLossConfig) +class DeepSVDDLoss(nn.Module, AnomalyScoreInputsMixin): + """Deep SVDD loss. + + Hard-boundary: L = mean(||z - c||^2) + Soft-boundary (nu > 0): L = R + (1/nu) * mean(max(0, dist_sq - R)) + + Reference: Ruff et al., ICML 2018. + """ + + def __init__(self, config: DeepSVDDLossConfig): + super().__init__() + self.nu = config.nu + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + dist_sq = preds + if self.nu > 0: + R = torch.quantile(dist_sq.detach(), 1.0 - self.nu).clamp(min=0.0) + loss = R + (1.0 / self.nu) * torch.mean(torch.clamp(dist_sq - R, min=0.0)) + else: + loss = torch.mean(dist_sq) + return loss + + +@register_loss(DeepSADLossConfig) +class DeepSADLoss(nn.Module, AnomalyScoreInputsMixin): + """Deep SAD loss (semi-supervised). + + Normal/unlabeled (target != 1): L_i = ||z - c||^2 + Labeled anomalies (target == 1): L_i = eta / (||z - c||^2 + eps) + + Reference: Ruff et al., ICLR 2020. + """ + + EPSILON = 1e-6 + + def __init__(self, config: DeepSADLossConfig): + super().__init__() + self.eta = config.eta + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + dist_sq = preds + target = target.float() + is_anomaly = (target == 1).float() + normal_loss = dist_sq * (1.0 - is_anomaly) + anomaly_loss = self.eta / (dist_sq + self.EPSILON) * is_anomaly + return torch.mean(normal_loss + anomaly_loss) + + +@register_loss(DROCCLossConfig) +class DROCCLoss(nn.Module, AnomalyScoreInputsMixin): + """DROCC loss: adversarial regularisation to prevent hypersphere collapse. + + Combines SVDD objective with hinge loss on score-space perturbations. + + Reference: Goyal et al., ICML 2020. + """ + + def __init__(self, config: DROCCLossConfig): + super().__init__() + self.perturbation_strength = config.perturbation_strength + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + dist_sq = preds + svdd_loss = torch.mean(dist_sq) + with torch.no_grad(): + noise_scale = self.perturbation_strength * dist_sq.detach().std().clamp(min=1e-6) + perturbed = dist_sq.detach() + noise_scale * torch.randn_like(dist_sq) + hinge = torch.mean(torch.clamp(dist_sq - perturbed, min=0.0)) + return svdd_loss + self.perturbation_strength * hinge + + +@register_loss(EntropicOpenSetLossConfig) +class EntropicOpenSetLoss(nn.Module, LogitsInputsMixin): + """Entropic Open-Set Loss from Dhamija et al., NeurIPS 2018. + + For known-class samples (target != background_class): + L = CrossEntropy(logits, target) + + For unknown/background samples (target == background_class): + L = sum_i( p_i * log(p_i) ) # negative entropy → maximise entropy + + Without a background_class this reduces to standard cross-entropy. + + Reference: https://arxiv.org/abs/1811.04110 + """ + + def __init__(self, config: EntropicOpenSetLossConfig): + super().__init__() + self.background_class = config.background_class + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + logits = preds[LOGITS] if isinstance(preds, dict) else preds + + if self.background_class is None: + return F.cross_entropy(logits, target) + + known_mask = target != self.background_class + unknown_mask = ~known_mask + + loss = logits.new_tensor(0.0) + + if known_mask.any(): + loss = loss + F.cross_entropy(logits[known_mask], target[known_mask]) + + if unknown_mask.any(): + probs = torch.softmax(logits[unknown_mask], dim=-1) + # Negative entropy: p * log(p). Minimising this maximises H(p). + loss = loss + (probs * torch.log(probs + EPSILON)).sum(dim=-1).mean() + + return loss + + +@register_loss(ObjectosphereLossConfig) +class ObjectosphereLoss(nn.Module, LogitsInputsMixin): + """Objectosphere Loss from Dhamija et al., NeurIPS 2018. + + For known-class samples: + L = CrossEntropy(logits, target) + hinge(||logits|| - xi) + + For unknown/background samples: + L = NegEntropy(logits) + zeta * ||logits||^2 + + The hinge term for known samples is max(0, xi - ||z||)^2, pushing logit + norms above xi. The magnitude term for unknowns pulls norms toward zero, + so out-of-distribution inputs produce near-uniform, low-magnitude outputs + that are easy to detect with a simple norm threshold at inference time. + + Without a background_class this reduces to CE + known-class hinge only. + + Reference: https://arxiv.org/abs/1811.04110 + """ + + def __init__(self, config: ObjectosphereLossConfig): + super().__init__() + self.background_class = config.background_class + self.xi = config.xi + self.zeta = config.zeta + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + logits = preds[LOGITS] if isinstance(preds, dict) else preds + + if self.background_class is None: + # No unknowns: CE + magnitude push for all samples. + ce = F.cross_entropy(logits, target) + mag = logits.norm(dim=-1) + hinge = torch.clamp(self.xi - mag, min=0.0).pow(2).mean() + return ce + hinge + + known_mask = target != self.background_class + unknown_mask = ~known_mask + + loss = logits.new_tensor(0.0) + + if known_mask.any(): + known_logits = logits[known_mask] + ce = F.cross_entropy(known_logits, target[known_mask]) + mag = known_logits.norm(dim=-1) + hinge = torch.clamp(self.xi - mag, min=0.0).pow(2).mean() + loss = loss + ce + hinge + + if unknown_mask.any(): + unknown_logits = logits[unknown_mask] + probs = torch.softmax(unknown_logits, dim=-1) + neg_entropy = (probs * torch.log(probs + EPSILON)).sum(dim=-1).mean() + mag_penalty = unknown_logits.norm(dim=-1).pow(2).mean() + loss = loss + neg_entropy + self.zeta * mag_penalty + + return loss + + +@register_loss(FocalLossConfig) +class FocalLoss(nn.Module, LogitsInputsMixin): + """Focal Loss for classification with class imbalance. + + Applies a modulating factor ``(1 - p_t)^gamma`` to the standard + cross-entropy loss so that easy examples contribute less to the gradient + and training is focused on hard, misclassified examples. + + Formula:: + + FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t) + + Supports both binary (scalar logits) and multi-class (logit vectors) inputs. + For binary inputs, ``alpha_t`` balances positive/negative classes. + For multi-class inputs, the modulating factor is applied without per-class alpha. + + Reference: + Lin et al., "Focal Loss for Dense Object Detection", ICCV 2017. + https://arxiv.org/abs/1708.02002 + """ + + def __init__(self, config: FocalLossConfig): + super().__init__() + self.alpha = config.alpha + self.gamma = config.gamma + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: [batch] logits (binary) or [batch x num_classes] logits (multi-class). + target: [batch] integer class labels (0/1 for binary). + """ + if preds.ndim == 1: + # Binary case: compute per-element BCE then reweight with alpha + bce = F.binary_cross_entropy_with_logits(preds, target.float(), reduction="none") + p_t = torch.exp(-bce) + alpha_t = self.alpha * target.float() + (1 - self.alpha) * (1 - target.float()) + focal = alpha_t * (1 - p_t) ** self.gamma * bce + return focal.mean() + else: + # Multi-class case: alpha not applied (symmetric across classes) + ce = F.cross_entropy(preds, target.long(), reduction="none") + p_t = torch.exp(-ce) + focal = (1 - p_t) ** self.gamma * ce + return focal.mean() + + +@register_loss(DiceLossConfig) +class DiceLoss(nn.Module, LogitsInputsMixin): + """Dice Loss for image segmentation. + + Computes one minus the Dice coefficient between predicted soft masks and + one-hot ground-truth masks. A ``smooth`` term prevents division by zero + when both prediction and target are empty. + + Formula:: + + Dice = 1 - (2 * sum(p * t) + smooth) / (sum(p) + sum(t) + smooth) + + Inputs are expected as class logits of shape ``[B, C, H, W]`` and integer + targets of shape ``[B, H, W]``. + + Reference: + Milletari et al., "V-Net: Fully Convolutional Neural Networks for + Volumetric Medical Image Segmentation", 3DV 2016. + https://arxiv.org/abs/1606.04797 + """ + + def __init__(self, config: DiceLossConfig): + super().__init__() + self.smooth = config.smooth + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Float tensor [B, C, H, W] of class logits. + target: Long tensor [B, H, W] of integer class indices. + """ + num_classes = preds.shape[1] + probs = F.softmax(preds, dim=1) # [B, C, H, W] + # One-hot encode target: [B, H, W] -> [B, C, H, W] + target_one_hot = F.one_hot(target.long(), num_classes=num_classes) # [B, H, W, C] + target_one_hot = target_one_hot.permute(0, 3, 1, 2).float() # [B, C, H, W] + # Flatten spatial dims for dot-product computation + probs_flat = probs.contiguous().view(probs.shape[0], num_classes, -1) # [B, C, N] + target_flat = target_one_hot.contiguous().view(target_one_hot.shape[0], num_classes, -1) + intersection = (probs_flat * target_flat).sum(dim=2) # [B, C] + union = probs_flat.sum(dim=2) + target_flat.sum(dim=2) # [B, C] + dice_coeff = (2.0 * intersection + self.smooth) / (union + self.smooth) + return 1.0 - dice_coeff.mean() + + +@register_loss(LovaszSoftmaxLossConfig) +class LovaszSoftmaxLoss(nn.Module, LogitsInputsMixin): + """Lovasz-Softmax Loss for multi-class semantic segmentation. + + Uses the Lovasz extension of submodular functions to construct a convex + surrogate for the per-class intersection-over-union (IoU) loss. Unlike + the Dice loss, it directly targets the mIoU metric used in segmentation + benchmarks. + + Inputs are expected as class logits of shape ``[B, C, H, W]`` and integer + targets of shape ``[B, H, W]``. + + Reference: + Berman et al., "The Lovasz-Softmax Loss: A Tractable Surrogate for + the Optimization of the Intersection-Over-Union Measure in Neural + Networks", CVPR 2018. + https://arxiv.org/abs/1705.08790 + """ + + def __init__(self, config: LovaszSoftmaxLossConfig): + super().__init__() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Float tensor [B, C, H, W] of class logits. + target: Long tensor [B, H, W] of integer class indices. + """ + num_classes = preds.shape[1] + probas = F.softmax(preds, dim=1) # [B, C, H, W] + return self._lovasz_softmax(probas, target, num_classes) + + @staticmethod + def _lovasz_grad(gt_sorted: Tensor) -> Tensor: + """Compute the Lovasz extension coefficients from a sorted ground-truth vector.""" + p = len(gt_sorted) + gts = gt_sorted.sum() + intersection = gts - gt_sorted.float().cumsum(0) + union = gts + (1 - gt_sorted).float().cumsum(0) + jaccard = 1.0 - intersection / union + if p > 1: + jaccard[1:p] = jaccard[1:p] - jaccard[0:-1] + return jaccard + + def _lovasz_softmax_flat(self, probas: Tensor, labels: Tensor, num_classes: int) -> Tensor: + """Compute the Lovasz-Softmax loss on pixel-flattened tensors.""" + if probas.numel() == 0: + return probas * 0.0 + loss = torch.zeros(1, device=probas.device, dtype=probas.dtype) + for c in range(num_classes): + fg = (labels == c).float() # foreground indicator for class c + if fg.sum() == 0: + continue + class_pred = probas[:, c] + errors = (fg - class_pred).abs() + errors_sorted, perm = torch.sort(errors, descending=True) + gt_sorted = fg[perm] + grad = self._lovasz_grad(gt_sorted) + loss += torch.dot(errors_sorted, grad) + return loss / num_classes + + def _lovasz_softmax(self, probas: Tensor, labels: Tensor, num_classes: int) -> Tensor: + B, C, H, W = probas.shape + probas_flat = probas.permute(0, 2, 3, 1).contiguous().view(-1, C) # [B*H*W, C] + labels_flat = labels.view(-1) # [B*H*W] + return self._lovasz_softmax_flat(probas_flat, labels_flat, num_classes) + + +@register_loss(NTXentLossConfig) +class NTXentLoss(nn.Module, LogitsInputsMixin): + """NT-Xent (Normalized Temperature-scaled Cross Entropy) contrastive loss. + + The SimCLR objective. Given a batch of ``N`` vector representations, the + loss is computed assuming that consecutive pairs ``(2i, 2i+1)`` are positive + pairs (augmented views of the same example) and all other ``2(N-1)`` + examples in the batch are negatives. + + Formula (for pair (i, j) with temperature tau):: + + L_i = -log( + exp(sim(z_i, z_j) / tau) / + sum_{k != i} exp(sim(z_i, z_k) / tau) + ) + + where sim denotes cosine similarity and tau is the temperature. + + Reference: + Chen et al., "A Simple Framework for Contrastive Learning of Visual + Representations" (SimCLR), ICML 2020. + https://arxiv.org/abs/2002.05709 + """ + + def __init__(self, config: NTXentLossConfig): + super().__init__() + self.temperature = config.temperature + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Float tensor [B, D] of vector embeddings (logits output). + target: Unused; required for interface compatibility. + """ + z = F.normalize(preds, dim=1) # [B, D] + sim = torch.mm(z, z.T) / self.temperature # [B, B] + B = z.shape[0] + # Mask out self-similarities + mask = torch.eye(B, dtype=torch.bool, device=z.device) + sim.masked_fill_(mask, float("-inf")) + # Positive-pair labels: pair (i, j) where j = i XOR 1 + # (even i -> i+1, odd i -> i-1) -- standard SimCLR pairing. + if B >= 2 and B % 2 == 0: + labels = torch.arange(B, device=z.device) ^ 1 + else: + # Degenerate batch: use nearest non-self neighbour as positive + labels = sim.argmax(dim=1) + return F.cross_entropy(sim, labels) + + +@register_loss(PolyLossConfig) +class PolyLoss(nn.Module, LogitsInputsMixin): + """PolyLoss for multi-class classification. + + Extends cross-entropy with a first-order polynomial correction term + ``epsilon * (1 - p_t)`` that upweights hard examples where the model + places low probability on the correct class. + + Formula:: + + PolyLoss = CE(p_t) + epsilon * (1 - p_t) + + where ``p_t`` is the predicted softmax probability of the ground-truth class. + + Reference: + Leng et al., "PolyLoss: A Polynomial Expansion Perspective of + Classification Loss Functions", ICLR 2022. + https://arxiv.org/abs/2204.12511 + """ + + def __init__(self, config: PolyLossConfig): + super().__init__() + self.epsilon = config.epsilon + self.ce_fn = nn.CrossEntropyLoss(reduction="none") + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Float tensor [B, C] of class logits. + target: Long tensor [B] of integer class indices. + """ + target = target.long() + ce = self.ce_fn(preds, target) # [B] + probs = F.softmax(preds, dim=-1) # [B, C] + p_t = probs.gather(dim=1, index=target.unsqueeze(1)).squeeze(1) # [B] + poly = ce + self.epsilon * (1.0 - p_t) + return poly.mean() + + +@register_loss(SparsemaxLossConfig) +class SparsemaxLoss(nn.Module, LogitsInputsMixin): + """Sparsemax Loss: a sparse alternative to softmax cross-entropy. + + The natural loss companion to the sparsemax activation, derived as the + Fenchel conjugate of the sparsemax Omega function. Assigns zero gradient + to classes outside the sparsemax support, producing exact sparsity in the + probability simplex. + + Reference: + Martins & Astudillo, "From Softmax to Sparsemax: A Sparse Model of + Attention and Multi-Label Classification", ICML 2016. + https://arxiv.org/abs/1602.02068 + """ + + def __init__(self, config: SparsemaxLossConfig): + super().__init__() + self._loss_fn = _SparsemaxLoss() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Float tensor [B, C] of class logits. + target: Long tensor [B] of integer class indices. + """ + return self._loss_fn(preds, target.long()) + + +@register_loss(Entmax15LossConfig) +class Entmax15Loss(nn.Module, LogitsInputsMixin): + """Entmax-1.5 Loss: a semi-sparse alternative to softmax cross-entropy. + + The Fenchel-conjugate loss of the alpha=1.5 entmax activation. Produces + moderately sparse probability distributions that lie between softmax + (dense) and sparsemax (maximally sparse). + + Reference: + Peters et al., "Sparse Sequence-to-Sequence Models", ACL 2019. + https://arxiv.org/abs/1905.05702 + """ + + def __init__(self, config: Entmax15LossConfig): + super().__init__() + self._loss_fn = _Entmax15Loss() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + """ + Params: + preds: Float tensor [B, C] of class logits. + target: Long tensor [B] of integer class indices. + """ + return self._loss_fn(preds, target.long()) diff --git a/ludwig/modules/lr_scheduler.py b/ludwig/modules/lr_scheduler.py new file mode 100644 index 0000000..f7322de --- /dev/null +++ b/ludwig/modules/lr_scheduler.py @@ -0,0 +1,452 @@ +import logging +import math +from collections.abc import Callable +from typing import Any + +from torch.optim import Optimizer +from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts, LambdaLR, OneCycleLR, ReduceLROnPlateau, SequentialLR + +from ludwig.constants import MINIMIZE, TRAINING, VALIDATION +from ludwig.modules.metric_registry import get_metric_objective +from ludwig.schema.lr_scheduler import LRSchedulerConfig +from ludwig.utils.metric_utils import TrainerMetric +from ludwig.utils.trainer_utils import ProgressTracker + +logger = logging.getLogger(__name__) + + +class ReduceLROnPLateauCappedDecreases(ReduceLROnPlateau): + """ReduceLROnPlateau with a cap on the number of allowed reductions. + + Use when: you want to reduce LR in response to plateaus, but want to prevent the LR + from collapsing to zero over a very long training run. + + Trade-offs: Requires a validation metric to be tracked. Does not interact well with + schedules that already decay LR aggressively (e.g., one_cycle). + """ + + def __init__(self, optimizer: Optimizer, mode: str, reduce_limit: int, factor: float, patience: int): + super().__init__(optimizer, mode=mode, factor=factor, patience=patience) + self.reduce_limit = reduce_limit + self._num_reduce_lr = 0 + + def step(self, metrics): + if self._num_reduce_lr >= self.reduce_limit: + # Already reduced the LR as many times as we will allow + return + + return super().step(metrics) + + @property + def num_reduce_lr(self) -> int: + return self._num_reduce_lr + + def _reduce_lr(self, epoch=None): + """Overrides the base ReduceLROnPlateau implementation.""" + self._num_reduce_lr += 1 + self.apply_lr() + + def apply_lr(self): + if self._num_reduce_lr == 0: + return + + for i, param_group in enumerate(self.optimizer.param_groups): + old_lr = float(param_group["lr"]) + new_lr = max(old_lr * math.pow(self.factor, self._num_reduce_lr), self.min_lrs[i]) + if old_lr - new_lr > self.eps: + param_group["lr"] = new_lr + logger.info(f"From ReduceLROnPLateauCappedDecreases, reducing learning rate to {new_lr}") + + +class LRScheduler: + def __init__( + self, + config: LRSchedulerConfig, + optimizer: Optimizer, + steps_per_checkpoint: int, + total_steps: int, + ): + self.config = config + self.optimizer = optimizer + + # Scheduler updated each training step + self.step_info = StepInfo(steps_per_checkpoint, total_steps, self.config) + self._train_scheduler = get_schedule_with_warmup_and_decay(self.config, self.optimizer, self.step_info) + + # Scheduler updated each eval step + self._eval_scheduler = None + if self.config.reduce_on_plateau > 0: + mode = "min" if get_metric_objective(self.config.reduce_eval_metric) == MINIMIZE else "max" + self._eval_scheduler = ReduceLROnPLateauCappedDecreases( + optimizer=self.optimizer, + mode=mode, + reduce_limit=self.config.reduce_on_plateau, + factor=self.config.reduce_on_plateau_rate, + patience=self.config.reduce_on_plateau_patience, + ) + + def step(self): + """Called every step of training.""" + self._train_scheduler.step() + + if self._eval_scheduler is not None: + # We apply this scheduler every eval step, not train step, so we don't want to call step() here. + # However, we need to re-apply the LR reduction to the LR from the train scheduler, as the first scheduler + # resets the LR back to the base LR. + self._eval_scheduler.apply_lr() + + def eval_step(self, progress_tracker: ProgressTracker, validation_field: str): + """Called every checkpoint evaluation step.""" + if self._eval_scheduler is None: + # No reduce on plateau + return + + if self.config.reduce_eval_split == TRAINING: + split_metrics = progress_tracker.train_metrics + elif self.config.reduce_eval_split == VALIDATION: + split_metrics = progress_tracker.validation_metrics + else: # if self.config.reduce_eval_split == TEST: + split_metrics = progress_tracker.test_metrics + + validation_metric = self.config.reduce_eval_metric + last_metric: TrainerMetric = split_metrics[validation_field][validation_metric][-1] + last_metric_value = last_metric[-1] + + prev_num_reductions = self._eval_scheduler.num_reduce_lr + self._eval_scheduler.step(last_metric_value) + + num_reductions = self._eval_scheduler.num_reduce_lr + if num_reductions > prev_num_reductions: + # LR reduction -> update progress tracker + progress_tracker.last_learning_rate_reduction_steps = progress_tracker.steps + progress_tracker.last_learning_rate_reduction = 0 + progress_tracker.num_reductions_learning_rate += 1 + else: + progress_tracker.last_learning_rate_reduction = ( + progress_tracker.steps - progress_tracker.last_learning_rate_reduction_steps + ) + + def state_dict(self) -> dict[str, Any]: + return { + "train_scheduler_state": self._train_scheduler.state_dict(), + "eval_scheduler_state": self._eval_scheduler.state_dict() if self._eval_scheduler is not None else {}, + } + + def load_state_dict(self, d: dict[str, Any]): + self._train_scheduler.load_state_dict(d["train_scheduler_state"]) + if self._eval_scheduler is not None: + self._eval_scheduler.load_state_dict(d["eval_scheduler_state"]) + + +class StepInfo: + """Stores the steps_per_checkpoint and total_steps used during the current training run. + + This class is needed by LambdaLR to allow us to update the steps on training init without resetting the entire + LRScheduler from scratch (which would result in resetting the optimizer learning rate). + """ + + def __init__(self, steps_per_checkpoint: int, total_steps: int, config: LRSchedulerConfig): + self.config = config + self.steps_per_checkpoint = steps_per_checkpoint + self.num_training_steps = total_steps + + if self.config.warmup_fraction > 0 and self.config.warmup_evaluations > 0: + logger.info( + "Both `learning_rate_scheduler.warmup_fraction` and `learning_rate_scheduler.warmup_evaluations` " + "provided. The larger of the two (as a function of the total training steps) will be used." + ) + + num_warmup_steps = 0 + if self.config.warmup_fraction > 0: + num_warmup_steps = max(self.config.warmup_fraction * self.num_training_steps, num_warmup_steps) + if self.config.warmup_evaluations > 0: + num_warmup_steps = max(self.config.warmup_evaluations * self.steps_per_checkpoint, num_warmup_steps) + self.num_warmup_steps = num_warmup_steps + + +def get_schedule_with_warmup_and_decay( + config: LRSchedulerConfig, + optimizer: Optimizer, + step_info: StepInfo, +) -> LambdaLR: + """Creates a learning rate scheduler that updates each training step.""" + decay = config.decay + + # OneCycleLR manages warmup and decay internally - skip the SequentialLR wrapper. + if decay == "one_cycle": + if step_info.num_warmup_steps > 0: + logger.warning( + "decay='one_cycle' includes its own warmup phase controlled by `pct_start`. " + "The `warmup_fraction`/`warmup_evaluations` settings will be ignored." + ) + return init_one_cycle(config, optimizer, step_info) + + # WSD manages its own warmup internally via wsd_warmup_fraction. + if decay == "wsd" and step_info.num_warmup_steps > 0: + logger.warning( + "decay='wsd' includes its own warmup phase controlled by `wsd_warmup_fraction`. " + "The external `warmup_fraction`/`warmup_evaluations` settings will be ignored for WSD." + ) + + schedulers = [] + + # Warmup scheduler. + if step_info.num_warmup_steps > 0 and decay != "wsd": + warmup_scheduler = LambdaLR( + optimizer, + lambda current_step: float(current_step) / float(max(1, step_info.num_warmup_steps)), + ) + schedulers.append(warmup_scheduler) + + # Decay scheduler. + decay_scheduler = decay_registry[decay](config, optimizer, step_info) + schedulers.append(decay_scheduler) + + if len(schedulers) == 1: + # Only one scheduler, so no need to wrap in a SequentialLR. + return schedulers[0] + + # Return a SequentialLR that applies the warmup and decay schedulers in order + # with the warmup scheduler only applied for the first num_warmup_steps steps. + return SequentialLR(optimizer, schedulers=schedulers, milestones=[step_info.num_warmup_steps]) + + +# --------------------------------------------------------------------------- +# Decay functions (used via wrap_decay_fn -> LambdaLR) +# --------------------------------------------------------------------------- + + +def no_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig): + """No decay: keep LR constant throughout training. + + Use when: you want full control via the optimizer's initial LR, or when using + reduce_on_plateau alone. + + Trade-offs: Simple but rarely optimal for long runs; the LR never adapts to the loss + landscape. + """ + return 1.0 + + +def linear_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig): + """Linear decay from base LR to 0 over the remaining training steps after warmup. + + Use when: fine-tuning pretrained models (catastrophic forgetting risk), or when a + predictable, monotone LR reduction is preferred. Popular for BERT-style fine-tuning. + + Trade-offs: Aggressive and inflexible - once the schedule starts, LR can only decrease. + May converge too fast if training is short. + """ + return max( + 0.0, + float(num_training_steps - num_warmup_steps - current_step) + / float(max(1, num_training_steps - num_warmup_steps)), + ) + + +def exponential_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig): + """Exponential decay: lr *= decay_rate^(step / decay_steps). + + Use when: training from scratch and a smooth, gradual LR reduction is desired. A good + default for most tabular and vision tasks. + + Trade-offs: Never reaches zero, so the optimizer always has a non-trivial step size. + Sensitive to the choice of decay_rate and decay_steps - too aggressive and the model + under-trains in the later stages; too gentle and the LR barely changes. + """ + decay_rate = float(config.decay_rate) + decay_steps = float(config.decay_steps) + step = float(current_step) + exponent = 1 + step / decay_steps + if config.staircase: + exponent = math.ceil(exponent) + return math.pow(decay_rate, exponent) + + +def wrap_decay_fn(decay_fn: Callable) -> Callable: + def init_fn(config: LRSchedulerConfig, optimizer: Optimizer, step_info: StepInfo) -> LambdaLR: + return LambdaLR( + optimizer, + lambda current_step: decay_fn( + current_step, step_info.num_training_steps, step_info.num_warmup_steps, config + ), + ) + + return init_fn + + +def init_cosine_decay( + config: LRSchedulerConfig, + optimizer: Optimizer, + step_info: StepInfo, +) -> CosineAnnealingWarmRestarts: + """Cosine annealing with warm restarts (Loshchilov & Hutter, 2017). + + The LR follows a cosine curve from base_lr down to eta_min over T_0 steps, then + restarts. Each restart can have a longer period controlled by t_mult. + + Use when: you want periodic exploration boosts during training (each restart resets LR + upward), which can help escape local minima. Popular for image classification. + + Trade-offs: The periodic restarts add variance to the loss curve. Selecting good T_0 + and T_mult values requires some tuning. Not well-suited for very short training runs. + """ + t_0 = config.t_0 + if not t_0: + t_0 = step_info.steps_per_checkpoint + if not t_0: + # A scheduler may be initialized with dummy values like at the start of training. + # Ensure that t_0 != 0, as this causes an error to be raised. + t_0 = 1 + + return CosineAnnealingWarmRestarts( + optimizer, + T_0=t_0, + T_mult=config.t_mult or 1, + eta_min=config.eta_min or 0, + ) + + +def init_one_cycle( + config: LRSchedulerConfig, + optimizer: Optimizer, + step_info: StepInfo, +) -> OneCycleLR: + """1cycle policy (Smith & Topin, 2018). + + Three phases: LR rises from initial_lr to max_lr over pct_start of total steps + (warmup), then decays to min_lr = initial_lr / final_div_factor via cosine annealing. + initial_lr = max_lr / div_factor. + + Use when: you want the fastest convergence in a single training run, especially for + image/audio tasks. Works best with a pre-determined total_steps budget. + + Trade-offs: Requires total_steps to be known in advance (cannot restart mid-training + without reinitializing). Not composable with an external warmup scheduler (warmup is + built in). The LR goes to near-zero at the end, so extending training is ineffective. + """ + total_steps = step_info.num_training_steps + if not total_steps: + total_steps = 1 # Avoid division-by-zero during dummy init + + # Determine max_lr: use config override or fall back to optimizer's param-group lr. + if config.max_lr is not None: + max_lr = config.max_lr + else: + max_lr = max(pg["lr"] for pg in optimizer.param_groups) + + return OneCycleLR( + optimizer, + max_lr=max_lr, + total_steps=total_steps, + pct_start=config.pct_start, + div_factor=config.div_factor, + final_div_factor=config.final_div_factor, + anneal_strategy="cos", + ) + + +def inverse_sqrt_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig): + """Inverse square root decay: lr = base_lr / sqrt(max(step, warmup_steps)). + + This is the original Transformer learning rate schedule from Vaswani et al. (2017). + The LR warms up linearly for warmup_steps steps (handled externally via SequentialLR), + then decays as 1/sqrt(step), which is slow and keeps the LR meaningfully large for a + long time. + + The peak step count is anchored to inverse_sqrt_warmup_steps so that the decay + transition is predictable regardless of the global warmup setting. + + Use when: training vanilla Transformer models from scratch (NLP, speech). Standard in + sequence-to-sequence and language model pretraining baselines. + + Trade-offs: LR never reaches zero, so training can be extended freely. Decays slower + than linear or exponential - may be too high for very long runs without tuning + warmup_steps. + """ + warmup = float(config.inverse_sqrt_warmup_steps) + return 1.0 / math.sqrt(max(float(current_step), warmup)) + + +def polynomial_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig): + """Polynomial decay with configurable power and end LR. + + lr = (base_lr - end_lr) * (1 - progress)^power + end_lr where progress = (step - + warmup_steps) / (total_steps - warmup_steps). + + With power=1.0 this is equivalent to linear decay. Higher powers give more concave + curves that stay high for longer and drop sharply near the end. + + Use when: replicating BERT, RoBERTa, or GPT-2 fine-tuning recipes. These models + commonly use polynomial (linear) decay with a small warmup fraction. + + Trade-offs: Requires total_steps to be known. end_lr > 0 prevents the LR from + collapsing to zero, which can help with very long training. Power > 1 risks a sudden + sharp drop near the end of training. + """ + if num_training_steps <= num_warmup_steps: + return 1.0 + + progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps)) + progress = min(progress, 1.0) + + # Compute the scale factor as a fraction of the base LR (base LR = 1.0 in LambdaLR). + end_lr_fraction = config.polynomial_end_lr # relative to base_lr; 0.0 means decay to 0 + return (1.0 - end_lr_fraction) * math.pow(1.0 - progress, config.polynomial_power) + end_lr_fraction + + +def wsd_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig): + """Warmup-Stable-Decay (WSD) schedule. + + Three phases: + + 1. Warmup: LR rises linearly from 0 to base_lr over wsd_warmup_fraction * total_steps + steps. + 2. Stable: LR stays constant at base_lr for wsd_stable_fraction * total_steps steps. + 3. Decay: LR decreases via cosine from base_lr to 0 over wsd_decay_fraction * + total_steps steps. + + wsd_warmup_fraction + wsd_stable_fraction + wsd_decay_fraction should sum to 1. + + Popular for LLM pretraining (MiniCPM, DeepSeek-V2). The long stable phase allows easy + checkpoint reuse: you can extend training by appending another stable phase and a final + decay. + + Use when: pretraining large language models where you want flexibility to extend the + training budget without restarting from scratch. + + Trade-offs: The sharp cosine decay at the end can cause instability if decay_fraction + is too small. The three fractions must be manually tuned and must sum to 1. Warmup is + managed internally - do not also set warmup_fraction or warmup_evaluations. + """ + T = float(num_training_steps) + t = float(current_step) + + warmup_end = config.wsd_warmup_fraction * T + stable_end = warmup_end + config.wsd_stable_fraction * T + + if t < warmup_end: + # Linear warmup phase + return t / max(1.0, warmup_end) + elif t < stable_end: + # Constant LR phase + return 1.0 + else: + # Cosine decay phase + decay_start = stable_end + decay_end = T + decay_progress = (t - decay_start) / max(1.0, decay_end - decay_start) + decay_progress = min(decay_progress, 1.0) + return 0.5 * (1.0 + math.cos(math.pi * decay_progress)) + + +decay_registry = { + None: wrap_decay_fn(no_decay), + "linear": wrap_decay_fn(linear_decay), + "exponential": wrap_decay_fn(exponential_decay), + "cosine": init_cosine_decay, + "inverse_sqrt": wrap_decay_fn(inverse_sqrt_decay), + "polynomial": wrap_decay_fn(polynomial_decay), + "wsd": wrap_decay_fn(wsd_decay), + # one_cycle is handled specially in get_schedule_with_warmup_and_decay +} diff --git a/ludwig/modules/metric_modules.py b/ludwig/modules/metric_modules.py new file mode 100644 index 0000000..9e2d644 --- /dev/null +++ b/ludwig/modules/metric_modules.py @@ -0,0 +1,770 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import sys +from abc import ABC, abstractmethod +from collections.abc import Callable, Generator +from contextlib import contextmanager +from typing import Any + +import torch +from torch import Tensor, tensor +from torchmetrics import MeanAbsoluteError, MeanAbsolutePercentageError, MeanSquaredError, Metric +from torchmetrics import MeanMetric as _MeanMetric +from torchmetrics.classification import ( + BinaryAccuracy, + BinaryAUROC, + BinaryPrecision, + BinaryRecall, + BinarySpecificity, + MulticlassAccuracy, + MulticlassAUROC, +) +from torchmetrics.functional.regression.r2 import _r2_score_compute, _r2_score_update +from torchmetrics.metric import jit_distributed_available +from torchmetrics.text import BLEUScore, CharErrorRate, WordErrorRate +from torchmetrics.text.perplexity import Perplexity +from torchmetrics.text.rouge import ROUGEScore + +from ludwig.constants import ( # RESPONSE, + ACCURACY, + ACCURACY_MICRO, + ANOMALY, + ANOMALY_AUROC, + ANOMALY_SCORE, + BINARY, + BINARY_WEIGHTED_CROSS_ENTROPY, + CATEGORY, + CATEGORY_DISTRIBUTION, + CORN, + F1_MAX, + HITS_AT_K, + HUBER, + IGNORE_INDEX_TOKEN_ID, + IMAGE, + JACCARD, + LOGITS, + LOSS, + MAXIMIZE, + MEAN_ABSOLUTE_ERROR, + MEAN_ABSOLUTE_PERCENTAGE_ERROR, + MEAN_ABSOLUTE_SCALED_ERROR, + MEAN_SQUARED_ERROR, + MINIMIZE, + NEXT_TOKEN_PERPLEXITY, + NUMBER, + PERPLEXITY, + PRECISION, + PREDICTIONS, + PROBABILITIES, + R2, + RECALL, + ROC_AUC, + ROOT_MEAN_SQUARED_ERROR, + ROOT_MEAN_SQUARED_PERCENTAGE_ERROR, + SEQUENCE, + SEQUENCE_ACCURACY, + SET, + SPECIFICITY, + SYMMETRIC_MEAN_ABSOLUTE_PERCENTAGE_ERROR, + TEXT, + TIMESERIES, + TOKEN_ACCURACY, + VECTOR, +) +from ludwig.distributed import get_current_dist_strategy +from ludwig.modules.loss_modules import ( + BWCEWLoss, + CORNLoss, + HuberLoss, + NextTokenSoftmaxCrossEntropyLoss, + SequenceSoftmaxCrossEntropyLoss, + SigmoidCrossEntropyLoss, + SoftmaxCrossEntropyLoss, +) +from ludwig.modules.metric_registry import get_metric_objective, get_metric_registry, register_metric +from ludwig.schema.features.loss.loss import ( + BWCEWLossConfig, + CORNLossConfig, + HuberLossConfig, + SequenceSoftmaxCrossEntropyLossConfig, + SigmoidCrossEntropyLossConfig, + SoftmaxCrossEntropyLossConfig, +) +from ludwig.utils.loss_utils import rmspe_loss +from ludwig.utils.metric_utils import masked_correct_predictions +from ludwig.utils.torch_utils import sequence_length_2D + +logger = logging.getLogger(__name__) + + +class LudwigMetric(Metric, ABC): + @classmethod + def can_report(cls, feature: "OutputFeature") -> bool: # noqa: F821 + return True + + @contextmanager + def sync_context( + self, + dist_sync_fn: Callable | None = None, + process_group: Any | None = None, + should_sync: bool = True, + should_unsync: bool = True, + distributed_available: Callable | None = jit_distributed_available, + ) -> Generator: + """Cross-process metric-state synchronisation, hooked into torchmetrics' compute() flow. + + ## How torchmetrics calls this + + torchmetrics wraps every ``Metric.compute()`` call in a ``sync_context()`` (see + ``torchmetrics/metric.py::wrapped_func``). That means **this method is invoked + automatically whenever ``compute()`` is called** — you do NOT need to call it + explicitly from ``get_metrics()`` or any other site. + + ⚠️ Do NOT wrap ``compute()`` in a manual ``sync_context()`` call from the outside. + If the distributed fallback is active (see below), ``_is_synced`` will be set to + ``True`` by the first (inner) call and torchmetrics will raise:: + + TorchMetricsUserError: The Metric has already been synced. + + when ``compute()`` then tries to call ``sync_context()`` again. This was the exact + bug encountered when ``base_feature.get_metrics()`` was briefly changed to wrap + ``metric_fn.compute()`` in ``with metric_fn.sync_context()``. + + ## Why we override the base-class implementation + + Ludwig registers a *distributed strategy* (``LocalStrategy`` or + ``AccelerateStrategy``) via ``init_dist_strategy()``. The base-class + ``sync_context()`` is unaware of Ludwig's strategy abstraction, so we override it + to ask the active strategy for a gather function. + + * **AccelerateStrategy** — ``gather_all_tensors_fn()`` returns + ``torchmetrics.utilities.distributed.gather_all_tensors``, enabling proper + all-gather across ranks. + * **LocalStrategy** — ``gather_all_tensors_fn()`` returns ``None`` and + ``is_available`` returns ``False``. This disables sync, which is correct for + single-process training. + + ## The Ray TorchTrainer special case (the hard part) + + ``eval_fn`` (``ludwig/backend/ray.py``) runs *inside a Ray TorchTrainer worker*. + TorchTrainer always calls ``torch.distributed.init_process_group()`` before + entering the worker function, so ``torch.distributed.is_initialized()`` is + ``True`` inside every ``eval_fn`` invocation, even though Ludwig has registered + ``LocalStrategy`` (not ``AccelerateStrategy``) for that function. + + If we blindly used ``LocalStrategy.gather_all_tensors_fn() == None``, every worker + would compute metrics **only on its own data shard** and report independent (wrong) + values. Only rank-0's results are written to the checkpoint, so with N workers + you'd get metrics computed on ~1/N of the dataset. + + The fallback below detects this situation: when the registered strategy provides no + gather function **but** ``torch.distributed`` is already initialised, we fall back + to torchmetrics' native ``gather_all_tensors``. This all-gathers the accumulator + state tensors across all workers before ``compute()`` runs, so the final metric + value reflects the **full** dataset. + + ## Summary of the selection logic + + 1. Ask the registered Ludwig strategy for a gather function. + 2. If the strategy provides one → use it. (AccelerateStrategy path.) + 3. If the strategy provides ``None`` **and** ``torch.distributed`` is initialised + → fall back to ``gather_all_tensors``. (Ray TorchTrainer / eval_fn path.) + 4. If the strategy provides ``None`` **and** ``torch.distributed`` is NOT + initialised → sync is a no-op. (Single-process local training path.) + """ + dist_strategy = get_current_dist_strategy() + gather_fn = dist_strategy.gather_all_tensors_fn() + dist_available = dist_strategy.is_available + + if gather_fn is None: + # LocalStrategy (and similar no-op strategies) return None for + # gather_all_tensors_fn. When torch.distributed is initialized (e.g. inside a + # Ray TorchTrainer worker) we can use torchmetrics' built-in all-gather so that + # metric accumulator state is aggregated across all shards before compute(). + import torch.distributed as _dist + + if _dist.is_available() and _dist.is_initialized(): + from torchmetrics.utilities.distributed import gather_all_tensors + + gather_fn = gather_all_tensors + # Pass a callable (not a bool) so torchmetrics evaluates liveness at call + # time rather than capturing the value at context-manager entry. + dist_available = _dist.is_initialized + + self.sync( + dist_sync_fn=gather_fn, + process_group=process_group, + should_sync=should_sync, + distributed_available=dist_available, + ) + + yield + + self.unsync(should_unsync=self._is_synced and should_unsync) + + +@register_metric(ROOT_MEAN_SQUARED_ERROR, [NUMBER, TIMESERIES], MINIMIZE, PREDICTIONS) +class RMSEMetric(MeanSquaredError, LudwigMetric): + """Root mean squared error metric.""" + + def __init__(self, **kwargs): + super().__init__(squared=False) + + +@register_metric(PRECISION, [BINARY], MAXIMIZE, PROBABILITIES) +class PrecisionMetric(BinaryPrecision, LudwigMetric): + """Precision metric.""" + + def __init__(self, **kwargs): + super().__init__() + + +@register_metric(RECALL, [BINARY], MAXIMIZE, PROBABILITIES) +class RecallMetric(BinaryRecall, LudwigMetric): + """Recall metric.""" + + def __init__(self, **kwargs): + super().__init__() + + +@register_metric(ROC_AUC, [BINARY], MAXIMIZE, PROBABILITIES) +class BinaryAUROCMetric(BinaryAUROC, LudwigMetric): + """Area under the receiver operating curve.""" + + def __init__(self, **kwargs): + super().__init__() + + def update(self, preds: Tensor, target: Tensor) -> None: + super().update(preds, target.type(torch.int8)) + + +@register_metric(ROC_AUC, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, PROBABILITIES) +class CategoryAUROCMetric(MulticlassAUROC, LudwigMetric): + """Area under the receiver operating curve.""" + + def __init__(self, num_classes: int, **kwargs): + super().__init__(num_classes=num_classes) + + def update(self, preds: Tensor, target: Tensor) -> None: + if len(target.shape) > 1: + target = torch.argmax(target, dim=1) + super().update(preds, target) + + +@register_metric(SPECIFICITY, [BINARY], MAXIMIZE, PROBABILITIES) +class SpecificityMetric(BinarySpecificity, LudwigMetric): + """Specificity metric.""" + + def __init__(self, **kwargs): + super().__init__() + + +class MeanMetric(LudwigMetric): + """Abstract class for computing mean of metrics.""" + + def __init__(self, **kwargs): + super().__init__() + self.avg = _MeanMetric() + + def update(self, preds: Tensor, target: Tensor) -> None: + self.avg.update(self.get_current_value(preds, target)) + + def compute(self) -> Tensor: + return self.avg.compute() + + def reset(self): + super().reset() + self.avg.reset() + + @abstractmethod + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + raise NotImplementedError() + + +@register_metric(ROOT_MEAN_SQUARED_PERCENTAGE_ERROR, [NUMBER], MINIMIZE, PREDICTIONS) +class RMSPEMetric(MeanMetric): + def __init__(self, **kwargs): + super().__init__() + + """ Root mean squared percentage error metric. """ + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + return rmspe_loss(target, preds) + + +@register_metric(R2, [NUMBER, VECTOR, TIMESERIES], MAXIMIZE, PREDICTIONS) +class R2Score(LudwigMetric): + """Custom R-squared metric implementation that modifies torchmetrics R-squared implementation to return Nan + when there is only sample. This is because R-squared is only defined for two or more samples. + + Custom implementation uses code from torchmetrics v0.9.2's implementation of R2: https://github.com/Lightning- + AI/metrics/blob/master/src/torchmetrics/regression/r2.py + """ + + def __init__( + self, num_outputs: int = 1, adjusted: int = 0, multioutput: str = "uniform_average", **kwargs: Any + ) -> None: + super().__init__(**kwargs) + + self.num_outputs = num_outputs + + if adjusted < 0 or not isinstance(adjusted, int): + raise ValueError("`adjusted` parameter should be an integer larger or equal to 0.") + self.adjusted = adjusted + + allowed_multioutput = ("raw_values", "uniform_average", "variance_weighted") + if multioutput not in allowed_multioutput: + raise ValueError( + f"Invalid input to argument `multioutput`. Choose one of the following: {allowed_multioutput}" + ) + self.multioutput = multioutput + + self.add_state("sum_squared_error", default=torch.zeros(self.num_outputs), dist_reduce_fx="sum") + self.add_state("sum_error", default=torch.zeros(self.num_outputs), dist_reduce_fx="sum") + self.add_state("residual", default=torch.zeros(self.num_outputs), dist_reduce_fx="sum") + self.add_state("total", default=tensor(0), dist_reduce_fx="sum") + + def update(self, preds: Tensor, target: Tensor) -> None: + """Update state with predictions and targets. + + Args: + preds: Predictions from model + target: Ground truth values + """ + sum_squared_error, sum_error, residual, n_obs = _r2_score_update(preds, target) + + self.sum_squared_error += sum_squared_error + self.sum_error += sum_error + self.residual += residual + self.total += n_obs + + def compute(self) -> Tensor: + """Computes r2 score over the metric states.""" + + # self.total maps to the number of observations in preds/target computed during update() + if self.total <= 1: + logger.warning( + """R-squared (r2) is not defined for one sample. It needs at least two samples. Returning NaN.""" + ) + return torch.tensor(float("nan")) + + return _r2_score_compute( + self.sum_squared_error, self.sum_error, self.residual, self.total, self.adjusted, self.multioutput + ) + + +@register_metric(LOSS, [], MINIMIZE, LOGITS) +class LossMetric(MeanMetric, ABC): + def __init__(self): + super().__init__() + + @abstractmethod + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + raise NotImplementedError() + + @classmethod + def can_report(cls, feature: "OutputFeature") -> bool: # noqa: F821 + return False + + +@register_metric(BINARY_WEIGHTED_CROSS_ENTROPY, [BINARY], MINIMIZE, LOGITS) +class BWCEWLMetric(LossMetric): + """Binary Weighted Cross Entropy Weighted Logits Score Metric.""" + + def __init__(self, config: BWCEWLossConfig, **kwargs): + super().__init__() + self.loss_function = BWCEWLoss(config) + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + return self.loss_function(preds, target) + + +@register_metric("softmax_cross_entropy", [CATEGORY, CATEGORY_DISTRIBUTION, IMAGE], MINIMIZE, LOGITS) +class SoftmaxCrossEntropyMetric(LossMetric): + def __init__(self, config: SoftmaxCrossEntropyLossConfig, **kwargs): + super().__init__() + self.softmax_cross_entropy_function = SoftmaxCrossEntropyLoss(config) + + def get_current_value(self, preds: Tensor, target: Tensor): + return self.softmax_cross_entropy_function(preds, target) + + +@register_metric("sequence_softmax_cross_entropy", [SEQUENCE, TEXT], MINIMIZE, LOGITS) +class SequenceSoftmaxCrossEntropyMetric(LossMetric): + def __init__(self, config: SequenceSoftmaxCrossEntropyLossConfig, **kwargs): + super().__init__() + self.sequence_softmax_cross_entropy_function = SequenceSoftmaxCrossEntropyLoss(config) + + def get_current_value(self, preds: Tensor, target: Tensor): + return self.sequence_softmax_cross_entropy_function(preds, target) + + +@register_metric("next_token_softmax_cross_entropy", [SEQUENCE, TEXT], MINIMIZE, LOGITS) +class NextTokenSoftmaxCrossEntropyMetric(LossMetric): + def __init__(self, config: SequenceSoftmaxCrossEntropyLossConfig, **kwargs): + super().__init__() + self.next_token_softmax_cross_entropy_function = NextTokenSoftmaxCrossEntropyLoss(config) + + def get_current_value(self, preds: Tensor, target: Tensor): + return self.next_token_softmax_cross_entropy_function(preds, target) + + +@register_metric("sigmoid_cross_entropy", [SET], MINIMIZE, LOGITS) +class SigmoidCrossEntropyMetric(LossMetric): + def __init__(self, config: SigmoidCrossEntropyLossConfig, **kwargs): + super().__init__() + self.sigmoid_cross_entropy_function = SigmoidCrossEntropyLoss(config) + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + return self.sigmoid_cross_entropy_function(preds, target) + + +@register_metric(TOKEN_ACCURACY, [SEQUENCE, TEXT], MAXIMIZE, PREDICTIONS) +class TokenAccuracyMetric(MeanMetric): + def __init__(self, **kwargs): + super().__init__() + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + target = target.type(preds.dtype) + target_sequence_length = sequence_length_2D(target) + masked_correct_preds = masked_correct_predictions(target, preds, target_sequence_length) + return torch.mean(masked_correct_preds) + + +@register_metric(SEQUENCE_ACCURACY, [SEQUENCE, TEXT], MAXIMIZE, PREDICTIONS) +class SequenceAccuracyMetric(MeanMetric): + def __init__(self, **kwargs): + super().__init__() + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + return torch.sum(torch.all(preds == target, dim=1)) / target.size()[0] + + +@register_metric(PERPLEXITY, [SEQUENCE, TEXT], MINIMIZE, PROBABILITIES) +class PerplexityMetric(Perplexity, LudwigMetric): + def __init__(self, **kwargs): + super().__init__(ignore_index=IGNORE_INDEX_TOKEN_ID) + + def update(self, preds: Tensor, target: Tensor) -> None: + super().update(preds, target.type(torch.int64)) + + +@register_metric(NEXT_TOKEN_PERPLEXITY, [SEQUENCE, TEXT], MINIMIZE, PROBABILITIES) +class NextTokenPerplexityMetric(MeanMetric): + def __init__(self, **kwargs): + super().__init__() + self.next_token_softmax_cross_entropy_function = NextTokenSoftmaxCrossEntropyLoss({}) + + def get_current_value(self, preds: Tensor, target: Tensor): + # Perplexity can be represented as the exponential of the cross-entropy loss. + # https://towardsdatascience.com/perplexity-in-language-models-87a196019a94 + # We can't use torchmetrics perplexity because it calculates normal cross-entropy + # loss as opposed to shifted cross entropy loss. + shifted_loss = self.next_token_softmax_cross_entropy_function(preds, target) + return torch.exp(shifted_loss) + + +# @register_metric("bleu", [TEXT], MAXIMIZE, RESPONSE) +# https://github.com/ludwig-ai/ludwig/issues/3953 +class BLEUScoreMetric(BLEUScore, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + +# @register_metric("rouge", [TEXT], MAXIMIZE, RESPONSE) +# https://github.com/ludwig-ai/ludwig/issues/3953 +class ROUGEScoreMetric(ROUGEScore, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + +# @register_metric("word_error_rate", [TEXT], MINIMIZE, RESPONSE) +# https://github.com/ludwig-ai/ludwig/issues/3953 +class WordErrorRateMetric(WordErrorRate, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + +# @register_metric("char_error_rate", [TEXT], MINIMIZE, RESPONSE) +# https://github.com/ludwig-ai/ludwig/issues/3953 +class CharErrorRateMetric(CharErrorRate, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + +@register_metric(ACCURACY, [BINARY], MAXIMIZE, PREDICTIONS) +class Accuracy(BinaryAccuracy, LudwigMetric): + """R-squared metric.""" + + def __init__(self, **kwargs): + super().__init__() + + +@register_metric(ACCURACY, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, PREDICTIONS) +class CategoryAccuracy(MulticlassAccuracy, LudwigMetric): + def __init__(self, num_classes: int, **kwargs): + super().__init__(num_classes=num_classes) + + def update(self, preds: Tensor, target: Tensor) -> None: + if len(target.shape) > 1: + target = torch.argmax(target, dim=1) + super().update(preds, target.type(torch.long)) + + +@register_metric(ACCURACY_MICRO, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, PREDICTIONS) +class CategoryAccuracyMicro(MulticlassAccuracy, LudwigMetric): + def __init__(self, num_classes: int, **kwargs): + super().__init__(num_classes=num_classes, average="micro") + + def update(self, preds: Tensor, target: Tensor) -> None: + if len(target.shape) > 1: + target = torch.argmax(target, dim=1) + super().update(preds, target.type(torch.long)) + + +@register_metric(HITS_AT_K, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, LOGITS) +class HitsAtKMetric(MulticlassAccuracy, LudwigMetric): + def __init__(self, num_classes: int, top_k: int, **kwargs): + super().__init__(num_classes=num_classes, top_k=top_k, **kwargs) + + def update(self, preds: Tensor, target: Tensor) -> None: + if len(target.shape) > 1: + target = torch.argmax(target, dim=1) + super().update(preds, target.type(torch.long)) + + @classmethod + def can_report(cls, feature: "OutputFeature") -> bool: # noqa: F821 + return feature.num_classes > feature.top_k + + +@register_metric(MEAN_ABSOLUTE_ERROR, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS) +class MAEMetric(MeanAbsoluteError, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + def update(self, preds: Tensor, target: Tensor) -> None: + super().update(preds.detach(), target) + + +@register_metric(MEAN_SQUARED_ERROR, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS) +class MSEMetric(MeanSquaredError, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + def update(self, preds: Tensor, target: Tensor) -> None: + super().update(preds, target) + + +@register_metric(MEAN_ABSOLUTE_PERCENTAGE_ERROR, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS) +class MAPEMetric(MeanAbsolutePercentageError, LudwigMetric): + def __init__(self, **kwargs): + super().__init__() + + def update(self, preds: Tensor, target: Tensor) -> None: + super().update(preds, target) + + +@register_metric(MEAN_ABSOLUTE_SCALED_ERROR, [TIMESERIES], MINIMIZE, PREDICTIONS) +class MASEMetric(MeanMetric): + """Mean Absolute Scaled Error — scale-free metric that normalizes by the in-sample naive baseline. + + For a forecast horizon, MASE = MAE(forecast) / MAE(naive_within_batch). + The naive baseline is computed as mean(|y_t - y_{t-1}|) over the target sequence. + When the target is a flat vector (horizon-only, no history), falls back to MAE / mean(|target|+eps). + """ + + def __init__(self, **kwargs): + super().__init__() + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + # preds, target: [batch, horizon] + mae = torch.mean(torch.abs(preds - target), dim=-1) # [batch] + if target.shape[-1] > 1: + naive_scale = torch.mean(torch.abs(target[..., 1:] - target[..., :-1]), dim=-1) + 1e-8 + else: + naive_scale = torch.mean(torch.abs(target), dim=-1) + 1e-8 + return torch.mean(mae / naive_scale) + + +@register_metric(SYMMETRIC_MEAN_ABSOLUTE_PERCENTAGE_ERROR, [TIMESERIES], MINIMIZE, PREDICTIONS) +class SMAPEMetric(MeanMetric): + """Symmetric Mean Absolute Percentage Error.""" + + def __init__(self, **kwargs): + super().__init__() + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + denom = torch.abs(target) + torch.abs(preds) + 1e-8 + return torch.mean(200.0 * torch.abs(preds - target) / denom) + + +@register_metric(JACCARD, [SET], MAXIMIZE, PROBABILITIES) +class JaccardMetric(MeanMetric): + def __init__(self, threshold: float = 0.5, **kwargs): + super().__init__() + self.threshold = threshold + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + # notation: b is batch size and nc is number of unique elements in the set + # preds: shape [b, nc] probabilities for each class + # target: shape [b, nc] bit-mapped set representation + preds = torch.greater_equal(preds, self.threshold) # now bit-mapped set + target = target.type(torch.bool) + + intersection = torch.sum(torch.logical_and(target, preds).type(torch.float32), dim=-1) + union = torch.sum(torch.logical_or(target, preds).type(torch.float32), dim=-1) + + return intersection / union # shape [b] + + +@register_metric(HUBER, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS) +class HuberMetric(LossMetric): + def __init__( + self, + config: HuberLossConfig, + **kwargs, + ): + super().__init__() + self.loss_function = HuberLoss(config=config) + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + return self.loss_function(preds, target) + + +@register_metric(CORN, [CATEGORY], MINIMIZE, PREDICTIONS) +class CORNMetric(LossMetric): + def __init__( + self, + config: CORNLossConfig, + **kwargs, + ): + super().__init__() + self.loss_function = CORNLoss(config=config) + + def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor: + return self.loss_function(preds, target) + + +def get_metric_cls(metric_name: str) -> type[LudwigMetric]: + return get_metric_registry()[metric_name] + + +def get_improved_fn(metric: str) -> Callable: + if get_metric_objective(metric) == MINIMIZE: + return lambda x, y: x < y + else: + return lambda x, y: x > y + + +def get_initial_validation_value(metric: str) -> float: + # Use finite floats instead of inf/-inf so that training_progress.json + # is valid JSON (RFC 8259). sys.float_info.max (~1.8e308) is larger than + # any real metric value, so comparison semantics are identical. + if get_metric_objective(metric) == MINIMIZE: + return sys.float_info.max + else: + return -sys.float_info.max + + +def get_best_function(metric: str) -> Callable: + if get_metric_objective(metric) == MINIMIZE: + return min + else: + return max + + +@register_metric(ANOMALY_AUROC, [ANOMALY], MAXIMIZE, ANOMALY_SCORE) +class AnomalyAUROCMetric(LudwigMetric): + """AUROC for anomaly detection scores. + + Computes area under the ROC curve between anomaly scores (higher = more anomalous) and binary ground-truth labels + (1=anomaly, 0=normal). The primary threshold-independent metric for anomaly detection quality. Requires labeled + validation/test data. + """ + + def __init__(self, **kwargs): + super().__init__() + self.add_state("scores", default=[], dist_reduce_fx="cat") + self.add_state("labels", default=[], dist_reduce_fx="cat") + + def update(self, preds: Tensor, target: Tensor) -> None: + self.scores.append(preds.detach().cpu().float()) + self.labels.append(target.detach().cpu().float()) + + def compute(self) -> Tensor: + if not self.scores: + return torch.tensor(float("nan")) + scores = torch.cat(self.scores) + labels = torch.cat(self.labels) + if labels.unique().numel() < 2: + return torch.tensor(float("nan")) + try: + from torchmetrics.functional.classification import binary_auroc + + return binary_auroc(scores, labels.long()) + except (ImportError, RuntimeError, ValueError): + return torch.tensor(float("nan")) + + def reset(self): + super().reset() + self.scores = [] + self.labels = [] + + +@register_metric(F1_MAX, [ANOMALY], MAXIMIZE, ANOMALY_SCORE) +class F1MaxMetric(LudwigMetric): + """Maximum F1 score over all anomaly score thresholds. + + Sweeps all unique threshold values and returns the best achievable F1, providing a threshold-independent estimate of + classification performance. + """ + + def __init__(self, **kwargs): + super().__init__() + self.add_state("scores", default=[], dist_reduce_fx="cat") + self.add_state("labels", default=[], dist_reduce_fx="cat") + + def update(self, preds: Tensor, target: Tensor) -> None: + self.scores.append(preds.detach().cpu().float()) + self.labels.append(target.detach().cpu().float()) + + def compute(self) -> Tensor: + if not self.scores: + return torch.tensor(float("nan")) + scores = torch.cat(self.scores) + labels = torch.cat(self.labels) + if labels.unique().numel() < 2: + return torch.tensor(float("nan")) + try: + best_f1 = torch.tensor(0.0) + labels_bool = labels.bool() + for t in torch.unique(scores): + preds_bin = scores >= t + tp = (preds_bin & labels_bool).float().sum() + fp = (preds_bin & ~labels_bool).float().sum() + fn = (~preds_bin & labels_bool).float().sum() + if tp + fp + fn > 0: + f1 = 2 * tp / (2 * tp + fp + fn) + if f1 > best_f1: + best_f1 = f1 + return best_f1 + except (RuntimeError, ValueError): + return torch.tensor(float("nan")) + + def reset(self): + super().reset() + self.scores = [] + self.labels = [] diff --git a/ludwig/modules/metric_registry.py b/ludwig/modules/metric_registry.py new file mode 100644 index 0000000..9555f16 --- /dev/null +++ b/ludwig/modules/metric_registry.py @@ -0,0 +1,88 @@ +from typing import Literal, TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import LOGITS, MAXIMIZE, MINIMIZE, PREDICTIONS, PROBABILITIES, RESPONSE +from ludwig.utils.registry import Registry + +if TYPE_CHECKING: + from ludwig.modules.metric_modules import LudwigMetric + + +metric_feature_type_registry = Registry() +metric_registry = Registry() +metric_objective_registry = Registry() +metric_tensor_input_registry = Registry() + + +def register_metric( + name: str, + feature_types: str | list[str], + objective: Literal[MINIMIZE, MAXIMIZE], + output_feature_tensor_name: Literal[PREDICTIONS, PROBABILITIES, LOGITS], +): + """Registers a metric class. + + Args: + name: The name of the metric. Used in metric reporting and in the config. + feature_types: The feature types that this metric can be used with. + objective: The objective of the metric. Either MINIMIZE or MAXIMIZE. + output_feature_tensor_name: Name of the tensor from output_feature::predictions() that should be used as input. + For example: PREDICTIONS would be used for accuracy metrics while LOGITS would be used for loss metrics. + """ + if isinstance(feature_types, str): + feature_types = [feature_types] + + def wrap(cls): + for feature_type in feature_types: + feature_registry = metric_feature_type_registry.get(feature_type, {}) + feature_registry[name] = cls + metric_feature_type_registry[feature_type] = feature_registry + metric_registry[name] = cls + metric_objective_registry[name] = objective + metric_tensor_input_registry[name] = output_feature_tensor_name + return cls + + return wrap + + +def get_metric_classes(feature_type: str) -> dict[str, "LudwigMetric"]: + return metric_feature_type_registry[feature_type] + + +def get_metric_cls(feature_type: str, name: str) -> "LudwigMetric": + return metric_feature_type_registry[feature_type][name] + + +@DeveloperAPI +def get_metric_feature_type_registry() -> Registry: + return metric_feature_type_registry + + +@DeveloperAPI +def get_metric_registry() -> Registry: + return metric_registry + + +@DeveloperAPI +def get_metric(metric_name: str) -> "LudwigMetric": + return get_metric_registry()[metric_name] + + +@DeveloperAPI +def get_metrics_for_type(feature_type: str) -> dict[str, "LudwigMetric"]: + return get_metric_feature_type_registry()[feature_type] + + +@DeveloperAPI +def get_metric_names_for_type(feature_type: str) -> list[str]: + return sorted(list(get_metric_feature_type_registry()[feature_type].keys())) + + +@DeveloperAPI +def get_metric_objective(metric_name: str) -> Literal[MINIMIZE, MAXIMIZE]: + return metric_objective_registry[metric_name] + + +@DeveloperAPI +def get_metric_tensor_input(metric_name: str) -> Literal[PREDICTIONS, PROBABILITIES, LOGITS, RESPONSE]: + return metric_tensor_input_registry[metric_name] diff --git a/ludwig/modules/mlp_mixer_modules.py b/ludwig/modules/mlp_mixer_modules.py new file mode 100644 index 0000000..cb53be5 --- /dev/null +++ b/ludwig/modules/mlp_mixer_modules.py @@ -0,0 +1,183 @@ +# Copyright (c) 2021 Linux Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import torch +import torch.nn as nn + +from ludwig.utils.torch_utils import LudwigModule + + +class MLP(LudwigModule): + def __init__( + self, + in_features: int | tuple[int], + hidden_size: int, + out_features: int | tuple[int] | None = None, + dropout: float = 0.0, + ): + super().__init__() + + out_features = out_features or in_features + + self._input_shape = in_features + self._output_shape = out_features + + self.linear1 = nn.Linear(in_features=in_features, out_features=hidden_size) + self.linear2 = nn.Linear(in_features=hidden_size, out_features=out_features) + self.dropout1 = nn.Dropout(dropout) + self.dropout2 = nn.Dropout(dropout) + + def forward(self, inputs, **kwargs): + hidden = self.dropout1(nn.functional.gelu(self.linear1(inputs))) + return self.dropout2(self.linear2(hidden)) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self._input_shape]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self._output_shape]) + + +class MixerBlock(LudwigModule): + def __init__(self, embed_size: int, n_patches: int, token_dim: int, channel_dim: int, dropout: float = 0.0): + super().__init__() + self._input_shape = (n_patches, embed_size) + self._output_shape = (n_patches, embed_size) + + self.mlp1 = MLP(in_features=n_patches, hidden_size=token_dim, dropout=dropout) + + self.mlp2 = MLP(in_features=embed_size, hidden_size=channel_dim, dropout=dropout) + + self.layernorm1 = nn.LayerNorm(normalized_shape=embed_size) + self.layernorm2 = nn.LayerNorm(normalized_shape=embed_size) + + def forward(self, inputs: torch.Tensor, **kwargs): + if inputs.shape[1:] != self.input_shape: + raise ValueError( + f"MixerBlock got input shape {tuple(inputs.shape[1:])}, expected {tuple(self.input_shape)}." + ) + + hidden = inputs + hidden = self.layernorm1(hidden).transpose(1, 2) + hidden = self.mlp1(hidden).transpose(1, 2) + + mid = hidden + inputs + + hidden = self.layernorm2(mid) + hidden = self.mlp2(hidden) + + output = hidden + mid + if output.shape[1:] != self.output_shape: + raise RuntimeError( + f"MixerBlock produced output shape {tuple(output.shape[1:])}, expected {tuple(self.output_shape)}. " + f"This is an internal error — please report it." + ) + return output + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + @property + def output_shape(self) -> torch.Size: + return torch.Size(self._output_shape) + + +class MLPMixer(LudwigModule): + """MLPMixer. + + Implements + MLP-Mixer: An all-MLP Architecture for Vision + https://arxiv.org/abs/2105.01601 + """ + + def __init__( + self, + img_height: int, + img_width: int, + in_channels: int, + patch_size: int = 16, + embed_size: int = 512, + token_size: int = 2048, + channel_dim: int = 256, + num_layers: int = 8, + dropout: float = 0.0, + avg_pool: bool = True, + ): + super().__init__() + if img_height % patch_size != 0 or img_width % patch_size != 0: + raise ValueError( + f"Image dimensions ({img_height}x{img_width}) must be divisible by patch_size={patch_size}.\n" + f"Fix: choose a patch_size that evenly divides both image height and width." + ) + + self._input_shape = (in_channels, img_height, img_width) + n_patches = int(img_height * img_width / (patch_size**2)) + + self.patch_conv = nn.Conv2d( + in_channels=in_channels, out_channels=embed_size, kernel_size=patch_size, stride=patch_size + ) + + self.mixer_blocks = nn.ModuleList( + [ + MixerBlock( + embed_size=embed_size, + n_patches=n_patches, + token_dim=token_size, + channel_dim=channel_dim, + dropout=dropout, + ) + for _ in range(num_layers) + ] + ) + + self.layer_norm = nn.LayerNorm(normalized_shape=embed_size) + + self.avg_pool = avg_pool + if self.avg_pool: + self._output_shape = torch.Size((embed_size,)) + else: + self._output_shape = torch.Size((n_patches, embed_size)) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + if inputs.shape[1:] != self.input_shape: + raise ValueError(f"MLPMixer got input shape {tuple(inputs.shape[1:])}, expected {tuple(self.input_shape)}.") + hidden = self.patch_conv(inputs) + hidden = hidden.flatten(2).transpose(1, 2) + + for mixer_block in self.mixer_blocks: + hidden = mixer_block(hidden) + hidden = self.layer_norm(hidden) + + if self.avg_pool: + hidden = torch.mean(hidden, dim=1) + + if hidden.shape[1:] != self.output_shape: + raise RuntimeError( + f"MLPMixer produced output shape {tuple(hidden.shape[1:])}, expected {tuple(self.output_shape)}. " + f"This is an internal error — please report it." + ) + + return hidden + + @property + def input_shape(self) -> torch.Size: + return torch.Size(self._input_shape) + + @property + def output_shape(self) -> torch.Size: + return self._output_shape diff --git a/ludwig/modules/modality_dropout.py b/ludwig/modules/modality_dropout.py new file mode 100644 index 0000000..b82c5a7 --- /dev/null +++ b/ludwig/modules/modality_dropout.py @@ -0,0 +1,55 @@ +"""Modality dropout: randomly drop encoder outputs during training for robustness. + +During training, each input feature's encoder output is randomly replaced with a learnable +"missing modality" embedding with probability `dropout_prob`. This improves robustness to +missing inputs at inference time. + +Based on: "Bag of Tricks for Multimodal AutoML" (arXiv 2412.16243, Dec 2024). +""" + +import torch +import torch.nn as nn + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.utils.torch_utils import LudwigModule + + +class ModalityDropout(LudwigModule): + """Drops entire modality encoder outputs during training and replaces with learned embeddings.""" + + def __init__(self, feature_shapes: dict[str, torch.Size], dropout_prob: float = 0.1): + """Initialize modality dropout. + + Args: + feature_shapes: Dict mapping feature names to their encoder output shapes. + dropout_prob: Probability of dropping each feature's output during training. + """ + super().__init__() + self.dropout_prob = dropout_prob + self.missing_embeddings = nn.ParameterDict( + {name: nn.Parameter(torch.zeros(*shape)) for name, shape in feature_shapes.items()} + ) + + def forward(self, encoder_outputs: dict[str, dict[str, torch.Tensor]]) -> dict[str, dict[str, torch.Tensor]]: + if not self.training or self.dropout_prob == 0.0: + return encoder_outputs + + result = {} + for name, output_dict in encoder_outputs.items(): + if name in self.missing_embeddings and torch.rand(1).item() < self.dropout_prob: + batch_size = output_dict[ENCODER_OUTPUT].shape[0] + missing = ( + self.missing_embeddings[name].unsqueeze(0).expand(batch_size, *self.missing_embeddings[name].shape) + ) + result[name] = {**output_dict, ENCODER_OUTPUT: missing} + else: + result[name] = output_dict + return result + + @property + def input_shape(self) -> torch.Size: + return torch.Size([len(self.missing_embeddings)]) + + @property + def output_shape(self) -> torch.Size: + return self.input_shape diff --git a/ludwig/modules/normalization_modules.py b/ludwig/modules/normalization_modules.py new file mode 100644 index 0000000..1c1ed69 --- /dev/null +++ b/ludwig/modules/normalization_modules.py @@ -0,0 +1,125 @@ +import logging + +import numpy as np +import torch +from torch.nn import BatchNorm1d, BatchNorm2d, LayerNorm, Module + +from ludwig.utils.torch_utils import LudwigModule + +logger = logging.getLogger(__name__) + + +# implementation adapted from https://github.com/dreamquark-ai/tabnet +class GhostBatchNormalization(LudwigModule): + def __init__( + self, num_features: int, momentum: float = 0.05, epsilon: float = 1e-3, virtual_batch_size: int | None = 128 + ): + super().__init__() + self.num_features = num_features + self.virtual_batch_size = virtual_batch_size + self.bn = torch.nn.BatchNorm1d(num_features, momentum=momentum, eps=epsilon) + + def forward(self, inputs): + batch_size = inputs.shape[0] + + if self.training and self.virtual_batch_size: + splits = inputs.chunk(int(np.ceil(batch_size / self.virtual_batch_size)), 0) + + if batch_size % self.virtual_batch_size == 1: + # Skip batch normalization for the last chunk if it is size 1. + logger.warning( + f"Virtual batch size `{self.virtual_batch_size}` is not a factor of the batch size `{batch_size}`, " + "resulting in a chunk of size 1. Skipping batch normalization for the last chunk of size 1." + ) + + if batch_size == 1: + logger.warning( + "Batch size is 1, but batch normalization requires batch size >= 2. Skipping batch normalization." + "Make sure to set `batch_size` to a value greater than 1." + ) + # We temporarily set the batch_norm module to eval mode as we can't compute the running statistics + # when the batch size is 1. + self.bn.eval() + splits_with_bn = [self.bn(x) if x.shape[0] >= 1 else x for x in splits] + self.bn.train() + else: + splits_with_bn = [self.bn(x) if x.shape[0] > 1 else x for x in splits] + + return torch.cat(splits_with_bn, 0) + + if batch_size != 1 or not self.training: + return self.bn(inputs) + return inputs + + @property + def moving_mean(self) -> torch.Tensor: + return self.bn.running_mean + + @property + def moving_variance(self) -> torch.Tensor: + return self.bn.running_var + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.num_features]) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.num_features]) + + +class BatchNorm1dOrIdentity(BatchNorm1d): + """BatchNorm1d or Identity layer if the batch_size is 1. + + Workaround for: https://github.com/pytorch/pytorch/issues/4534 + """ + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if input.shape[0] == 1: + logger.warning( + "Batch size is 1, but batch normalization requires batch size >= 2. Skipping batch normalization." + "Make sure to set `batch_size` to a value greater than 1." + ) + return input + return super().forward(input) + + +class BatchNorm2dOrIdentity(BatchNorm2d): + """BatchNorm2d or Identity layer if the batch_size is 1. + + Workaround for: https://github.com/pytorch/pytorch/issues/4534 + """ + + def forward(self, input: torch.Tensor) -> torch.Tensor: + if input.shape[0] == 1: + logger.warning( + "Batch size is 1, but batch normalization requires batch size >= 2. Skipping batch normalization." + "Make sure to set `batch_size` to a value greater than 1." + ) + return input + return super().forward(input) + + +norm_registry = { + "batch_1d": BatchNorm1dOrIdentity, + "batch_2d": BatchNorm2dOrIdentity, + "layer": LayerNorm, + "ghost": GhostBatchNormalization, +} + + +def create_norm_layer(norm: str, input_rank: int, num_features: int, **norm_params) -> Module: + if norm == "batch": + # We use a different batch norm depending on the input_rank. + # TODO(travis): consider moving this behind a general BatchNorm interface to avoid this kludge. + if input_rank not in {2, 3}: + ValueError(f"`input_rank` parameter expected to be either 2 or 3, but found {input_rank}.") + norm = f"{norm}_{input_rank - 1}d" + + norm_cls = norm_registry.get(norm) + if norm_cls is None: + raise ValueError( + f"Unsupported value for `norm` param: {norm}. Supported values are: {list(norm_registry.keys())}" + ) + + return norm_cls(num_features, **norm_params) diff --git a/ludwig/modules/optimization_modules.py b/ludwig/modules/optimization_modules.py new file mode 100644 index 0000000..f6e2f45 --- /dev/null +++ b/ludwig/modules/optimization_modules.py @@ -0,0 +1,162 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import dataclasses +from typing import TYPE_CHECKING + +import torch + +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.torch_utils import LudwigModule + +if TYPE_CHECKING: + from ludwig.schema.optimizers import BaseOptimizerConfig, GradientClippingConfig + + +def create_clipper(gradient_clipping_config: "GradientClippingConfig | None"): + from ludwig.schema.optimizers import GradientClippingConfig + + """Utility function that will convert a None-type gradient clipping config to the correct form.""" + if isinstance(gradient_clipping_config, GradientClippingConfig): + return gradient_clipping_config + # Return default config if provided value is None: + return GradientClippingConfig() + + +def get_optimizer_class_and_kwargs( + optimizer_config: "BaseOptimizerConfig", learning_rate: float +) -> tuple[type[torch.optim.Optimizer], dict]: + """Returns the optimizer class and kwargs for the optimizer. + + Returns: + Tuple of optimizer class and kwargs for the optimizer. + """ + from ludwig.schema.optimizers import optimizer_registry + + # Get the corresponding torch optimizer class for the given config: + optimizer_cls = get_from_registry(optimizer_config.type.lower(), optimizer_registry)[0] + + # Create a dict of parameters to be passed to torch (i.e. everything except `type`): + if dataclasses.is_dataclass(optimizer_config): + config_dict = dataclasses.asdict(optimizer_config) + elif hasattr(optimizer_config, "to_dict"): + config_dict = optimizer_config.to_dict() + else: + config_dict = vars(optimizer_config) + cls_kwargs = {field: value for field, value in config_dict.items() if field != "type"} + + # Most optimizers accept lr from Ludwig's trainer config. However, some optimizers + # manage their own LR schedule and expect lr=None (e.g. Adafactor with relative_step=True). + # Only override lr if the config does not already set it to None explicitly. + if cls_kwargs.get("lr") is None and "lr" in cls_kwargs: + # Config explicitly set lr=None (e.g. Adafactor relative_step mode) -- respect it. + pass + else: + cls_kwargs["lr"] = learning_rate + + return optimizer_cls, cls_kwargs + + +def _get_loraplus_lr_ratio(model) -> float | None: + """Check if the model has a LoRA+ lr ratio configured.""" + try: + config_obj = getattr(model, "config_obj", None) + if config_obj is None: + return None + adapter = getattr(config_obj, "adapter", None) + if adapter is None: + return None + return getattr(adapter, "loraplus_lr_ratio", None) + except AttributeError: + return None + + +def _create_loraplus_param_groups(model, optimizer_kwargs, loraplus_lr_ratio): + """Create separate param groups for LoRA A and B matrices with different learning rates. + + LoRA+ (Hayou et al., ICML 2024) uses a higher learning rate for B matrices and the base learning rate for A + matrices. This provides 1-2% accuracy gain and up to 2x speedup. + """ + import logging + + logger = logging.getLogger(__name__) + + base_lr = optimizer_kwargs["lr"] + b_lr = base_lr * loraplus_lr_ratio + + a_params = [] + b_params = [] + other_params = [] + + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + if "lora_A" in name: + a_params.append(param) + elif "lora_B" in name: + b_params.append(param) + else: + other_params.append(param) + + logger.info( + f"LoRA+ enabled: A matrices ({len(a_params)} params) lr={base_lr}, " + f"B matrices ({len(b_params)} params) lr={b_lr}, " + f"other ({len(other_params)} params) lr={base_lr}" + ) + + param_groups = [] + if a_params: + param_groups.append({"params": a_params, "lr": base_lr}) + if b_params: + param_groups.append({"params": b_params, "lr": b_lr}) + if other_params: + param_groups.append({"params": other_params, "lr": base_lr}) + + return param_groups + + +def create_optimizer( + model: LudwigModule, + optimizer_config: "BaseOptimizerConfig", + learning_rate: float, +) -> torch.optim.Optimizer: + """Returns a ready-to-use torch optimizer instance based on the given optimizer config. + + Args: + model: Underlying Ludwig model. + learning_rate: Initial learning rate for the optimizer. + optimizer_config: Instance of `ludwig.modules.optimization_modules.BaseOptimizerConfig`. + + Returns: + Initialized instance of a torch optimizer. + """ + # Make sure the optimizer is compatible with the available resources: + if (optimizer_config.is_paged or optimizer_config.is_8bit) and ( + not torch.cuda.is_available() or torch.cuda.device_count() == 0 + ): + raise ValueError( + "Cannot use a paged or 8-bit optimizer on a non-GPU machine. " + "Please use a different optimizer or run on a machine with a GPU." + ) + + optimizer_cls, optimizer_kwargs = get_optimizer_class_and_kwargs(optimizer_config, learning_rate) + + # LoRA+ support: use different learning rates for A and B matrices + # (Hayou et al., ICML 2024). B matrices get lr * loraplus_lr_ratio. + loraplus_lr_ratio = _get_loraplus_lr_ratio(model) + if loraplus_lr_ratio is not None and loraplus_lr_ratio > 0: + param_groups = _create_loraplus_param_groups(model, optimizer_kwargs, loraplus_lr_ratio) + return optimizer_cls(param_groups, **{k: v for k, v in optimizer_kwargs.items() if k != "lr"}) + + return optimizer_cls(model.parameters(), **optimizer_kwargs) diff --git a/ludwig/modules/preference_losses.py b/ludwig/modules/preference_losses.py new file mode 100644 index 0000000..217138a --- /dev/null +++ b/ludwig/modules/preference_losses.py @@ -0,0 +1,175 @@ +"""Preference optimization loss functions beyond DPO. + +Implements: +- KTO: Kahneman-Tversky Optimization (Ethayarajh et al., 2024) +- ORPO: Odds Ratio Preference Optimization (Hong et al., 2024) +- GRPO: Group Relative Policy Optimization (Shao et al., 2024, used in DeepSeek-R1) + +All functions expect per-token logits and labels, and compute preference-based +losses that train models to prefer certain completions over others. +""" + +import torch +import torch.nn.functional as F + +from ludwig.modules.dpo_loss import compute_token_log_probs + + +def kto_loss( + policy_chosen_logits: torch.Tensor, + policy_rejected_logits: torch.Tensor, + chosen_labels: torch.Tensor, + rejected_labels: torch.Tensor, + reference_chosen_log_probs: torch.Tensor | None = None, + reference_rejected_log_probs: torch.Tensor | None = None, + beta: float = 0.1, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Kahneman-Tversky Optimization loss (Ethayarajh et al., 2024). + + Unlike DPO which requires paired chosen/rejected data, KTO can work with + unpaired preferences (just "this is good" or "this is bad" labels). However, + when both are available, it uses them together. + + The loss applies prospect theory: losses loom larger than gains, so the model + is penalized more for generating rejected text than rewarded for chosen text. + + L_KTO = (1 - sigmoid(beta * (log_ratio_chosen - KL))) for chosen + + (1 - sigmoid(beta * (KL - log_ratio_rejected))) for rejected + + where KL is the average KL divergence between policy and reference. + """ + policy_chosen_lp = compute_token_log_probs(policy_chosen_logits, chosen_labels) + policy_rejected_lp = compute_token_log_probs(policy_rejected_logits, rejected_labels) + + if reference_chosen_log_probs is not None: + chosen_log_ratios = policy_chosen_lp - reference_chosen_log_probs + else: + chosen_log_ratios = policy_chosen_lp + + if reference_rejected_log_probs is not None: + rejected_log_ratios = policy_rejected_lp - reference_rejected_log_probs + else: + rejected_log_ratios = policy_rejected_lp + + # KL divergence estimate (mean of absolute log ratios) + kl = 0.5 * (chosen_log_ratios.abs().mean() + rejected_log_ratios.abs().mean()) + + # KTO loss: asymmetric treatment of chosen vs rejected + chosen_loss = 1 - F.sigmoid(beta * (chosen_log_ratios - kl)) + rejected_loss = 1 - F.sigmoid(beta * (kl - rejected_log_ratios)) + + loss = 0.5 * (chosen_loss.mean() + rejected_loss.mean()) + + chosen_rewards = beta * chosen_log_ratios.detach() + rejected_rewards = beta * rejected_log_ratios.detach() + + return loss, chosen_rewards.mean(), rejected_rewards.mean() + + +def orpo_loss( + policy_chosen_logits: torch.Tensor, + policy_rejected_logits: torch.Tensor, + chosen_labels: torch.Tensor, + rejected_labels: torch.Tensor, + beta: float = 0.1, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Odds Ratio Preference Optimization loss (Hong et al., 2024). + + ORPO combines SFT and preference alignment into a single objective by + using the odds ratio of chosen vs rejected log probabilities. Does not + require a reference model at all. + + L_ORPO = L_SFT(chosen) - beta * log(odds_ratio) + + where odds_ratio = odds(chosen) / odds(rejected) + and odds(y) = P(y) / (1 - P(y)) + """ + chosen_lp = compute_token_log_probs(policy_chosen_logits, chosen_labels) + rejected_lp = compute_token_log_probs(policy_rejected_logits, rejected_labels) + + # Log odds ratio + chosen_log_odds = chosen_lp - torch.log1p(-torch.exp(chosen_lp).clamp(max=1 - 1e-7)) + rejected_log_odds = rejected_lp - torch.log1p(-torch.exp(rejected_lp).clamp(max=1 - 1e-7)) + log_odds_ratio = chosen_log_odds - rejected_log_odds + + # SFT loss on chosen (standard next-token cross-entropy) + shift_logits = policy_chosen_logits[:, :-1, :].contiguous() + shift_labels = chosen_labels[:, 1:].contiguous() + sft_loss = F.cross_entropy( + shift_logits.reshape(-1, shift_logits.size(-1)), + shift_labels.reshape(-1), + ignore_index=-100, + ) + + # Combined loss + loss = sft_loss - beta * F.logsigmoid(log_odds_ratio).mean() + + chosen_rewards = chosen_lp.detach() + rejected_rewards = rejected_lp.detach() + + return loss, chosen_rewards.mean(), rejected_rewards.mean() + + +def grpo_loss( + policy_logits: torch.Tensor, + labels: torch.Tensor, + rewards: torch.Tensor, + old_log_probs: torch.Tensor | None = None, + reference_log_probs: torch.Tensor | None = None, + beta: float = 0.04, + epsilon: float = 0.2, +) -> tuple[torch.Tensor, torch.Tensor]: + """Group Relative Policy Optimization loss (Shao et al., 2024). + + GRPO is used in DeepSeek-R1 and DeepSeek-Math. Unlike PPO, it does not + require a critic/value model. Instead, it uses group-level rewards: + for each prompt, generate multiple completions, score them with a reward + function, normalize rewards within the group, and use the normalized + rewards as advantages. + + This function computes the GRPO objective for a single group of completions + from the same prompt. The caller is responsible for generating multiple + completions and computing rewards. + + L_GRPO = -mean(min(ratio * advantage, clip(ratio, 1-eps, 1+eps) * advantage)) + + beta * KL(policy || reference) + + where ratio = exp(log_prob_new - log_prob_old) + and advantage = (reward - mean(reward)) / std(reward) + + Args: + policy_logits: [group_size, seq_len, vocab_size] current policy logits + labels: [group_size, seq_len] token IDs with -100 for masked positions + rewards: [group_size] scalar rewards for each completion in the group + old_log_probs: [group_size] log probs from the old policy (for importance sampling) + reference_log_probs: [group_size] log probs from the reference model (for KL penalty) + beta: KL penalty coefficient + epsilon: PPO-style clipping parameter + + Returns: + (loss, mean_advantage) + """ + policy_lp = compute_token_log_probs(policy_logits, labels) + + # Group-relative advantage normalization + advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-8) + + # Importance sampling ratio + if old_log_probs is not None: + ratio = torch.exp(policy_lp - old_log_probs) + else: + ratio = torch.ones_like(policy_lp) + + # PPO-style clipped objective + surr1 = ratio * advantages + surr2 = torch.clamp(ratio, 1 - epsilon, 1 + epsilon) * advantages + policy_loss = -torch.min(surr1, surr2).mean() + + # KL penalty against reference model + kl_penalty = torch.tensor(0.0, device=policy_logits.device) + if reference_log_probs is not None: + kl_penalty = (policy_lp - reference_log_probs).mean() + + loss = policy_loss + beta * kl_penalty + + return loss, advantages.mean().detach() diff --git a/ludwig/modules/recurrent_modules.py b/ludwig/modules/recurrent_modules.py new file mode 100644 index 0000000..a3faba7 --- /dev/null +++ b/ludwig/modules/recurrent_modules.py @@ -0,0 +1,81 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch +from torch.nn import GRU, LSTM, RNN + +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.torch_utils import LudwigModule + +logger = logging.getLogger(__name__) + +rnn_layers_registry = { + "rnn": RNN, + "gru": GRU, + "lstm": LSTM, +} + + +class RecurrentStack(LudwigModule): + def __init__( + self, + input_size: int | None = None, + hidden_size: int = 256, + cell_type: str = "rnn", + max_sequence_length: int | None = None, + num_layers: int = 1, + bidirectional: bool = False, + use_bias: bool = True, + dropout: float = 0.0, + **kwargs, + ): + super().__init__() + self.supports_masking = True + self.input_size = input_size # api doc: H_in + self.hidden_size = hidden_size # api doc: H_out + self.max_sequence_length = max_sequence_length # api doc: L (sequence length) + + rnn_layer_class = get_from_registry(cell_type, rnn_layers_registry) + + rnn_params = {"num_layers": num_layers, "bias": use_bias, "dropout": dropout, "bidirectional": bidirectional} + + # Delegate recurrent params to PyTorch's RNN/GRU/LSTM implementations. + self.layers = rnn_layer_class(input_size, hidden_size, batch_first=True, **rnn_params) + + @property + def input_shape(self) -> torch.Size: + if self.max_sequence_length: + return torch.Size([self.max_sequence_length, self.input_size]) + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + hidden_size = self.hidden_size * (2 if self.layers.bidirectional else 1) + if self.max_sequence_length: + return torch.Size([self.max_sequence_length, hidden_size]) + return torch.Size([hidden_size]) + + def forward(self, inputs: torch.Tensor, mask=None): + hidden, final_state = self.layers(inputs) + + if isinstance(final_state, tuple): + # lstm cell type + final_state = final_state[0][-1], final_state[1][-1] + else: + # rnn or gru cell type + final_state = final_state[-1] + + return hidden, final_state diff --git a/ludwig/modules/reduction_modules.py b/ludwig/modules/reduction_modules.py new file mode 100644 index 0000000..15c674f --- /dev/null +++ b/ludwig/modules/reduction_modules.py @@ -0,0 +1,173 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging + +import torch +from torch import nn + +from ludwig.modules.attention_modules import FeedForwardAttentionReducer +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.torch_utils import LudwigModule, sequence_length_3D + +logger = logging.getLogger(__name__) + + +class AttentionPooling(nn.Module): + """Learnable attention-weighted pooling over sequence positions. + + Uses a learnable query vector that attends to all positions via scaled dot-product + attention. Better than mean/max pooling when different positions have different + importance, as the model learns which positions to attend to. + + Unlike FeedForwardAttentionReducer (which uses a two-layer feedforward network to + compute attention scores), this module uses a single learnable query vector with + scaled dot-product attention, making it more parameter-efficient. + + Input shape: [batch, seq_len, hidden_size] + Output shape: [batch, hidden_size] + """ + + def __init__(self, input_size: int, **kwargs): + super().__init__() + self.query = nn.Parameter(torch.randn(1, 1, input_size)) + self.scale = input_size**-0.5 + + def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: + # x: [batch, seq_len, hidden] + attn = (self.query * self.scale) @ x.transpose(-2, -1) # [batch, 1, seq_len] + if mask is not None: + attn = attn.masked_fill(~mask.unsqueeze(1).bool(), float("-inf")) + attn = torch.softmax(attn, dim=-1) + return (attn @ x).squeeze(1) # [batch, hidden] + + +class SequenceReducer(LudwigModule): + """Reduces the sequence dimension of an input tensor according to the specified reduce_mode. Any additional + kwargs are passed on to the reduce mode's constructor. If using reduce_mode=="attention", the input_size kwarg + must also be specified. + + A sequence is a tensor of 2 or more dimensions, where the shape is [batch size x sequence length x ...]. + + Args: + reduce_mode: The reduction mode, one of {"last", "sum", "mean", "max", "concat", "attention", + "attention_pooling", "none"}. + max_sequence_length: The maximum sequence length. Only used for computation of shapes - inputs passed + at runtime may have a smaller sequence length. + encoding_size: The size of each sequence element/embedding vector, or None if input is a sequence of + scalars. + """ + + def __init__( + self, reduce_mode: str | None = None, max_sequence_length: int = 256, encoding_size: int | None = None, **kwargs + ): + super().__init__() + # save as private variable for debugging + self._reduce_mode = reduce_mode + self._max_sequence_length = max_sequence_length + self._encoding_size = encoding_size + # If embedding size specified and mode is attention/attention_pooling, use embedding size as + # attention module input size unless the input_size kwarg is provided. + if reduce_mode in ("attention", "attention_pooling") and encoding_size and "input_size" not in kwargs: + kwargs["input_size"] = encoding_size + # use registry to find required reduction function + self._reduce_obj = get_from_registry(reduce_mode, reduce_mode_registry)(**kwargs) + + def forward(self, inputs, mask=None): + """Forward pass of reducer. + + Args: + inputs: A tensor of 2 or more dimensions, where the shape is [batch size x sequence length x ...]. + mask: A mask tensor of 2 dimensions [batch size x sequence length]. Not yet implemented. + + Returns: + The input after applying the reduction operation to sequence dimension. + """ + return self._reduce_obj(inputs, mask=mask) + + @property + def input_shape(self) -> torch.Size: + """Returns size of the input tensor without the batch dimension.""" + if self._encoding_size is None: + return torch.Size([self._max_sequence_length]) + else: + return torch.Size([self._max_sequence_length, self._encoding_size]) + + @property + def output_shape(self) -> torch.Size: + """Returns size of the output tensor without the batch dimension.""" + input_shape = self.input_shape + if self._reduce_mode in {None, "none", "None"}: + return input_shape + elif self._reduce_mode == "concat": + if len(input_shape) > 1: + return input_shape[:-2] + (input_shape[-1] * input_shape[-2],) + return input_shape + else: + return input_shape[1:] # Reduce sequence dimension. + + +class ReduceLast(torch.nn.Module): + def forward(self, inputs, mask=None): + # inputs: [batch_size, seq_size, hidden_size] + batch_size = inputs.shape[0] + # gather the correct outputs from the the RNN outputs (the outputs after sequence_length are all 0s) + # todo: review for generality + sequence_length = sequence_length_3D(inputs) - 1 + sequence_length[sequence_length < 0] = 0 + gathered = inputs[torch.arange(batch_size), sequence_length.type(torch.int64)] + return gathered + + +class ReduceSum(torch.nn.Module): + def forward(self, inputs, mask=None): + return torch.sum(inputs, dim=1) + + +class ReduceMean(torch.nn.Module): + def forward(self, inputs, mask=None): + return torch.mean(inputs, dim=1) + + +class ReduceMax(torch.nn.Module): + def forward(self, inputs, mask=None): + return torch.amax(inputs, dim=1) + + +class ReduceConcat(torch.nn.Module): + def forward(self, inputs, mask=None): + if inputs.dim() > 2: + return inputs.reshape(-1, inputs.shape[-1] * inputs.shape[-2]) + return inputs + + +class ReduceNone(torch.nn.Module): + def forward(self, inputs, mask=None): + return inputs + + +reduce_mode_registry = { + "last": ReduceLast, + "sum": ReduceSum, + "mean": ReduceMean, + "avg": ReduceMean, + "max": ReduceMax, + "concat": ReduceConcat, + "attention": FeedForwardAttentionReducer, + "attention_pooling": AttentionPooling, + # TODO: Simplify this. + "none": ReduceNone, + "None": ReduceNone, + None: ReduceNone, +} diff --git a/ludwig/modules/tabnet_modules.py b/ludwig/modules/tabnet_modules.py new file mode 100644 index 0000000..8a78c17 --- /dev/null +++ b/ludwig/modules/tabnet_modules.py @@ -0,0 +1,353 @@ +import torch +import torch.nn as nn + +from ludwig.modules.normalization_modules import GhostBatchNormalization +from ludwig.utils.entmax import Entmax15, EntmaxBisect, Sparsemax +from ludwig.utils.torch_utils import LudwigModule + + +class TabNet(LudwigModule): + def __init__( + self, + input_size: int, + size: int, + output_size: int, + num_steps: int = 1, + num_total_blocks: int = 4, + num_shared_blocks: int = 2, + relaxation_factor: float = 1.5, + bn_momentum: float = 0.3, + bn_epsilon: float = 1e-3, + bn_virtual_bs: int | None = None, + sparsity: float = 1e-5, + entmax_mode: str = "sparsemax", + entmax_alpha: float = 1.5, + ): + """TabNet Will output a vector of size output_dim. + + Args: + input_size: concatenated size of input feature encoder outputs + size: Embedding feature dimension + output_size: Output dimension for TabNet + num_steps: Total number of steps. + num_total_blocks: Total number of feature transformer blocks. + num_shared_blocks: Number of shared feature transformer blocks. + relaxation_factor: >1 will allow features to be used more than once. + bn_momentum: Batch normalization, momentum. + bn_epsilon: Batch normalization, epsilon. + bn_virtual_bs: Virtual batch ize for ghost batch norm. + entmax_mode: Entmax is a sparse family of probability mapping which generalizes softmax and sparsemax. + entmax_mode controls the sparsity. One of {"sparsemax", "entmax15", "constant", "adaptive"}. + entmax_alpha: Must be a number between 1.0 and 2.0. If entmax_mode is "adaptive", entmax_alpha is used + as the initial value for the learnable parameter. + """ + super().__init__() + self.input_size = input_size + self.size = size + self.output_size = output_size + self.num_steps = num_steps + self.bn_virtual_bs = bn_virtual_bs + self.relaxation_factor = relaxation_factor + self.sparsity = torch.tensor(sparsity) + self.batch_norm = nn.BatchNorm1d(input_size, momentum=bn_momentum, eps=bn_epsilon) + + kargs = { + "num_total_blocks": num_total_blocks, + "num_shared_blocks": num_shared_blocks, + "bn_momentum": bn_momentum, + "bn_epsilon": bn_epsilon, + "bn_virtual_bs": bn_virtual_bs, + } + + # first feature transformer block is built first + # to get the shared blocks + self.feature_transforms = nn.ModuleList([FeatureTransformer(input_size, size + output_size, **kargs)]) + self.attentive_transforms = nn.ModuleList([None]) + for _i in range(num_steps): + self.feature_transforms.append( + FeatureTransformer( + input_size, + size + output_size, + **kargs, + shared_fc_layers=self.feature_transforms[0].shared_fc_layers, + ) + ) + # attentive transformers are initialized in build + # because their outputs size depends on the number + # of features that we determine by looking at the + # last dimension of the input tensor + self.attentive_transforms.append( + AttentiveTransformer( + size, input_size, bn_momentum, bn_epsilon, bn_virtual_bs, entmax_mode, entmax_alpha + ) + ) + self.final_projection = nn.Linear(output_size, output_size) + + # Register tensors to be used in forward pass. This is needed in order to move these tensors + # to the correct device (GPU/CPU) during the forward pass. + self.register_buffer("out_accumulator", torch.zeros(output_size)) + self.register_buffer("aggregated_mask", torch.zeros(input_size)) + self.register_buffer("prior_scales", torch.ones(input_size)) + + def forward(self, features: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor]]: + if features.dim() != 2: + raise ValueError(f"Expecting incoming tensor to be dim 2, instead dim={features.dim()}") + + # shape notation + # i_s: input_size + # s: size + # o_s: output_size + # b_s: batch_size + batch_size = features.shape[0] # b_s + # Tile out_accumulator, aggregated_mask, and prior_scales to add batch dimension. + out_accumulator = torch.tile(self.out_accumulator, (batch_size, 1)) + aggregated_mask = torch.tile(self.aggregated_mask, (batch_size, 1)) + prior_scales = torch.tile(self.prior_scales, (batch_size, 1)) + masks = [] + total_entropy = 0.0 + + if batch_size != 1 or not self.training: + # Skip batch normalization training if the batch size is 1. + features = self.batch_norm(features) # [b_s, i_s] + elif batch_size == 1: + # We temporarily set the batch_norm module to eval mode as we can't compute the running statistics + # when the batch size is 1. + self.batch_norm.eval() + features = self.batch_norm(features) # [b_s, i_s] + self.batch_norm.train() + masked_features = features + + x = self.feature_transforms[0](masked_features) # [b_s, s + o_s] + + for step_i in range(1, self.num_steps + 1): + ######################### + # Attentive Transformer # + ######################### + # x in following is shape [b_s, s] + mask_values = self.attentive_transforms[step_i](x[:, self.output_size :], prior_scales) # [b_s, i_s] + + # relaxation factor 1 forces the feature to be only used once + prior_scales = prior_scales * (self.relaxation_factor - mask_values) # [b_s, i_s] + + # entropy is used to penalize the amount of sparsity + # in feature selection + if self.sparsity.item() != 0.0: + total_entropy += ( + torch.mean(torch.sum(-mask_values * torch.log(mask_values + 0.00001), dim=1)) / self.num_steps + ) + + masks.append(torch.unsqueeze(torch.unsqueeze(mask_values, 0), 3)) # [1, b_s, i_s, 1] + + ####################### + # Feature Transformer # + ####################### + masked_features = torch.multiply(mask_values, features) + + x = self.feature_transforms[step_i](masked_features) # [b_s, s + o_s] + + # x in following is shape [b_s, o_s] + out = nn.functional.relu(x[:, : self.output_size]) # [b_s, o_s] + out_accumulator += out + + # Aggregated masks are used for visualization of the + # feature importance attributes. + scale = torch.sum(out, dim=1, keepdim=True) / self.num_steps + aggregated_mask += mask_values * scale # [b_s, i_s] + + final_output = self.final_projection(out_accumulator) # [b_s, o_s] + + sparsity_loss = torch.multiply(self.sparsity, total_entropy) + self.update_loss("sparsity_loss", sparsity_loss) + + return final_output, aggregated_mask, masks + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.output_size]) + + +class FeatureBlock(LudwigModule): + def __init__( + self, + input_size: int, + size: int, + apply_glu: bool = True, + bn_momentum: float = 0.1, + bn_epsilon: float = 1e-3, + bn_virtual_bs: int | None = None, + shared_fc_layer: LudwigModule = None, + ): + super().__init__() + self.input_size = input_size + self.apply_glu = apply_glu + self.size = size + units = size * 2 if apply_glu else size + + # Initialize fc_layer before assigning to shared layer for torchscript compatibilty + self.fc_layer = nn.Linear(input_size, units, bias=False) + if shared_fc_layer is not None: + if shared_fc_layer.weight.shape != self.fc_layer.weight.shape: + raise RuntimeError( + f"shared_fc_layer weight shape {tuple(shared_fc_layer.weight.shape)} doesn't match " + f"expected shape {tuple(self.fc_layer.weight.shape)} for input_size={input_size}, size={size}." + ) + self.fc_layer = shared_fc_layer + + self.batch_norm = GhostBatchNormalization( + units, virtual_batch_size=bn_virtual_bs, momentum=bn_momentum, epsilon=bn_epsilon + ) + + def forward(self, inputs): + # shape notation + # i_s: input_size + # s: size + # u: units + # b_s: batch_size + + # inputs shape [b_s, i_s] + hidden = self.fc_layer(inputs) # [b_s, u] + hidden = self.batch_norm(hidden) # [b_s, u] + if self.apply_glu: + hidden = nn.functional.glu(hidden, dim=-1) # [bs, s] + return hidden # [b_s, 2*s] if apply_glu else [b_s, s] + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + +class AttentiveTransformer(LudwigModule): + def __init__( + self, + input_size: int, + size: int, + bn_momentum: float = 0.1, + bn_epsilon: float = 1e-3, + bn_virtual_bs: int | None = None, + entmax_mode: str = "sparsemax", + entmax_alpha: float = 1.5, + ): + super().__init__() + self.input_size = input_size + self.size = size + self.entmax_mode = entmax_mode + if entmax_mode == "adaptive": + self.register_buffer("trainable_alpha", torch.tensor(entmax_alpha, requires_grad=True)) + else: + self.trainable_alpha = entmax_alpha + + if self.entmax_mode == "sparsemax": + self.entmax_module = Sparsemax() + elif self.entmax_mode == "entmax15": + self.entmax_module = Entmax15() + else: + self.entmax_module = EntmaxBisect(alpha=self.trainable_alpha) + + self.feature_block = FeatureBlock( + input_size, + size, + bn_momentum=bn_momentum, + bn_epsilon=bn_epsilon, + bn_virtual_bs=bn_virtual_bs, + apply_glu=False, + ) + + def forward(self, inputs, prior_scales): + # shape notation + # i_s: input_size + # s: size + # b_s: batch_size + + # inputs shape [b_s, i_s], prior_scales shape [b_s, s] + hidden = self.feature_block(inputs) # [b_s, s] + hidden = hidden * prior_scales # [b_s, s] + + # removing the mean to try to avoid numerical instability + # https://github.com/tensorflow/addons/issues/2314 + # https://github.com/tensorflow/tensorflow/pull/21183/files + # In (Arik and Pfister, 2019), they call the logits z. + # The mean(logits) can be substracted from logits to make the algorithm + # more numerically stable. the instability in this algorithm comes mostly + # from the z_cumsum. Substacting the mean will cause z_cumsum to be close + # to zero. + # hidden = hidden - tf.math.reduce_mean(hidden, axis=1)[:, tf.newaxis] + return self.entmax_module(hidden) + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.size]) + + +# adapted and modified from: +# https://github.com/ostamand/tensorflow-tabnet/blob/master/tabnet/models/transformers.py +class FeatureTransformer(LudwigModule): + def __init__( + self, + input_size: int, + size: int, + shared_fc_layers: list | None = None, + num_total_blocks: int = 4, + num_shared_blocks: int = 2, + bn_momentum: float = 0.1, + bn_epsilon: float = 1e-3, + bn_virtual_bs: int | None = None, + ): + super().__init__() + if shared_fc_layers is None: + shared_fc_layers = [] + self.input_size = input_size + self.num_total_blocks = num_total_blocks + self.num_shared_blocks = num_shared_blocks + self.size = size + + kwargs = { + "bn_momentum": bn_momentum, + "bn_epsilon": bn_epsilon, + "bn_virtual_bs": bn_virtual_bs, + } + + # build blocks + self.blocks = nn.ModuleList() + for n in range(num_total_blocks): + # Ensure the sizes fed into FeatureBlock are correct regardless of presence of shared_fc_layer + if n == 0: + in_features = input_size + else: + in_features = size + + if shared_fc_layers and n < len(shared_fc_layers): + self.blocks.append(FeatureBlock(in_features, size, **kwargs, shared_fc_layer=shared_fc_layers[n])) + else: + self.blocks.append(FeatureBlock(in_features, size, **kwargs)) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + # shape notation + # i_s: input_size + # s: size + # b_s: batch_size + + # inputs shape [b_s, i_s] + hidden = self.blocks[0](inputs) # [b_s, s] + for n in range(1, self.num_total_blocks): + hidden = (self.blocks[n](hidden) + hidden) * (0.5**0.5) # [b_s, s] + return hidden # [b_s, s] + + @property + def shared_fc_layers(self): + return [self.blocks[i].fc_layer for i in range(self.num_shared_blocks)] + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return torch.Size([self.size]) diff --git a/ludwig/modules/training_hooks.py b/ludwig/modules/training_hooks.py new file mode 100644 index 0000000..509a3e0 --- /dev/null +++ b/ludwig/modules/training_hooks.py @@ -0,0 +1,102 @@ +import logging +from abc import ABC, abstractmethod + +import torch + +logger = logging.getLogger(__name__) + + +class TrainingHook(ABC): + """A base class for training hooks in PyTorch. + + This class provides a template for implementing custom training hooks + that can be activated, deactivated, and maintain a handle to the hook. + + Attributes: + _hook_handle (Optional[torch.utils.hooks.RemovableHandle]): A handle to the + registered forward hook, initially set to None. + """ + + def __init__(self, *args, **kwargs) -> None: + self._hook_handle = None + + @abstractmethod + def hook_fn(self, module: torch.nn.Module, inputs: torch.tensor, outputs: torch.Tensor) -> torch.Tensor: + """Abstract method to be implemented by subclasses. This is the method that defines the custom behavior of + the training hook during a forward pass for the specified module. + + Args: + module (nn.Module): The PyTorch module for which the hook is activated. + inputs (torch.Tensor): The input to the module during the forward pass. + outputs (torch.Tensor): The output from the module during the forward pass. + + Returns: + torch.Tensor: The output tensor from the module. + + Raises: + NotImplementedError: If the method is not implemented in a subclass. + """ + + def activate_hook(self, module: torch.nn.Module) -> torch.nn.Module: + """Activates the training hook for a given module. + + Args: + module (nn.Module): The PyTorch module for which the hook is activated. + + Returns: + nn.Module: The input module with the training hook activated. + """ + self._hook_handle = module.register_forward_hook(self.hook_fn) + return module + + def deactivate_hook(self): + """Deactivates and removes the training hook.""" + if self._hook_handle is not None: + self._hook_handle.remove() + self._hook_handle = None + + +class NEFTuneHook(TrainingHook): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.neftune_noise_alpha = kwargs.get("neftune_noise_alpha") + + def hook_fn(self, module: torch.nn.Module, input: torch.Tensor, output: torch.Tensor) -> torch.Tensor: + """Implements the NEFTune forward pass for the model using forward hooks. Note this works only for + torch.nn. Embedding layers. This method is slightly adapted from the original source code that can be found + here: https://github.com/neelsjain/NEFTune. + + The input tensor is ignored since the noise is added to the output of the embedding layer. + + Returns: + torch.Tensor: The output tensor from the module. + """ + if module.training: + dims = torch.tensor(output.size(1) * output.size(2)) + mag_norm = module.neftune_noise_alpha / torch.sqrt(dims) + output = output + torch.zeros_like(output).uniform_(-mag_norm, mag_norm) + return output + + def activate_hook(self, module: torch.nn.Module) -> torch.nn.Module: + """Activates the neftune as presented in this code and paper: + + Code: https://github.com/neelsjain/NEFTune + Paper: https://arxiv.org/abs/2310.05914 + + Args: + module (nn.Module): The PyTorch module for which the hook is activated. + + Returns: + nn.Module: The input module with the training hook activated. + """ + from peft import PeftModel + + if isinstance(module, PeftModel): + embeddings = module.base_model.model.get_input_embeddings() + else: + embeddings = module.get_input_embeddings() + + embeddings.neftune_noise_alpha = self.neftune_noise_alpha + self._hook_handle = embeddings.register_forward_hook(self.hook_fn) + + return module diff --git a/ludwig/predict.py b/ludwig/predict.py new file mode 100644 index 0000000..cef32bd --- /dev/null +++ b/ludwig/predict.py @@ -0,0 +1,228 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import sys +from ast import literal_eval + +import pandas as pd + +from ludwig.api import LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.constants import FULL, TEST, TRAINING, VALIDATION +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + + +def predict_cli( + model_path: str, + dataset: str | dict | pd.DataFrame = None, + data_format: str | None = None, + split: str = FULL, + batch_size: int = 128, + generation_config: str | None = None, + skip_save_unprocessed_output: bool = False, + skip_save_predictions: bool = False, + output_directory: str = "results", + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + logging_level: int = logging.INFO, + **kwargs, +) -> None: + """Load a pre-trained model and generate predictions on the provided dataset. + + Args: + model_path: Filepath to the pre-trained model directory. + dataset: Source containing the dataset to predict on. + data_format: Format to interpret data sources. Inferred automatically + if not specified. Valid values: ``'auto'``, ``'csv'``, + ``'excel'``, ``'feather'``, ``'fwf'``, ``'hdf5'``, + ``'html'``, ``'json'``, ``'jsonl'``, ``'parquet'``, + ``'pickle'``, ``'sas'``, ``'spss'``, ``'stata'``, ``'tsv'``. + split: Split to perform predictions on. Valid values: + ``'training'``, ``'validation'``, ``'test'``, ``'full'``. + batch_size: Number of samples per prediction batch. + generation_config: JSON-formatted string of generation parameters + for LLM predictions (merged with the model's generation config). + skip_save_unprocessed_output: If ``True``, skip saving raw numpy + output files; only postprocessed CSV files are saved. + skip_save_predictions: If ``True``, skip saving prediction CSV files. + output_directory: Directory that will contain prediction results. + gpus: List of GPUs available for inference. + gpu_memory_limit: Maximum memory fraction ``[0, 1]`` allowed to + allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading + parallelism (improves performance at the cost of determinism). + callbacks: List of ``Callback`` objects providing hooks into the + Ludwig pipeline. + backend: Backend or string name of the backend to use. + logging_level: Log level sent to stderr. + """ + model = LudwigModel.load( + model_path, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + model.predict( + dataset=dataset, + data_format=data_format, + split=split, + batch_size=batch_size, + generation_config=literal_eval(generation_config) if generation_config else None, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + output_directory=output_directory, + return_type="dict", + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script loads a pretrained model and uses it to predict", + prog="ludwig predict", + usage="%(prog)s [options]", + ) + + # --------------- + # Data parameters + # --------------- + parser.add_argument("--dataset", help="input data file path", required=True) + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "html", + "tables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + parser.add_argument( + "-s", "--split", default=FULL, choices=[TRAINING, VALIDATION, TEST, FULL], help="the split to test the model on" + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=True) + parser.add_argument("-gc", "--generation_config", help="generation config (LLMs only)", default=None) + + # ------------------------- + # Output results parameters + # ------------------------- + parser.add_argument( + "-od", "--output_directory", type=str, default="results", help="directory that contains the results" + ) + parser.add_argument( + "-ssuo", + "--skip_save_unprocessed_output", + help="skips saving intermediate NPY output files", + action="store_true", + default=False, + ) + parser.add_argument( + "-sstp", + "--skip_save_predictions", + help="skips saving predictions CSV files", + action="store_true", + default=False, + ) + + # ------------------ + # Generic parameters + # ------------------ + parser.add_argument("-bs", "--batch_size", type=int, default=128, help="size of batches") + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument("-g", "--gpus", type=int, default=0, help="list of gpu to use") + parser.add_argument( + "-gml", + "--gpu_memory_limit", + type=float, + default=None, + help="maximum memory fraction [0, 1] allowed to allocate per GPU device", + ) + parser.add_argument( + "-dpt", + "--disable_parallel_threads", + action="store_false", + dest="allow_parallel_threads", + help="disable PyTorch from using multithreading for reproducibility", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("predict", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.predict") + + args.backend = initialize_backend(args.backend) + if args.backend.is_coordinator(): + print_ludwig("Predict", LUDWIG_VERSION) + logger.info(f"Dataset path: {args.dataset}") + logger.info(f"Model path: {args.model_path}") + logger.info("") + + predict_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/preprocess.py b/ludwig/preprocess.py new file mode 100644 index 0000000..4ea2af8 --- /dev/null +++ b/ludwig/preprocess.py @@ -0,0 +1,212 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import sys + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.data_utils import load_yaml +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + + +def preprocess_cli( + preprocessing_config: str | dict | None = None, + dataset: str | dict | pd.DataFrame = None, + training_set: str | dict | pd.DataFrame = None, + validation_set: str | dict | pd.DataFrame = None, + test_set: str | dict | pd.DataFrame = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + random_seed: int = default_random_seed, + logging_level: int = logging.INFO, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + **kwargs, +) -> None: + """Preprocess a dataset and cache the result to disk. + + Args: + preprocessing_config: In-memory config dict or path to a YAML config + file. Only preprocessing settings are used; encoder/decoder/ + combiner/training parameters are ignored. + dataset: Source containing the entire dataset. If it has a split + column, it will be used for splitting (0: train, 1: validation, + 2: test); otherwise the dataset will be randomly split. + training_set: Source containing training data. + validation_set: Source containing validation data. + test_set: Source containing test data. + training_set_metadata: Metadata JSON file or loaded metadata dict. + data_format: Format to interpret data sources. Inferred automatically + if not specified. Valid values: ``'auto'``, ``'csv'``, + ``'excel'``, ``'feather'``, ``'fwf'``, ``'hdf5'``, + ``'html'``, ``'json'``, ``'jsonl'``, ``'parquet'``, + ``'pickle'``, ``'sas'``, ``'spss'``, ``'stata'``, ``'tsv'``. + random_seed: Random seed for splits and any other random function. + logging_level: Log level sent to stderr. + callbacks: List of ``Callback`` objects providing hooks into the + Ludwig pipeline. + backend: Backend or string name of the backend to use. + """ + model = LudwigModel( + config=preprocessing_config, + logging_level=logging_level, + callbacks=callbacks, + backend=backend, + ) + model.preprocess( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + skip_save_processed_input=False, + random_seed=random_seed, + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script preprocess a dataset", prog="ludwig preprocess", usage="%(prog)s [options]" + ) + + # --------------- + # Data parameters + # --------------- + parser.add_argument( + "--dataset", + help="input data file path. " + "If it has a split column, it will be used for splitting " + "(0: train, 1: validation, 2: test), " + "otherwise the dataset will be randomly split", + ) + parser.add_argument("--training_set", help="input train data file path") + parser.add_argument("--validation_set", help="input validation data file path") + parser.add_argument("--test_set", help="input test data file path") + + parser.add_argument( + "--training_set_metadata", + help="input metadata JSON file path. An intermediate preprocessed file " + "containing the mappings of the input file created " + "the first time a file is used, in the same directory " + "with the same name and a .json extension", + ) + + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "htmltables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + + # ---------------- + # Model parameters + # ---------------- + preprocessing_def = parser.add_mutually_exclusive_group(required=True) + preprocessing_def.add_argument( + "-pc", + "--preprocessing_config", + dest="preprocessing_config", + type=load_yaml, + help="YAML file describing the preprocessing. " + "Ignores --preprocessing_config." + "Uses the same format of config, " + "but ignores encoder specific parameters, " + "decoder specific parameters, combiner and training parameters", + ) + preprocessing_def.add_argument( + "-pcs", + "--preprocessing_config_str", + type=yaml.safe_load, + help="preproceesing config. " + "Uses the same format of config, " + "but ignores encoder specific parameters, " + "decoder specific parameters, combiner and training parameters", + ) + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument( + "-rs", + "--random_seed", + type=int, + default=42, + help="a random seed that is going to be used anywhere there is a call " + "to a random number generator: data splitting, parameter " + "initialization and training set shuffling", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("preprocess", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.preprocess") + + args.backend = initialize_backend(args.backend) + if args.backend.is_coordinator(): + print_ludwig("Preprocess", LUDWIG_VERSION) + + preprocess_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/presets.py b/ludwig/presets.py new file mode 100644 index 0000000..e4c06bc --- /dev/null +++ b/ludwig/presets.py @@ -0,0 +1,98 @@ +"""Quality presets for Ludwig ECD models. + +Inspired by AutoGluon's quality presets, these provide sensible defaults for different quality/speed tradeoffs. User- +specified config values always take precedence over preset defaults. +""" + +import copy +import logging + +logger = logging.getLogger(__name__) + + +QUALITY_PRESETS = { + "medium_quality": { + "combiner": {"type": "concat", "num_fc_layers": 2, "output_size": 128}, + "trainer": { + "epochs": 50, + "early_stop": 5, + "batch_size": 256, + }, + }, + "high_quality": { + "combiner": {"type": "transformer", "num_layers": 2, "hidden_size": 256, "num_heads": 8}, + "trainer": { + "epochs": 100, + "early_stop": 10, + "batch_size": 128, + "loss_balancing": "uncertainty", + }, + }, + "best_quality": { + "combiner": {"type": "ft_transformer", "num_layers": 4, "hidden_size": 256, "num_heads": 8}, + "trainer": { + "epochs": 200, + "early_stop": 20, + "batch_size": 64, + "loss_balancing": "uncertainty", + "model_soup": "uniform", + "model_soup_top_k": 5, + }, + }, + # RealMLP defaults (Holzmüller et al., 2024). Strong "boring baseline" for tabular DL: + # robust (interquartile) scaling on number features, mild-but-not-trivial FC stack, AdamW + # with decoupled weight decay, cosine LR decay, long training horizon with early stopping. + # Source: https://arxiv.org/abs/2407.04491 + "tabular_realmlp": { + "combiner": {"type": "concat", "num_fc_layers": 4, "output_size": 256, "dropout": 0.15}, + "defaults": { + "number": { + # 'iq' is the Ludwig interquartile-range normalizer — the closest available + # match to scikit-learn's RobustScaler used by the RealMLP paper. + "preprocessing": {"normalization": "iq"}, + }, + }, + "trainer": { + "epochs": 300, + "early_stop": 40, + "batch_size": 256, + "learning_rate": 0.0005, + "optimizer": {"type": "adamw", "weight_decay": 0.01}, + "learning_rate_scheduler": {"decay": "cosine"}, + }, + }, +} + + +def _deep_merge(base: dict, override: dict) -> dict: + """Deep merge override into base. + + Override values take precedence. + """ + result = copy.deepcopy(base) + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = _deep_merge(result[key], value) + else: + result[key] = copy.deepcopy(value) + return result + + +def apply_preset(config_dict: dict, preset_name: str) -> dict: + """Apply a quality preset to a config dict. User config takes precedence over preset defaults. + + Args: + config_dict: The user's config dict. + preset_name: Name of the preset to apply. + + Returns: + Config dict with preset defaults applied (user overrides win). + """ + if preset_name not in QUALITY_PRESETS: + raise ValueError(f"Unknown preset '{preset_name}'. Available: {list(QUALITY_PRESETS.keys())}") + + preset = QUALITY_PRESETS[preset_name] + # Merge: preset is the base, user config overrides + result = _deep_merge(preset, config_dict) + logger.info(f"Applied quality preset '{preset_name}'") + return result diff --git a/ludwig/progress_bar.py b/ludwig/progress_bar.py new file mode 100644 index 0000000..01ad933 --- /dev/null +++ b/ludwig/progress_bar.py @@ -0,0 +1,49 @@ +import tqdm + +try: + import ray.train as rt +except ImportError: + rt = None + + +class LudwigProgressBar: + """Progress bar that works both locally and inside Ray Train workers. + + When ``report_to_ray=True`` the bar is silently suppressed so that Ray + worker subprocesses do not spam the driver log with tqdm escape codes, and + — critically — so that ``rt.report()`` is *not* called on every training + step. Calling ``rt.report()`` every batch costs ~1.9 s per call (it + requires a round-trip through the Ray GCS) and completely dominates + wall-clock training time at ~2 s/batch overhead vs ~0.3 s of actual GPU + compute. Training metrics are already reported at eval/checkpoint time via + the proper ``rt.report(checkpoint=...)`` call in the backend; per-batch + progress updates via Ray Train are unnecessary. + """ + + def __init__( + self, + report_to_ray: bool, + config: dict, + is_coordinator: bool, + ) -> None: + self.report_to_ray = report_to_ray + self.is_coordinator = is_coordinator + self.config = config + self.total_steps = 0 + self.progress_bar = None + + if not report_to_ray and is_coordinator: + self.progress_bar = tqdm.tqdm(**config) + + def set_postfix(self, ordered_dict: dict | None = None, **kwargs) -> None: + if self.progress_bar: + self.progress_bar.set_postfix(ordered_dict, **kwargs) + + def update(self, steps: int) -> None: + self.total_steps += steps + if self.progress_bar: + self.progress_bar.update(steps) + + def close(self) -> None: + if self.progress_bar: + self.progress_bar.close() diff --git a/ludwig/py.typed b/ludwig/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/schema/__init__.py b/ludwig/schema/__init__.py new file mode 100644 index 0000000..77ecd60 --- /dev/null +++ b/ludwig/schema/__init__.py @@ -0,0 +1,5 @@ +# TODO(travis): figure out why we need these imports to avoid circular import error +from ludwig.schema.combiners.utils import get_combiner_jsonschema # noqa +from ludwig.schema.features.utils import get_input_feature_jsonschema, get_output_feature_jsonschema # noqa +from ludwig.schema.hyperopt import get_hyperopt_jsonschema # noqa +from ludwig.schema.trainer import get_model_type_jsonschema, get_trainer_jsonschema # noqa diff --git a/ludwig/schema/combiners/__init__.py b/ludwig/schema/combiners/__init__.py new file mode 100644 index 0000000..bcd0ba3 --- /dev/null +++ b/ludwig/schema/combiners/__init__.py @@ -0,0 +1,14 @@ +import ludwig.schema.combiners.comparator +import ludwig.schema.combiners.concat +import ludwig.schema.combiners.cross_attention +import ludwig.schema.combiners.ft_transformer +import ludwig.schema.combiners.gated_fusion +import ludwig.schema.combiners.hypernetwork +import ludwig.schema.combiners.perceiver +import ludwig.schema.combiners.project_aggregate +import ludwig.schema.combiners.sequence +import ludwig.schema.combiners.sequence_concat +import ludwig.schema.combiners.tab_transformer +import ludwig.schema.combiners.tabnet +import ludwig.schema.combiners.tabpfn_v2 +import ludwig.schema.combiners.transformer # noqa: F401 diff --git a/ludwig/schema/combiners/base.py b/ludwig/schema/combiners/base.py new file mode 100644 index 0000000..4dc9472 --- /dev/null +++ b/ludwig/schema/combiners/base.py @@ -0,0 +1,9 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils + + +@DeveloperAPI +class BaseCombinerConfig(schema_utils.LudwigBaseConfig): + """Base combiner config class.""" + + type: str diff --git a/ludwig/schema/combiners/common_transformer_options.py b/ludwig/schema/combiners/common_transformer_options.py new file mode 100644 index 0000000..55cae3e --- /dev/null +++ b/ludwig/schema/combiners/common_transformer_options.py @@ -0,0 +1,83 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +class CommonTransformerConfig: + """Common transformer parameter values.""" + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="Dropout rate for the transformer block.", + parameter_metadata=COMBINER_METADATA["transformer"]["dropout"], + ) + + transformer_output_size: int = schema_utils.NonNegativeInteger( + default=256, + description="Size of the fully connected layer after self attention in the transformer block. This is usually " + "the same as `hidden_size` and `embedding_size`.", + parameter_metadata=COMBINER_METADATA["transformer"]["transformer_output_size"], + ) + + hidden_size: int = schema_utils.NonNegativeInteger( + default=256, + description="The number of hidden units of the TransformerStack as well as the dimension that each incoming " + "input feature is projected to before feeding to the TransformerStack.", + parameter_metadata=COMBINER_METADATA["transformer"]["hidden_size"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of transformer layers.", + parameter_metadata=COMBINER_METADATA["transformer"]["num_layers"], + ) + + num_heads: int = schema_utils.NonNegativeInteger( + default=8, + description="Number of heads of the self attention in the transformer block.", + parameter_metadata=COMBINER_METADATA["transformer"]["num_heads"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=COMBINER_METADATA["transformer"]["use_bias"], + ) + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + # TODO(#1673): Add conditional logic for fields like this one: + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=0, + description="The number of stacked fully connected layers (only applies if `reduce_output` is not null).", + parameter_metadata=COMBINER_METADATA["transformer"]["num_fc_layers"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of a fully connected layer.", + parameter_metadata=COMBINER_METADATA["transformer"]["output_size"], + ) + + norm: str | None = common_fields.NormField() + + norm_params: dict | None = common_fields.NormParamsField() + + fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField() + + fc_dropout: float = common_fields.DropoutField() + + fc_activation: str = schema_utils.ActivationOptions( + default="relu", + parameter_metadata=COMBINER_METADATA["transformer"]["fc_activation"], + ) + + fc_residual: bool = common_fields.ResidualField() diff --git a/ludwig/schema/combiners/comparator.py b/ludwig/schema/combiners/comparator.py new file mode 100644 index 0000000..adb203c --- /dev/null +++ b/ludwig/schema/combiners/comparator.py @@ -0,0 +1,83 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +@register_combiner_config("comparator") +class ComparatorCombinerConfig(BaseCombinerConfig): + """Parameters for comparator combiner.""" + + def __post_init__(self): + if self.num_fc_layers == 0 and self.fc_layers is None: + raise ConfigValidationError( + "`combiner.type=comparator` requires at least one fully connected layer. " + "Set `num_fc_layers > 0` or `fc_layers`." + ) + + if not self.entity_1: + raise ConfigValidationError( + "`combiner.entity_1` is required and must contain as least one input feature name." + ) + + if not self.entity_2: + raise ConfigValidationError( + "`combiner.entity_2` is required and must contain as least one input feature name." + ) + + type: str = schema_utils.ProtectedString( + "comparator", + description=COMBINER_METADATA["comparator"]["type"].long_description, + ) + + entity_1: list[str] = schema_utils.List( + default=None, + description=( + "The list of input feature names `[feature_1, feature_2, ...]` constituting the first entity to compare. " + "*Required*." + ), + parameter_metadata=COMBINER_METADATA["comparator"]["entity_1"], + ) + + entity_2: list[str] = schema_utils.List( + default=None, + description=( + "The list of input feature names `[feature_1, feature_2, ...]` constituting the second entity to compare. " + "*Required*." + ), + parameter_metadata=COMBINER_METADATA["comparator"]["entity_2"], + ) + + dropout: float = common_fields.DropoutField() + + activation: str = schema_utils.ActivationOptions(default="relu") + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=COMBINER_METADATA["comparator"]["use_bias"], + ) + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + num_fc_layers: int = common_fields.NumFCLayersField(default=1) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of a fully connected layer.", + parameter_metadata=COMBINER_METADATA["comparator"]["output_size"], + ) + + norm: str | None = common_fields.NormField() + + norm_params: dict | None = common_fields.NormParamsField() + + fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField() diff --git a/ludwig/schema/combiners/concat.py b/ludwig/schema/combiners/concat.py new file mode 100644 index 0000000..4ac27c4 --- /dev/null +++ b/ludwig/schema/combiners/concat.py @@ -0,0 +1,69 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +@register_combiner_config("concat") +class ConcatCombinerConfig(BaseCombinerConfig): + """Parameters for concat combiner.""" + + type: str = schema_utils.ProtectedString( + "concat", + description=COMBINER_METADATA["concat"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField() + + activation: str = schema_utils.ActivationOptions(default="relu") + + flatten_inputs: bool = schema_utils.Boolean( + default=False, + description="Whether to flatten input tensors to a vector.", + parameter_metadata=COMBINER_METADATA["concat"]["flatten_inputs"], + ) + + residual: bool = common_fields.ResidualField() + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=COMBINER_METADATA["concat"]["use_bias"], + ) + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + num_fc_layers: int = common_fields.NumFCLayersField() + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of a fully connected layer.", + parameter_metadata=COMBINER_METADATA["concat"]["output_size"], + ) + + norm: str | None = common_fields.NormField() + + norm_params: dict | None = common_fields.NormParamsField() + + fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField() + + batch_ensemble: bool = schema_utils.Boolean( + default=False, + description=( + "Whether to use BatchEnsemble (TabM-style) for parameter-efficient ensembling. " + "Adds per-member rank-1 scaling vectors to the output layer, providing " + "ensemble-level performance at single-model cost." + ), + ) + + num_ensemble_members: int = schema_utils.PositiveInteger( + default=4, + description="Number of ensemble members when batch_ensemble is enabled.", + ) diff --git a/ludwig/schema/combiners/cross_attention.py b/ludwig/schema/combiners/cross_attention.py new file mode 100644 index 0000000..8ec32cc --- /dev/null +++ b/ludwig/schema/combiners/cross_attention.py @@ -0,0 +1,65 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config + + +@DeveloperAPI +@register_combiner_config("cross_attention") +class CrossAttentionCombinerConfig(BaseCombinerConfig): + """Parameters for cross-attention combiner.""" + + type: str = schema_utils.ProtectedString( + "cross_attention", + description="Combines features using cross-attention, where each feature attends to all other features " + "through a multi-head cross-attention mechanism.", + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=256, + description="Hidden size of the cross-attention layers. Each input feature is projected to this size.", + ) + + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of attention heads in the cross-attention layers.", + ) + + num_layers: int = schema_utils.PositiveInteger( + default=1, + description="Number of stacked cross-attention layers.", + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="Dropout rate for the cross-attention layers.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of the fully connected layer after cross-attention.", + ) + + num_fc_layers: int = common_fields.NumFCLayersField() + + fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField() + + activation: str = schema_utils.ActivationOptions(default="relu") + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + ) + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + norm: str | None = common_fields.NormField() + + norm_params: dict | None = common_fields.NormParamsField() diff --git a/ludwig/schema/combiners/ft_transformer.py b/ludwig/schema/combiners/ft_transformer.py new file mode 100644 index 0000000..e3b966d --- /dev/null +++ b/ludwig/schema/combiners/ft_transformer.py @@ -0,0 +1,19 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.common_transformer_options import CommonTransformerConfig +from ludwig.schema.combiners.utils import register_combiner_config + + +@DeveloperAPI +@register_combiner_config("ft_transformer") +class FTTransformerCombinerConfig(BaseCombinerConfig, CommonTransformerConfig): + """FT-Transformer combiner: project each feature to a token, prepend [CLS], apply Transformer self-attention. + + Output is the [CLS] token embedding, optionally followed by FC layers. Based on Gorishniy et al., NeurIPS 2021. + """ + + type: str = schema_utils.ProtectedString( + "ft_transformer", + description="FT-Transformer combiner with [CLS] token aggregation.", + ) diff --git a/ludwig/schema/combiners/gated_fusion.py b/ludwig/schema/combiners/gated_fusion.py new file mode 100644 index 0000000..39f94a1 --- /dev/null +++ b/ludwig/schema/combiners/gated_fusion.py @@ -0,0 +1,55 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config + + +@DeveloperAPI +@register_combiner_config("gated_fusion") +class GatedFusionCombinerConfig(BaseCombinerConfig): + """Parameters for gated fusion combiner.""" + + type: str = schema_utils.ProtectedString( + "gated_fusion", + description="Combines features using a gating mechanism that learns to weight each input feature's " + "contribution through sigmoid gates.", + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=256, + description="Hidden size of the gating layers. Each input feature is projected to this size.", + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="Dropout rate for the gating layers.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of the fully connected layer after gated fusion.", + ) + + num_fc_layers: int = common_fields.NumFCLayersField() + + fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField() + + activation: str = schema_utils.ActivationOptions(default="relu") + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + ) + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + norm: str | None = common_fields.NormField() + + norm_params: dict | None = common_fields.NormParamsField() diff --git a/ludwig/schema/combiners/hypernetwork.py b/ludwig/schema/combiners/hypernetwork.py new file mode 100644 index 0000000..8428010 --- /dev/null +++ b/ludwig/schema/combiners/hypernetwork.py @@ -0,0 +1,33 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config + + +@DeveloperAPI +@register_combiner_config("hypernetwork") +class HyperNetworkCombinerConfig(BaseCombinerConfig): + """HyperNetwork combiner: one modality generates weights for processing another. + + Based on HyperFusion (arXiv 2403.13319, 2024). + """ + + type: str = schema_utils.ProtectedString( + "hypernetwork", + description="HyperNetwork combiner where one feature generates processing weights for others.", + ) + + hidden_size: int = schema_utils.PositiveInteger(default=128, description="Hidden size for feature projections.") + + hyper_hidden_size: int = schema_utils.PositiveInteger( + default=64, description="Hidden size inside the hypernetwork weight generator." + ) + + output_size: int = schema_utils.PositiveInteger(default=128, description="Output size of the FC stack.") + + num_fc_layers: int = common_fields.NumFCLayersField() + + dropout: float = schema_utils.FloatRange(default=0.1, min=0, max=1, description="Dropout rate.") + + activation: str = schema_utils.ActivationOptions(default="relu") diff --git a/ludwig/schema/combiners/perceiver.py b/ludwig/schema/combiners/perceiver.py new file mode 100644 index 0000000..7f405ae --- /dev/null +++ b/ludwig/schema/combiners/perceiver.py @@ -0,0 +1,75 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config + + +@DeveloperAPI +@register_combiner_config("perceiver") +class PerceiverCombinerConfig(BaseCombinerConfig): + """Parameters for Perceiver combiner.""" + + type: str = schema_utils.ProtectedString( + "perceiver", + description="Combines features using a Perceiver-style architecture with learned latent queries that " + "cross-attend to the input features, followed by self-attention layers.", + ) + + num_latents: int = schema_utils.PositiveInteger( + default=32, + description="Number of learned latent query vectors.", + ) + + latent_dim: int = schema_utils.PositiveInteger( + default=256, + description="Dimensionality of each latent query vector.", + ) + + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of attention heads in the cross-attention and self-attention layers.", + ) + + num_self_attention_layers: int = schema_utils.PositiveInteger( + default=2, + description="Number of self-attention layers applied to the latent queries after cross-attention.", + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="Dropout rate for the attention layers.", + ) + + reduce_output: str | None = schema_utils.ReductionOptions( + default="mean", + description="Strategy to use to aggregate the latent vectors before the FC stack.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of the fully connected layer after the Perceiver block.", + ) + + num_fc_layers: int = common_fields.NumFCLayersField() + + fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField() + + activation: str = schema_utils.ActivationOptions(default="relu") + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + ) + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + norm: str | None = common_fields.NormField() + + norm_params: dict | None = common_fields.NormParamsField() diff --git a/ludwig/schema/combiners/project_aggregate.py b/ludwig/schema/combiners/project_aggregate.py new file mode 100644 index 0000000..1d527c2 --- /dev/null +++ b/ludwig/schema/combiners/project_aggregate.py @@ -0,0 +1,90 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +@register_combiner_config("project_aggregate") +class ProjectAggregateCombinerConfig(BaseCombinerConfig): + type: str = schema_utils.ProtectedString( + "project_aggregate", + description=COMBINER_METADATA["project_aggregate"]["type"].long_description, + ) + + projection_size: int = schema_utils.PositiveInteger( + default=128, + description="All combiner inputs are projected to this size before being aggregated.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["projection_size"], + ) + + residual: bool = schema_utils.Boolean( + default=True, + description="Whether to add residual skip connection between the fully connected layers in the stack.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["residual"], + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout rate to apply to each fully connected layer.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + default="relu", + description="Activation to apply to each fully connected layer.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["activation"], + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=2, + description="Number of fully connected layers after aggregation.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["num_fc_layers"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=128, + description="Output size of each layer of the stack of fully connected layers.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["output_size"], + ) + + norm: str | None = schema_utils.StringOptions( + ["batch", "layer"], + default="layer", + description="Normalization to apply to each projection and fully connected layer.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["norm"], + ) + + norm_params: dict | None = schema_utils.Dict( + description="Parameters of the normalization to apply to each projection and fully connected layer.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["norm_params"], + ) + + fc_layers: list[dict[str, Any]] | None = schema_utils.DictList( + description="Full specification of the fully connected layers after the aggregation. It should be a list of " + "dict, each dict representing one layer of the fully connected layer stack. ", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["fc_layers"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layers use a bias vector.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["use_bias"], + ) + + bias_initializer: str | dict = schema_utils.InitializerOrDict( + default="zeros", + description="Initializer to use for the bias of the projection and for the fully connected layers.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["bias_initializer"], + ) + + weights_initializer: str | dict = schema_utils.InitializerOrDict( + default="xavier_uniform", + description="Initializer to use for the weights of the projection and for the fully connected layers.", + parameter_metadata=COMBINER_METADATA["project_aggregate"]["weights_initializer"], + ) diff --git a/ludwig/schema/combiners/sequence.py b/ludwig/schema/combiners/sequence.py new file mode 100644 index 0000000..15062c0 --- /dev/null +++ b/ludwig/schema/combiners/sequence.py @@ -0,0 +1,48 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MODEL_ECD, SEQUENCE +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.sequence_concat import MAIN_SEQUENCE_FEATURE_DESCRIPTION +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.metadata import COMBINER_METADATA + +""" +SEQUENCE encoders that always return 2D [batch_size, hidden_size] tensors, regardless of how they are parameterized. +These should never be used with modules that expect 3D tensors, such as the SequenceCombiner. +""" +_2D_SEQUENCE_ENCODERS = ["embed"] + + +@DeveloperAPI +@register_combiner_config("sequence") +class SequenceCombinerConfig(BaseCombinerConfig): + """Parameters for sequence combiner.""" + + type: str = schema_utils.ProtectedString( + "sequence", + description=COMBINER_METADATA["sequence"]["type"].long_description, + ) + + main_sequence_feature: str | None = schema_utils.String( + default=None, + allow_none=True, + description=MAIN_SEQUENCE_FEATURE_DESCRIPTION, + parameter_metadata=COMBINER_METADATA["sequence"]["main_sequence_feature"], + ) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=SEQUENCE, + default="parallel_cnn", + description="Encoder to apply to `main_sequence_feature`. The encoder must produce" + " a tensor of size [batch_size, sequence_length, hidden_size]", + blocklist=_2D_SEQUENCE_ENCODERS, + ) + + reduce_output: str | None = schema_utils.ReductionOptions( + default=None, + description="Strategy to use to aggregate the embeddings of the items of the set.", + parameter_metadata=COMBINER_METADATA["sequence"]["reduce_output"], + ) diff --git a/ludwig/schema/combiners/sequence_concat.py b/ludwig/schema/combiners/sequence_concat.py new file mode 100644 index 0000000..5ae74fe --- /dev/null +++ b/ludwig/schema/combiners/sequence_concat.py @@ -0,0 +1,43 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + +MAIN_SEQUENCE_FEATURE_DESCRIPTION = """ +Name of a sequence, text, or time series feature to concatenate the outputs +of the other features to. If no `main_sequence_feature` is specified, the combiner will look through all the features in +the order they are defined in the configuration and will look for a feature with a rank 3 tensor output (sequence, text +or time series). If it cannot find one it will raise an exception, otherwise the output of that feature will be used for +concatenating the other features along the sequence `s` dimension. If there are other input features with a rank 3 +output tensor, the combiner will concatenate them alongside the `s` dimension. All sequence-like input features must +have identical `s` dimension, otherwise an error will be thrown. +""" + + +@DeveloperAPI +@register_combiner_config("sequence_concat") +class SequenceConcatCombinerConfig(BaseCombinerConfig): + """Parameters for sequence concat combiner.""" + + @staticmethod + def module_name(): + return "sequence_concat" + + type: str = schema_utils.ProtectedString( + "sequence_concat", + description=COMBINER_METADATA["sequence_concat"]["type"].long_description, + ) + + main_sequence_feature: str | None = schema_utils.String( + default=None, + allow_none=True, + description=MAIN_SEQUENCE_FEATURE_DESCRIPTION, + parameter_metadata=COMBINER_METADATA["sequence_concat"]["main_sequence_feature"], + ) + + reduce_output: str | None = schema_utils.ReductionOptions( + default=None, + description="Strategy to use to aggregate the embeddings of the items of the set.", + parameter_metadata=COMBINER_METADATA["sequence_concat"]["reduce_output"], + ) diff --git a/ludwig/schema/combiners/tab_transformer.py b/ludwig/schema/combiners/tab_transformer.py new file mode 100644 index 0000000..c011207 --- /dev/null +++ b/ludwig/schema/combiners/tab_transformer.py @@ -0,0 +1,30 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.common_transformer_options import CommonTransformerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +@register_combiner_config("tabtransformer") +class TabTransformerCombinerConfig(BaseCombinerConfig, CommonTransformerConfig): + """Parameters for tab transformer combiner.""" + + type: str = schema_utils.ProtectedString( + "tabtransformer", + description=COMBINER_METADATA["tabtransformer"]["type"].long_description, + ) + + embed_input_feature_name: str | int | None = schema_utils.Embed( + description="This value controls the size of the embeddings. Valid values are `add` which uses the " + "`hidden_size` value or an integer that is set to a specific value. In the case of an integer " + "value, it must be smaller than hidden_size.", + parameter_metadata=COMBINER_METADATA["tabtransformer"]["embed_input_feature_name"], + ) + + reduce_output: str = schema_utils.ReductionOptions( + default="concat", + description="Strategy to use to aggregate the output of the transformer.", + parameter_metadata=COMBINER_METADATA["tabtransformer"]["reduce_output"], + ) diff --git a/ludwig/schema/combiners/tabnet.py b/ludwig/schema/combiners/tabnet.py new file mode 100644 index 0000000..928a75a --- /dev/null +++ b/ludwig/schema/combiners/tabnet.py @@ -0,0 +1,111 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +@register_combiner_config("tabnet") +class TabNetCombinerConfig(BaseCombinerConfig): + """Parameters for tabnet combiner.""" + + type: str = schema_utils.ProtectedString( + "tabnet", + description=COMBINER_METADATA["tabnet"]["type"].long_description, + ) + + size: int = schema_utils.PositiveInteger( + default=32, + description="Size of the hidden layers. `N_a` in (Arik and Pfister, 2019).", + parameter_metadata=COMBINER_METADATA["tabnet"]["size"], + ) + + dropout: float = schema_utils.FloatRange( + default=0.05, + min=0, + max=1, + description="Dropout rate for the transformer block.", + parameter_metadata=COMBINER_METADATA["tabnet"]["dropout"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=128, + description="Output size of a fully connected layer. `N_d` in (Arik and Pfister, 2019).", + parameter_metadata=COMBINER_METADATA["tabnet"]["output_size"], + ) + + num_steps: int = schema_utils.NonNegativeInteger( + default=3, + description="Number of steps / repetitions of the the attentive transformer and feature transformer " + "computations. `N_steps` in (Arik and Pfister, 2019).", + parameter_metadata=COMBINER_METADATA["tabnet"]["num_steps"], + ) + + num_total_blocks: int = schema_utils.NonNegativeInteger( + default=4, + description="Total number of feature transformer blocks at each step.", + parameter_metadata=COMBINER_METADATA["tabnet"]["num_total_blocks"], + ) + + num_shared_blocks: int = schema_utils.NonNegativeInteger( + default=2, + description="Number of shared feature transformer blocks across the steps.", + parameter_metadata=COMBINER_METADATA["tabnet"]["num_shared_blocks"], + ) + + relaxation_factor: float = schema_utils.FloatRange( + default=1.5, + description="Factor that influences how many times a feature should be used across the steps of computation. " + "a value of 1 implies it each feature should be use once, a higher value allows for multiple " + "usages. `gamma` in (Arik and Pfister, 2019).", + parameter_metadata=COMBINER_METADATA["tabnet"]["relaxation_factor"], + ) + + bn_epsilon: float = schema_utils.FloatRange( + default=1e-3, + description="Epsilon to be added to the batch norm denominator.", + parameter_metadata=COMBINER_METADATA["tabnet"]["bn_epsilon"], + ) + + bn_momentum: float = schema_utils.FloatRange( + default=0.05, + description="Momentum of the batch norm. 1 - `m_B` from the TabNet paper.", + parameter_metadata=COMBINER_METADATA["tabnet"]["bn_momentum"], + ) + + bn_virtual_bs: int | None = schema_utils.PositiveInteger( + default=1024, + allow_none=True, + description="Size of the virtual batch size used by ghost batch norm. If null, regular batch norm is used " + "instead. `B_v` from the TabNet paper.", + parameter_metadata=COMBINER_METADATA["tabnet"]["bn_virtual_bs"], + ) + + sparsity: float = schema_utils.FloatRange( + default=1e-4, + description="Multiplier of the sparsity inducing loss. `lambda_sparse` in (Arik and Pfister, 2019).", + parameter_metadata=COMBINER_METADATA["tabnet"]["sparsity"], + ) + + entmax_mode: str = schema_utils.StringOptions( + ["entmax15", "sparsemax", "constant", "adaptive"], + default="sparsemax", + description=( + "Entmax is a sparse family of probability mapping which generalizes softmax and sparsemax. " + "`entmax_mode` controls the sparsity" + ), + parameter_metadata=COMBINER_METADATA["tabnet"]["entmax_mode"], + ) + + entmax_alpha: float = schema_utils.FloatRange( + default=1.5, + min=1, + max=2, + description=( + "Must be a number between 1.0 and 2.0. If entmax_mode is `adaptive`, " + "`entmax_alpha` is used as the initial value for the learnable parameter. " + "1 corresponds to softmax, 2 is sparsemax." + ), + parameter_metadata=COMBINER_METADATA["tabnet"]["entmax_alpha"], + ) diff --git a/ludwig/schema/combiners/tabpfn_v2.py b/ludwig/schema/combiners/tabpfn_v2.py new file mode 100644 index 0000000..2a0fef8 --- /dev/null +++ b/ludwig/schema/combiners/tabpfn_v2.py @@ -0,0 +1,50 @@ +"""Schema for the TabPFN v2 combiner (Phase 6.5).""" + +from __future__ import annotations + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config + + +@DeveloperAPI +@register_combiner_config("tabpfn_v2") +class TabPFNV2CombinerConfig(BaseCombinerConfig): + """TabPFN v2 foundation-model combiner. + + Wraps the pretrained TabPFN v2 (Hollmann et al., 2022 / 2025) as the ECD fusion + block. Best suited for small tabular datasets (<=10k rows) where in-context + learning outperforms gradient-based fine-tuning. Requires the optional ``tabpfn`` + Python package — ``pip install tabpfn``. + """ + + type: str = schema_utils.ProtectedString( + "tabpfn_v2", + description="TabPFN v2 foundation-model combiner for tabular data.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=128, + description="Width of the learnable projection head applied to TabPFN's encoder output.", + ) + + tabpfn_hidden_size: int = schema_utils.PositiveInteger( + default=512, + description="TabPFN v2's internal hidden width. The v2 default is 512; only change this if loading a variant.", + ) + + n_estimators: int = schema_utils.PositiveInteger( + default=4, + description=( + "Number of TabPFN ensemble members to use during prediction. Higher values improve accuracy at the cost of " + "inference latency." + ), + ) + + device: str = schema_utils.StringOptions( + options=["auto", "cpu", "cuda"], + default="auto", + allow_none=False, + description="Device used for TabPFN inference. 'auto' picks CUDA if available.", + ) diff --git a/ludwig/schema/combiners/transformer.py b/ludwig/schema/combiners/transformer.py new file mode 100644 index 0000000..34b6039 --- /dev/null +++ b/ludwig/schema/combiners/transformer.py @@ -0,0 +1,23 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.common_transformer_options import CommonTransformerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.metadata import COMBINER_METADATA + + +@DeveloperAPI +@register_combiner_config("transformer") +class TransformerCombinerConfig(BaseCombinerConfig, CommonTransformerConfig): + """Parameters for transformer combiner.""" + + type: str = schema_utils.ProtectedString( + "transformer", + description=COMBINER_METADATA["transformer"]["type"].long_description, + ) + + reduce_output: str | None = schema_utils.ReductionOptions( + default="mean", + description="Strategy to use to aggregate the output of the transformer.", + parameter_metadata=COMBINER_METADATA["transformer"]["reduce_output"], + ) diff --git a/ludwig/schema/combiners/utils.py b/ludwig/schema/combiners/utils.py new file mode 100644 index 0000000..61f4033 --- /dev/null +++ b/ludwig/schema/combiners/utils.py @@ -0,0 +1,106 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import TYPE +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.metadata import COMBINER_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata +from ludwig.utils.registry import Registry + +DEFAULT_VALUE = "concat" +DESCRIPTION = "Select the combiner type." + +combiner_config_registry = Registry[type[BaseCombinerConfig]]() + + +@DeveloperAPI +def register_combiner_config(name: str): + def wrap(cls: type[BaseCombinerConfig]): + combiner_config_registry[name] = cls + return cls + + return wrap + + +@DeveloperAPI +def get_combiner_registry(): + return combiner_config_registry + + +@DeveloperAPI +def get_combiner_jsonschema(): + """Returns a JSON schema structured to only require a `type` key and then conditionally apply a corresponding + combiner's field constraints.""" + combiner_types = sorted(list(combiner_config_registry.keys())) + parameter_metadata = convert_metadata_to_json( + ParameterMetadata.from_dict( + { + "commonly_used": True, + "expected_impact": 3, + "ui_display_name": "Combiner Type", + } + ) + ) + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": combiner_types, + "enumDescriptions": get_combiner_descriptions(), + "default": DEFAULT_VALUE, + "title": "combiner_options", + "description": DESCRIPTION, + "parameter_metadata": parameter_metadata, + }, + }, + "allOf": get_combiner_conds(), + "required": ["type"], + } + + +@DeveloperAPI +def get_combiner_descriptions(): + """This function returns a dictionary of combiner descriptions available at the type selection. + + The process works as follows - 1) Get a dictionary of valid combiners from the combiner config registry, + but inverse the key/value pairs since we need to index `valid_combiners` later with an altered version + of the combiner config class name. 2) Loop through Combiner Metadata entries, if a metadata entry has a + combiner name that matches a valid combiner, add the description metadata to the output dictionary. + + Returns: + dict: A dictionary of combiner descriptions. + """ + return {k: convert_metadata_to_json(v[TYPE]) for k, v in COMBINER_METADATA.items() if k in combiner_config_registry} + + +@DeveloperAPI +def get_combiner_conds() -> list[dict[str, Any]]: + """Returns a list of if-then JSON clauses for each combiner type in `combiner_registry` and its properties' + constraints.""" + combiner_types = sorted(list(combiner_config_registry.keys())) + conds = [] + for combiner_type in combiner_types: + combiner_cls = combiner_config_registry[combiner_type] + schema_cls = combiner_cls + combiner_schema = schema_utils.unload_jsonschema_from_config_class(schema_cls) + combiner_props = combiner_schema["properties"] + schema_utils.remove_duplicate_fields(combiner_props) + combiner_cond = schema_utils.create_cond({"type": combiner_type}, combiner_props) + conds.append(combiner_cond) + return conds + + +class CombinerSelection(schema_utils.TypeSelection): + def __init__(self): + # For registration of all combiners + import ludwig.combiners.combiners # noqa + + super().__init__(registry=combiner_config_registry, default_value=DEFAULT_VALUE, description=DESCRIPTION) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return self.registry[key] + + def _jsonschema_type_mapping(self): + return get_combiner_jsonschema() diff --git a/ludwig/schema/common_fields.py b/ludwig/schema/common_fields.py new file mode 100644 index 0000000..67cb876 --- /dev/null +++ b/ludwig/schema/common_fields.py @@ -0,0 +1,293 @@ +from dataclasses import Field + +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import COMMON_METADATA +from ludwig.schema.metadata.parameter_metadata import ParameterMetadata +from ludwig.utils.torch_utils import initializer_registry + + +def DropoutField( + default: float = 0.0, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or "Default dropout rate applied to fully connected layers." + full_description = description + ( + " Increasing dropout is a common form of regularization to combat overfitting. " + "The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout)." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["dropout"] + return schema_utils.FloatRange( + default=default, + min=0, + max=1, + description=full_description, + parameter_metadata=parameter_metadata, + ) + + +def ResidualField( + default: bool = False, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "Whether to add a residual connection to each fully connected layer block. " + "Requires all fully connected layers to have the same `output_size`." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["residual"] + return schema_utils.Boolean( + default=False, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def NumFCLayersField( + default: int = 0, description: str | None = None, parameter_metadata: ParameterMetadata = None, non_zero=False +) -> Field: + if non_zero and default <= 0: + raise ValueError(f"NumFCLayersField: when non_zero=True, default must be > 0, got default={default}.") + + description = description or "Number of stacked fully connected layers to apply." + full_description = description + ( + " Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["num_fc_layers"] + + # When using a dense encoder, the number of fully connected layers must be strictly greater than 0. + if non_zero: + return schema_utils.PositiveInteger( + default=default, allow_none=False, description=full_description, parameter_metadata=parameter_metadata + ) + return schema_utils.NonNegativeInteger( + default=default, + allow_none=False, + description=full_description, + parameter_metadata=parameter_metadata, + ) + + +def NormField( + default: str | None = None, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or "Default normalization applied at the beginnging of fully connected layers." + parameter_metadata = parameter_metadata or COMMON_METADATA["norm"] + return schema_utils.StringOptions( + ["batch", "layer", "ghost"], + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def NormParamsField(description: str | None = None, parameter_metadata: ParameterMetadata = None) -> Field: + description = description or "Default parameters passed to the `norm` module." + parameter_metadata = parameter_metadata or COMMON_METADATA["norm_params"] + return schema_utils.Dict( + description=description, + parameter_metadata=parameter_metadata, + ) + + +def FCLayersField(description: str | None = None, parameter_metadata: ParameterMetadata = None) -> Field: + description = description or ( + "List of dictionaries containing the parameters of all the fully connected layers. " + "The length of the list determines the number of stacked fully connected layers " + "and the content of each dictionary determines the parameters for a specific layer. " + "The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, " + "`output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values " + "is missing from the dictionary, the default one provided as a standalone parameter will be used instead." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["fc_layers"] + return schema_utils.DictList( + description=description, + parameter_metadata=parameter_metadata, + ) + + +INITIALIZER_SUFFIX = """ +Alternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and +other keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each +initializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html). +""" + + +def BiasInitializerField( + default: str = "zeros", description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + initializers_str = ", ".join([f"`{i}`" for i in initializer_registry]) + description = description or "Initializer for the bias vector." + full_description = f"{description} Options: {initializers_str}. {INITIALIZER_SUFFIX}" + parameter_metadata = parameter_metadata or COMMON_METADATA["bias_initializer"] + return schema_utils.InitializerOrDict( + default=default, + description=full_description, + parameter_metadata=parameter_metadata, + ) + + +def WeightsInitializerField( + default: str = "xavier_uniform", description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + initializers_str = ", ".join([f"`{i}`" for i in initializer_registry]) + description = description or "Initializer for the weight matrix." + full_description = f"{description} Options: {initializers_str}. {INITIALIZER_SUFFIX}" + parameter_metadata = parameter_metadata or COMMON_METADATA["weights_initializer"] + return schema_utils.InitializerOrDict( + default=default, + description=full_description, + parameter_metadata=parameter_metadata, + ) + + +def EmbeddingInitializerField( + default: str | None = None, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or "Initializer for the embedding matrix." + parameter_metadata = parameter_metadata or COMMON_METADATA["embedding_initializer"] + return schema_utils.StringOptions( + list(initializer_registry.keys()), + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def EmbeddingSizeField( + default: int = 256, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for " + "`dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` " + "is the number of unique strings appearing in the training set input column plus the number of " + "special tokens (``, ``, ``, ``)." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["embedding_size"] + return schema_utils.PositiveInteger( + default=default, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def EmbeddingsOnCPUField( + default: bool = False, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. " + "By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, " + "but in some cases the embedding matrix may be too large. This parameter forces the placement of the " + "embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the " + "process as a result of data transfer between CPU and GPU memory." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["embeddings_on_cpu"] + return schema_utils.Boolean( + default=default, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def EmbeddingsTrainableField( + default: bool = True, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. " + "It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter " + "has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["embeddings_trainable"] + return schema_utils.Boolean( + default=default, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def PretrainedEmbeddingsField( + default: str | None = None, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized " + "randomly, but this parameter allows to specify a path to a file containing embeddings in the " + "[GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is " + "loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. " + "If the vocabulary contains strings that have no match in the embeddings file, their embeddings are " + "initialized with the average of all other embedding plus some random noise to make them different " + "from each other. This parameter has effect only if `representation` is `dense`." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["pretrained_embeddings"] + return schema_utils.String( + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def MaxSequenceLengthField( + default: int | None = None, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or "[internal] Maximum sequence length from preprocessing." + parameter_metadata = parameter_metadata or COMMON_METADATA["max_sequence_length"] + return schema_utils.PositiveInteger( + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def VocabField( + default: list | None = None, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or "[internal] Vocabulary for the encoder from preprocessing." + parameter_metadata = parameter_metadata or COMMON_METADATA["vocab"] + return schema_utils.List( + default=default, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def VocabSizeField( + default: list | None = None, description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or "[internal] Size of the vocabulary from preprocessing." + parameter_metadata = parameter_metadata or COMMON_METADATA["vocab_size"] + return schema_utils.PositiveInteger( + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def RepresentationField( + default: str = "dense", description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "Representation of the embedding. `dense` means the embeddings are initialized randomly, " + "`sparse` means they are initialized to be one-hot encodings." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["representation"] + return schema_utils.StringOptions( + ["dense", "sparse"], + default=default, + description=description, + parameter_metadata=parameter_metadata, + ) + + +def ReduceOutputField( + default: str | None = "sum", description: str | None = None, parameter_metadata: ParameterMetadata = None +) -> Field: + description = description or ( + "How to reduce the output tensor along the `s` sequence length dimension if the rank of the " + "tensor is greater than 2." + ) + parameter_metadata = parameter_metadata or COMMON_METADATA["reduce_output"] + return schema_utils.ReductionOptions( + default=default, + description=description, + parameter_metadata=parameter_metadata, + ) diff --git a/ludwig/schema/decoders/__init__.py b/ludwig/schema/decoders/__init__.py new file mode 100644 index 0000000..b1ac39f --- /dev/null +++ b/ludwig/schema/decoders/__init__.py @@ -0,0 +1,5 @@ +# Register all decoders +import ludwig.schema.decoders.base +import ludwig.schema.decoders.image_decoders +import ludwig.schema.decoders.llm_decoders +import ludwig.schema.decoders.sequence_decoders # noqa diff --git a/ludwig/schema/decoders/base.py b/ludwig/schema/decoders/base.py new file mode 100644 index 0000000..249de40 --- /dev/null +++ b/ludwig/schema/decoders/base.py @@ -0,0 +1,429 @@ +from abc import ABC + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ANOMALY, BINARY, CATEGORY, MODEL_ECD, MODEL_LLM, NUMBER, SET, TIMESERIES, VECTOR +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.utils import register_decoder_config +from ludwig.schema.metadata import DECODER_METADATA + + +@DeveloperAPI +class BaseDecoderConfig(schema_utils.LudwigBaseConfig, ABC): + """Base class for decoders.""" + + type: str = schema_utils.StringOptions( + ["regressor", "classifier", "projector", "generator", "tagger"], + default=None, + allow_none=True, + description="The type of decoder to use.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["type"], + ) + + fc_layers: list[dict] = common_fields.FCLayersField() + + num_fc_layers: int = common_fields.NumFCLayersField( + description="Number of fully-connected layers if `fc_layers` not specified." + ) + + fc_output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output size of fully connected stack.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_output_size"], + ) + + fc_use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector in the fc_stack.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_use_bias"], + ) + + fc_weights_initializer: str | dict = schema_utils.OneOfOptionsField( + default="xavier_uniform", + allow_none=True, + description="The weights initializer to use for the layers in the fc_stack", + field_options=[ + schema_utils.InitializerOptions( + description="Preconfigured initializer to use for the layers in the fc_stack.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_weights_initializer"], + ), + schema_utils.Dict( + description="Custom initializer to use for the layers in the fc_stack.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_weights_initializer"], + ), + ], + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_weights_initializer"], + ) + + fc_bias_initializer: str | dict = schema_utils.OneOfOptionsField( + default="zeros", + allow_none=True, + description="The bias initializer to use for the layers in the fc_stack", + field_options=[ + schema_utils.InitializerOptions( + description="Preconfigured bias initializer to use for the layers in the fc_stack.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_bias_initializer"], + ), + schema_utils.Dict( + description="Custom bias initializer to use for the layers in the fc_stack.", + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_bias_initializer"], + ), + ], + parameter_metadata=DECODER_METADATA["BaseDecoder"]["fc_bias_initializer"], + ) + + fc_norm: str = common_fields.NormField() + + fc_norm_params: dict = common_fields.NormParamsField() + + fc_activation: str = schema_utils.ActivationOptions(default="relu") + + fc_dropout: float = common_fields.DropoutField() + + +@DeveloperAPI +class PassthroughDecoderConfig(BaseDecoderConfig): + """PassthroughDecoderConfig is a dataclass that configures the parameters used for a passthrough decoder.""" + + @classmethod + def module_name(cls): + return "PassthroughDecoder" + + type: str = schema_utils.ProtectedString( + "passthrough", + description="The passthrough decoder simply returns the raw numerical values coming from the combiner as " + "outputs", + parameter_metadata=DECODER_METADATA["PassthroughDecoder"]["type"], + ) + + input_size: int = schema_utils.PositiveInteger( + default=1, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["PassthroughDecoder"]["input_size"], + ) + + +@DeveloperAPI +@register_decoder_config("regressor", [BINARY, NUMBER], model_types=[MODEL_ECD]) +class RegressorConfig(BaseDecoderConfig): + """RegressorConfig is a dataclass that configures the parameters used for a regressor decoder.""" + + @classmethod + def module_name(cls): + return "Regressor" + + type: str = schema_utils.ProtectedString( + "regressor", + description=DECODER_METADATA["Regressor"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["Regressor"]["input_size"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=DECODER_METADATA["Regressor"]["use_bias"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer for the weight matrix.", + parameter_metadata=DECODER_METADATA["Regressor"]["weights_initializer"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer for the bias vector.", + parameter_metadata=DECODER_METADATA["Regressor"]["bias_initializer"], + ) + + +@DeveloperAPI +@register_decoder_config("projector", [VECTOR, TIMESERIES], model_types=[MODEL_ECD]) +class ProjectorConfig(BaseDecoderConfig): + """ProjectorConfig is a dataclass that configures the parameters used for a projector decoder.""" + + @classmethod + def module_name(cls): + return "Projector" + + type: str = schema_utils.ProtectedString( + "projector", + description=DECODER_METADATA["Projector"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["Projector"]["input_size"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the output of the decoder.", + parameter_metadata=DECODER_METADATA["Projector"]["output_size"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=DECODER_METADATA["Projector"]["use_bias"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer for the weight matrix.", + parameter_metadata=DECODER_METADATA["Projector"]["weights_initializer"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer for the bias vector.", + parameter_metadata=DECODER_METADATA["Projector"]["bias_initializer"], + ) + + activation: str = schema_utils.ActivationOptions( + default=None, + description=" Indicates the activation function applied to the output.", + parameter_metadata=DECODER_METADATA["Projector"]["activation"], + ) + + multiplier: float = schema_utils.FloatRange( + default=1.0, + min=0, + min_inclusive=False, + description=( + "Multiplier to scale the activated outputs by. Useful when setting `activation` to something " + "that outputs a value between [-1, 1] like tanh to re-scale values back to order of magnitude of " + "the data you're trying to predict. A good rule of thumb in such cases is to pick a value like " + "`x * (max - min)` where x is a scalar in the range [1, 2]. For example, if you're trying to predict " + "something like temperature, it might make sense to pick a multiplier on the order of `100`." + ), + ) + + clip: list[int] | tuple[int] = schema_utils.FloatRangeTupleDataclassField( + n=2, + default=None, + allow_none=True, + min=0, + max=999999999, + description="Clip the output of the decoder to be within the given range.", + parameter_metadata=DECODER_METADATA["Projector"]["clip"], + ) + + +@DeveloperAPI +@register_decoder_config("classifier", [CATEGORY, SET], model_types=[MODEL_ECD, MODEL_LLM]) +class ClassifierConfig(BaseDecoderConfig): + @classmethod + def module_name(cls): + return "Classifier" + + type: str = schema_utils.ProtectedString( + "classifier", + description=DECODER_METADATA["Classifier"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["Classifier"]["input_size"], + ) + + num_classes: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of classes to predict.", + parameter_metadata=DECODER_METADATA["Classifier"]["num_classes"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=DECODER_METADATA["Classifier"]["use_bias"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer for the weight matrix.", + parameter_metadata=DECODER_METADATA["Classifier"]["weights_initializer"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer for the bias vector.", + parameter_metadata=DECODER_METADATA["Classifier"]["bias_initializer"], + ) + + calibration: str | None = schema_utils.StringOptions( + options=["temperature_scaling"], + default=None, + allow_none=True, + description=( + "Post-training calibration method to apply to the decoder logits. " + "'temperature_scaling' learns a single scalar T that divides logits " + "(calibrated_logits = logits / T) using NLL minimisation on the validation set. " + "It never changes argmax predictions but improves probability reliability. " + "See: Guo et al., 'On Calibration of Modern Neural Networks', ICML 2017. " + "Set to null (default) to disable calibration." + ), + ) + + mc_dropout_samples: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "Number of Monte Carlo forward passes to run at inference time with dropout enabled. " + "When > 0, the decoder is run mc_dropout_samples times and the mean of the resulting " + "probability distributions is used as the prediction; the variance across runs is reported " + "as an 'uncertainty' tensor alongside 'predictions' and 'probabilities'. " + "Setting this to 0 (default) disables MC Dropout. " + "See: Gal & Ghahramani, 'Dropout as a Bayesian Approximation', ICML 2016." + ), + ) + + +@DeveloperAPI +@register_decoder_config("mlp_classifier", [CATEGORY, BINARY], model_types=[MODEL_ECD]) +class MLPClassifierConfig(BaseDecoderConfig): + """Configuration for the MLPClassifier decoder. + + MLPClassifier stacks one or more fully-connected hidden layers (with configurable size, + activation, and dropout) before the final linear projection to class logits. This is useful + when the combiner output is too raw for a single-layer linear projection. + + When num_fc_layers=1 (the default), it applies a single hidden layer of size output_size + before projecting to class logits. When num_fc_layers=0 the behaviour is equivalent to the + standard Classifier. Increase num_fc_layers for more expressive capacity on harder + classification problems. + + References: + - Guo et al., "On Calibration of Modern Neural Networks", ICML 2017 + (for the calibration field). + - Gal & Ghahramani, "Dropout as a Bayesian Approximation: Representing + Model Uncertainty in Deep Learning", ICML 2016 (for mc_dropout_samples). + """ + + @classmethod + def module_name(cls): + return "MLPClassifier" + + type: str = schema_utils.ProtectedString( + "mlp_classifier", + description=( + "Multi-layer perceptron classifier decoder. Stacks num_fc_layers fully-connected " + "layers (each of size output_size) with activation and dropout, followed " + "by a final linear projection to num_classes logits. " + "Use this instead of the standard classifier when the combiner output benefits " + "from additional non-linear transformation before the classification head." + ), + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder. Set automatically from the combiner output size.", + ) + + num_classes: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of classes to predict. Set automatically from the feature vocabulary size.", + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=1, + description=( + "Number of fully-connected hidden layers to stack before the classification head. " + "When set to 1 (default) a single hidden layer of size output_size is applied. " + "Set to 0 to make this decoder equivalent to the standard single-layer Classifier." + ), + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of each hidden fully-connected layer. Only used when num_fc_layers > 0.", + ) + + activation: str = schema_utils.ActivationOptions( + default="relu", + description="Activation function applied after each hidden fully-connected layer.", + ) + + dropout: float = common_fields.DropoutField() + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether each fully-connected layer (and the final projection) uses a bias vector.", + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer for the weight matrices.", + parameter_metadata=DECODER_METADATA["Classifier"]["weights_initializer"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer for the bias vectors.", + parameter_metadata=DECODER_METADATA["Classifier"]["bias_initializer"], + ) + + calibration: str | None = schema_utils.StringOptions( + options=["temperature_scaling"], + default=None, + allow_none=True, + description=( + "Post-training calibration method to apply to the decoder logits. " + "'temperature_scaling' learns a single scalar T that divides logits " + "(calibrated_logits = logits / T) using NLL minimisation on the validation set. " + "It never changes argmax predictions but improves probability reliability. " + "See: Guo et al., 'On Calibration of Modern Neural Networks', ICML 2017. " + "Set to null (default) to disable calibration." + ), + ) + + mc_dropout_samples: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "Number of Monte Carlo forward passes to run at inference time with dropout enabled. " + "When > 0, the decoder is run mc_dropout_samples times and the mean of the resulting " + "probability distributions is used as the prediction; the variance across runs is reported " + "as an 'uncertainty' tensor alongside 'predictions' and 'probabilities'. " + "Setting this to 0 (default) disables MC Dropout. " + "See: Gal & Ghahramani, 'Dropout as a Bayesian Approximation', ICML 2016." + ), + ) + + +@DeveloperAPI +@register_decoder_config("anomaly", [ANOMALY], model_types=[MODEL_ECD]) +class AnomalyDecoderConfig(BaseDecoderConfig): + """AnomalyDecoderConfig configures the anomaly decoder. + + The anomaly decoder computes ``||z - c||^2`` as the anomaly score, where ``z`` is + the encoder/combiner output and ``c`` is the hypersphere center, which is initialized + after the first epoch by computing the mean of all encoder outputs. + + This implements the geometric core of Deep SVDD (Ruff et al., ICML 2018). + With Ludwig's ECD combiner you get free multimodal anomaly detection: any combination + of tabular, image, text, or audio inputs is fused and mapped to the hypersphere. + """ + + @classmethod + def module_name(cls): + return "AnomalyDecoder" + + type: str = schema_utils.ProtectedString( + "anomaly", + description="Computes ||z - c||^2 as the anomaly score.", + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the encoder output. Set automatically from the FC stack output shape.", + ) diff --git a/ludwig/schema/decoders/image_decoders.py b/ludwig/schema/decoders/image_decoders.py new file mode 100644 index 0000000..746483d --- /dev/null +++ b/ludwig/schema/decoders/image_decoders.py @@ -0,0 +1,220 @@ +from typing import TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import IMAGE, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import register_decoder_config +from ludwig.schema.metadata import DECODER_METADATA + +if TYPE_CHECKING: + from ludwig.schema.features.preprocessing.image import ImagePreprocessingConfig + + +class ImageDecoderConfig(BaseDecoderConfig): + def set_fixed_preprocessing_params(self, model_type: str, preprocessing: "ImagePreprocessingConfig"): + preprocessing.requires_equal_dimensions = False + preprocessing.height = None + preprocessing.width = None + + +@DeveloperAPI +@register_decoder_config("unet", [IMAGE], model_types=[MODEL_ECD]) +class UNetDecoderConfig(ImageDecoderConfig): + @staticmethod + def module_name(): + return "UNetDecoder" + + type: str = schema_utils.ProtectedString( + "unet", + description=DECODER_METADATA["UNetDecoder"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=1024, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["UNetDecoder"]["input_size"], + ) + + height: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Height of the output image.", + parameter_metadata=DECODER_METADATA["UNetDecoder"]["height"], + ) + + width: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Width of the output image.", + parameter_metadata=DECODER_METADATA["UNetDecoder"]["width"], + ) + + num_channels: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of channels in the output image. ", + parameter_metadata=DECODER_METADATA["UNetDecoder"]["num_channels"], + ) + + conv_norm: str | None = schema_utils.StringOptions( + ["batch"], + default="batch", + allow_none=True, + description="This is the default norm that will be used for each double conv layer.It can be null or batch.", + parameter_metadata=DECODER_METADATA["UNetDecoder"]["conv_norm"], + ) + + num_classes: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of classes to predict in the output. ", + parameter_metadata=DECODER_METADATA["UNetDecoder"]["num_classes"], + ) + + num_stages: int = schema_utils.PositiveInteger( + default=4, + description=( + "Number of encoder/decoder stage pairs in the UNet. " + "The input image dimensions must be divisible by 2^num_stages. " + "Increasing this value lets the model capture features at more spatial scales." + ), + parameter_metadata=DECODER_METADATA["UNetDecoder"]["num_stages"], + ) + + +@DeveloperAPI +@register_decoder_config("segformer", [IMAGE], model_types=[MODEL_ECD]) +class SegFormerDecoderConfig(ImageDecoderConfig): + """Config for the SegFormer MLP decoder head. + + Reference: Xie et al., "SegFormer: Simple and Efficient Design for Semantic + Segmentation with Transformers", NeurIPS 2021. + https://arxiv.org/abs/2105.15203 + """ + + @staticmethod + def module_name(): + return "SegFormerDecoder" + + type: str = schema_utils.ProtectedString( + "segformer", + description=DECODER_METADATA["SegFormerDecoder"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input feature vector from the combiner.", + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["input_size"], + ) + + height: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Height of the output segmentation map.", + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["height"], + ) + + width: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Width of the output segmentation map.", + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["width"], + ) + + num_channels: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of channels in the input image (informational; set from preprocessing).", + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["num_channels"], + ) + + num_classes: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of segmentation classes to predict.", + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["num_classes"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=256, + description=( + "Width of the hidden MLP projection applied to the feature map before upsampling. " + "Larger values increase capacity but also compute cost." + ), + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["hidden_size"], + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0.0, + max=1.0, + description="Dropout probability applied after the hidden MLP projection.", + parameter_metadata=DECODER_METADATA["SegFormerDecoder"]["dropout"], + ) + + +@DeveloperAPI +@register_decoder_config("fpn", [IMAGE], model_types=[MODEL_ECD]) +class FPNDecoderConfig(ImageDecoderConfig): + """Config for the Feature Pyramid Network (FPN) decoder. + + Reference: Lin et al., "Feature Pyramid Networks for Object Detection", + CVPR 2017. https://arxiv.org/abs/1612.03144 + """ + + @staticmethod + def module_name(): + return "FPNDecoder" + + type: str = schema_utils.ProtectedString( + "fpn", + description=DECODER_METADATA["FPNDecoder"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input feature vector from the combiner.", + parameter_metadata=DECODER_METADATA["FPNDecoder"]["input_size"], + ) + + height: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Height of the output segmentation map.", + parameter_metadata=DECODER_METADATA["FPNDecoder"]["height"], + ) + + width: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Width of the output segmentation map.", + parameter_metadata=DECODER_METADATA["FPNDecoder"]["width"], + ) + + num_classes: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of segmentation classes to predict.", + parameter_metadata=DECODER_METADATA["FPNDecoder"]["num_classes"], + ) + + num_channels: int = schema_utils.PositiveInteger( + default=256, + description=( + "Number of channels in each FPN level after the lateral 1x1 projection. " + "All pyramid levels are projected to this width before the top-down merge." + ), + parameter_metadata=DECODER_METADATA["FPNDecoder"]["num_channels"], + ) + + num_levels: int = schema_utils.PositiveInteger( + default=4, + description=( + "Number of pyramid levels to build in the top-down pathway. " + "More levels capture coarser context; typical range is 2-5." + ), + parameter_metadata=DECODER_METADATA["FPNDecoder"]["num_levels"], + ) diff --git a/ludwig/schema/decoders/llm_decoders.py b/ludwig/schema/decoders/llm_decoders.py new file mode 100644 index 0000000..4040560 --- /dev/null +++ b/ludwig/schema/decoders/llm_decoders.py @@ -0,0 +1,99 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import CATEGORY, MODEL_LLM, TEXT +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import register_decoder_config +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +class BaseExtractorDecoderConfig(LudwigBaseConfig): + tokenizer: str = "hf_tokenizer" + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder.", + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="", + allow_none=True, + description="Path to the pretrained model or model identifier from huggingface.co/models.", + ) + + vocab_file: str = schema_utils.String( + default="", + allow_none=True, + description="Path to the vocabulary file.", + ) + + max_new_tokens: int = schema_utils.Integer( + default=None, + allow_none=True, + description="Maximum number of new tokens that will be generated.", + ) + + match_strategy: str = schema_utils.StringOptions( + options=["contains", "regex", "json_schema"], + default="contains", + allow_none=False, + description=( + "Strategy used to parse and validate the LLM's generated text. " + "'contains': substring matching (default). " + "'regex': match via a regular expression. " + "'json_schema': parse the output as JSON and validate the value." + ), + ) + + +@DeveloperAPI +@register_decoder_config("text_extractor", [TEXT], model_types=[MODEL_LLM]) +class TextExtractorDecoderConfig(BaseExtractorDecoderConfig, BaseDecoderConfig): + @classmethod + def module_name(cls): + return "TextExtractorDecoder" + + type: str = schema_utils.ProtectedString("text_extractor") + + +@DeveloperAPI +@register_decoder_config("category_extractor", [CATEGORY], model_types=[MODEL_LLM]) +class CategoryExtractorDecoderConfig(BaseExtractorDecoderConfig, BaseDecoderConfig): + @classmethod + def module_name(cls): + return "CategoryExtractorDecoder" + + type: str = schema_utils.ProtectedString("category_extractor") + + # Match is a dict of label class + match: dict[str, dict[str, Any]] = schema_utils.Dict( + default=None, + allow_none=False, + description="A dictionary of label classes and their corresponding " + "match patterns definitions that will be used to parse the output " + "of the LLM.", + ) + + str2idx: dict[str, int] = schema_utils.Dict( + default=None, + allow_none=True, + description="A dictionary of label classes and their corresponding " + "indices that will be used to parse the output of the LLM.", + ) + + fallback_label: str = schema_utils.String( + default="", + allow_none=True, + description="The label to use if the parser fails to parse the input.", + ) + + constrain_to_vocabulary: bool = schema_utils.Boolean( + default=False, + description=( + "When True, restrict generation to tokens that are valid prefixes of the category labels. " + "Implemented via a HuggingFace LogitsProcessor. Requires tokenizer='hf_tokenizer'." + ), + ) diff --git a/ludwig/schema/decoders/sequence_decoders.py b/ludwig/schema/decoders/sequence_decoders.py new file mode 100644 index 0000000..3a2d68c --- /dev/null +++ b/ludwig/schema/decoders/sequence_decoders.py @@ -0,0 +1,237 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MODEL_ECD, SEQUENCE, TEXT +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import register_decoder_config +from ludwig.schema.metadata import DECODER_METADATA + + +@DeveloperAPI +@register_decoder_config("generator", [SEQUENCE, TEXT], model_types=[MODEL_ECD]) +class SequenceGeneratorDecoderConfig(BaseDecoderConfig): + @staticmethod + def module_name(): + return "SequenceGeneratorDecoder" + + type: str = schema_utils.ProtectedString( + "generator", + description=DECODER_METADATA["SequenceGeneratorDecoder"]["type"].long_description, + ) + + vocab_size: int = common_fields.VocabSizeField() + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + cell_type: str = schema_utils.StringOptions( + ["rnn", "lstm", "gru"], + default="gru", + description="Type of recurrent cell to use.", + parameter_metadata=DECODER_METADATA["SequenceGeneratorDecoder"]["cell_type"], + ) + + input_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["SequenceGeneratorDecoder"]["input_size"], + ) + + reduce_input: str = schema_utils.StringOptions( + ["sum", "mean", "avg", "max", "concat", "last"], + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=DECODER_METADATA["SequenceGeneratorDecoder"]["reduce_input"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of stacked recurrent layers.", + parameter_metadata=DECODER_METADATA["SequenceGeneratorDecoder"]["num_layers"], + ) + + # Scheduled sampling (Bengio et al., NeurIPS 2015) + teacher_forcing_decay: str = schema_utils.StringOptions( + ["none", "linear", "exponential"], + default="none", + description=( + "Decay schedule for teacher forcing probability during training. " + "none always uses full teacher forcing; linear linearly decays the probability to zero; " + "exponential applies exponential decay. " + "Implements scheduled sampling (Bengio et al., NeurIPS 2015)." + ), + ) + + teacher_forcing_decay_rate: float = schema_utils.FloatRange( + default=0.01, + min=0.0, + max=1.0, + description=( + "Rate of decay for the teacher forcing probability per decoding step when " + "teacher_forcing_decay is linear or exponential." + ), + ) + + # Beam search + beam_width: int = schema_utils.PositiveInteger( + default=1, + description=( + "Width of the beam for beam search decoding. 1 = greedy decoding (default). " + "Values > 1 enable beam search at inference time, keeping the top beam_width " + "candidate sequences at each step." + ), + ) + + beam_length_penalty: float = schema_utils.FloatRange( + default=1.0, + min=0.0, + description=( + "Length penalty exponent applied to beam search scores. " + "Score = log_prob / (length ^ beam_length_penalty). " + "Values > 1 penalise longer sequences; values < 1 favour them. " + "Only used when beam_width > 1." + ), + ) + + +@DeveloperAPI +@register_decoder_config("transformer_generator", [SEQUENCE, TEXT], model_types=[MODEL_ECD]) +class TransformerDecoderConfig(BaseDecoderConfig): + """Configuration for the Transformer-based sequence/text decoder. + + References: + Vaswani et al., Attention Is All You Need, NeurIPS 2017. + https://arxiv.org/abs/1706.03762 + """ + + @staticmethod + def module_name(): + return "SequenceTransformerDecoder" + + type: str = schema_utils.ProtectedString( + "transformer_generator", + description=( + "Transformer-based autoregressive sequence decoder. " + "Uses teacher forcing during training and autoregressive generation at inference. " + "Based on Vaswani et al., Attention Is All You Need, NeurIPS 2017." + ), + ) + + vocab_size: int = common_fields.VocabSizeField() + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + input_size: int = schema_utils.PositiveInteger( + default=256, + description=( + "Size of the encoder output (d_model). The encoder output is projected to this size " + "before being used as cross-attention memory in the transformer decoder." + ), + ) + + d_model: int = schema_utils.PositiveInteger( + default=256, + description=( + "Dimensionality of the transformer decoder layers (embedding size and hidden size). " + "Must match input_size or a projection will be applied." + ), + ) + + num_layers: int = schema_utils.PositiveInteger( + default=2, + description="Number of transformer decoder layers.", + ) + + num_heads: int = schema_utils.PositiveInteger( + default=8, + description=( + "Number of attention heads in each multi-head attention sub-layer. d_model must be divisible by num_heads." + ), + ) + + ffn_size: int = schema_utils.PositiveInteger( + default=1024, + description="Size of the feed-forward network (dim_feedforward) inside each transformer decoder layer.", + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0.0, + max=1.0, + description="Dropout probability applied within transformer decoder layers.", + ) + + reduce_input: str = schema_utils.StringOptions( + ["sum", "mean", "avg", "max", "concat", "last"], + default="sum", + description=( + "How to reduce a 3-D encoder output (batch x seq x hidden) to a 2-D context vector " + "(batch x hidden). Ignored when the encoder output is already 2-D." + ), + ) + + # Beam search + beam_width: int = schema_utils.PositiveInteger( + default=1, + description=( + "Width of the beam for beam search decoding at inference time. " + "1 = greedy decoding (default). Values > 1 keep the top beam_width candidates at each step." + ), + ) + + beam_length_penalty: float = schema_utils.FloatRange( + default=1.0, + min=0.0, + description=( + "Length penalty exponent for beam search. " + "Score = log_prob / (length ^ beam_length_penalty). " + "Only active when beam_width > 1." + ), + ) + + +@DeveloperAPI +@register_decoder_config("tagger", [SEQUENCE, TEXT], model_types=[MODEL_ECD]) +class SequenceTaggerDecoderConfig(BaseDecoderConfig): + @classmethod + def module_name(cls): + return "SequenceTaggerDecoder" + + type: str = schema_utils.ProtectedString( + "tagger", + description=DECODER_METADATA["SequenceTaggerDecoder"]["type"].long_description, + ) + + input_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the input to the decoder.", + parameter_metadata=DECODER_METADATA["SequenceTaggerDecoder"]["input_size"], + ) + + vocab_size: int = common_fields.VocabSizeField() + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + use_attention: bool = schema_utils.Boolean( + default=False, + description="Whether to apply a multi-head self attention layer before prediction.", + parameter_metadata=DECODER_METADATA["SequenceTaggerDecoder"]["use_attention"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=DECODER_METADATA["SequenceTaggerDecoder"]["use_bias"], + ) + + attention_embedding_size: int = schema_utils.PositiveInteger( + default=256, + description="The embedding size of the multi-head self attention layer.", + parameter_metadata=DECODER_METADATA["SequenceTaggerDecoder"]["attention_embedding_size"], + ) + + attention_num_heads: int = schema_utils.PositiveInteger( + default=8, + description="The number of attention heads in the multi-head self attention layer.", + parameter_metadata=DECODER_METADATA["SequenceTaggerDecoder"]["attention_num_heads"], + ) diff --git a/ludwig/schema/decoders/utils.py b/ludwig/schema/decoders/utils.py new file mode 100644 index 0000000..c5c570e --- /dev/null +++ b/ludwig/schema/decoders/utils.py @@ -0,0 +1,121 @@ +from dataclasses import Field +from typing import Any, TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MODEL_ECD, TYPE +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import DECODER_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json +from ludwig.utils.registry import Registry + +if TYPE_CHECKING: + from ludwig.schema.decoders.base import BaseDecoderConfig + + +decoder_config_registry = Registry() + + +@DeveloperAPI +def register_decoder_config(name: str, features: str | list[str], model_types: list[str] | None = None): + if model_types is None: + model_types = [MODEL_ECD] + + if isinstance(features, str): + features = [features] + + def wrap(cls): + for model_type in model_types: + for feature in features: + key = (model_type, feature) + feature_registry = decoder_config_registry.get(key, {}) + feature_registry[name] = cls + decoder_config_registry[key] = feature_registry + return cls + + return wrap + + +@DeveloperAPI +def get_decoder_cls(model_type: str, feature: str, name: str): + return decoder_config_registry[(model_type, feature)][name] + + +@DeveloperAPI +def get_decoder_classes(model_type: str, feature: str) -> dict[str, type["BaseDecoderConfig"]]: + return decoder_config_registry[(model_type, feature)] + + +@DeveloperAPI +def get_decoder_descriptions(model_type: str, feature_type: str): + """This function returns a dictionary of decoder descriptions available at the type selection. + + The process works as follows - 1) Get a dictionary of valid decoders from the decoder config registry, + but inverse the key/value pairs since we need to index `valid_decoders` later with an altered version + of the decoder config class name. 2) Loop through Decoder Metadata entries, if a metadata entry has a + decoder name that matches a valid decoder, add the description metadata to the output dictionary. + + Args: + model_type (str): The model type to get decoder descriptions for + feature_type (str): The feature type to get decoder descriptions for + Returns: + dict: A dictionary of decoder descriptions + """ + output = {} + valid_decoders = { + cls.module_name() if hasattr(cls, "module_name") else None: registered_name + for registered_name, cls in get_decoder_classes(model_type, feature_type).items() + } + + for k, v in DECODER_METADATA.items(): + if k in valid_decoders: + output[valid_decoders[k]] = convert_metadata_to_json(v[TYPE]) + + return output + + +@DeveloperAPI +def get_decoder_conds(decoder_classes: dict[str, type["BaseDecoderConfig"]]) -> list[dict[str, Any]]: + """Returns a JSON schema of conditionals to validate against decoder types for specific feature types.""" + conds = [] + for decoder_type, decoder_cls in decoder_classes.items(): + other_props = schema_utils.unload_jsonschema_from_config_class(decoder_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + decoder_cond = schema_utils.create_cond( + {"type": decoder_type}, + other_props, + ) + conds.append(decoder_cond) + return conds + + +@DeveloperAPI +def DecoderDataclassField(model_type: str, feature_type: str, default: str) -> Field: + """Custom dataclass field that when used inside a dataclass will allow the user to specify a decoder config. + + Returns: Initialized dataclass field that converts an untyped dict with params to a decoder config. + """ + decoder_registry = get_decoder_classes(model_type, feature_type) + + class DecoderSelection(schema_utils.TypeSelection): + def __init__(self): + super().__init__(registry=decoder_registry, default_value=default, allow_str_value=True) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return decoder_registry[key] + + def _jsonschema_type_mapping(self): + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": list(decoder_registry.keys()), + "enumDescriptions": get_decoder_descriptions(model_type, feature_type), + "default": default, + }, + }, + "title": "decoder_options", + "allOf": get_decoder_conds(decoder_registry), + } + + return DecoderSelection().get_default_field() diff --git a/ludwig/schema/defaults/__init__.py b/ludwig/schema/defaults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/schema/defaults/base.py b/ludwig/schema/defaults/base.py new file mode 100644 index 0000000..ba367ce --- /dev/null +++ b/ludwig/schema/defaults/base.py @@ -0,0 +1,7 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils + + +@DeveloperAPI +class BaseDefaultsConfig(schema_utils.LudwigBaseConfig): + """Base defaults config class.""" diff --git a/ludwig/schema/defaults/ecd.py b/ludwig/schema/defaults/ecd.py new file mode 100644 index 0000000..bd6d0bc --- /dev/null +++ b/ludwig/schema/defaults/ecd.py @@ -0,0 +1,55 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + AUDIO, + BAG, + BINARY, + CATEGORY, + DATE, + H3, + IMAGE, + NUMBER, + SEQUENCE, + SET, + TEXT, + TIMESERIES, + VECTOR, +) +from ludwig.schema import utils as schema_utils +from ludwig.schema.defaults.base import BaseDefaultsConfig +from ludwig.schema.defaults.utils import DefaultsDataclassField +from ludwig.schema.features.base import BaseFeatureConfig + + +@DeveloperAPI +class ECDDefaultsConfig(BaseDefaultsConfig): + audio: BaseFeatureConfig = DefaultsDataclassField(feature_type=AUDIO) + + bag: BaseFeatureConfig = DefaultsDataclassField(feature_type=BAG) + + binary: BaseFeatureConfig = DefaultsDataclassField(feature_type=BINARY) + + category: BaseFeatureConfig = DefaultsDataclassField(feature_type=CATEGORY) + + date: BaseFeatureConfig = DefaultsDataclassField(feature_type=DATE) + + h3: BaseFeatureConfig = DefaultsDataclassField(feature_type=H3) + + image: BaseFeatureConfig = DefaultsDataclassField(feature_type=IMAGE) + + number: BaseFeatureConfig = DefaultsDataclassField(feature_type=NUMBER) + + sequence: BaseFeatureConfig = DefaultsDataclassField(feature_type=SEQUENCE) + + set: BaseFeatureConfig = DefaultsDataclassField(feature_type=SET) + + text: BaseFeatureConfig = DefaultsDataclassField(feature_type=TEXT) + + timeseries: BaseFeatureConfig = DefaultsDataclassField(feature_type=TIMESERIES) + + vector: BaseFeatureConfig = DefaultsDataclassField(feature_type=VECTOR) + + +@DeveloperAPI +class ECDDefaultsField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(ECDDefaultsConfig) diff --git a/ludwig/schema/defaults/llm.py b/ludwig/schema/defaults/llm.py new file mode 100644 index 0000000..e96e53a --- /dev/null +++ b/ludwig/schema/defaults/llm.py @@ -0,0 +1,18 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import TEXT +from ludwig.schema import utils as schema_utils +from ludwig.schema.defaults.base import BaseDefaultsConfig +from ludwig.schema.defaults.utils import DefaultsDataclassField +from ludwig.schema.features.base import BaseFeatureConfig +from ludwig.schema.features.utils import llm_defaults_config_registry + + +@DeveloperAPI +class LLMDefaultsConfig(BaseDefaultsConfig): + text: BaseFeatureConfig = DefaultsDataclassField(feature_type=TEXT, defaults_registry=llm_defaults_config_registry) + + +@DeveloperAPI +class LLMDefaultsField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(LLMDefaultsConfig) diff --git a/ludwig/schema/defaults/utils.py b/ludwig/schema/defaults/utils.py new file mode 100644 index 0000000..b32f477 --- /dev/null +++ b/ludwig/schema/defaults/utils.py @@ -0,0 +1,66 @@ +from dataclasses import field + +import pydantic + +import ludwig.schema.utils as schema_utils +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema.features.utils import ecd_defaults_config_registry +from ludwig.utils.registry import Registry + + +@DeveloperAPI +def DefaultsDataclassField(feature_type: str, defaults_registry: Registry = ecd_defaults_config_registry): + """Custom dataclass field that when used inside a dataclass will allow the user to specify a nested default + config for a specific feature type. + + Returns: Initialized dataclass field that converts an untyped dict with params to a defaults config. + """ + + class DefaultConfigField(schema_utils.SchemaField): + """Custom field that deserializes a dict for a valid defaults config from the feature_registry and creates + a corresponding JSON schema for external usage.""" + + def _deserialize(self, value, attr, data, **kwargs): + if value is None: + return None + if isinstance(value, dict): + defaults_class = defaults_registry[feature_type] + try: + return defaults_class.model_validate(value) + except (TypeError, ConfigValidationError) as error: + raise ConfigValidationError(f"Invalid params: {value}, see `{attr}` definition. Error: {error}") + raise ConfigValidationError(f"Invalid params: {value}") + + def _jsonschema_type_mapping(self): + defaults_cls = defaults_registry[feature_type] + props = schema_utils.unload_jsonschema_from_config_class(defaults_cls)["properties"] + return { + "type": "object", + "properties": props, + "additionalProperties": False, + "title": "defaults_options", + } + + try: + defaults_cls = defaults_registry[feature_type] + try: + dump_default = defaults_cls.model_validate({}).to_dict() + except pydantic.ValidationError: + dump_default = {} + load_default = lambda: defaults_cls.model_validate({}) + + return field( + metadata={ + "marshmallow_field": DefaultConfigField( + allow_none=False, + dump_default=dump_default, + load_default=load_default, + ) + }, + default_factory=load_default, + ) + except Exception as e: + raise ConfigValidationError( + f"Unsupported feature type: {feature_type}. Allowed: {defaults_registry.keys()}. Details: {e}" + ) diff --git a/ludwig/schema/encoders/__init__.py b/ludwig/schema/encoders/__init__.py new file mode 100644 index 0000000..51d17be --- /dev/null +++ b/ludwig/schema/encoders/__init__.py @@ -0,0 +1,13 @@ +# Register all encoder schemas +import ludwig.schema.encoders.audio_encoders +import ludwig.schema.encoders.bag_encoders +import ludwig.schema.encoders.category_encoders +import ludwig.schema.encoders.date_encoders +import ludwig.schema.encoders.h3_encoders +import ludwig.schema.encoders.image +import ludwig.schema.encoders.mamba_hybrid +import ludwig.schema.encoders.number_encoders +import ludwig.schema.encoders.sequence_encoders +import ludwig.schema.encoders.set_encoders +import ludwig.schema.encoders.text_encoders +import ludwig.schema.encoders.timeseries_encoders # noqa diff --git a/ludwig/schema/encoders/audio_encoders.py b/ludwig/schema/encoders/audio_encoders.py new file mode 100644 index 0000000..e6f576d --- /dev/null +++ b/ludwig/schema/encoders/audio_encoders.py @@ -0,0 +1,105 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config + + +class PretrainedAudioEncoderConfig(BaseEncoderConfig): + """Base config for pretrained audio encoders.""" + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Download model weights from pre-trained model.", + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Whether to load weights from the saved checkpoint.", + ) + + trainable: bool = schema_utils.Boolean( + default=True, + description="Is the encoder trainable.", + ) + + reduce_output: str | None = schema_utils.StringOptions( + ["mean", "last", "cls_pooled"], + default="mean", + allow_none=True, + description=( + "How to reduce the output tensor along the time/sequence dimension. " + "'mean' averages over all time steps, 'last' takes the last time step, " + "'cls_pooled' takes the first time step (CLS token position), " + "None returns the full sequence." + ), + ) + + def is_pretrained(self) -> bool: + return self.use_pretrained + + +@DeveloperAPI +@register_encoder_config("wav2vec2", AUDIO) +class Wav2Vec2EncoderConfig(PretrainedAudioEncoderConfig): + """Config for the Wav2Vec2 pretrained audio encoder.""" + + @staticmethod + def module_name(): + return "Wav2Vec2Encoder" + + type: str = schema_utils.ProtectedString("wav2vec2", description="Type of encoder.") + + pretrained_model_name_or_path: str = schema_utils.String( + default="facebook/wav2vec2-base", + allow_none=False, + description=( + "Name or path of the pretrained model. Can be a HuggingFace model hub identifier " + "(e.g. 'facebook/wav2vec2-base', 'facebook/wav2vec2-large-xlsr-53') " + "or a local path to a saved model directory." + ), + ) + + +@DeveloperAPI +@register_encoder_config("whisper", AUDIO) +class WhisperEncoderConfig(PretrainedAudioEncoderConfig): + """Config for the Whisper pretrained audio encoder.""" + + @staticmethod + def module_name(): + return "WhisperEncoder" + + type: str = schema_utils.ProtectedString("whisper", description="Type of encoder.") + + pretrained_model_name_or_path: str = schema_utils.String( + default="openai/whisper-base", + allow_none=False, + description=( + "Name or path of the pretrained model. Can be a HuggingFace model hub identifier " + "(e.g. 'openai/whisper-base', 'openai/whisper-small', 'openai/whisper-medium', " + "'openai/whisper-large-v3') or a local path to a saved model directory." + ), + ) + + +@DeveloperAPI +@register_encoder_config("hubert", AUDIO) +class HuBERTEncoderConfig(PretrainedAudioEncoderConfig): + """Config for the HuBERT pretrained audio encoder.""" + + @staticmethod + def module_name(): + return "HuBERTEncoder" + + type: str = schema_utils.ProtectedString("hubert", description="Type of encoder.") + + pretrained_model_name_or_path: str = schema_utils.String( + default="facebook/hubert-base-ls960", + allow_none=False, + description=( + "Name or path of the pretrained model. Can be a HuggingFace model hub identifier " + "(e.g. 'facebook/hubert-base-ls960', 'facebook/hubert-large-ls960-ft') " + "or a local path to a saved model directory." + ), + ) diff --git a/ludwig/schema/encoders/bag_encoders.py b/ludwig/schema/encoders/bag_encoders.py new file mode 100644 index 0000000..05f5cb7 --- /dev/null +++ b/ludwig/schema/encoders/bag_encoders.py @@ -0,0 +1,142 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BAG +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +@DeveloperAPI +@register_encoder_config("embed", BAG) +class BagEmbedWeightedConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "BagEmbedWeighted" + + type: str = schema_utils.ProtectedString( + "embed", + description=ENCODER_METADATA["BagEmbedWeighted"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout probability for the embedding.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["activation"], + ) + + vocab: list[str] = schema_utils.List( + default=None, + description="Vocabulary of the encoder", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["vocab"], + ) + + representation: str = schema_utils.StringOptions( + ["dense", "sparse"], + default="dense", + description="The representation of the embedding. Either dense or sparse.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["representation"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=50, + description="The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for " + "dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size " + "is the number of different strings appearing in the training set in the input column (plus 1 for " + "the unknown token placeholder ).", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["embedding_size"], + ) + + force_embedding_size: bool = schema_utils.Boolean( + default=False, + description="Force the embedding size to be equal to the vocabulary size. This parameter has effect only if " + "representation is dense.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["force_embedding_size"], + ) + + embeddings_on_cpu: bool = schema_utils.Boolean( + default=False, + description="By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster " + "access, but in some cases the embedding matrix may be too large. This parameter forces the " + "placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, " + "slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["embeddings_on_cpu"], + ) + + embeddings_trainable: bool = schema_utils.Boolean( + default=True, + description="If true embeddings are trained during the training process, if false embeddings are fixed. It " + "may be useful when loading pretrained embeddings for avoiding fine tuning them. This parameter " + "has effect only when representation is dense as sparse one-hot encodings are not trainable.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["embeddings_trainable"], + ) + + pretrained_embeddings: str = schema_utils.String( + default=None, + allow_none=True, + description="By default dense embeddings are initialized randomly, but this parameter allows to specify a " + "path to a file containing embeddings in the GloVe format. When the file containing the " + "embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, " + "the others are discarded. If the vocabulary contains strings that have no match in the " + "embeddings file, their embeddings are initialized with the average of all other embedding plus " + "some random noise to make them different from each other. This parameter has effect only if " + "representation is dense.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["pretrained_embeddings"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["use_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["weights_initializer"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=10, + description="If output_size is not already specified in fc_layers this is the default output_size that will " + "be used for each layer. It indicates the size of the output of a fully connected layer.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=0, + description="This is the number of stacked fully connected layers that the input to the feature passes " + "through. Their output is projected in the feature's output space.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["BagEmbedWeighted"]["fc_layers"], + ) diff --git a/ludwig/schema/encoders/base.py b/ludwig/schema/encoders/base.py new file mode 100644 index 0000000..03e1178 --- /dev/null +++ b/ludwig/schema/encoders/base.py @@ -0,0 +1,111 @@ +from abc import ABC +from typing import TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, MODEL_ECD, MODEL_LLM, NUMBER, TEXT, TIMESERIES, VECTOR +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + +if TYPE_CHECKING: + from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig + + +@DeveloperAPI +class BaseEncoderConfig(schema_utils.LudwigBaseConfig, ABC): + """Base class for encoders.""" + + type: str + + skip: bool = schema_utils.Boolean( + False, + "[internal] Whether to skip encoder and use input as output.", + parameter_metadata=ENCODER_METADATA["BaseEncoder"]["skip"], + ) + + adapter: dict | None = schema_utils.Dict( + default=None, + allow_none=True, + description=( + "PEFT adapter configuration for parameter-efficient fine-tuning of pretrained encoders. " + "Supports any adapter type registered in Ludwig (lora, vera, loha, etc.). " + "Example: {type: lora, r: 8, alpha: 16, target_modules: [query, value]}. " + "Only applicable to pretrained encoders (HuggingFace text encoders, TIMM image encoders)." + ), + ) + + def set_fixed_preprocessing_params(self, model_type: str, preprocessing: "BasePreprocessingConfig"): + pass + + def is_pretrained(self) -> bool: + return False + + def can_cache_embeddings(self) -> bool: + return False + + +@DeveloperAPI +@register_encoder_config("passthrough", [TEXT], model_types=[MODEL_LLM]) +@register_encoder_config("passthrough", [BINARY, NUMBER, VECTOR], model_types=[MODEL_ECD]) +class PassthroughEncoderConfig(BaseEncoderConfig): + """PassthroughEncoderConfig is a dataclass that configures the parameters used for a passthrough encoder.""" + + @staticmethod + def module_name(): + return "PassthroughEncoder" + + type: str = schema_utils.ProtectedString( + "passthrough", + description=ENCODER_METADATA["PassthroughEncoder"]["type"].long_description, + ) + + +@DeveloperAPI +@register_encoder_config("dense", [BINARY, NUMBER, VECTOR, TIMESERIES]) +class DenseEncoderConfig(BaseEncoderConfig): + """DenseEncoderConfig is a dataclass that configures the parameters used for a dense encoder.""" + + @staticmethod + def module_name(): + return "DenseEncoder" + + type: str = schema_utils.ProtectedString( + "dense", + description=ENCODER_METADATA["DenseEncoder"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField() + + activation: str = schema_utils.ActivationOptions() + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the dense encoder.", + parameter_metadata=ENCODER_METADATA["DenseEncoder"]["input_size"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the output of the feature.", + parameter_metadata=ENCODER_METADATA["DenseEncoder"]["output_size"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["DenseEncoder"]["use_bias"], + ) + + bias_initializer: str | dict = common_fields.BiasInitializerField() + + weights_initializer: str | dict = common_fields.WeightsInitializerField() + + norm: str = common_fields.NormField() + + norm_params: dict = common_fields.NormParamsField() + + num_layers: int = common_fields.NumFCLayersField(default=1, non_zero=True) + + fc_layers: list[dict] = common_fields.FCLayersField() diff --git a/ludwig/schema/encoders/category_encoders.py b/ludwig/schema/encoders/category_encoders.py new file mode 100644 index 0000000..3c08829 --- /dev/null +++ b/ludwig/schema/encoders/category_encoders.py @@ -0,0 +1,158 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import CATEGORY, MODEL_ECD +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +@DeveloperAPI +@register_encoder_config("passthrough", CATEGORY, model_types=[MODEL_ECD]) +class CategoricalPassthroughEncoderConfig(BaseEncoderConfig): + """CategoricalPassthroughEncoderConfig is a dataclass that configures the parameters used for a categorical + passthrough encoder.""" + + @staticmethod + def module_name(): + return "CategoricalPassthroughEncoder" + + type: str = schema_utils.ProtectedString( + "passthrough", + description=ENCODER_METADATA["PassthroughEncoder"]["type"].long_description, + ) + + +@DeveloperAPI +@register_encoder_config("dense", CATEGORY) +class CategoricalEmbedConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "CategoricalEmbed" + + type: str = schema_utils.ProtectedString( + "dense", + description=ENCODER_METADATA["CategoricalEmbed"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField() + + vocab: list[str] = common_fields.VocabField() + + embedding_initializer: str = common_fields.EmbeddingInitializerField() + + embedding_size: int = common_fields.EmbeddingSizeField( + default=50, + description=( + "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for " + "dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size " + "is the number of different strings appearing in the training set in the column the feature is " + "named after (plus 1 for )." + ), + ) + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + +@DeveloperAPI +@register_encoder_config("sparse", CATEGORY) +class CategoricalSparseConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "CategorySparse" + + type: str = schema_utils.ProtectedString( + "sparse", + description=ENCODER_METADATA["CategoricalSparse"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField() + + vocab: list[str] = common_fields.VocabField() + + embedding_initializer: str = common_fields.EmbeddingInitializerField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + # TODO(travis): seems like this is not really a valid user option. We should probably just remove these + # params entirely and update the encoder implementation. + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField(default=False) + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + +@DeveloperAPI +@register_encoder_config("onehot", CATEGORY, model_types=[MODEL_ECD]) +class CategoricalOneHotEncoderConfig(BaseEncoderConfig): + """CategoricalOneHotEncoderConfig is a dataclass that configures the parameters used for a categorical onehot + encoder.""" + + type: str = schema_utils.ProtectedString( + "onehot", + description="Type of encoder.", + ) + + vocab: list[str] = common_fields.VocabField() + + def can_cache_embeddings(self) -> bool: + return True + + +@DeveloperAPI +@register_encoder_config("target", CATEGORY, model_types=[MODEL_ECD]) +class CategoricalTargetEncoderConfig(BaseEncoderConfig): + """Target encoding: encode categories by smoothed mean target value. + + Cite: Micci-Barreca, "A Preprocessing Scheme for High-Cardinality Categorical + Attributes in Classification and Prediction Problems", ACM SIGKDD 2001. + """ + + @staticmethod + def module_name(): + return "CategoricalTargetEncoder" + + type: str = schema_utils.ProtectedString( + "target", + description="Target encoding: maps each category to a learned embedding initialized from target statistics.", + ) + + vocab: list[str] = common_fields.VocabField() + + output_size: int = schema_utils.PositiveInteger( + default=1, + description="Size of the target encoding output per category.", + ) + + +@DeveloperAPI +@register_encoder_config("hash", CATEGORY, model_types=[MODEL_ECD]) +class CategoricalHashEncoderConfig(BaseEncoderConfig): + """Feature hashing encoder for ultra-high-cardinality categoricals. + + Cite: Weinberger et al., "Feature Hashing for Large Scale Multitask Learning", ICML 2009. + """ + + @staticmethod + def module_name(): + return "CategoricalHashEncoder" + + type: str = schema_utils.ProtectedString( + "hash", + description="Feature hashing: maps categories to a fixed number of hash buckets with learned embeddings.", + ) + + vocab: list[str] = common_fields.VocabField() + + num_hash_buckets: int = schema_utils.PositiveInteger( + default=1024, + description="Number of hash buckets to map categories into.", + ) + + embedding_size: int = common_fields.EmbeddingSizeField( + default=50, + description="Size of the embedding for each hash bucket.", + ) diff --git a/ludwig/schema/encoders/date_encoders.py b/ludwig/schema/encoders/date_encoders.py new file mode 100644 index 0000000..9514b13 --- /dev/null +++ b/ludwig/schema/encoders/date_encoders.py @@ -0,0 +1,173 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DATE +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +@DeveloperAPI +@register_encoder_config("embed", DATE) +class DateEmbedConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "DateEmbed" + + type: str = schema_utils.ProtectedString( + "embed", + description=ENCODER_METADATA["DateEmbed"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout probability for the embedding.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["activation"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["use_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["weights_initializer"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=10, + description="The maximum embedding size adopted.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["embedding_size"], + ) + + embeddings_on_cpu: bool = schema_utils.Boolean( + default=False, + description="Whether to force the placement of the embedding matrix in regular memory and have the CPU " + "resolve them.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["embeddings_on_cpu"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=10, + description="If an output_size is not already specified in fc_layers this is the default output_size that " + "will be used for each layer. It indicates the size of the output of a fully connected layer.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=0, + description="The number of stacked fully connected layers.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["num_fc_layers"], + ) + + # TODO (Connor): Add nesting logic for fc_layers, see fully_connected_module.py + fc_layers: list[dict] = schema_utils.DictList( + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["DateEmbed"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("wave", DATE) +class DateWaveConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "DateWave" + + type: str = schema_utils.ProtectedString( + "wave", + description=ENCODER_METADATA["DateWave"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout probability for the embedding.", + parameter_metadata=ENCODER_METADATA["DateWave"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["DateWave"]["activation"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["DateWave"]["use_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["DateWave"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["DateWave"]["weights_initializer"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=10, + description="If an output_size is not already specified in fc_layers this is the default output_size that " + "will be used for each layer. It indicates the size of the output of a fully connected layer.", + parameter_metadata=ENCODER_METADATA["DateWave"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["DateWave"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["DateWave"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of stacked fully connected layers.", + parameter_metadata=ENCODER_METADATA["DateWave"]["num_fc_layers"], + ) + + # TODO (Connor): Add nesting logic for fc_layers, see fully_connected_module.py + fc_layers: list[dict] = schema_utils.DictList( + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["DateWave"]["fc_layers"], + ) diff --git a/ludwig/schema/encoders/h3_encoders.py b/ludwig/schema/encoders/h3_encoders.py new file mode 100644 index 0000000..7bbf235 --- /dev/null +++ b/ludwig/schema/encoders/h3_encoders.py @@ -0,0 +1,319 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import H3 +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +@DeveloperAPI +@register_encoder_config("embed", H3) +class H3EmbedConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "H3Embed" + + type: str = schema_utils.ProtectedString( + "embed", + description=ENCODER_METADATA["H3Embed"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout probability for the embedding.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["activation"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["use_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["weights_initializer"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=10, + description="The maximum embedding size adopted.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["embedding_size"], + ) + + embeddings_on_cpu: bool = schema_utils.Boolean( + default=False, + description="Whether to force the placement of the embedding matrix in regular memory and have the CPU " + "resolve them.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["embeddings_on_cpu"], + ) + + reduce_output: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the output tensor along the `s` sequence length dimension if the rank of the " + "tensor is greater than 2.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["reduce_output"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=10, + description="If an output_size is not already specified in fc_layers this is the default output_size that " + "will be used for each layer. It indicates the size of the output of a fully connected layer.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=0, + description="The number of stacked fully connected layers.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["H3Embed"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("weighted_sum", H3) +class H3WeightedSumConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "H3WeightedSum" + + type: str = schema_utils.ProtectedString( + "weighted_sum", + description=ENCODER_METADATA["H3WeightedSum"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout probability for the embedding.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["activation"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["use_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["weights_initializer"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=10, + description="The maximum embedding size adopted.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["embedding_size"], + ) + + embeddings_on_cpu: bool = schema_utils.Boolean( + default=False, + description="Whether to force the placement of the embedding matrix in regular memory and have the CPU " + "resolve them.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["embeddings_on_cpu"], + ) + + should_softmax: bool = schema_utils.Boolean( + default=False, + description="Determines if the weights of the weighted sum should be passed though a softmax layer before " + "being used.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["should_softmax"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=10, + description="If an output_size is not already specified in fc_layers this is the default output_size that " + "will be used for each layer. It indicates the size of the output of a fully connected layer.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=0, + description="The number of stacked fully connected layers.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["H3WeightedSum"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("rnn", H3) +class H3RNNConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "H3RNN" + + type: str = schema_utils.ProtectedString( + "rnn", + description=ENCODER_METADATA["H3RNN"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout rate", + parameter_metadata=ENCODER_METADATA["H3RNN"]["dropout"], + ) + + recurrent_dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout rate for the recurrent state", + parameter_metadata=ENCODER_METADATA["H3RNN"]["recurrent_dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + default="tanh", + description="The activation function to use", + parameter_metadata=ENCODER_METADATA["H3RNN"]["activation"], + ) + + recurrent_activation: str = schema_utils.ActivationOptions( + default="sigmoid", + description="The activation function to use in the recurrent step", + parameter_metadata=ENCODER_METADATA["H3RNN"]["recurrent_activation"], + ) + + cell_type: str = schema_utils.StringOptions( + ["rnn", "lstm", "lstm_block", "ln", "lstm_cudnn", "gru", "gru_block", "gru_cudnn"], + default="rnn", + description="The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `lstm_block`, `lstm`, " + "`ln`, `lstm_cudnn`, `gru`, `gru_block`, `gru_cudnn`. For reference about the differences between " + "the cells please refer to PyTorch's documentation. We suggest to use the `block` variants on " + "CPU and the `cudnn` variants on GPU because of their increased speed. ", + parameter_metadata=ENCODER_METADATA["H3RNN"]["cell_type"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of stacked recurrent layers.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["num_layers"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=10, + description="The size of the hidden representation within the transformer block. It is usually the same as " + "the embedding_size, but if the two values are different, a projection layer will be added before " + "the first transformer block.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["hidden_size"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["use_bias"], + ) + + unit_forget_bias: bool = schema_utils.Boolean( + default=True, + description="If true, add 1 to the bias of the forget gate at initialization", + parameter_metadata=ENCODER_METADATA["H3RNN"]["unit_forget_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["weights_initializer"], + ) + + recurrent_initializer: str = schema_utils.InitializerOptions( + default="orthogonal", + description="The initializer for recurrent matrix weights", + parameter_metadata=ENCODER_METADATA["H3RNN"]["recurrent_initializer"], + ) + + reduce_output: str = schema_utils.ReductionOptions( + default="last", + description="How to reduce the output tensor along the `s` sequence length dimension if the rank of the " + "tensor is greater than 2.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["reduce_output"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=10, + description="The maximum embedding size adopted.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["embedding_size"], + ) + + embeddings_on_cpu: bool = schema_utils.Boolean( + default=False, + description="Whether to force the placement of the embedding matrix in regular memory and have the CPU " + "resolve them.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["embeddings_on_cpu"], + ) + + bidirectional: bool = schema_utils.Boolean( + default=False, + description="If true, two recurrent networks will perform encoding in the forward and backward direction and " + "their outputs will be concatenated.", + parameter_metadata=ENCODER_METADATA["H3RNN"]["bidirectional"], + ) diff --git a/ludwig/schema/encoders/image/__init__.py b/ludwig/schema/encoders/image/__init__.py new file mode 100644 index 0000000..0e9ad6a --- /dev/null +++ b/ludwig/schema/encoders/image/__init__.py @@ -0,0 +1,4 @@ +import ludwig.schema.encoders.image.base +import ludwig.schema.encoders.image.pretrained +import ludwig.schema.encoders.image.timm +import ludwig.schema.encoders.image.torchvision # noqa diff --git a/ludwig/schema/encoders/image/base.py b/ludwig/schema/encoders/image/base.py new file mode 100644 index 0000000..d6bf414 --- /dev/null +++ b/ludwig/schema/encoders/image/base.py @@ -0,0 +1,429 @@ +from typing import Any, TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import IMAGE +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA +from ludwig.utils.torch_utils import initializer_registry + +if TYPE_CHECKING: + from ludwig.schema.features.preprocessing.image import ImagePreprocessingConfig + + +class ImageEncoderConfig(BaseEncoderConfig): + def set_fixed_preprocessing_params(self, model_type: str, preprocessing: "ImagePreprocessingConfig"): + preprocessing.requires_equal_dimensions = False + preprocessing.height = None + preprocessing.width = None + + +@DeveloperAPI +@register_encoder_config("stacked_cnn", IMAGE) +class Stacked2DCNNConfig(ImageEncoderConfig): + @staticmethod + def module_name(): + return "Stacked2DCNN" + + type: str = schema_utils.ProtectedString( + "stacked_cnn", + description=ENCODER_METADATA["Stacked2DCNN"]["type"].long_description, + ) + + conv_dropout: int | None = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout rate", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["conv_dropout"], + ) + + conv_activation: str = schema_utils.ActivationOptions( + description="If an activation is not already specified in conv_layers this is the default activation that " + "will be used for each layer. It indicates the activation function applied to the output.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["conv_activation"], + ) + + height: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Height of the input image.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["height"], + ) + + width: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Width of the input image.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["width"], + ) + + num_channels: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of channels to use in the encoder. ", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["num_channels"], + ) + + out_channels: int | None = schema_utils.NonNegativeInteger( + default=32, + description="Indicates the number of filters, and by consequence the output channels of the 2d convolution. " + "If out_channels is not already specified in conv_layers this is the default out_channels that " + "will be used for each layer. ", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["out_channels"], + ) + + kernel_size: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=3, + description="An integer or pair of integers specifying the kernel size. A single integer specifies a square " + "kernel, while a pair of integers specifies the height and width of the kernel in that order (h, " + "w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for " + "each layer.", + field_options=[ + schema_utils.PositiveInteger(allow_none=False, description="", default=3), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["kernel_size"], + ) + + stride: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=1, + description="An integer or pair of integers specifying the stride of the convolution along the height and " + "width. If a stride is not already specified in conv_layers, specifies the default stride of the " + "2D convolutional kernel that will be used for each layer.", + field_options=[ + schema_utils.PositiveInteger(allow_none=False, description="", default=1), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["stride"], + ) + + padding_mode: str | None = schema_utils.StringOptions( + options=["zeros", "reflect", "replicate", "circular"], + default="zeros", + description="If padding_mode is not already specified in conv_layers, specifies the default padding_mode of " + "the 2D convolutional kernel that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["padding_mode"], + ) + + padding: int | tuple[int] | str | None = schema_utils.OneOfOptionsField( + default="valid", + allow_none=True, + description="An int, pair of ints (h, w), or one of ['valid', 'same'] specifying the padding used for" + "convolution kernels.", + field_options=[ + schema_utils.NonNegativeInteger(allow_none=True, description="", default=None), + schema_utils.List(list_type=int, allow_none=False), + schema_utils.StringOptions(options=["valid", "same"], default="valid", allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["padding"], + ) + + dilation: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=1, + allow_none=True, + description="An int or pair of ints specifying the dilation rate to use for dilated convolution. If dilation " + "is not already specified in conv_layers, specifies the default dilation of the 2D convolutional " + "kernel that will be used for each layer.", + field_options=[ + schema_utils.PositiveInteger(allow_none=True, description="", default=None), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["dilation"], + ) + + groups: int | None = schema_utils.PositiveInteger( + default=1, + description="Groups controls the connectivity between convolution inputs and outputs. When groups = 1, each " + "output channel depends on every input channel. When groups > 1, input and output channels are " + "divided into groups separate groups, where each output channel depends only on the inputs in its " + "respective input channel group. in_channels and out_channels must both be divisible by groups.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["groups"], + ) + + pool_function: str | None = schema_utils.StringOptions( + ["max", "average", "avg", "mean"], + default="max", + description="Pooling function to use.", + parameter_metadata=ENCODER_METADATA["conv_params"]["pool_function"], + ) + + pool_kernel_size: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=2, + allow_none=True, + description="An integer or pair of integers specifying the pooling size. If pool_kernel_size is not specified " + "in conv_layers this is the default value that will be used for each layer.", + field_options=[ + schema_utils.PositiveInteger(allow_none=True, description="", default=None), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["pool_kernel_size"], + ) + + pool_stride: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + description="An integer or pair of integers specifying the pooling stride, which is the factor by which the " + "pooling layer downsamples the feature map. Defaults to pool_kernel_size.", + field_options=[ + schema_utils.PositiveInteger(allow_none=True, description="", default=None), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["pool_stride"], + ) + + pool_padding: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=0, + allow_none=True, + description="An integer or pair of ints specifying pooling padding (h, w).", + field_options=[ + schema_utils.NonNegativeInteger(allow_none=True, description="", default=None), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["pool_padding"], + ) + + pool_dilation: int | tuple[int] | None = schema_utils.OneOfOptionsField( + default=1, + allow_none=True, + description="An integer or pair of ints specifying pooling dilation rate (h, w).", + field_options=[ + schema_utils.PositiveInteger(default=None, allow_none=True, description=""), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["pool_dilation"], + ) + + output_size: int | None = schema_utils.PositiveInteger( + default=128, + description="If output_size is not already specified in fc_layers this is the default output_size that will " + "be used for each layer. It indicates the size of the output of a fully connected layer. ", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["output_size"], + ) + + conv_use_bias: bool | None = schema_utils.Boolean( + default=True, + description="If bias not already specified in conv_layers, specifies if the 2D convolutional kernel should " + "have a bias term.", + ) + + conv_norm: str | None = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="If a norm is not already specified in conv_layers this is the default norm that will be used for " + "each layer. It indicates the normalization applied to the activations and can be null, " + "batch or layer.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["conv_norm"], + ) + + conv_norm_params: dict[str, Any] | None = schema_utils.Dict( + default=None, + description="Parameters used if conv_norm is either batch or layer. ", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["conv_norm_params"], + ) + + num_conv_layers: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of convolutional layers to use in the encoder. ", + parameter_metadata=ENCODER_METADATA["conv_params"]["num_conv_layers"], + ) + + conv_layers: list[dict] | None = schema_utils.DictList( + default=None, + description="List of convolutional layers to use in the encoder. ", + parameter_metadata=ENCODER_METADATA["conv_params"]["conv_layers"], + ) + + fc_dropout: float | None = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout rate", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_dropout"], + ) + + fc_activation: str | None = schema_utils.ActivationOptions( + description="If an activation is not already specified in fc_layers this is the default activation that will " + "be used for each layer. It indicates the activation function applied to the output.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_activation"], + ) + + fc_use_bias: bool | None = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_use_bias"], + ) + + fc_bias_initializer: str | None = schema_utils.StringOptions( + sorted(list(initializer_registry.keys())), + default="zeros", + description="Initializer for the bias vector.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_bias_initializer"], + ) + + fc_weights_initializer: str | None = schema_utils.StringOptions( + sorted(list(initializer_registry.keys())), + default="xavier_uniform", + description="Initializer for the weights matrix.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_weights_initializer"], + ) + + fc_norm: str | None = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="If a norm is not already specified in fc_layers this is the default norm that will be used for " + "each layer. It indicates the norm of the output and can be null, batch or layer.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_norm"], + ) + + fc_norm_params: dict[str, Any] | None = schema_utils.Dict( + default=None, + description="Parameters used if norm is either batch or layer. For information on parameters used with batch " + "see Torch's documentation on batch normalization or for layer see Torch's documentation on layer " + "normalization.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_norm_params"], + ) + + num_fc_layers: int | None | None = schema_utils.PositiveInteger( + default=1, + description="The number of stacked fully connected layers.", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["num_fc_layers"], + ) + + fc_layers: list[dict] | None | None = schema_utils.DictList( + default=None, + description="A list of dictionaries containing the parameters of all the fully connected layers. The length " + "of the list determines the number of stacked fully connected layers and the content of each " + "dictionary determines the parameters for a specific layer. The available parameters for each " + "layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and " + "weights_initializer. If any of those values is missing from the dictionary, the default one " + "specified as a parameter of the encoder will be used instead. ", + parameter_metadata=ENCODER_METADATA["Stacked2DCNN"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("mlp_mixer", IMAGE) +class MLPMixerConfig(ImageEncoderConfig): + @staticmethod + def module_name(): + return "MLPMixer" + + type: str = schema_utils.ProtectedString( + "mlp_mixer", + description=ENCODER_METADATA["MLPMixer"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout rate.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["dropout"], + ) + + height: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Height of the input image.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["height"], + ) + + width: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Width of the input image.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["width"], + ) + + num_channels: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of channels to use in the encoder. ", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["num_channels"], + ) + + patch_size: int = schema_utils.PositiveInteger( + default=16, + description="The image patch size. Each patch is patch_size² pixels. Must evenly divide the image width and " + "height.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["patch_size"], + ) + + embed_size: int = schema_utils.PositiveInteger( + default=512, + description="The patch embedding size, the output size of the mixer if avg_pool is true.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["embed_size"], + ) + + token_size: int = schema_utils.PositiveInteger( + default=2048, + description="The per-patch embedding size.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["token_size"], + ) + + channel_dim: int = schema_utils.PositiveInteger( + default=256, + description="Number of channels in hidden layer.", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["channel_dim"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=8, + description="The depth of the network (the number of Mixer blocks).", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["num_layers"], + ) + + avg_pool: bool = schema_utils.Boolean( + default=True, + description="If true, pools output over patch dimension, outputs a vector of shape (embed_size). If false, " + "the output tensor is of shape (n_patches, embed_size), where n_patches is img_height x img_width " + "/ patch_size².", + parameter_metadata=ENCODER_METADATA["MLPMixer"]["avg_pool"], + ) + + +@DeveloperAPI +@register_encoder_config("unet", IMAGE) +class UNetEncoderConfig(ImageEncoderConfig): + @staticmethod + def module_name(): + return "UNetEncoder" + + type: str = schema_utils.ProtectedString( + "unet", + description=ENCODER_METADATA["UNetEncoder"]["type"].long_description, + ) + + height: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Height of the input image.", + parameter_metadata=ENCODER_METADATA["UNetEncoder"]["height"], + ) + + width: int = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Width of the input image.", + parameter_metadata=ENCODER_METADATA["UNetEncoder"]["width"], + ) + + num_channels: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of channels in the input image. ", + parameter_metadata=ENCODER_METADATA["UNetEncoder"]["num_channels"], + ) + + conv_norm: str | None = schema_utils.StringOptions( + ["batch"], + default="batch", + allow_none=True, + description="This is the default norm that will be used for each double conv layer.It can be null or batch.", + parameter_metadata=ENCODER_METADATA["UNetEncoder"]["conv_norm"], + ) diff --git a/ludwig/schema/encoders/image/pretrained.py b/ludwig/schema/encoders/image/pretrained.py new file mode 100644 index 0000000..425466b --- /dev/null +++ b/ludwig/schema/encoders/image/pretrained.py @@ -0,0 +1,80 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import IMAGE +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config + + +class PretrainedImageEncoderConfig(BaseEncoderConfig): + """Base config for HuggingFace pretrained image encoders.""" + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use pretrained weights from HuggingFace.", + ) + trainable: bool = schema_utils.Boolean( + default=True, + description="Whether encoder parameters are trainable.", + ) + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description=( + "Are the pretrained encoder weights saved in this model's checkpoint? " + "Automatically set to True for trained models to prevent loading pretrained " + "encoder weights from the model hub." + ), + ) + + def is_pretrained(self) -> bool: + return self.use_pretrained + + +@DeveloperAPI +@register_encoder_config("clip", IMAGE) +class CLIPImageEncoderConfig(PretrainedImageEncoderConfig): + @staticmethod + def module_name(): + return "CLIPImageEncoder" + + type: str = schema_utils.ProtectedString( + "clip", + description="CLIP image encoder using a pretrained vision transformer from OpenAI.", + ) + pretrained_model_name_or_path: str = schema_utils.String( + default="openai/clip-vit-base-patch32", + description="HuggingFace model path or name for the CLIP vision model.", + ) + + +@DeveloperAPI +@register_encoder_config("dinov2", IMAGE) +class DINOv2ImageEncoderConfig(PretrainedImageEncoderConfig): + @staticmethod + def module_name(): + return "DINOv2ImageEncoder" + + type: str = schema_utils.ProtectedString( + "dinov2", + description="DINOv2 image encoder using self-supervised visual features from Meta.", + ) + pretrained_model_name_or_path: str = schema_utils.String( + default="facebook/dinov2-base", + description="HuggingFace model path or name for the DINOv2 model.", + ) + + +@DeveloperAPI +@register_encoder_config("siglip", IMAGE) +class SigLIPImageEncoderConfig(PretrainedImageEncoderConfig): + @staticmethod + def module_name(): + return "SigLIPImageEncoder" + + type: str = schema_utils.ProtectedString( + "siglip", + description="SigLIP image encoder using sigmoid loss for image-text pre-training from Google.", + ) + pretrained_model_name_or_path: str = schema_utils.String( + default="google/siglip-base-patch16-224", + description="HuggingFace model path or name for the SigLIP vision model.", + ) diff --git a/ludwig/schema/encoders/image/timm.py b/ludwig/schema/encoders/image/timm.py new file mode 100644 index 0000000..6b3dd83 --- /dev/null +++ b/ludwig/schema/encoders/image/timm.py @@ -0,0 +1,187 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import IMAGE +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +class TimmBaseConfig(BaseEncoderConfig): + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Download model weights from pretrained model.", + parameter_metadata=ENCODER_METADATA["TimmEncoder"]["use_pretrained"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Whether to use weights saved in the Ludwig checkpoint instead of pretrained weights.", + parameter_metadata=ENCODER_METADATA["TimmEncoder"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=True, + description="Whether the encoder parameters are trainable.", + parameter_metadata=ENCODER_METADATA["TimmEncoder"]["trainable"], + ) + + def is_pretrained(self) -> bool: + return self.use_pretrained + + +@DeveloperAPI +@register_encoder_config("timm", IMAGE) +class TimmEncoderConfig(TimmBaseConfig): + type: str = schema_utils.ProtectedString("timm", description="Type of encoder.") + + model_name: str = schema_utils.String( + default="caformer_s18", + description=( + "Name of the timm model to use. Any model from the timm library is supported. " + "See https://huggingface.co/docs/timm for available models." + ), + parameter_metadata=ENCODER_METADATA["TimmEncoder"]["model_name"], + ) + + +# Convenience aliases for MetaFormer variants with curated model_name options + +CAFORMER_MODELS = [ + "caformer_s18", + "caformer_s36", + "caformer_m36", + "caformer_b36", + "caformer_s18.sail_in22k_ft_in1k", + "caformer_s18.sail_in22k_ft_in1k_384", + "caformer_s36.sail_in22k_ft_in1k", + "caformer_s36.sail_in22k_ft_in1k_384", + "caformer_m36.sail_in22k_ft_in1k", + "caformer_m36.sail_in22k_ft_in1k_384", + "caformer_b36.sail_in22k_ft_in1k", + "caformer_b36.sail_in22k_ft_in1k_384", +] + +CONVFORMER_MODELS = [ + "convformer_s18", + "convformer_s36", + "convformer_m36", + "convformer_b36", + "convformer_s18.sail_in22k_ft_in1k", + "convformer_s18.sail_in22k_ft_in1k_384", + "convformer_s36.sail_in22k_ft_in1k", + "convformer_s36.sail_in22k_ft_in1k_384", + "convformer_m36.sail_in22k_ft_in1k", + "convformer_m36.sail_in22k_ft_in1k_384", + "convformer_b36.sail_in22k_ft_in1k", + "convformer_b36.sail_in22k_ft_in1k_384", +] + +POOLFORMER_MODELS = [ + "poolformerv2_s12", + "poolformerv2_s24", + "poolformerv2_s36", + "poolformerv2_m36", + "poolformerv2_m48", + "poolformer_s12", + "poolformer_s24", + "poolformer_s36", + "poolformer_m36", + "poolformer_m48", +] + + +@DeveloperAPI +@register_encoder_config("caformer", IMAGE) +class TimmCAFormerEncoderConfig(TimmBaseConfig): + type: str = schema_utils.ProtectedString("caformer", description="Type of encoder.") + + model_name: str = schema_utils.StringOptions( + CAFORMER_MODELS, + default="caformer_s18", + allow_none=False, + description=( + "CAFormer model variant. Hybrid Conv+Attention MetaFormer achieving SOTA accuracy. " + "Variants with '.sail_in22k_ft_in1k' are pretrained on ImageNet-21K and finetuned on ImageNet-1K. " + "Variants with '_384' use 384x384 input resolution." + ), + parameter_metadata=ENCODER_METADATA["TimmCAFormerEncoder"]["model_name"], + ) + + +@DeveloperAPI +@register_encoder_config("convformer", IMAGE) +class TimmConvFormerEncoderConfig(TimmBaseConfig): + type: str = schema_utils.ProtectedString("convformer", description="Type of encoder.") + + model_name: str = schema_utils.StringOptions( + CONVFORMER_MODELS, + default="convformer_s18", + allow_none=False, + description=( + "ConvFormer model variant. Pure CNN MetaFormer that outperforms ConvNeXt. " + "Variants with '.sail_in22k_ft_in1k' are pretrained on ImageNet-21K and finetuned on ImageNet-1K." + ), + parameter_metadata=ENCODER_METADATA["TimmConvFormerEncoder"]["model_name"], + ) + + +@DeveloperAPI +@register_encoder_config("poolformer", IMAGE) +class TimmPoolFormerEncoderConfig(TimmBaseConfig): + type: str = schema_utils.ProtectedString("poolformer", description="Type of encoder.") + + model_name: str = schema_utils.StringOptions( + POOLFORMER_MODELS, + default="poolformerv2_s12", + allow_none=False, + description=( + "PoolFormer model variant. MetaFormer using simple average pooling as token mixer. " + "V2 variants use StarReLU activation and improved training recipe." + ), + parameter_metadata=ENCODER_METADATA["TimmPoolFormerEncoder"]["model_name"], + ) + + +CONVNEXTV2_MODELS = [ + "convnextv2_atto", + "convnextv2_femto", + "convnextv2_pico", + "convnextv2_nano", + "convnextv2_tiny", + "convnextv2_base", + "convnextv2_large", + "convnextv2_huge", + "convnextv2_atto.fcmae_ft_in1k", + "convnextv2_femto.fcmae_ft_in1k", + "convnextv2_pico.fcmae_ft_in1k", + "convnextv2_nano.fcmae_ft_in1k", + "convnextv2_tiny.fcmae_ft_in1k", + "convnextv2_base.fcmae_ft_in1k", + "convnextv2_large.fcmae_ft_in1k", + "convnextv2_huge.fcmae_ft_in1k", + "convnextv2_base.fcmae_ft_in22k_in1k", + "convnextv2_large.fcmae_ft_in22k_in1k", + "convnextv2_huge.fcmae_ft_in22k_in1k", + "convnextv2_base.fcmae_ft_in22k_in1k_384", + "convnextv2_large.fcmae_ft_in22k_in1k_384", + "convnextv2_huge.fcmae_ft_in22k_in1k_384", +] + + +@DeveloperAPI +@register_encoder_config("convnextv2", IMAGE) +class TimmConvNeXtV2EncoderConfig(TimmBaseConfig): + type: str = schema_utils.ProtectedString("convnextv2", description="Type of encoder.") + + model_name: str = schema_utils.StringOptions( + CONVNEXTV2_MODELS, + default="convnextv2_base", + allow_none=False, + description=( + "ConvNeXt V2 model variant. Improved ConvNeXt with Global Response Normalization (GRN) " + "and FCMAE pre-training. Variants with '.fcmae_ft_in1k' are fine-tuned on ImageNet-1K. " + "Variants with '.fcmae_ft_in22k_in1k' are pre-trained on ImageNet-22K and fine-tuned on " + "ImageNet-1K. Variants with '_384' use 384x384 input resolution." + ), + parameter_metadata=ENCODER_METADATA["TimmConvNeXtV2Encoder"]["model_name"], + ) diff --git a/ludwig/schema/encoders/image/torchvision.py b/ludwig/schema/encoders/image/torchvision.py new file mode 100644 index 0000000..4ab1604 --- /dev/null +++ b/ludwig/schema/encoders/image/torchvision.py @@ -0,0 +1,375 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import IMAGE +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +class TVBaseEncoderConfig(BaseEncoderConfig): + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Download model weights from pre-trained model.", + parameter_metadata=ENCODER_METADATA["TVBaseEncoder"]["use_pretrained"], + ) + + model_cache_dir: str | None = schema_utils.String( + default=None, + allow_none=True, + description="Directory path to cache pretrained model weights.", + parameter_metadata=ENCODER_METADATA["TVBaseEncoder"]["model_cache_dir"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Whether to save the weights in the checkpoint.", + parameter_metadata=ENCODER_METADATA["TVBaseEncoder"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=True, + description="Is the encoder trainable.", + parameter_metadata=ENCODER_METADATA["TVBaseEncoder"]["trainable"], + ) + + def is_pretrained(self) -> bool: + return self.use_pretrained + + +@DeveloperAPI +@register_encoder_config("alexnet", IMAGE) +class TVAlexNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("alexnet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["base"], + default="base", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVAlexNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("convnext", IMAGE) +class TVConvNeXtEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("convnext", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["tiny", "small", "base", "large"], + default="base", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVConvNeXtEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("densenet", IMAGE) +class TVDenseNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("densenet", description="Type of encoder.") + + model_variant: int = schema_utils.IntegerOptions( + [121, 161, 169, 201], + default=121, + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVDenseNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("efficientnet", IMAGE) +class TVEfficientNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("efficientnet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "v2_s", + "v2_m", + "v2_l", + ], + default="b0", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVEfficientNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("googlenet", IMAGE) +class TVGoogLeNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("googlenet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["base"], + default="base", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVGoogLeNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("inceptionv3", IMAGE) +class TVInceptionV3EncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("inceptionv3", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["base"], + default="base", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVGoogLeNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("maxvit", IMAGE) +class TVMaxVitEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("maxvit", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["t"], + default="t", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVMNASNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("mnasnet", IMAGE) +class TVMNASNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("mnasnet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["0_5", "0_75", "1_0", "1_3"], + default="0_5", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVMNASNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("mobilenetv2", IMAGE) +class TVMobileNetV2EncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("mobilenetv2", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["base"], + default="base", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVMobileNetV2Encoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("mobilenetv3", IMAGE) +class TVMobileNetV3EncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("mobilenetv3", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "small", + "large", + ], + default="small", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVMobileNetV3Encoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("regnet", IMAGE) +class TVRegNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("regnet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "x_1_6gf", + "x_16gf", + "x_32gf", + "x_3_2gf", + "x_400mf", + "x_800mf", + "x_8gf", + "y_128gf", + "y_16gf", + "y_1_6gf", + "y_32gf", + "y_3_2gf", + "y_400mf", + "y_800mf", + "y_8gf", + ], + default="x_1_6gf", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVRegNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("resnet", IMAGE) +class TVResNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("resnet", description="Type of encoder.") + + model_variant: int = schema_utils.IntegerOptions( + [18, 34, 50, 101, 152], + default=50, + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVResNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("resnext", IMAGE) +class TVResNeXtEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("resnext", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + ["50_32x4d", "101_32x8d", "101_64x4d"], + default="50_32x4d", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVResNeXtEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("shufflenet_v2", IMAGE) +class TVShuffleNetV2EncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("shufflenet_v2", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "x0_5", + "x1_0", + "x1_5", + "x2_0", + ], + default="x0_5", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVShuffleNetV2Encoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("squeezenet", IMAGE) +class TVSqueezeNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("squeezenet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "1_0", + "1_1", + ], + default="1_0", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVSqueezeNetEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("swin_transformer", IMAGE) +class TVSwinTransformerEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("swin_transformer", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "t", + "s", + "b", + ], + default="t", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVSwinTransformerEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("vit", IMAGE) +class TVViTEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("vit", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "b_16", + "b_32", + "l_16", + "l_32", + "h_14", + ], + default="b_16", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVViTEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("vgg", IMAGE) +class TVVGGEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("vgg", description="Type of encoder.") + + model_variant: int | str = schema_utils.OneOfOptionsField( + default=11, + description="Pretrained model variant to use.", + field_options=[ + schema_utils.IntegerOptions( + [ + 11, + 13, + 16, + 19, + ], + default=11, + allow_none=False, + ), + schema_utils.StringOptions( + [ + "11_bn", + "13_bn", + "16_bn", + "19_bn", + ], + default="11_bn", + allow_none=False, + ), + ], + allow_none=False, + parameter_metadata=ENCODER_METADATA["TVVGGEncoder"]["model_variant"], + ) + + +@DeveloperAPI +@register_encoder_config("wide_resnet", IMAGE) +class TVWideResNetEncoderConfig(TVBaseEncoderConfig): + type: str = schema_utils.ProtectedString("wide_resnet", description="Type of encoder.") + + model_variant: str = schema_utils.StringOptions( + [ + "50_2", + "101_2", + ], + default="50_2", + allow_none=False, + description="Pretrained model variant to use.", + parameter_metadata=ENCODER_METADATA["TVViTEncoder"]["model_variant"], + ) diff --git a/ludwig/schema/encoders/mamba_hybrid.py b/ludwig/schema/encoders/mamba_hybrid.py new file mode 100644 index 0000000..8bbd4db --- /dev/null +++ b/ludwig/schema/encoders/mamba_hybrid.py @@ -0,0 +1,133 @@ +"""Schemas for Mamba-2 and Jamba encoders (Phase 6.6.2).""" + +from __future__ import annotations + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, SEQUENCE, TEXT, TIMESERIES +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.sequence_encoders import SequenceEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config + + +@DeveloperAPI +@register_encoder_config("mamba2", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class Mamba2EncoderConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "Mamba2Encoder" + + type: str = schema_utils.ProtectedString( + "mamba2", + description=( + "Mamba-2 (Dao & Gu, 2024) multi-head selective state space encoder. Linear-time " + "in sequence length with per-head scalar decay; pure-PyTorch approximation of the " + "SSD formulation — no mamba_ssm CUDA kernel required." + ), + ) + + dropout: float = common_fields.DropoutField(default=0.1, description="Dropout rate.") + max_sequence_length: int = common_fields.MaxSequenceLengthField() + representation: str = common_fields.RepresentationField() + vocab: list = common_fields.VocabField() + embedding_size: int = common_fields.EmbeddingSizeField() + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + reduce_output: str = common_fields.ReduceOutputField(default="mean") + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings.", + ) + + d_model: int = schema_utils.PositiveInteger( + default=256, + description="Hidden width of each Mamba-2 block.", + ) + n_layers: int = schema_utils.PositiveInteger( + default=4, + description="Number of stacked Mamba-2 blocks.", + ) + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of SSD heads. `d_model * expand_factor` must be divisible by `num_heads`.", + ) + d_conv: int = schema_utils.PositiveInteger( + default=4, + description="Width of the depthwise 1D convolution inside each block.", + ) + expand_factor: int = schema_utils.PositiveInteger( + default=2, + description="Inner expansion factor for each block.", + ) + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output feature width emitted by the encoder.", + ) + + +@DeveloperAPI +@register_encoder_config("jamba", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class JambaEncoderConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "JambaEncoder" + + type: str = schema_utils.ProtectedString( + "jamba", + description=( + "Jamba-style hybrid encoder (Lieber et al., 2024) interleaving Mamba-2 SSM blocks " + "with TransformerEncoderLayer attention blocks. Every `attention_every_k`-th layer " + "is attention; the rest are SSM." + ), + ) + + dropout: float = common_fields.DropoutField(default=0.1, description="Dropout rate.") + max_sequence_length: int = common_fields.MaxSequenceLengthField() + representation: str = common_fields.RepresentationField() + vocab: list = common_fields.VocabField() + embedding_size: int = common_fields.EmbeddingSizeField() + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + reduce_output: str = common_fields.ReduceOutputField(default="mean") + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings.", + ) + + d_model: int = schema_utils.PositiveInteger( + default=256, + description="Hidden width of every block — SSM and attention share the same d_model.", + ) + n_layers: int = schema_utils.PositiveInteger( + default=8, + description="Total number of stacked blocks (SSM + attention combined).", + ) + attention_every_k: int = schema_utils.PositiveInteger( + default=4, + description=( + "Every `attention_every_k`-th block is attention, the remainder are SSM. " + "Default 4 gives a 1:3 attention:SSM ratio matching the Jamba paper." + ), + ) + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of attention heads (and SSD heads, shared).", + ) + ffn_size: int = schema_utils.PositiveInteger( + default=1024, + description="Feed-forward width inside each attention block.", + ) + d_conv: int = schema_utils.PositiveInteger( + default=4, + description="Width of the depthwise 1D convolution inside each SSM block.", + ) + expand_factor: int = schema_utils.PositiveInteger( + default=2, + description="Inner expansion factor inside each SSM block.", + ) + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output feature width emitted by the encoder.", + ) diff --git a/ludwig/schema/encoders/number_encoders.py b/ludwig/schema/encoders/number_encoders.py new file mode 100644 index 0000000..bda6f8f --- /dev/null +++ b/ludwig/schema/encoders/number_encoders.py @@ -0,0 +1,112 @@ +"""Schema configs for number-specific encoders (PLE, Periodic).""" + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import NUMBER +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config + + +@DeveloperAPI +@register_encoder_config("ple", [NUMBER]) +class PLEEncoderConfig(BaseEncoderConfig): + """Piecewise Linear Encoding for numerical features. + + Computes quantile-based bin edges from training data, then produces a piecewise-linear interpolation vector per + input value. Most impactful improvement for tabular deep learning accuracy. + + Based on Gorishniy et al., "On Embeddings for Numerical Features in Tabular Deep Learning", NeurIPS 2022. + """ + + @staticmethod + def module_name(): + return "PLEEncoder" + + type: str = schema_utils.ProtectedString( + "ple", + description="Piecewise Linear Encoding: quantile-based binning with learned projection.", + ) + + num_bins: int = schema_utils.PositiveInteger( + default=64, + description="Number of quantile bins for piecewise linear encoding.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the output embedding.", + ) + + # Internal field: set from training data metadata, not user-configurable + ple_bin_edges: list[float] | None = schema_utils.List( + default=None, + description="[Internal] Quantile bin edges computed from training data. Set automatically.", + ) + + +@DeveloperAPI +@register_encoder_config("periodic", [NUMBER]) +class PeriodicEncoderConfig(BaseEncoderConfig): + """Periodic encoding for numerical features using learned sinusoidal features. + + Based on Gorishniy et al., "On Embeddings for Numerical Features in Tabular Deep Learning", NeurIPS 2022. + """ + + @staticmethod + def module_name(): + return "PeriodicEncoder" + + type: str = schema_utils.ProtectedString( + "periodic", + description="Periodic encoding: learned sin(2*pi*f*x + phi) features with projection.", + ) + + num_frequencies: int = schema_utils.PositiveInteger( + default=64, + description="Number of learnable sinusoidal frequencies.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the output embedding.", + ) + + sigma: float = schema_utils.Float( + default=1.0, + description="Standard deviation for initializing frequency parameters.", + ) + + +@DeveloperAPI +@register_encoder_config("bins", [NUMBER]) +class BinsEncoderConfig(BaseEncoderConfig): + """Binning encoder: discretize numbers into equal-width or equal-frequency bins. + + Simpler alternative to PLE. Good for small/medium datasets where PLE may overfit. + Each bin gets a learned embedding. + """ + + @staticmethod + def module_name(): + return "BinsEncoder" + + type: str = schema_utils.ProtectedString( + "bins", + description="Binning encoder: discretize numbers into bins with learned embeddings.", + ) + + num_bins: int = schema_utils.PositiveInteger( + default=32, + description="Number of bins to discretize numeric values into.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the output embedding per bin.", + ) + + # Internal field: set from training data metadata, not user-configurable + bins_bin_edges: list[float] | None = schema_utils.List( + default=None, + description="[Internal] Bin edges computed from training data. Set automatically.", + ) diff --git a/ludwig/schema/encoders/sequence_encoders.py b/ludwig/schema/encoders/sequence_encoders.py new file mode 100644 index 0000000..44e4deb --- /dev/null +++ b/ludwig/schema/encoders/sequence_encoders.py @@ -0,0 +1,987 @@ +from dataclasses import Field +from typing import TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, SEQUENCE, TEXT, TIMESERIES +from ludwig.schema import common_fields +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + +if TYPE_CHECKING: + from ludwig.schema.features.preprocessing.sequence import SequencePreprocessingConfig + +CONV_LAYERS_DESCRIPTION = """ +A list of dictionaries containing the parameters of all the convolutional layers. +The length of the list determines the number of stacked convolutional layers and the content of each dictionary +determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, +`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`, +`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is +missing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both +`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value +`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null}, +{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`. +""" + +NUM_CONV_LAYERS_DESCRIPTION = "The number of stacked convolutional layers when `conv_layers` is `null`." + + +def NumFiltersField(default: int = 256) -> Field: + return schema_utils.PositiveInteger( + default=default, + description="Number of filters, and by consequence number of output channels of the 1d convolution.", + parameter_metadata=ENCODER_METADATA["conv_params"]["num_filters"], + ) + + +def FilterSizeField(default: int = 3) -> Field: + return schema_utils.PositiveInteger( + default=default, + description="Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + parameter_metadata=ENCODER_METADATA["conv_params"]["filter_size"], + ) + + +def PoolFunctionField(default: str = "max") -> Field: + return schema_utils.ReductionOptions( + default=default, + description=( + "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or " + "`mean` will compute the mean value" + ), + parameter_metadata=ENCODER_METADATA["conv_params"]["pool_function"], + ) + + +def PoolSizeField(default: int | None = None) -> Field: + return schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=( + "The default pool_size that will be used for each layer. If a pool_size is not already specified " + "in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of " + "the max pooling that will be performed along the `s` sequence dimension after the convolution operation." + ), + parameter_metadata=ENCODER_METADATA["conv_params"]["pool_size"], + ) + + +@DeveloperAPI +class SequenceEncoderConfig(BaseEncoderConfig): + """Base class for sequence encoders.""" + + def set_fixed_preprocessing_params(self, model_type: str, preprocessing: "SequencePreprocessingConfig"): + if isinstance(preprocessing, dict): + preprocessing["cache_encoder_embeddings"] = False + else: + preprocessing.cache_encoder_embeddings = False + + +@DeveloperAPI +@register_encoder_config("passthrough", [TIMESERIES]) +class SequencePassthroughConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "SequencePassthrough" + + type: str = schema_utils.ProtectedString( + "passthrough", + description=ENCODER_METADATA["SequencePassthrough"]["type"].long_description, + ) + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + encoding_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The size of the encoding vector, or None if sequence elements are scalars.", + parameter_metadata=ENCODER_METADATA["SequencePassthrough"]["encoding_size"], + ) + + reduce_output: str = common_fields.ReduceOutputField(default=None) + + +@DeveloperAPI +@register_encoder_config("embed", [SEQUENCE, TEXT]) +class SequenceEmbedConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "SequenceEmbed" + + type: str = schema_utils.ProtectedString( + "embed", + description=ENCODER_METADATA["SequenceEmbed"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(description="Dropout rate applied to the embedding.") + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + weights_initializer: str = common_fields.WeightsInitializerField(default="uniform") + + reduce_output: str = common_fields.ReduceOutputField() + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + +@DeveloperAPI +@register_encoder_config("parallel_cnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class ParallelCNNConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "ParallelCNN" + + type: str = schema_utils.ProtectedString( + "parallel_cnn", + description=ENCODER_METADATA["ParallelCNN"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(description="Dropout rate applied to the embedding.") + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer." + ) + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["use_bias"], + ) + + bias_initializer: str = common_fields.BiasInitializerField() + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="Whether to embed the input sequence.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["should_embed"], + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField() + + num_conv_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=NUM_CONV_LAYERS_DESCRIPTION, + parameter_metadata=ENCODER_METADATA["conv_params"]["num_conv_layers"], + ) + + conv_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for conv_layers + default=None, + description=CONV_LAYERS_DESCRIPTION, + parameter_metadata=ENCODER_METADATA["conv_params"]["conv_layers"], + ) + + num_filters: int = NumFiltersField() + + filter_size: int = FilterSizeField() + + pool_function: str = PoolFunctionField() + + pool_size: int = PoolSizeField() + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of parallel fully connected layers to use.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["ParallelCNN"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("stacked_cnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedCNNConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "StackedCNN" + + type: str = schema_utils.ProtectedString( + "stacked_cnn", + description=ENCODER_METADATA["StackedCNN"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(description="Dropout rate applied to the embedding.") + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer." + ) + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + num_conv_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=NUM_CONV_LAYERS_DESCRIPTION, + parameter_metadata=ENCODER_METADATA["conv_params"]["num_conv_layers"], + ) + + conv_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for conv_layers + default=None, + description=CONV_LAYERS_DESCRIPTION, + parameter_metadata=ENCODER_METADATA["conv_params"]["conv_layers"], + ) + + num_filters: int = NumFiltersField() + + filter_size: int = FilterSizeField() + + pool_function: str = PoolFunctionField() + + pool_size: int = PoolSizeField() + + strides: int = schema_utils.PositiveInteger( + default=1, + description="Stride length of the convolution.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["strides"], + ) + + padding: str = schema_utils.StringOptions( + ["valid", "same"], + default="same", + description="Padding to use.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["padding"], + ) + + dilation_rate: int = schema_utils.PositiveInteger( + default=1, + description="Dilation rate to use for dilated convolution.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["dilation_rate"], + ) + + pool_strides: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Factor to scale down.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["pool_strides"], + ) + + pool_padding: str = schema_utils.StringOptions( + ["valid", "same"], + default="same", + description="Padding to use.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["pool_padding"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["use_bias"], + ) + + bias_initializer: str = common_fields.BiasInitializerField() + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="Whether to embed the input sequence.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["should_embed"], + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField() + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of parallel fully connected layers to use.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["StackedCNN"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("stacked_parallel_cnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedParallelCNNConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "StackedParallelCNN" + + type: str = schema_utils.ProtectedString( + "stacked_parallel_cnn", + description=ENCODER_METADATA["StackedParallelCNN"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(description="Dropout rate applied to the embedding.") + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer." + ) + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + num_stacked_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="If stacked_layers is null, this is the number of elements in the stack of parallel convolutional " + "layers. ", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["num_stacked_layers"], + ) + + stacked_layers: list[dict] = schema_utils.DictList( + default=None, + description="a nested list of lists of dictionaries containing the parameters of the stack of parallel " + "convolutional layers. The length of the list determines the number of stacked parallel " + "convolutional layers, length of the sub-lists determines the number of parallel conv layers and " + "the content of each dictionary determines the parameters for a specific layer. ", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["stacked_layers"], + ) + + num_filters: int = NumFiltersField() + + filter_size: int = FilterSizeField() + + pool_function: str = PoolFunctionField() + + pool_size: int = PoolSizeField() + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["use_bias"], + ) + + bias_initializer: str = common_fields.BiasInitializerField() + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["should_embed"], + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField() + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of parallel fully connected layers to use.", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["StackedParallelCNN"]["fc_layers"], + ) + + +@DeveloperAPI +@register_encoder_config("rnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedRNNConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "StackedRNN" + + type: str = schema_utils.ProtectedString( + "rnn", + description=ENCODER_METADATA["StackedRNN"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(description="Dropout rate.") + + recurrent_dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout rate for the recurrent state", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["recurrent_dropout"], + ) + + activation: str = schema_utils.ActivationOptions(default="tanh", description="The default activation function.") + + recurrent_activation: str = schema_utils.ActivationOptions( + default="sigmoid", + description="The activation function to use in the recurrent step", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["recurrent_activation"], + ) + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + cell_type: str = schema_utils.StringOptions( + ["rnn", "lstm", "gru"], + default="rnn", + description="The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference " + "about the differences between the cells please refer to " + "[torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["cell_type"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of stacked recurrent layers.", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["num_layers"], + ) + + state_size: int = schema_utils.PositiveInteger( + default=256, + description="The size of the state of the rnn.", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["state_size"], + ) + + bidirectional: bool = schema_utils.Boolean( + default=False, + description="If true, two recurrent networks will perform encoding in the forward and backward direction and " + "their outputs will be concatenated.", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["bidirectional"], + ) + + unit_forget_bias: bool = schema_utils.Boolean( + default=True, + description="If true, add 1 to the bias of the forget gate at initialization", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["unit_forget_bias"], + ) + + recurrent_initializer: str = schema_utils.InitializerOptions( + default="orthogonal", + description="The initializer for recurrent matrix weights", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["recurrent_initializer"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["use_bias"], + ) + + bias_initializer: str = common_fields.BiasInitializerField() + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["should_embed"], + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField(default="last") + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedRNN"]["output_size"], + ) + + norm: str = common_fields.NormField(description="The default norm that will be used for each layer.") + + norm_params: dict = common_fields.NormParamsField() + + num_fc_layers: int = common_fields.NumFCLayersField(description="Number of parallel fully connected layers to use.") + + fc_activation: str = schema_utils.ActivationOptions() + + fc_dropout: float = common_fields.DropoutField() + + fc_layers: list[dict] = common_fields.FCLayersField() + + +@DeveloperAPI +@register_encoder_config("cnnrnn", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class StackedCNNRNNConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "StackedCNNRNN" + + type: str = schema_utils.ProtectedString( + "cnnrnn", + description=ENCODER_METADATA["StackedCNNRNN"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(description="Dropout rate.") + + recurrent_dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout rate for the recurrent state", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["recurrent_dropout"], + ) + + conv_dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout rate for the convolutional layers", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["conv_dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + default="tanh", description="The default activation function to use." + ) + + recurrent_activation: str = schema_utils.ActivationOptions( + default="sigmoid", + description="The activation function to use in the recurrent step", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["recurrent_activation"], + ) + + conv_activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each convolutional layer.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["conv_activation"], + ) + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + cell_type: str = schema_utils.StringOptions( + ["rnn", "lstm", "gru"], + default="rnn", + description="The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference " + "about the differences between the cells please refer to " + "[torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["cell_type"], + ) + + num_conv_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=NUM_CONV_LAYERS_DESCRIPTION, + parameter_metadata=ENCODER_METADATA["conv_params"]["num_conv_layers"], + ) + + conv_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for conv_layers + default=None, + description=CONV_LAYERS_DESCRIPTION, + parameter_metadata=ENCODER_METADATA["conv_params"]["conv_layers"], + ) + + num_filters: int = NumFiltersField() + + filter_size: int = FilterSizeField(default=5) + + pool_function: str = PoolFunctionField() + + pool_size: int = PoolSizeField(default=2) + + strides: int = schema_utils.PositiveInteger( + default=1, + description="Stride length of the convolution.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["strides"], + ) + + padding: str = schema_utils.StringOptions( + ["valid", "same"], + default="same", + description="Padding to use.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["padding"], + ) + + dilation_rate: int = schema_utils.PositiveInteger( + default=1, + description="Dilation rate to use for dilated convolution.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["dilation_rate"], + ) + + pool_strides: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Factor to scale down.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["pool_strides"], + ) + + pool_padding: str = schema_utils.StringOptions( + ["valid", "same"], + default="same", + description="Padding to use.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["pool_padding"], + ) + + num_rec_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of stacked recurrent layers.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["num_rec_layers"], + ) + + state_size: int = schema_utils.PositiveInteger( + default=256, + description="The size of the state of the rnn.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["state_size"], + ) + + bidirectional: bool = schema_utils.Boolean( + default=False, + description="If true, two recurrent networks will perform encoding in the forward and backward direction and " + "their outputs will be concatenated.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["bidirectional"], + ) + + unit_forget_bias: bool = schema_utils.Boolean( + default=True, + description="If true, add 1 to the bias of the forget gate at initialization", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["unit_forget_bias"], + ) + + recurrent_initializer: str = schema_utils.InitializerOptions( + default="orthogonal", + description="The initializer for recurrent matrix weights", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["recurrent_initializer"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["use_bias"], + ) + + bias_initializer: str = common_fields.BiasInitializerField() + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["should_embed"], + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField(default="last") + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedCNNRNN"]["output_size"], + ) + + norm: str = common_fields.NormField(description="The default norm that will be used for each layer.") + + norm_params: dict = common_fields.NormParamsField() + + num_fc_layers: int = common_fields.NumFCLayersField(description="Number of parallel fully connected layers to use.") + + fc_activation: str = schema_utils.ActivationOptions() + + fc_dropout: float = common_fields.DropoutField() + + fc_layers: list[dict] = common_fields.FCLayersField() + + +@DeveloperAPI +@register_encoder_config("mamba", [AUDIO, SEQUENCE, TEXT, TIMESERIES]) +class MambaEncoderConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "MambaEncoder" + + type: str = schema_utils.ProtectedString( + "mamba", + description=ENCODER_METADATA["MambaEncoder"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(default=0.1, description="Dropout rate.") + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + d_model: int = schema_utils.PositiveInteger( + default=256, + description="Dimensionality of the model (hidden size for SSM layers).", + parameter_metadata=ENCODER_METADATA["MambaEncoder"]["d_model"], + ) + + n_layers: int = schema_utils.PositiveInteger( + default=4, + description="Number of stacked SSM layers.", + parameter_metadata=ENCODER_METADATA["MambaEncoder"]["n_layers"], + ) + + d_state: int = schema_utils.PositiveInteger( + default=16, + description="Dimensionality of the state space in each SSM layer.", + parameter_metadata=ENCODER_METADATA["MambaEncoder"]["d_state"], + ) + + d_conv: int = schema_utils.PositiveInteger( + default=4, + description="Width of the 1D convolution in each SSM layer.", + parameter_metadata=ENCODER_METADATA["MambaEncoder"]["d_conv"], + ) + + expand_factor: int = schema_utils.PositiveInteger( + default=2, + description="Expansion factor for the inner dimension of SSM layers.", + parameter_metadata=ENCODER_METADATA["MambaEncoder"]["expand_factor"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use bias in linear projections.", + ) + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings.", + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField(default="mean") + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each FC layer.", + ) + + norm: str = common_fields.NormField(description="The default norm that will be used for each layer.") + + norm_params: dict = common_fields.NormParamsField() + + num_fc_layers: int = common_fields.NumFCLayersField(description="Number of fully connected layers to use.") + + fc_activation: str = schema_utils.ActivationOptions() + + fc_dropout: float = common_fields.DropoutField() + + fc_layers: list[dict] = common_fields.FCLayersField() + + +@DeveloperAPI +@register_encoder_config("transformer", [SEQUENCE, TEXT, TIMESERIES]) +class StackedTransformerConfig(SequenceEncoderConfig): + @staticmethod + def module_name(): + return "StackedTransformer" + + type: str = schema_utils.ProtectedString( + "transformer", + description=ENCODER_METADATA["StackedTransformer"]["type"].long_description, + ) + + dropout: float = common_fields.DropoutField(default=0.1, description="The dropout rate for the transformer block.") + + max_sequence_length: int = common_fields.MaxSequenceLengthField() + + representation: str = common_fields.RepresentationField() + + vocab: list = common_fields.VocabField() + + num_layers: int = schema_utils.PositiveInteger( + default=1, + description="The number of transformer layers.", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["num_layers"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=256, + description="The size of the hidden representation within the transformer block. It is usually the same as " + "the embedding_size, but if the two values are different, a projection layer will be added before " + "the first transformer block.", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["hidden_size"], + ) + + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of attention heads in each transformer block.", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["num_heads"], + ) + + transformer_output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the fully connected layer after self attention in the transformer block. This is usually " + "the same as hidden_size and embedding_size.", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["transformer_output_size"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether to use a bias vector.", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["use_bias"], + ) + + bias_initializer: str = common_fields.BiasInitializerField() + + weights_initializer: str = common_fields.WeightsInitializerField() + + should_embed: bool = schema_utils.Boolean( + default=True, + description="If True the input sequence is expected to be made of integers and will be mapped into embeddings", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["should_embed"], + ) + + embedding_size: int = common_fields.EmbeddingSizeField() + + embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField() + + embeddings_trainable: bool = common_fields.EmbeddingsTrainableField() + + pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField() + + reduce_output: str = common_fields.ReduceOutputField(default="last") + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="The default output_size that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["StackedTransformer"]["output_size"], + ) + + norm: str = common_fields.NormField(description="The default norm that will be used for each layer.") + + norm_params: dict = common_fields.NormParamsField() + + num_fc_layers: int = common_fields.NumFCLayersField(description="Number of parallel fully connected layers to use.") + + fc_activation: str = schema_utils.ActivationOptions() + + fc_dropout: float = common_fields.DropoutField() + + fc_layers: list[dict] = common_fields.FCLayersField() + + use_rope: bool = schema_utils.Boolean( + default=False, + description="If True, use Rotary Position Embeddings (RoPE) instead of absolute positional embeddings. " + "RoPE encodes position by rotating query and key vectors, providing better length generalization " + "and relative position awareness. Used by modern LLMs like LLaMA and Mistral.", + ) diff --git a/ludwig/schema/encoders/set_encoders.py b/ludwig/schema/encoders/set_encoders.py new file mode 100644 index 0000000..f22fb64 --- /dev/null +++ b/ludwig/schema/encoders/set_encoders.py @@ -0,0 +1,135 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import SET +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.metadata import ENCODER_METADATA + + +@DeveloperAPI +@register_encoder_config("embed", SET) +class SetSparseEncoderConfig(BaseEncoderConfig): + @staticmethod + def module_name(): + return "SetSparseEncoder" + + type: str = schema_utils.ProtectedString( + "embed", + description=ENCODER_METADATA["SetSparseEncoder"]["type"].long_description, + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Dropout probability for the embedding.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["dropout"], + ) + + activation: str = schema_utils.ActivationOptions( + description="The default activation function that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["activation"], + ) + + representation: str = schema_utils.StringOptions( + ["dense", "sparse"], + default="dense", + description="The representation of the embedding. Either dense or sparse.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["representation"], + ) + + vocab: list[str] = schema_utils.List( + default=None, + description="Vocabulary of the encoder", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["vocab"], + ) + + use_bias: bool = schema_utils.Boolean( + default=True, + description="Whether the layer uses a bias vector.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["use_bias"], + ) + + bias_initializer: str = schema_utils.InitializerOptions( + default="zeros", + description="Initializer to use for the bias vector.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["bias_initializer"], + ) + + weights_initializer: str = schema_utils.InitializerOptions( + description="Initializer to use for the weights matrix.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["weights_initializer"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=50, + description="The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for " + "dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size " + "is the number of different strings appearing in the training set in the input column (plus 1 for " + "the unknown token placeholder ).", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["embedding_size"], + ) + + embeddings_on_cpu: bool = schema_utils.Boolean( + default=False, + description="By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster " + "access, but in some cases the embedding matrix may be too large. This parameter forces the " + "placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, " + "slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["embeddings_on_cpu"], + ) + + embeddings_trainable: bool = schema_utils.Boolean( + default=True, + description="If true embeddings are trained during the training process, if false embeddings are fixed. It " + "may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter " + "has effect only when representation is dense as sparse one-hot encodings are not trainable.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["embeddings_trainable"], + ) + + pretrained_embeddings: str = schema_utils.String( + default=None, + allow_none=True, + description="By default dense embeddings are initialized randomly, but this parameter allows to specify a " + "path to a file containing embeddings in the GloVe format. When the file containing the " + "embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, " + "the others are discarded. If the vocabulary contains strings that have no match in the " + "embeddings file, their embeddings are initialized with the average of all other embedding plus " + "some random noise to make them different from each other. This parameter has effect only if " + "representation is dense.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["pretrained_embeddings"], + ) + + output_size: int = schema_utils.PositiveInteger( + default=10, + description="If output_size is not already specified in fc_layers this is the default output_size that will " + "be used for each layer. It indicates the size of the output of a fully connected layer.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["output_size"], + ) + + norm: str = schema_utils.StringOptions( + ["batch", "layer"], + default=None, + allow_none=True, + description="The default norm that will be used for each layer.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["norm"], + ) + + norm_params: dict = schema_utils.Dict( + default=None, + description="Parameters used if norm is either `batch` or `layer`.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["norm_params"], + ) + + num_fc_layers: int = schema_utils.NonNegativeInteger( + default=0, + description="This is the number of stacked fully connected layers that the input to the feature passes " + "through. Their output is projected in the feature's output space.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["num_fc_layers"], + ) + + fc_layers: list[dict] = schema_utils.DictList( # TODO (Connor): Add nesting logic for fc_layers + default=None, + description="List of dictionaries containing the parameters for each fully connected layer.", + parameter_metadata=ENCODER_METADATA["SetSparseEncoder"]["fc_layers"], + ) diff --git a/ludwig/schema/encoders/text/__init__.py b/ludwig/schema/encoders/text/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/schema/encoders/text/hf_model_params.py b/ludwig/schema/encoders/text/hf_model_params.py new file mode 100644 index 0000000..f0f5bdf --- /dev/null +++ b/ludwig/schema/encoders/text/hf_model_params.py @@ -0,0 +1,160 @@ +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY + +""" +NOTE TO DEVELOPERS: the implementation of the schema classes below must match the parameters of the HF PretrainedConfig +class exactly. This is because we convert this object into the matching HF PretrainedConfig object before passing it to +the model. Additionally, for loading and saving pretrained models, we take the config from the existing model and load +it into this config before saving. As such, if any params needed by the pretrained model are missing, we will not be +able to load checkpoints correctly. + +A common mistake is to look at the PretrainedConfig __init__ method params and ignore any additional **kwargs. In some +cases, these kwargs are used to set additional params on the config object. For example, the DebertaConfig class has +`position_buckets` as a kwarg param, but it nonetheless requires this to construct the model architecture. + +To debug issues with missing parameters, try printing out the `model.config` of the pretrained transformer and check +for any params it includes that are not present in your schema config. +""" + + +class DebertaModelParams(schema_utils.LudwigBaseConfig): + @classmethod + def get_hf_config_param_names(cls) -> set[str]: + return DebertaModelParams.get_valid_field_names() + + # Model architecture params for training from scratch + # TODO(travis): conditionally disable setting these when `use_pretrained=True`. + vocab_size: int = schema_utils.PositiveInteger( + default=None, + description="", + parameter_metadata=INTERNAL_ONLY, + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=1536, + description="Dimensionality of the encoder layers and the pooler layer.", + ) + + num_hidden_layers: int = schema_utils.PositiveInteger( + default=24, + description="Number of hidden layers in the Transformer encoder.", + ) + + num_attention_heads: int = schema_utils.PositiveInteger( + default=24, + description="Number of attention heads for each attention layer in the Transformer encoder.", + ) + + intermediate_size: int = schema_utils.PositiveInteger( + default=6144, + description="Dimensionality of the 'intermediate' (often named feed-forward) layer in the Transformer encoder.", + ) + + hidden_act: str = schema_utils.StringOptions( + options=["gelu", "relu", "silu", "tanh", "gelu_fast", "mish", "linear", "sigmoid", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler.", + ) + + hidden_dropout_prob: float = schema_utils.NonNegativeFloat( + default=0.1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + ) + + attention_probs_dropout_prob: float = schema_utils.NonNegativeFloat( + default=0.1, + description="The dropout ratio for the attention probabilities.", + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=512, + description=( + "The maximum sequence length that this model might ever be used with. Typically set this to something " + "large just in case (e.g., 512 or 1024 or 2048)." + ), + ) + + type_vocab_size: int = schema_utils.NonNegativeInteger( + default=0, + description=("The vocabulary size of the `token_type_ids`."), + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description=( + "The standard deviation of the truncated_normal_initializer for initializing all weight matrices." + ), + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-7, + description="The epsilon used by the layer normalization layers.", + ) + + relative_attention: bool = schema_utils.Boolean( + default=True, + description="Whether use relative position encoding.", + ) + + max_relative_positions: int = schema_utils.Integer( + default=-1, + description=( + "The range of relative positions `[-max_position_embeddings, max_position_embeddings]`. Use the same " + "value as `max_position_embeddings`." + ), + ) + + pad_token_id: int = schema_utils.Integer( + default=0, + description="The value used to pad input_ids.", + ) + + position_biased_input: bool = schema_utils.Boolean( + default=False, + description="Whether add absolute position embedding to content embedding.", + ) + + pos_att_type: list[str] = schema_utils.List( + default=["p2c", "c2p"], + description=( + "The type of relative position attention, it can be a combination of `['p2c', 'c2p']`, e.g. `['p2c']`, " + "`['p2c', 'c2p']`, `['p2c', 'c2p']`." + ), + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-12, + description="The epsilon used by the layer normalization layers.", + ) + + pooler_hidden_size: int = schema_utils.PositiveInteger( + default=1536, + description="The hidden size of the pooler layers.", + ) + + pooler_dropout: float = schema_utils.NonNegativeFloat( + default=0, + description="The dropout ratio for the pooler layers.", + ) + + pooler_hidden_act: str = schema_utils.StringOptions( + options=["gelu", "relu", "silu", "tanh", "gelu_fast", "mish", "linear", "sigmoid", "gelu_new"], + default="gelu", + description="The activation function (function or string) in the pooler.", + ) + + position_buckets: int = schema_utils.PositiveInteger( + default=256, + description="The number of buckets to use for each attention layer.", + ) + + share_att_key: bool = schema_utils.Boolean( + default=True, + description="Whether to share attention key across layers.", + ) + + norm_rel_ebd: str = schema_utils.StringOptions( + options=["layer_norm", "none"], + default="layer_norm", + description="The normalization method for relative embeddings.", + ) diff --git a/ludwig/schema/encoders/text_encoders.py b/ludwig/schema/encoders/text_encoders.py new file mode 100644 index 0000000..4c42058 --- /dev/null +++ b/ludwig/schema/encoders/text_encoders.py @@ -0,0 +1,2724 @@ +from collections.abc import Callable +from typing import TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MODEL_ECD, TEXT +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.sequence_encoders import SequenceEncoderConfig +from ludwig.schema.encoders.text.hf_model_params import DebertaModelParams +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.llms.base_model import BaseModelDataclassField +from ludwig.schema.llms.model_parameters import ModelParametersConfig, ModelParametersConfigField +from ludwig.schema.llms.peft import AdapterDataclassField, BaseAdapterConfig +from ludwig.schema.llms.quantization import QuantizationConfig, QuantizationConfigField +from ludwig.schema.metadata import ENCODER_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY, ParameterMetadata + +if TYPE_CHECKING: + from ludwig.schema.features.preprocessing.text import TextPreprocessingConfig + + +class HFEncoderConfig(SequenceEncoderConfig): + trainable: bool + use_pretrained: bool + pretrained_model_name_or_path: str + reduce_output: str + + def set_fixed_preprocessing_params(self, model_type: str, preprocessing: "TextPreprocessingConfig"): + model_name = self.pretrained_model_name_or_path + if model_name is None and self.use_pretrained: + # no default model name, so model name is required by the subclass + raise ValueError( + f"Missing required parameter for `{self.type}` encoder: `pretrained_model_name_or_path` when " + "`use_pretrained` is True." + ) + preprocessing.tokenizer = "hf_tokenizer" + preprocessing.pretrained_model_name_or_path = model_name + if not self.can_cache_embeddings(): + preprocessing.cache_encoder_embeddings = False + + @classmethod + def get_hf_config_param_names(cls) -> set[str]: + """Returns the set of HF PretrainedConfig parameter names for this encoder config. + + Subclasses that add HF-native fields (e.g. DebertaV2Config via DebertaModelParams) must + override this to return the union of their fields. The default returns an empty set so that + generic encoders don't forward unexpected kwargs to the HF config constructor. + """ + return set() + + def is_pretrained(self) -> bool: + return self.use_pretrained + + def can_cache_embeddings(self) -> bool: + """Returns true if the encoder's output embeddings will not change during training.""" + return not self.trainable and self.reduce_output != "attention" + + +@DeveloperAPI +class HFEncoderImplConfig(HFEncoderConfig): + """This dataclass configures the base HF encoder implmenetation.""" + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["HFEncoder"]["use_pretrained"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["HFEncoder"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + ) + + # Internal params set based on preprocessing metadata + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="", + parameter_metadata=INTERNAL_ONLY, + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=None, + description="", + parameter_metadata=INTERNAL_ONLY, + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description=( + "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub." + ), + parameter_metadata=INTERNAL_ONLY, + ) + + +@DeveloperAPI +@register_encoder_config("albert", TEXT) +class ALBERTConfig(HFEncoderConfig): + """This dataclass configures the schema used for an ALBERT encoder.""" + + @staticmethod + def module_name(): + return "ALBERT" + + type: str = schema_utils.ProtectedString( + "albert", + description=ENCODER_METADATA["ALBERT"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="albert-base-v2", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + reduce_output: str = schema_utils.String( + default="cls_pooled", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["reduce_output"], + ) + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["ALBERT"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=30000, + description="Vocabulary size of the ALBERT model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["vocab_size"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=128, + description="Dimensionality of vocabulary embeddings.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["embedding_size"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["hidden_size"], + ) + + num_hidden_layers: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["num_hidden_layers"], + ) + + num_hidden_groups: int = schema_utils.PositiveInteger( + default=1, + description="Number of groups for the hidden layers, parameters in the same group are shared.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["num_hidden_groups"], + ) + + num_attention_heads: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["num_attention_heads"], + ) + + intermediate_size: int = schema_utils.PositiveInteger( + default=3072, + description="The dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer " + "encoder.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["intermediate_size"], + ) + + inner_group_num: int = schema_utils.PositiveInteger( + default=1, + description="The number of inner repetition of attention and ffn.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["inner_group_num"], + ) + + hidden_act: str = schema_utils.StringOptions( + ["gelu", "relu", "silu", "gelu_new"], + default="gelu_new", + description="The non-linear activation function (function or string) in the encoder and pooler.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["hidden_act"], + ) + + hidden_dropout_prob: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["hidden_dropout_prob"], + ) + + attention_probs_dropout_prob: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout ratio for the attention probabilities.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["attention_probs_dropout_prob"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=512, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["ALBERT"]["max_position_embeddings"], + ) + + type_vocab_size: int = schema_utils.PositiveInteger( + default=2, + description="The vocabulary size of the token_type_ids passed when calling AlbertModel or TFAlbertModel.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["type_vocab_size"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["initializer_range"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-12, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["layer_norm_eps"], + ) + + classifier_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout ratio for attached classifiers.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["classifier_dropout_prob"], + ) + + position_embedding_type: str = schema_utils.StringOptions( + ["absolute", "relative_key", "relative_key_query"], + default="absolute", + description="", + parameter_metadata=ENCODER_METADATA["ALBERT"]["position_embedding_type"], + ) + + pad_token_id: int = schema_utils.Integer( + default=0, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["pad_token_id"], + ) + + bos_token_id: int = schema_utils.Integer( + default=2, + description="The beginning of sequence token ID.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["bos_token_id"], + ) + + eos_token_id: int = schema_utils.Integer( + default=3, + description="The end of sequence token ID.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["eos_token_id"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["ALBERT"]["pretrained_kwargs"], + ) + + +# TODO: uncomment when sentencepiece doesn't cause segfaults: https://github.com/ludwig-ai/ludwig/issues/2983 +@DeveloperAPI +# @register_encoder_config("mt5", TEXT) +class MT5Config(HFEncoderConfig): + """This dataclass configures the schema used for an MT5 encoder.""" + + @staticmethod + def module_name(): + return "MT5" + + type: str = schema_utils.ProtectedString( + "mt5", + description=ENCODER_METADATA["MT5"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["MT5"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["MT5"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="google/mt5-base", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["MT5"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["MT5"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["MT5"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["MT5"]["reduce_output"], + ) + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["MT5"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=250112, + description="Vocabulary size of the T5 model. Defines the number of different tokens that can be represented " + "by the inputs_ids passed when calling T5Model or TFT5Model.", + parameter_metadata=ENCODER_METADATA["MT5"]["vocab_size"], + ) + + d_model: int = schema_utils.PositiveInteger( + default=512, + description="Size of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["MT5"]["d_model"], + ) + + d_kv: int = schema_utils.PositiveInteger( + default=64, + description="Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // " + "num_heads.", + parameter_metadata=ENCODER_METADATA["MT5"]["d_kv"], + ) + + d_ff: int = schema_utils.PositiveInteger( + default=1024, + description="Size of the intermediate feed forward layer in each T5Block.", + parameter_metadata=ENCODER_METADATA["MT5"]["d_ff"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=8, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["MT5"]["num_layers"], + ) + + num_decoder_layers: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not " + "set.", + parameter_metadata=ENCODER_METADATA["MT5"]["num_decoder_layers"], + ) + + num_heads: int = schema_utils.PositiveInteger( + default=6, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["MT5"]["num_heads"], + ) + + relative_attention_num_buckets: int = schema_utils.PositiveInteger( + default=32, + description="The number of buckets to use for each attention layer.", + parameter_metadata=ENCODER_METADATA["MT5"]["relative_attention_num_buckets"], + ) + + dropout_rate: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The ratio for all dropout layers.", + parameter_metadata=ENCODER_METADATA["MT5"]["dropout_rate"], + ) + + layer_norm_epsilon: float = schema_utils.NonNegativeFloat( + default=1e-06, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["MT5"]["layer_norm_epsilon"], + ) + + initializer_factor: float = schema_utils.NonNegativeFloat( + default=1.0, + description="A factor for initializing all weight matrices (should be kept to 1, used internally for " + "initialization testing)", + parameter_metadata=ENCODER_METADATA["MT5"]["initializer_factor"], + ) + + feed_forward_proj: str = schema_utils.StringOptions( + ["relu", "gated-gelu"], + default="gated-gelu", + description="Type of feed forward layer to be used. ", + parameter_metadata=ENCODER_METADATA["MT5"]["feed_forward_proj"], + ) + + is_encoder_decoder: bool = schema_utils.Boolean( + default=True, + description="", + parameter_metadata=ENCODER_METADATA["MT5"]["is_encoder_decoder"], + ) + + use_cache: bool = schema_utils.Boolean( + default=True, + description="", + parameter_metadata=ENCODER_METADATA["MT5"]["use_cache"], + ) + + tokenizer_class: str = schema_utils.String( + default="T5Tokenizer", + description="", + parameter_metadata=ENCODER_METADATA["MT5"]["tokenizer_class"], + ) + + tie_word_embeddings: bool = schema_utils.Boolean( + default=False, + description="Whether the model's input and output word embeddings should be tied.", + parameter_metadata=ENCODER_METADATA["MT5"]["tie_word_embeddings"], + ) + + pad_token_id: int = schema_utils.Integer( + default=0, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["MT5"]["pad_token_id"], + ) + + eos_token_id: int = schema_utils.Integer( + default=1, + description="The end of sequence token ID.", + parameter_metadata=ENCODER_METADATA["MT5"]["eos_token_id"], + ) + + decoder_start_token_id: int = schema_utils.Integer( + default=0, + description="If an encoder-decoder model starts decoding with a different token than _bos_, the id of that " + "token.", + parameter_metadata=ENCODER_METADATA["MT5"]["decoder_start_token_id"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["MT5"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("xlmroberta", TEXT) +class XLMRoBERTaConfig(HFEncoderConfig): + """This dataclass configures the schema used for an XLMRoBERTa encoder.""" + + @staticmethod + def module_name(): + return "XLMRoBERTa" + + type: str = schema_utils.ProtectedString( + "xlmroberta", + description=ENCODER_METADATA["XLMRoBERTa"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="xlm-roberta-base", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["saved_weights_in_checkpoint"], + ) + + reduce_output: str = schema_utils.String( + default="cls_pooled", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Vocabulary size of the XLMRoBERTa model.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["vocab_size"], + ) + + pad_token_id: int = schema_utils.Integer( + default=1, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["pad_token_id"], + ) + + bos_token_id: int = schema_utils.Integer( + default=0, + description="The beginning of sequence token ID.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["bos_token_id"], + ) + + eos_token_id: int = schema_utils.Integer( + default=2, + description="The end of sequence token ID.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["eos_token_id"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=514, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["max_position_embeddings"], + ) + + type_vocab_size: int = schema_utils.PositiveInteger( + default=1, + description="The vocabulary size of the token_type_ids passed in.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["type_vocab_size"], + ) + + add_pooling_layer: bool = schema_utils.Boolean( + default=True, + description="Whether to add a pooling layer to the encoder.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["add_pooling_layer"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["XLMRoBERTa"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("bert", TEXT) +class BERTConfig(HFEncoderConfig): + """This dataclass configures the schema used for an BERT encoder.""" + + @staticmethod + def module_name(): + return "BERT" + + type: str = schema_utils.ProtectedString( + "bert", + description=ENCODER_METADATA["BERT"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["BERT"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["BERT"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="bert-base-uncased", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["BERT"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["BERT"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["BERT"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + reduce_output: str = schema_utils.String( + default="cls_pooled", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["BERT"]["reduce_output"], + ) + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["BERT"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=30522, + description="Vocabulary size of the BERT model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling BertModel or TFBertModel.", + parameter_metadata=ENCODER_METADATA["BERT"]["vocab_size"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["BERT"]["hidden_size"], + ) + + num_hidden_layers: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["BERT"]["num_hidden_layers"], + ) + + num_attention_heads: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["BERT"]["num_attention_heads"], + ) + + intermediate_size: int = schema_utils.PositiveInteger( + default=3072, + description="Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["BERT"]["intermediate_size"], + ) + + hidden_act: str | Callable = schema_utils.StringOptions( # TODO: add support for callable + ["gelu", "relu", "silu", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler.", + parameter_metadata=ENCODER_METADATA["BERT"]["hidden_act"], + ) + + hidden_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["BERT"]["hidden_dropout_prob"], + ) + + attention_probs_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout ratio for the attention probabilities.", + parameter_metadata=ENCODER_METADATA["BERT"]["attention_probs_dropout_prob"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=512, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["BERT"]["max_position_embeddings"], + ) + + type_vocab_size: int = schema_utils.PositiveInteger( + default=2, + description="The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.", + parameter_metadata=ENCODER_METADATA["BERT"]["type_vocab_size"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["BERT"]["initializer_range"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-12, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["BERT"]["layer_norm_eps"], + ) + + pad_token_id: int = schema_utils.Integer( + default=0, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["BERT"]["pad_token_id"], + ) + + gradient_checkpointing: bool = schema_utils.Boolean( + default=False, + description="Whether to use gradient checkpointing.", + parameter_metadata=ENCODER_METADATA["BERT"]["gradient_checkpointing"], + ) + + position_embedding_type: str = schema_utils.StringOptions( + ["absolute", "relative_key", "relative_key_query"], + default="absolute", + description="Type of position embedding.", + parameter_metadata=ENCODER_METADATA["BERT"]["position_embedding_type"], + ) + + classifier_dropout: float = schema_utils.FloatRange( + default=None, + allow_none=True, + min=0, + max=1, + description="The dropout ratio for the classification head.", + parameter_metadata=ENCODER_METADATA["BERT"]["classifier_dropout"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["BERT"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("modernbert", TEXT) +class ModernBERTConfig(HFEncoderConfig): + """This dataclass configures the schema used for a ModernBERT encoder.""" + + @staticmethod + def module_name(): + return "ModernBERT" + + type: str = schema_utils.ProtectedString( + "modernbert", + description=ENCODER_METADATA["ModernBERT"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="answerdotai/ModernBERT-base", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + reduce_output: str = schema_utils.String( + default="cls_pooled", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["reduce_output"], + ) + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=50368, + description="Vocabulary size of the ModernBERT model.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["vocab_size"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["hidden_size"], + ) + + num_hidden_layers: int = schema_utils.PositiveInteger( + default=22, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["num_hidden_layers"], + ) + + num_attention_heads: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["num_attention_heads"], + ) + + intermediate_size: int = schema_utils.PositiveInteger( + default=1152, + description="Dimensionality of the 'intermediate' (often named feed-forward) layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["intermediate_size"], + ) + + hidden_act: str | Callable = schema_utils.StringOptions( + ["gelu", "relu", "silu", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["hidden_act"], + ) + + hidden_dropout_prob: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["hidden_dropout_prob"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=8192, + description="The maximum sequence length that this model might ever be used with.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["max_position_embeddings"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["initializer_range"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-5, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["layer_norm_eps"], + ) + + pad_token_id: int = schema_utils.Integer( + default=50283, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["pad_token_id"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["ModernBERT"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("deberta", TEXT) +class DebertaV2Config(HFEncoderImplConfig, DebertaModelParams): + """This dataclass configures the schema used for a DeBERTa-v2 / v3 encoder.""" + + @staticmethod + def module_name(): + return "DeBERTa" + + type: str = schema_utils.ProtectedString( + "deberta", + description=ENCODER_METADATA["DeBERTa"]["type"].long_description, + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="tasksource/deberta-base-long-nli", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["DeBERTa"]["pretrained_model_name_or_path"], + ) + + reduce_output: str = schema_utils.StringOptions( + ["cls_pooled", "last", "sum", "mean", "max", "concat", "attention"], + default="sum", + allow_none=True, + description="The method used to reduce a sequence of tensors down to a single tensor.", + ) + + +# TODO: uncomment once we figure out host memory issue: https://github.com/ludwig-ai/ludwig/issues/3107 +@DeveloperAPI +# @register_encoder_config("xlm", TEXT) +class XLMConfig(HFEncoderConfig): + """This dataclass configures the schema used for an XLM encoder.""" + + @staticmethod + def module_name(): + return "XLM" + + type: str = schema_utils.ProtectedString( + "xlm", + description=ENCODER_METADATA["XLM"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["XLM"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["XLM"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="xlm-mlm-en-2048", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["XLM"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["XLM"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["XLM"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["XLM"]["reduce_output"], + ) + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["XLM"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=30145, + description="Vocabulary size of the BERT model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling XLMModel or TFXLMModel.", + parameter_metadata=ENCODER_METADATA["XLM"]["vocab_size"], + ) + + emb_dim: int = schema_utils.PositiveInteger( + default=2048, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["XLM"]["emb_dim"], + ) + + n_layers: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["XLM"]["n_layers"], + ) + + n_heads: int = schema_utils.PositiveInteger( + default=16, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["XLM"]["n_heads"], + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["XLM"]["dropout"], + ) + + attention_dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for the attention mechanism.", + parameter_metadata=ENCODER_METADATA["XLM"]["attention_dropout"], + ) + + gelu_activation: bool = schema_utils.Boolean( + default=True, + description="Whether or not to use gelu for the activations instead of relu.", + parameter_metadata=ENCODER_METADATA["XLM"]["gelu_activation"], + ) + + sinusoidal_embeddings: bool = schema_utils.Boolean( + default=False, + description="Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.", + parameter_metadata=ENCODER_METADATA["XLM"]["sinusoidal_embeddings"], + ) + + causal: bool = schema_utils.Boolean( + default=False, + description="Whether or not the model should behave in a causal manner. Causal models use a triangular " + "attention mask in order to only attend to the left-side context instead if a bidirectional " + "context.", + parameter_metadata=ENCODER_METADATA["XLM"]["causal"], + ) + + asm: bool = schema_utils.Boolean( + default=False, + description="Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the " + "prediction layer.", + parameter_metadata=ENCODER_METADATA["XLM"]["asm"], + ) + + n_langs: int = schema_utils.PositiveInteger( + default=1, + description="The number of languages the model handles. Set to 1 for monolingual models.", + parameter_metadata=ENCODER_METADATA["XLM"]["n_langs"], + ) + + use_lang_emb: bool = schema_utils.Boolean( + default=True, + description="Whether to use language embeddings. Some models use additional language embeddings, " + "see the multilingual models page for information on how to use them.", + parameter_metadata=ENCODER_METADATA["XLM"]["use_lang_emb"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=512, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["XLM"]["max_position_embeddings"], + ) + + embed_init_std: float = schema_utils.NonNegativeFloat( + default=2048**-0.5, + description="The standard deviation of the truncated_normal_initializer for initializing the embedding " + "matrices.", + parameter_metadata=ENCODER_METADATA["XLM"]["embed_init_std"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-12, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["XLM"]["layer_norm_eps"], + ) + + init_std: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices " + "except the embedding matrices.", + parameter_metadata=ENCODER_METADATA["XLM"]["init_std"], + ) + + bos_index: int = schema_utils.NonNegativeInteger( + default=0, + description="The index of the beginning of sentence token in the vocabulary.", + parameter_metadata=ENCODER_METADATA["XLM"]["bos_index"], + ) + + eos_index: int = schema_utils.NonNegativeInteger( + default=1, + description="The index of the end of sentence token in the vocabulary.", + parameter_metadata=ENCODER_METADATA["XLM"]["eos_index"], + ) + + pad_index: int = schema_utils.NonNegativeInteger( + default=2, + description="The index of the padding token in the vocabulary.", + parameter_metadata=ENCODER_METADATA["XLM"]["pad_index"], + ) + + unk_index: int = schema_utils.NonNegativeInteger( + default=3, + description="The index of the unknown token in the vocabulary.", + parameter_metadata=ENCODER_METADATA["XLM"]["unk_index"], + ) + + mask_index: int = schema_utils.NonNegativeInteger( + default=5, + description="The index of the masking token in the vocabulary.", + parameter_metadata=ENCODER_METADATA["XLM"]["mask_index"], + ) + + is_encoder: bool = schema_utils.Boolean( + default=True, + description="Whether or not the initialized model should be a transformer encoder or decoder as seen in " + "Vaswani et al.", + parameter_metadata=ENCODER_METADATA["XLM"]["is_encoder"], + ) + + start_n_top: int = schema_utils.PositiveInteger( + default=5, + description="Used in the SQuAD evaluation script.", + parameter_metadata=ENCODER_METADATA["XLM"]["start_n_top"], + ) + + end_n_top: int = schema_utils.PositiveInteger( + default=5, + description="Used in the SQuAD evaluation script.", + parameter_metadata=ENCODER_METADATA["XLM"]["end_n_top"], + ) + + mask_token_id: int = schema_utils.Integer( + default=0, + description="Model agnostic parameter to identify masked tokens when generating text in an MLM context.", + parameter_metadata=ENCODER_METADATA["XLM"]["mask_token_id"], + ) + + lang_id: int = schema_utils.Integer( + default=0, + description="The ID of the language used by the model. This parameter is used when generating text in a given " + "language.", + parameter_metadata=ENCODER_METADATA["XLM"]["lang_id"], + ) + + pad_token_id: int = schema_utils.Integer( + default=2, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["XLM"]["pad_token_id"], + ) + + bos_token_id: int = schema_utils.Integer( + default=0, + description="The beginning of sequence token ID.", + parameter_metadata=ENCODER_METADATA["XLM"]["bos_token_id"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["XLM"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("gpt", TEXT) +class GPTConfig(HFEncoderConfig): + """This dataclass configures the schema used for an GPT encoder.""" + + @staticmethod + def module_name(): + return "GPT" + + type: str = schema_utils.ProtectedString( + "gpt", + description=ENCODER_METADATA["GPT"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["GPT"]["max_sequence_length"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["GPT"]["reduce_output"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["GPT"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="openai-gpt", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["GPT"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["GPT"]["saved_weights_in_checkpoint"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["GPT"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["GPT"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=30522, + description="Vocabulary size of the GPT model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling OpenAIGPTModel or TFOpenAIGPTModel.", + parameter_metadata=ENCODER_METADATA["GPT"]["vocab_size"], + ) + + n_positions: int = schema_utils.PositiveInteger( + default=40478, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["GPT"]["n_positions"], + ) + + n_ctx: int = schema_utils.PositiveInteger( + default=512, + description="Dimensionality of the causal mask (usually same as n_positions)", + parameter_metadata=ENCODER_METADATA["GPT"]["n_ctx"], + ) + + n_embd: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the embeddings and hidden states.", + parameter_metadata=ENCODER_METADATA["GPT"]["n_embd"], + ) + + n_layer: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["GPT"]["n_layer"], + ) + + n_head: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["GPT"]["n_head"], + ) + + afn: str = schema_utils.StringOptions( + ["gelu", "relu", "silu"], # gelu_new results in a KeyError. + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler.", + parameter_metadata=ENCODER_METADATA["GPT"]["afn"], + ) + + resid_pdrop: float = schema_utils.FloatRange( + default=0.1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["GPT"]["resid_pdrop"], + ) + + embd_pdrop: float = schema_utils.FloatRange( + default=0.1, + description="The dropout ratio for the embeddings.", + parameter_metadata=ENCODER_METADATA["GPT"]["embd_pdrop"], + ) + + attn_pdrop: float = schema_utils.FloatRange( + default=0.1, + description="The dropout ratio for the attention.", + parameter_metadata=ENCODER_METADATA["GPT"]["attn_pdrop"], + ) + + layer_norm_epsilon: float = schema_utils.NonNegativeFloat( + default=1e-5, + description="The epsilon to use in the layer normalization layers", + parameter_metadata=ENCODER_METADATA["GPT"]["layer_norm_epsilon"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["GPT"]["initializer_range"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["GPT"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("gpt2", TEXT) +class GPT2Config(HFEncoderConfig): + """This dataclass configures the schema used for an GPT2 encoder.""" + + @staticmethod + def module_name(): + return "GPT2" + + type: str = schema_utils.ProtectedString( + "gpt2", + description=ENCODER_METADATA["GPT2"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["GPT2"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["GPT2"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="gpt2", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["GPT2"]["pretrained_model_name_or_path"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["GPT2"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["GPT2"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["GPT2"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=50257, + description="Vocabulary size of the GPT-2 model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling GPT2Model or TFGPT2Model.", + parameter_metadata=ENCODER_METADATA["GPT2"]["vocab_size"], + ) + + n_positions: int = schema_utils.PositiveInteger( + default=1024, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["GPT2"]["n_positions"], + ) + + n_ctx: int = schema_utils.PositiveInteger( + default=1024, + description="Dimensionality of the causal mask (usually same as n_positions)", + parameter_metadata=ENCODER_METADATA["GPT2"]["n_ctx"], + ) + + n_embd: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the embeddings and hidden states.", + parameter_metadata=ENCODER_METADATA["GPT2"]["n_embd"], + ) + + n_layer: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["GPT2"]["n_layer"], + ) + + n_head: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["GPT2"]["n_head"], + ) + + n_inner: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Dimensionality of the inner feed-forward layers. None will set it to 4 times n_embd", + parameter_metadata=ENCODER_METADATA["GPT2"]["n_inner"], + ) + + activation_function: str = schema_utils.StringOptions( + ["relu", "silu", "gelu", "tanh", "gelu_new"], + default="gelu_new", + description="Activation function, to be selected in the list ['relu', 'silu', 'gelu', 'tanh', 'gelu_new'].", + parameter_metadata=ENCODER_METADATA["GPT2"]["activation_function"], + ) + + resid_pdrop: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["GPT2"]["resid_pdrop"], + ) + + embd_pdrop: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout ratio for the embeddings.", + parameter_metadata=ENCODER_METADATA["GPT2"]["embd_pdrop"], + ) + + attn_pdrop: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout ratio for the attention.", + parameter_metadata=ENCODER_METADATA["GPT2"]["attn_pdrop"], + ) + + layer_norm_epsilon: float = schema_utils.NonNegativeFloat( + default=1e-5, + description="The epsilon to use in the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["GPT2"]["layer_norm_epsilon"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["GPT2"]["initializer_range"], + ) + + scale_attn_weights: bool = schema_utils.Boolean( + default=True, + description="Scale attention weights by dividing by sqrt(hidden_size).", + parameter_metadata=ENCODER_METADATA["GPT2"]["scale_attn_weights"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["GPT2"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("roberta", TEXT) +class RoBERTaConfig(HFEncoderConfig): + """This dataclass configures the schema used for an RoBERTa encoder.""" + + @staticmethod + def module_name(): + return "RoBERTa" + + type: str = schema_utils.ProtectedString( + "roberta", + description=ENCODER_METADATA["RoBERTa"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="roberta-base", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["saved_weights_in_checkpoint"], + ) + + reduce_output: str = schema_utils.String( + default="cls_pooled", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Vocabulary size of the RoBERTa model.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["vocab_size"], + ) + + pad_token_id: int = schema_utils.Integer( + default=1, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["pad_token_id"], + ) + + bos_token_id: int = schema_utils.Integer( + default=0, + description="The beginning of sequence token ID.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["bos_token_id"], + ) + + eos_token_id: int = schema_utils.Integer( + default=2, + description="The end of sequence token ID.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["eos_token_id"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["RoBERTa"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("xlnet", TEXT) +class XLNetConfig(HFEncoderConfig): + """This dataclass configures the schema used for an XLNet encoder.""" + + @staticmethod + def module_name(): + return "XLNet" + + type: str = schema_utils.ProtectedString( + "xlnet", + description=ENCODER_METADATA["XLNet"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["XLNet"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["XLNet"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="xlnet-base-cased", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["XLNet"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["XLNet"]["saved_weights_in_checkpoint"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["XLNet"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["XLNet"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["XLNet"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=32000, + description="Vocabulary size of the XLNet model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling XLNetModel or TFXLNetModel.", + parameter_metadata=ENCODER_METADATA["XLNet"]["vocab_size"], + ) + + d_model: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["XLNet"]["d_model"], + ) + + n_layer: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["XLNet"]["n_layer"], + ) + + n_head: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["XLNet"]["n_head"], + ) + + d_inner: int = schema_utils.PositiveInteger( + default=3072, + description="Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["XLNet"]["d_inner"], + ) + + ff_activation: str = schema_utils.StringOptions( + ["gelu", "relu", "silu", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler. If string, " + "'gelu', 'relu', 'silu' and 'gelu_new' are supported.", + parameter_metadata=ENCODER_METADATA["XLNet"]["ff_activation"], + ) + + untie_r: bool = schema_utils.Boolean( + default=True, + description="Whether or not to untie relative position biases", + parameter_metadata=ENCODER_METADATA["XLNet"]["untie_r"], + ) + + attn_type: str = schema_utils.StringOptions( + ["bi"], + default="bi", + description="The attention type used by the model. Currently only 'bi' is supported.", + parameter_metadata=ENCODER_METADATA["XLNet"]["attn_type"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["XLNet"]["initializer_range"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-12, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["XLNet"]["layer_norm_eps"], + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["XLNet"]["dropout"], + ) + + mem_len: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The number of tokens to cache. The key/value pairs that have already been pre-computed in a " + "previous forward pass won’t be re-computed. ", + parameter_metadata=ENCODER_METADATA["XLNet"]["mem_len"], + ) + + reuse_len: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The number of tokens in the current batch to be cached and reused in the future.", + parameter_metadata=ENCODER_METADATA["XLNet"]["reuse_len"], + ) + + use_mems_eval: bool = schema_utils.Boolean( + default=True, + description="Whether or not the model should make use of the recurrent memory mechanism in evaluation mode.", + parameter_metadata=ENCODER_METADATA["XLNet"]["use_mems_eval"], + ) + + use_mems_train: bool = schema_utils.Boolean( + default=False, + description="Whether or not the model should make use of the recurrent memory mechanism in train mode.", + parameter_metadata=ENCODER_METADATA["XLNet"]["use_mems_train"], + ) + + bi_data: bool = schema_utils.Boolean( + default=False, + description="Whether or not to use bidirectional input pipeline. Usually set to True during pretraining and " + "False during finetuning.", + parameter_metadata=ENCODER_METADATA["XLNet"]["bi_data"], + ) + + clamp_len: int = schema_utils.Integer( + default=-1, + description="Clamp all relative distances larger than clamp_len. Setting this attribute to -1 means no " + "clamping.", + parameter_metadata=ENCODER_METADATA["XLNet"]["clamp_len"], + ) + + same_length: bool = schema_utils.Boolean( + default=False, + description="Whether or not to use the same attention length for each token.", + parameter_metadata=ENCODER_METADATA["XLNet"]["same_length"], + ) + + summary_type: str = schema_utils.StringOptions( + ["last", "first", "mean", "cls_index", "attn"], + default="last", + description="Argument used when doing sequence summary. Used in the sequence classification and multiple " + "choice models.", + parameter_metadata=ENCODER_METADATA["XLNet"]["summary_type"], + ) + + summary_use_proj: bool = schema_utils.Boolean( + default=True, + description="", + parameter_metadata=ENCODER_METADATA["XLNet"]["summary_use_proj"], + ) + + summary_activation: str = schema_utils.String( + default="tanh", + description="Argument used when doing sequence summary. Used in the sequence classification and multiple " + "choice models.", + parameter_metadata=ENCODER_METADATA["XLNet"]["summary_activation"], + ) + + summary_last_dropout: float = schema_utils.FloatRange( + default=0.1, + description="Used in the sequence classification and multiple choice models.", + parameter_metadata=ENCODER_METADATA["XLNet"]["summary_last_dropout"], + ) + + start_n_top: int = schema_utils.PositiveInteger( + default=5, + description="Used in the SQuAD evaluation script.", + parameter_metadata=ENCODER_METADATA["XLNet"]["start_n_top"], + ) + + end_n_top: int = schema_utils.PositiveInteger( + default=5, + description=" Used in the SQuAD evaluation script.", + parameter_metadata=ENCODER_METADATA["XLNet"]["end_n_top"], + ) + + pad_token_id: int = schema_utils.Integer( + default=5, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["XLNet"]["pad_token_id"], + ) + + bos_token_id: int = schema_utils.Integer( + default=1, + description="The beginning of sequence token ID.", + parameter_metadata=ENCODER_METADATA["XLNet"]["bos_token_id"], + ) + + eos_token_id: int = schema_utils.Integer( + default=2, + description="The end of sequence token ID.", + parameter_metadata=ENCODER_METADATA["XLNet"]["eos_token_id"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["XLNet"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("distilbert", TEXT) +class DistilBERTConfig(HFEncoderConfig): + """This dataclass configures the schema used for an DistilBERT encoder.""" + + @staticmethod + def module_name(): + return "DistilBERT" + + type: str = schema_utils.ProtectedString( + "distilbert", + description=ENCODER_METADATA["DistilBERT"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="distilbert-base-uncased", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["saved_weights_in_checkpoint"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=30522, + description="Vocabulary size of the DistilBERT model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling DistilBertModel or TFDistilBertModel.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["vocab_size"], + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["dropout"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=512, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["max_position_embeddings"], + ) + + sinusoidal_pos_embds: bool = schema_utils.Boolean( + default=False, + description="Whether to use sinusoidal positional embeddings.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["sinusoidal_pos_embds"], + ) + + n_layers: int = schema_utils.PositiveInteger( + default=6, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["n_layers"], + ) + + n_heads: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["n_heads"], + ) + + dim: int = schema_utils.PositiveInteger( + default=768, + description=" Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["dim"], + ) + + hidden_dim: int = schema_utils.PositiveInteger( + default=3072, + description="The size of the “intermediate” (often named feed-forward) layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["hidden_dim"], + ) + + attention_dropout: float = schema_utils.NonNegativeFloat( + default=0.1, + description="The dropout ratio for the attention probabilities.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["attention_dropout"], + ) + + activation: str | Callable = schema_utils.StringOptions( # TODO: Add support for callable + ["gelu", "relu", "silu", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler. If string, " + "'gelu', 'relu', 'silu' and 'gelu_new' are supported.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["activation"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["initializer_range"], + ) + + qa_dropout: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probabilities used in the question answering model DistilBertForQuestionAnswering.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["qa_dropout"], + ) + + seq_classif_dropout: float = schema_utils.FloatRange( + default=0.2, + min=0, + max=1, + description="The dropout probabilities used in the sequence classification and the multiple choice model " + "DistilBertForSequenceClassification.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["seq_classif_dropout"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["DistilBERT"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("camembert", TEXT) +class CamemBERTConfig(HFEncoderConfig): + """This dataclass configures the schema used for an CamemBERT encoder.""" + + @staticmethod + def module_name(): + return "CamemBERT" + + type: str = schema_utils.ProtectedString( + "camembert", + description=ENCODER_METADATA["CamemBERT"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["use_pretrained"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["saved_weights_in_checkpoint"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="camembert-base", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["pretrained_model_name_or_path"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=32005, + description="Vocabulary size of the CamemBERT model.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["vocab_size"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=768, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["hidden_size"], + ) + + num_hidden_layers: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["num_hidden_layers"], + ) + + num_attention_heads: int = schema_utils.PositiveInteger( + default=12, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["num_attention_heads"], + ) + + intermediate_size: int = schema_utils.PositiveInteger( + default=3072, + description="Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["intermediate_size"], + ) + + hidden_act: str | Callable = schema_utils.StringOptions( # TODO: add support for callable + ["gelu", "relu", "silu", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["hidden_act"], + ) + + hidden_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["hidden_dropout_prob"], + ) + + attention_probs_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout ratio for the attention probabilities.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["attention_probs_dropout_prob"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=514, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["max_position_embeddings"], + ) + + type_vocab_size: int = schema_utils.PositiveInteger( + default=1, + description="The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["type_vocab_size"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["initializer_range"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-05, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["layer_norm_eps"], + ) + + pad_token_id: int = schema_utils.Integer( + default=1, + description="The ID of the token to use as padding.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["pad_token_id"], + ) + + gradient_checkpointing: bool = schema_utils.Boolean( + default=False, + description="Whether to use gradient checkpointing.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["gradient_checkpointing"], + ) + + position_embedding_type: str = schema_utils.StringOptions( + ["absolute", "relative_key", "relative_key_query"], + default="absolute", + description="Type of position embedding.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["position_embedding_type"], + ) + + classifier_dropout: float = schema_utils.FloatRange( + default=None, + allow_none=True, + min=0, + max=1, + description="The dropout ratio for the classification head.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["classifier_dropout"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["CamemBERT"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("t5", TEXT) +class T5Config(HFEncoderConfig): + """This dataclass configures the schema used for an T5 encoder.""" + + @staticmethod + def module_name(): + return "T5" + + type: str = schema_utils.ProtectedString( + "t5", + description=ENCODER_METADATA["T5"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["T5"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["T5"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="t5-small", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["T5"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["T5"]["saved_weights_in_checkpoint"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["T5"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["T5"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["T5"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=32128, + description="Vocabulary size of the T5 model. Defines the number of different tokens that can be represented " + "by the inputs_ids passed when calling T5Model or TFT5Model.", + parameter_metadata=ENCODER_METADATA["T5"]["vocab_size"], + ) + + d_model: int = schema_utils.PositiveInteger( + default=512, + description="Size of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["T5"]["d_model"], + ) + + d_kv: int = schema_utils.PositiveInteger( + default=64, + description="Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // " + "num_heads.", + parameter_metadata=ENCODER_METADATA["T5"]["d_kv"], + ) + + d_ff: int = schema_utils.PositiveInteger( + default=2048, + description="Size of the intermediate feed forward layer in each T5Block.", + parameter_metadata=ENCODER_METADATA["T5"]["d_ff"], + ) + + num_layers: int = schema_utils.PositiveInteger( + default=6, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["T5"]["num_layers"], + ) + + num_decoder_layers: int = schema_utils.PositiveInteger( + default=6, + description="Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not " + "set.", + parameter_metadata=ENCODER_METADATA["T5"]["num_decoder_layers"], + ) + + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["T5"]["num_heads"], + ) + + relative_attention_num_buckets: int = schema_utils.PositiveInteger( + default=32, + description="The number of buckets to use for each attention layer.", + parameter_metadata=ENCODER_METADATA["T5"]["relative_attention_num_buckets"], + ) + + dropout_rate: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The ratio for all dropout layers.", + parameter_metadata=ENCODER_METADATA["T5"]["dropout_rate"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-6, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["T5"]["layer_norm_eps"], + ) + + initializer_factor: float = schema_utils.NonNegativeFloat( + default=1, + description="A factor for initializing all weight matrices (should be kept to 1, used internally for " + "initialization testing).", + parameter_metadata=ENCODER_METADATA["T5"]["initializer_factor"], + ) + + feed_forward_proj: str = schema_utils.StringOptions( + ["relu", "gated-gelu"], + default="relu", + description="Type of feed forward layer to be used. Should be one of 'relu' or 'gated-gelu'. T5v1.1 uses the " + "'gated-gelu' feed forward projection. Original T5 uses 'relu'.", + parameter_metadata=ENCODER_METADATA["T5"]["feed_forward_proj"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["T5"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("electra", TEXT) +class ELECTRAConfig(HFEncoderConfig): + """This dataclass configures the schema used for an ELECTRA encoder.""" + + @staticmethod + def module_name(): + return "ELECTRA" + + type: str = schema_utils.ProtectedString( + "electra", + description=ENCODER_METADATA["ELECTRA"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["use_pretrained"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="google/electra-small-discriminator", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["saved_weights_in_checkpoint"], + ) + + reduce_output: str = schema_utils.String( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=30522, + description="Vocabulary size of the ELECTRA model. Defines the number of different tokens that can be " + "represented by the inputs_ids passed when calling ElectraModel or TFElectraModel.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["vocab_size"], + ) + + embedding_size: int = schema_utils.PositiveInteger( + default=128, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["embedding_size"], + ) + + hidden_size: int = schema_utils.PositiveInteger( + default=256, + description="Dimensionality of the encoder layers and the pooler layer.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["hidden_size"], + ) + + num_hidden_layers: int = schema_utils.PositiveInteger( + default=12, + description="Number of hidden layers in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["num_hidden_layers"], + ) + + num_attention_heads: int = schema_utils.PositiveInteger( + default=4, + description="Number of attention heads for each attention layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["num_attention_heads"], + ) + + intermediate_size: int = schema_utils.PositiveInteger( + default=1024, + description="Dimensionality of the “intermediate” (i.e., feed-forward) layer in the Transformer encoder.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["intermediate_size"], + ) + + hidden_act: str | Callable = schema_utils.StringOptions( # TODO: add support for callable + ["gelu", "relu", "silu", "gelu_new"], + default="gelu", + description="The non-linear activation function (function or string) in the encoder and pooler.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["hidden_act"], + ) + + hidden_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["hidden_dropout_prob"], + ) + + attention_probs_dropout_prob: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="The dropout ratio for the attention probabilities.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["attention_probs_dropout_prob"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=512, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["max_position_embeddings"], + ) + + type_vocab_size: int = schema_utils.PositiveInteger( + default=2, + description="The vocabulary size of the token_type_ids passed when calling ElectraModel or TFElectraModel.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["type_vocab_size"], + ) + + initializer_range: float = schema_utils.NonNegativeFloat( + default=0.02, + description="The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["initializer_range"], + ) + + layer_norm_eps: float = schema_utils.NonNegativeFloat( + default=1e-12, + description="The epsilon used by the layer normalization layers.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["layer_norm_eps"], + ) + + position_embedding_type: str = schema_utils.StringOptions( + ["absolute", "relative_key", "relative_key_query"], + default="absolute", + description="Type of position embedding.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["position_embedding_type"], + ) + + classifier_dropout: float = schema_utils.FloatRange( + default=None, + allow_none=True, + min=0, + max=1, + description="The dropout ratio for the classification head.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["classifier_dropout"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["ELECTRA"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("longformer", TEXT) +class LongformerConfig(HFEncoderConfig): + """This dataclass configures the schema used for a Longformer encoder.""" + + @staticmethod + def module_name(): + return "Longformer" + + type: str = schema_utils.ProtectedString( + "longformer", + description=ENCODER_METADATA["Longformer"]["type"].long_description, + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["Longformer"]["max_sequence_length"], + ) + + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. If false, the model will train from " + "scratch which is very computationally expensive.", + parameter_metadata=ENCODER_METADATA["Longformer"]["use_pretrained"], + ) + + attention_window: list[int] | int = schema_utils.OneOfOptionsField( + default=512, + allow_none=False, + description="Size of an attention window around each token. If an int, use the same size for all layers. To " + "specify a different window size for each layer, use a List[int] where len(attention_window) == " + "num_hidden_layers.", + field_options=[ + schema_utils.PositiveInteger(allow_none=False, description="", default=512), + schema_utils.List(list_type=int, allow_none=False), + ], + parameter_metadata=ENCODER_METADATA["Longformer"]["attention_window"], + ) + + sep_token_id: int = schema_utils.Integer( + default=2, + description="ID of the separator token, which is used when building a sequence from multiple sequences", + parameter_metadata=ENCODER_METADATA["Longformer"]["sep_token_id"], + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default="allenai/longformer-base-4096", + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["Longformer"]["pretrained_model_name_or_path"], + ) + + saved_weights_in_checkpoint: bool = schema_utils.Boolean( + default=False, + description="Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to" + "True for trained models to prevent loading pretrained encoder weights from model hub.", + parameter_metadata=ParameterMetadata(internal_only=True), + ) + + reduce_output: str = schema_utils.String( + default="cls_pooled", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["Longformer"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["Longformer"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["Longformer"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=50265, + description="Vocabulary size of the Longformer model.", + parameter_metadata=ENCODER_METADATA["Longformer"]["vocab_size"], + ) + + max_position_embeddings: int = schema_utils.PositiveInteger( + default=4098, + description="The maximum sequence length that this model might ever be used with. Typically set this to " + "something large just in case (e.g., 512 or 1024 or 2048).", + parameter_metadata=ENCODER_METADATA["Longformer"]["max_position_embeddings"], + ) + + type_vocab_size: int = schema_utils.PositiveInteger( + default=1, + description="The vocabulary size of the token_type_ids passed when calling LongformerEncoder", + parameter_metadata=ENCODER_METADATA["Longformer"]["type_vocab_size"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["Longformer"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("auto_transformer", TEXT) +class AutoTransformerConfig(HFEncoderConfig): + """This dataclass configures the schema used for an AutoTransformer encoder.""" + + def __post_init__(self): + # Always force use_pretrained=True — we don't support training from scratch for AutoTransformers + self.use_pretrained = True + if self.pretrained_model_name_or_path is None: + raise ConfigValidationError( + "`pretrained_model_name_or_path` must be specified for encoder: `auto_transformer`." + ) + + @staticmethod + def module_name(): + return "AutoTransformer" + + # Always True — we don't support training from scratch for AutoTransformers + use_pretrained: bool = schema_utils.Boolean( + default=True, + description="Whether to use the pretrained weights for the model. Always True for AutoTransformers.", + ) + + type: str = schema_utils.ProtectedString( + "auto_transformer", + description=ENCODER_METADATA["AutoTransformer"]["type"].long_description, + ) + + pretrained_model_name_or_path: str = schema_utils.String( + default=None, + allow_none=True, + description="Name or path of the pretrained model.", + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["pretrained_model_name_or_path"], + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Maximum length of the input sequence.", + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["max_sequence_length"], + ) + + reduce_output: str = schema_utils.ReductionOptions( + default="sum", + description="The method used to reduce a sequence of tensors down to a single tensor.", + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["reduce_output"], + ) + + trainable: bool = schema_utils.Boolean( + default=False, + description="Whether to finetune the model on your dataset.", + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["trainable"], + ) + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + + vocab: list = schema_utils.List( + default=None, + description="Vocabulary for the encoder", + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["vocab"], + ) + + vocab_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=( + "Vocabulary size of the AutoTransformer model. If None, the vocab size will be inferred " + "from the given pretrained model" + ), + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["vocab_size"], + ) + + pretrained_kwargs: dict = schema_utils.Dict( + default=None, + description="Additional kwargs to pass to the pretrained model.", + parameter_metadata=ENCODER_METADATA["AutoTransformer"]["pretrained_kwargs"], + ) + + +@DeveloperAPI +@register_encoder_config("tf_idf", TEXT, model_types=[MODEL_ECD]) +class TfIdfEncoderConfig(SequenceEncoderConfig): + type: str = schema_utils.ProtectedString("tf_idf") + + max_sequence_length: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY) + + str2idf: dict[str, int] = schema_utils.Dict(parameter_metadata=INTERNAL_ONLY) + + vocab: list = schema_utils.List(default=None, parameter_metadata=INTERNAL_ONLY) + + vocab_size: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY) + + ngram_range: list = schema_utils.List( + default=[1, 1], + description=( + "The range of n-gram sizes to use for tokenization, as a (min_n, max_n) tuple. " + "For example, (1, 1) means only unigrams, (1, 2) means unigrams and bigrams, " + "(1, 3) means unigrams, bigrams, and trigrams. Applied during preprocessing." + ), + ) + + max_df: float = schema_utils.FloatRange( + default=1.0, + min=0.0, + max=1.0, + description=( + "Maximum document frequency threshold for pruning. Terms that appear in more than " + "this fraction of documents are ignored. Set to a value < 1.0 to remove corpus-specific " + "stop words. Applied during preprocessing." + ), + ) + + min_df: int = schema_utils.Integer( + default=1, + description=( + "Minimum document frequency threshold for pruning. Terms that appear in fewer than " + "this many documents are ignored. Useful for removing very rare terms. Applied during " + "preprocessing." + ), + ) + + def set_fixed_preprocessing_params(self, model_type: str, preprocessing: "TextPreprocessingConfig"): + preprocessing.compute_idf = True + + def can_cache_embeddings(self) -> bool: + return True + + +@DeveloperAPI +@register_encoder_config("llm", TEXT, model_types=[MODEL_ECD]) +class LLMEncoderConfig(SequenceEncoderConfig): + type: str = schema_utils.ProtectedString("llm") + base_model: str = BaseModelDataclassField() + max_sequence_length: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY) + adapter: BaseAdapterConfig | None = AdapterDataclassField() + quantization: QuantizationConfig | None = QuantizationConfigField().get_default_field() + model_parameters: ModelParametersConfig | None = ModelParametersConfigField().get_default_field() diff --git a/ludwig/schema/encoders/timeseries_encoders.py b/ludwig/schema/encoders/timeseries_encoders.py new file mode 100644 index 0000000..b896a43 --- /dev/null +++ b/ludwig/schema/encoders/timeseries_encoders.py @@ -0,0 +1,123 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import TIMESERIES +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config + + +@DeveloperAPI +@register_encoder_config("patchtst", [TIMESERIES]) +class PatchTSTConfig(BaseEncoderConfig): + """PatchTST encoder config. + + From: "A Time Series is Worth 64 Words: Long-term Forecasting with Transformers" (Nie et al., 2023). + Splits the time series into fixed-length patches, projects each patch to an embedding, then applies + a standard Transformer encoder. Channel-independent: each variate is processed separately. + """ + + @staticmethod + def module_name(): + return "PatchTSTEncoder" + + type: str = schema_utils.ProtectedString("patchtst") + + patch_size: int = schema_utils.PositiveInteger( + default=16, + description="Length of each patch (number of time steps per patch).", + ) + + patch_stride: int = schema_utils.PositiveInteger( + default=8, + description="Stride between consecutive patches. Use patch_stride < patch_size for overlapping patches.", + ) + + d_model: int = schema_utils.PositiveInteger( + default=128, + description="Transformer hidden dimension.", + ) + + num_heads: int = schema_utils.PositiveInteger( + default=8, + description="Number of attention heads.", + ) + + num_layers: int = schema_utils.PositiveInteger( + default=3, + description="Number of Transformer encoder layers.", + ) + + ffn_dim: int = schema_utils.PositiveInteger( + default=256, + description="Feed-forward network hidden dimension inside Transformer blocks.", + ) + + dropout: float = schema_utils.FloatRange( + default=0.1, + min=0.0, + max=1.0, + description="Dropout rate.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Output embedding size after pooling patches.", + ) + + reduce_output: str = schema_utils.StringOptions( + options=["mean", "last", "first"], + default="mean", + description=( + "How to aggregate patch representations: 'mean' pools all patches, " + "'last' uses the last, 'first' uses the first." + ), + ) + + +@DeveloperAPI +@register_encoder_config("nbeats", [TIMESERIES]) +class NBEATSConfig(BaseEncoderConfig): + """N-BEATS encoder config. + + From: "N-BEATS: Neural basis expansion analysis for interpretable time series forecasting" + (Oreshkin et al., 2020). A pure MLP architecture with doubly residual stacking: each block produces + a backcast (reconstruction of input) and a forecast, which are summed across blocks. The encoder + returns a fixed-size representation of the backcast residual suitable for downstream use. + """ + + @staticmethod + def module_name(): + return "NBEATSEncoder" + + type: str = schema_utils.ProtectedString("nbeats") + + num_stacks: int = schema_utils.PositiveInteger( + default=2, + description="Number of N-BEATS stacks.", + ) + + num_blocks: int = schema_utils.PositiveInteger( + default=3, + description="Number of blocks per stack.", + ) + + num_layers: int = schema_utils.PositiveInteger( + default=4, + description="Number of fully-connected layers per block.", + ) + + layer_size: int = schema_utils.PositiveInteger( + default=256, + description="Width of fully-connected layers in each block.", + ) + + output_size: int = schema_utils.PositiveInteger( + default=256, + description="Size of the final output representation.", + ) + + dropout: float = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=1.0, + description="Dropout rate inside blocks.", + ) diff --git a/ludwig/schema/encoders/utils.py b/ludwig/schema/encoders/utils.py new file mode 100644 index 0000000..aca95cc --- /dev/null +++ b/ludwig/schema/encoders/utils.py @@ -0,0 +1,132 @@ +from dataclasses import Field +from typing import Any, TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MODEL_ECD, TYPE +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import ENCODER_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json +from ludwig.utils.registry import Registry + +if TYPE_CHECKING: + from ludwig.schema.encoders.base import BaseEncoderConfig + + +encoder_config_registry = Registry() + + +@DeveloperAPI +def register_encoder_config(name: str, features: str | list[str], model_types: list[str] | None = None): + if model_types is None: + model_types = [MODEL_ECD] + + if isinstance(features, str): + features = [features] + + def wrap(cls): + for model_type in model_types: + for feature in features: + key = (model_type, feature) + feature_registry = encoder_config_registry.get(key, {}) + feature_registry[name] = cls + encoder_config_registry[key] = feature_registry + return cls + + return wrap + + +@DeveloperAPI +def get_encoder_cls(model_type: str, feature: str, name: str): + return encoder_config_registry[(model_type, feature)][name] + + +@DeveloperAPI +def get_encoder_classes(model_type: str, feature: str) -> dict[str, type["BaseEncoderConfig"]]: + return encoder_config_registry[(model_type, feature)] + + +@DeveloperAPI +def get_encoder_descriptions(model_type: str, feature_type: str) -> dict[str, Any]: + """This function returns a dictionary of encoder descriptions available at the type selection. + + The process works as follows - 1) Get a dictionary of valid encoders from the encoder config registry, + but inverse the key/value pairs since we need to index `valid_encoders` later with an altered version + of the encoder config class name. 2) Loop through Encoder Metadata entries, if a metadata entry has an + encoder name that matches a valid encoder, add the description metadata to the output dictionary. + + Args: + model_type (str): The model type to get encoder descriptions for + feature_type (str): The feature type to get encoder descriptions for + Returns: + dict: A dictionary mapping encoder registered names to their respective description metadata. + """ + output = {} + valid_encoders = { + cls.module_name() if hasattr(cls, "module_name") else None: registered_name + for registered_name, cls in get_encoder_classes(model_type, feature_type).items() + } + + for k, v in ENCODER_METADATA.items(): + if k in valid_encoders: + output[valid_encoders[k]] = convert_metadata_to_json(v[TYPE]) + + return output + + +@DeveloperAPI +def get_encoder_conds(encoder_classes: dict[str, type["BaseEncoderConfig"]]) -> list[dict[str, Any]]: + """Returns a JSON schema of conditionals to validate against encoder types for specific feature types.""" + conds = [] + for encoder_type, encoder_cls in encoder_classes.items(): + other_props = schema_utils.unload_jsonschema_from_config_class(encoder_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + encoder_cond = schema_utils.create_cond( + {"type": encoder_type}, + other_props, + ) + conds.append(encoder_cond) + return conds + + +@DeveloperAPI +def EncoderDataclassField( + model_type: str, feature_type: str, default: str, description: str = "", blocklist: list[str] | None = None +) -> Field: + """Custom dataclass field that when used inside a dataclass will allow the user to specify an encoder config. + + Returns: Initialized dataclass field that converts an untyped dict with params to an encoder config. + """ + encoder_registry = get_encoder_classes(model_type, feature_type) + + class EncoderSelection(schema_utils.TypeSelection): + def __init__(self): + super().__init__( + registry=encoder_registry, default_value=default, description=description, allow_str_value=True + ) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return encoder_registry[key] + + def _jsonschema_type_mapping(self): + # NOTE: Edit carefully if necessary! We want these enums to remain in a consistent order, so do not use sets + # or other unordered data structures to chaperone the registry keys around. + # + # Also, note the placement inside this function - since this is a list, it will not update with any late + # additions to the registry (e.g. in our tests)! + enum = [e for e in encoder_registry if not blocklist or e not in blocklist] + + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": enum, + "enumDescriptions": get_encoder_descriptions(model_type, feature_type), + "default": default, + }, + }, + "title": "encoder_options", + "allOf": get_encoder_conds(encoder_registry), + } + + return EncoderSelection().get_default_field() diff --git a/ludwig/schema/export_schema.py b/ludwig/schema/export_schema.py new file mode 100644 index 0000000..e436ca6 --- /dev/null +++ b/ludwig/schema/export_schema.py @@ -0,0 +1,106 @@ +"""Export Ludwig config JSON schema. + +Usage: + python -m ludwig.schema.export_schema [--model-type ecd|llm|combined] [--output FILE] + ludwig export_schema [--model-type ecd|llm|combined] [--output FILE] + +Generates a JSON Schema (Draft 7) for Ludwig config validation. +""" + +import argparse +import json + +from ludwig.config_validation.validation import get_schema +from ludwig.constants import MODEL_ECD, MODEL_LLM +from ludwig.globals import LUDWIG_VERSION + +SCHEMA_BASE_URL = "https://ludwig-ai.github.io/schema" + + +def _strip_parameter_metadata(obj): + """Recursively remove ``parameter_metadata`` keys from a schema dict. + + The Ludwig schema generator attaches ``parameter_metadata`` objects to + every field (UI display hints, suggested values, etc.). These are useful + internally but add significant bloat to the published JSON Schema and are + not relevant for validation or IDE auto-complete. + """ + if isinstance(obj, dict): + return {k: _strip_parameter_metadata(v) for k, v in obj.items() if k != "parameter_metadata"} + if isinstance(obj, list): + return [_strip_parameter_metadata(item) for item in obj] + return obj + + +def export_schema(model_type: str = MODEL_ECD, *, strip_metadata: bool = True) -> dict: + """Export the full Ludwig config JSON schema for a given model type.""" + schema = get_schema(model_type) + schema["$schema"] = "http://json-schema.org/draft-07/schema#" + schema["$id"] = f"{SCHEMA_BASE_URL}/ludwig-config-{model_type}.json" + schema["title"] = f"Ludwig {model_type.upper()} Configuration" + schema["description"] = f"Configuration schema for Ludwig {model_type.upper()} models (v{LUDWIG_VERSION})" + if strip_metadata: + schema = _strip_parameter_metadata(schema) + return schema + + +def export_combined_schema(*, strip_metadata: bool = True) -> dict: + """Export a combined schema that covers both ECD and LLM model types.""" + ecd_schema = get_schema(MODEL_ECD) + llm_schema = get_schema(MODEL_LLM) + + # Merge properties from both schemas + all_properties = {} + all_properties.update(ecd_schema.get("properties", {})) + all_properties.update(llm_schema.get("properties", {})) + + combined = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": f"{SCHEMA_BASE_URL}/ludwig-config.json", + "title": "Ludwig Configuration", + "description": f"Configuration schema for Ludwig models (v{LUDWIG_VERSION})", + "type": "object", + "properties": all_properties, + "required": ["input_features", "output_features"], + "additionalProperties": True, + } + if strip_metadata: + combined = _strip_parameter_metadata(combined) + return combined + + +def main(sys_argv=None): + parser = argparse.ArgumentParser(description="Export Ludwig config JSON schema") + parser.add_argument( + "--model-type", + choices=[MODEL_ECD, MODEL_LLM, "combined"], + default="combined", + help="Model type to export schema for (default: combined)", + ) + parser.add_argument("--output", "-o", type=str, default=None, help="Output file (default: stdout)") + parser.add_argument( + "--full", + action="store_true", + help="Include parameter_metadata in the output (default: stripped)", + ) + args = parser.parse_args(sys_argv) + + strip_metadata = not args.full + + if args.model_type == "combined": + schema = export_combined_schema(strip_metadata=strip_metadata) + else: + schema = export_schema(args.model_type, strip_metadata=strip_metadata) + + output = json.dumps(schema, indent=2, sort_keys=False) + + if args.output: + with open(args.output, "w") as f: + f.write(output) + f.write("\n") + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/ludwig/schema/features/__init__.py b/ludwig/schema/features/__init__.py new file mode 100644 index 0000000..04bede5 --- /dev/null +++ b/ludwig/schema/features/__init__.py @@ -0,0 +1,14 @@ +import ludwig.schema.features.anomaly_feature +import ludwig.schema.features.audio_feature +import ludwig.schema.features.bag_feature +import ludwig.schema.features.binary_feature +import ludwig.schema.features.category_feature +import ludwig.schema.features.date_feature +import ludwig.schema.features.h3_feature +import ludwig.schema.features.image_feature +import ludwig.schema.features.number_feature +import ludwig.schema.features.sequence_feature +import ludwig.schema.features.set_feature +import ludwig.schema.features.text_feature +import ludwig.schema.features.timeseries_feature +import ludwig.schema.features.vector_feature # noqa diff --git a/ludwig/schema/features/anomaly_feature.py b/ludwig/schema/features/anomaly_feature.py new file mode 100644 index 0000000..8d4a3ab --- /dev/null +++ b/ludwig/schema/features/anomaly_feature.py @@ -0,0 +1,151 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ANOMALY, ANOMALY_AUROC, DEEP_SVDD, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.features.base import BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_output_config_registry, + output_mixin_registry, +) +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@output_mixin_registry.register(ANOMALY) +class AnomalyOutputFeatureConfigMixin(LudwigBaseConfig): + """AnomalyOutputFeatureConfigMixin configures parameters shared between the anomaly output feature and the + anomaly global defaults section of the Ludwig config.""" + + decoder: BaseDecoderConfig = None + + loss: BaseLossConfig = LossDataclassField( + feature_type=ANOMALY, + default=DEEP_SVDD, + ) + + +@DeveloperAPI +class AnomalyOutputFeatureConfig(AnomalyOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """AnomalyOutputFeatureConfig configures the parameters for the anomaly output feature. + + The anomaly output feature implements Deep One-Class Classification: the encoder maps + all inputs into a latent space, and the decoder computes the squared Euclidean distance + from a learned hypersphere center c. This distance is the *anomaly score* — the higher + the score, the more anomalous the input. + + Three loss functions are available: + + - ``deep_svdd`` (default): Geometric hypersphere objective. Pulls all training points + toward center c. Simple, interpretable, and effective for homogeneous normal data. + Ruff et al., ICML 2018. + + - ``deep_sad``: Semi-supervised extension. Requires a target column with 0 (normal), + 1 (confirmed anomaly), or -1 (unlabeled). Labeled anomalies are pushed *away* from c + while normal/unlabeled samples are pulled toward it. + Ruff et al., ICLR 2020. + + - ``drocc``: Adversarially robust variant. Adds a perturbation-based regularizer to + prevent hypersphere collapse — a degenerate solution where all representations + converge to c. Recommended when using expressive encoders (e.g. transformer-based). + Goyal et al., ICML 2020. + + **Multimodal anomaly detection** works out of the box: simply add multiple input features + (text, image, tabular, audio, etc.) to the ECD model. The combiner will fuse them before + the anomaly decoder. + + **Threshold selection**: after training, the ``threshold`` determines when an anomaly + score is classified as an anomaly. Set ``threshold="auto"`` to automatically select + the threshold as the `threshold_percentile`-th percentile of validation scores + (e.g., 95th percentile means 5% of validation examples are flagged). + """ + + type: str = schema_utils.ProtectedString(ANOMALY) + + default_validation_metric: str = schema_utils.StringOptions( + [ANOMALY_AUROC], + default=ANOMALY_AUROC, + description="Internal only: default validation metric for anomaly output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + ) + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description=( + "How to reduce an input that is not a vector, but a matrix or higher-order tensor, on the first dimension " + "(second if you count the batch dimension)." + ), + ) + + threshold: float | str = schema_utils.OneOfOptionsField( + default="auto", + description=( + "Decision threshold for classifying a sample as anomalous. Anomaly scores above this value are " + "predicted as anomalies. Set to ``'auto'`` to automatically select the threshold as the " + "``threshold_percentile``-th percentile of anomaly scores on the validation set. " + "Set to a float in [0, inf) to use a fixed threshold." + ), + field_options=[ + schema_utils.FloatRange(default=0.5, min=0.0), + schema_utils.StringOptions(["auto"], default="auto"), + ], + ) + + threshold_percentile: float = schema_utils.FloatRange( + default=95.0, + min=0.0, + max=100.0, + description=( + "When ``threshold='auto'``, this is the percentile of validation-set anomaly scores used as the " + "decision threshold. For example, 95.0 means 5% of validation examples are flagged as anomalies. " + "Ignored when ``threshold`` is a fixed float." + ), + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(ANOMALY) +class ECDAnomalyOutputFeatureConfig(AnomalyOutputFeatureConfig): + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=ANOMALY, + default="anomaly", + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(ANOMALY) +class AnomalyDefaultsConfig(AnomalyOutputFeatureConfigMixin): + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=ANOMALY, + default="anomaly", + ) diff --git a/ludwig/schema/features/audio_feature.py b/ludwig/schema/features/audio_feature.py new file mode 100644 index 0000000..806347a --- /dev/null +++ b/ludwig/schema/features/audio_feature.py @@ -0,0 +1,34 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@ecd_defaults_config_registry.register(AUDIO) +@input_mixin_registry.register(AUDIO) +class AudioInputFeatureConfigMixin(LudwigBaseConfig): + """AudioInputFeatureConfigMixin is a dataclass that configures the parameters used in both the audio input + feature and the audio global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=AUDIO) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=AUDIO, + default="parallel_cnn", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(AUDIO) +class AudioInputFeatureConfig(AudioInputFeatureConfigMixin, BaseInputFeatureConfig): + """AudioInputFeatureConfig is a dataclass that configures the parameters used for an audio input feature.""" + + type: str = schema_utils.ProtectedString(AUDIO) diff --git a/ludwig/schema/features/augmentation/__init__.py b/ludwig/schema/features/augmentation/__init__.py new file mode 100644 index 0000000..3091a95 --- /dev/null +++ b/ludwig/schema/features/augmentation/__init__.py @@ -0,0 +1,2 @@ +# Register all augmentation schemas +import ludwig.schema.features.augmentation.image # noqa: F401 diff --git a/ludwig/schema/features/augmentation/base.py b/ludwig/schema/features/augmentation/base.py new file mode 100644 index 0000000..5e7e4c1 --- /dev/null +++ b/ludwig/schema/features/augmentation/base.py @@ -0,0 +1,9 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils + + +@DeveloperAPI +class BaseAugmentationConfig(schema_utils.LudwigBaseConfig): + """Base class for augmentation.""" + + type: str diff --git a/ludwig/schema/features/augmentation/image.py b/ludwig/schema/features/augmentation/image.py new file mode 100644 index 0000000..4dde841 --- /dev/null +++ b/ludwig/schema/features/augmentation/image.py @@ -0,0 +1,122 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUGMENTATION, IMAGE, TYPE +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.augmentation.base import BaseAugmentationConfig +from ludwig.schema.features.augmentation.utils import register_augmentation_config +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_augmentation_config(name="auto_augmentation", features=IMAGE) +class AutoAugmentationConfig(BaseAugmentationConfig): + """Automatic augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "auto_augmentation", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE], + ) + method: str = schema_utils.String( + default="trivial_augment", + description="Specifies the method for applying automatic data augmentation", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["auto_augmentation_method"], + ) + + +@DeveloperAPI +@register_augmentation_config(name="random_horizontal_flip", features=IMAGE) +class RandomHorizontalFlipConfig(BaseAugmentationConfig): + """Random horizontal flip augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "random_horizontal_flip", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE], + ) + + +@DeveloperAPI +@register_augmentation_config(name="random_vertical_flip", features=IMAGE) +class RandomVerticalFlipConfig(BaseAugmentationConfig): + """Random vertical flip augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "random_vertical_flip", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE], + ) + + +@DeveloperAPI +@register_augmentation_config(name="random_rotate", features=IMAGE) +class RandomRotateConfig(BaseAugmentationConfig): + """Random rotation augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "random_rotate", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["type"], + ) + degree: int = schema_utils.Integer( + default=15, + description="Range of angle for random rotation, i.e., [-degree, +degree].", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["rotation_degree"], + ) + + +@DeveloperAPI +@register_augmentation_config(name="random_blur", features=IMAGE) +class RandomBlurConfig(BaseAugmentationConfig): + """Random blur augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "random_blur", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE], + ) + kernel_size: int = schema_utils.Integer( + default=3, + description="Kernel size for random blur.", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["kernel_size"], + ) + + +@DeveloperAPI +@register_augmentation_config(name="random_brightness", features=IMAGE) +class RandomBrightnessConfig(BaseAugmentationConfig): + """Random brightness augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "random_brightness", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE], + ) + + min: float = schema_utils.FloatRange( + default=0.5, + description="Minimum factor for random brightness.", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["min_brightness"], + ) + + max: float = schema_utils.FloatRange( + default=2.0, + description="Maximum factor for random brightness.", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["max_brightness"], + ) + + +@DeveloperAPI +@register_augmentation_config(name="random_contrast", features=IMAGE) +class RandomContrastConfig(BaseAugmentationConfig): + """Random Contrast augmentation operation.""" + + type: str = schema_utils.ProtectedString( + "random_contrast", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE], + ) + + min: float = schema_utils.FloatRange( + default=0.5, + description="Minimum factor for random contrast.", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["min_contrast"], + ) + + max: float = schema_utils.FloatRange( + default=2.0, + description="Maximum factor for random contrast.", + parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION]["max_contrast"], + ) diff --git a/ludwig/schema/features/augmentation/utils.py b/ludwig/schema/features/augmentation/utils.py new file mode 100644 index 0000000..b9520ac --- /dev/null +++ b/ludwig/schema/features/augmentation/utils.py @@ -0,0 +1,195 @@ +import copy +from dataclasses import field +from typing import Any + +import pydantic + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import TYPE +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.augmentation.base import BaseAugmentationConfig +from ludwig.utils.registry import Registry + +_augmentation_config_registry = Registry() + + +@DeveloperAPI +def get_augmentation_config_registry() -> Registry: + return _augmentation_config_registry + + +@DeveloperAPI +def register_augmentation_config(name: str, features: str | list[str]): + if isinstance(features, str): + features = [features] + + def wrap(cls): + for feature in features: + augmentation_registry = get_augmentation_config_registry().get(feature, {}) + augmentation_registry[name] = cls + get_augmentation_config_registry()[feature] = augmentation_registry + return cls + + return wrap + + +@DeveloperAPI +def get_augmentation_cls(feature: str, name: str): + return get_augmentation_config_registry()[feature][name] + + +@DeveloperAPI +def get_augmentation_classes(feature: str): + return get_augmentation_config_registry()[feature] + + +@DeveloperAPI +def AugmentationDataclassField( + feature_type: str, + default: str | BaseAugmentationConfig = False, + default_augmentations: list[BaseAugmentationConfig] | None = None, + description: str = "", +): + """Custom dataclass field that when used inside a dataclass will allow the user to specify an augmentation + config. + + Args: + default: The default augmentation config to use. + default_augmentations: The default list of augmentations to use when param value is set to `True`. + description: The description of the augmentation config. + + Returns: Initialized dataclass field that converts a list with params to an augmentation config. + """ + + default_augmentations = default_augmentations or [] + default_augmentations = [a.to_dict() for a in default_augmentations] + + if isinstance(default, bool): + default = default_augmentations if default else [] + + class AugmentationContainerConfigField(schema_utils.SchemaField): + """Custom field that deserializes a list for a valid augmentation config from the augmentation_registry and + creates a corresponding JSON schema for external usage.""" + + def _deserialize(self, value, attr, data, **kwargs): + if isinstance(value, bool): + value = default_augmentations if value else [] + + if not isinstance(value, list): + raise ConfigValidationError(f"Augmentation config must be a list, found: {type(value)}") + + augmentation_classes = get_augmentation_classes(feature_type) + augmentation_list = [] + for augmentation in value: + augmentation_op = augmentation[TYPE] + if augmentation_op in augmentation_classes: + augmentation_cls = augmentation_classes[augmentation_op] + pre = augmentation_cls() + try: + augmentation_list.append(pre.model_validate(augmentation)) + except (TypeError, ConfigValidationError) as error: + raise ConfigValidationError( + f"Invalid augmentation params: {value}, see `{pre}` definition. Error: {error}" + ) + else: + raise ConfigValidationError( + f"Invalid augmentation type: '{augmentation_op}', " + f"expected one of: {list(augmentation_classes.keys())}" + ) + return augmentation_list + + def _jsonschema_type_mapping(self): + return get_augmentation_list_jsonschema(feature_type, default) + + try: + if not isinstance(default, list): + raise TypeError( + f"Augmentation config must be a list, got {type(default).__name__}.\n" + f"Fix: provide augmentation as a list of dicts, e.g. [{{'type': 'random_horizontal_flip'}}]." + ) + load_augmentation_list = [] + dump_augmentation_list = [] + for augmentation in default: + augmentation_op = augmentation[TYPE] + augmentation_cls = get_augmentation_cls(feature_type, augmentation_op) + pre = augmentation_cls() + try: + load_augmentation_list.append(pre.model_validate(augmentation)) + try: + dump_augmentation_list.append(pre.model_validate(augmentation).to_dict()) + except pydantic.ValidationError: + dump_augmentation_list.append(augmentation if isinstance(augmentation, dict) else {}) + except (TypeError, ConfigValidationError) as error: + raise ConfigValidationError( + f"Invalid augmentation params: {default}, see `{pre}` definition. Error: {error}" + ) + + load_default = lambda: copy.deepcopy(load_augmentation_list) + dump_default = dump_augmentation_list + + return field( + metadata={ + "marshmallow_field": AugmentationContainerConfigField( + allow_none=False, + dump_default=dump_default, + load_default=load_default, + ) + }, + default_factory=load_default, + ) + except Exception as e: + raise ConfigValidationError(f"Unsupported augmentation type. See augmentation_registry. Details: {e}") + + +@DeveloperAPI +def get_augmentation_list_jsonschema(feature_type: str, default: list[dict[str, Any]]): + """This function returns a JSON augmentation schema. + + Returns: JSON Schema + """ + augmentation_types = sorted(list(get_augmentation_config_registry()[feature_type].keys())) + schema = { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": augmentation_types, + "title": "type", + "description": "Type of augmentation to apply.", + }, + }, + "additionalProperties": True, + "allOf": get_augmentation_list_conds(feature_type), + "required": ["type"], + }, + "title": "array_option", + }, + {"type": "boolean", "description": "Apply standard augmentation pipeline.", "title": "boolean_option"}, + ], + "title": "augmentation", + } + + return schema + + +@DeveloperAPI +def get_augmentation_list_conds(feature_type: str): + """This function returns a list of if-then JSON clauses for each augmentation type along with their properties + and constraints. + + Returns: List of JSON clauses + """ + conds = [] + for augmentation_op in get_augmentation_classes(feature_type): + schema_cls = get_augmentation_cls(feature_type, augmentation_op) + augmentation_schema = schema_utils.unload_jsonschema_from_config_class(schema_cls) + augmentation_props = augmentation_schema["properties"] + schema_utils.remove_duplicate_fields(augmentation_props) + augmentation_cond = schema_utils.create_cond({"type": augmentation_op}, augmentation_props) + conds.append(augmentation_cond) + return conds diff --git a/ludwig/schema/features/bag_feature.py b/ludwig/schema/features/bag_feature.py new file mode 100644 index 0000000..18076f9 --- /dev/null +++ b/ludwig/schema/features/bag_feature.py @@ -0,0 +1,34 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BAG, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@ecd_defaults_config_registry.register(BAG) +@input_mixin_registry.register(BAG) +class BagInputFeatureConfigMixin(LudwigBaseConfig): + """BagInputFeatureConfigMixin is a dataclass that configures the parameters used in both the bag input feature + and the bag global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=BAG) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=BAG, + default="embed", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(BAG) +class BagInputFeatureConfig(BagInputFeatureConfigMixin, BaseInputFeatureConfig): + """BagInputFeatureConfig is a dataclass that configures the parameters used for a bag input feature.""" + + type: str = schema_utils.ProtectedString(BAG) diff --git a/ludwig/schema/features/base.py b/ludwig/schema/features/base.py new file mode 100644 index 0000000..7387ada --- /dev/null +++ b/ludwig/schema/features/base.py @@ -0,0 +1,343 @@ +import logging +from collections.abc import Iterable +from dataclasses import field +from typing import Any, Generic, TypeVar + +from rich.console import Console + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + ANOMALY, + AUDIO, + BAG, + BINARY, + CATEGORY, + DATE, + H3, + IMAGE, + MODEL_ECD, + MODEL_LLM, + NUMBER, + SEQUENCE, + SET, + TEXT, + TIMESERIES, + VECTOR, +) +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.utils import ( + ecd_input_config_registry, + ecd_output_config_registry, + get_input_feature_jsonschema, + get_output_feature_jsonschema, + llm_input_config_registry, + llm_output_config_registry, +) +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY, ParameterMetadata + +logger = logging.getLogger(__name__) +_error_console = Console(stderr=True, style="bold red") +_info_console = Console(stderr=True, style="bold green") + + +@DeveloperAPI +class BaseFeatureConfig(schema_utils.LudwigBaseConfig): + """Base class for feature configs.""" + + def __post_init__(self): + # TODO(travis): this should be done through marshmallow dataclass' `required` field param, + # but requires a refactor` + if self.name is None: + raise ConfigValidationError("All features must have a name.") + if self.type is None: + raise ConfigValidationError(f"Feature {self.name} must have a type.") + + active: bool = True + + name: str = schema_utils.String( + default=None, + allow_none=True, + description="Name of the feature.", + ) + + type: str = schema_utils.StringOptions( + default=None, + allow_none=True, + options=[ + ANOMALY, + AUDIO, + BAG, + BINARY, + CATEGORY, + DATE, + H3, + IMAGE, + NUMBER, + SEQUENCE, + SET, + TEXT, + TIMESERIES, + VECTOR, + ], + description="Type of the feature.", + ) + + column: str = schema_utils.String( + allow_none=True, + default=None, + description="The column name of this feature. Defaults to name if not specified.", + ) + + proc_column: str = schema_utils.String( + allow_none=True, + default=None, + description="The name of the preprocessed column name of this feature. Internal only.", + parameter_metadata=ParameterMetadata(internal_only=True), + ) + + def enable(self): + """This function allows the user to specify which features from a dataset should be included during model + training. This is the equivalent to toggling on a feature in the model creation UI. + + Returns: + None + """ + if self.active: + _error_console.print("This feature is already enabled!") + else: + self.active = True + _info_console.print(f"{self.name} feature enabled!\n") + logger.info(self.__repr__()) + + def disable(self): + """This function allows the user to specify which features from a dataset should not be included during + model training. This is the equivalent to toggling off a feature in the model creation UI. + + Returns: + None + """ + if not self.active: + _error_console.print("This feature is already disabled!") + else: + self.active = False + _info_console.print(f"{self.name} feature disabled!\n") + logger.info(self.__repr__()) + + +@DeveloperAPI +class BaseInputFeatureConfig(BaseFeatureConfig): + """Base input feature config class.""" + + tied: str = schema_utils.String( + default=None, + allow_none=True, + description="Name of input feature to tie the weights of the encoder with. It needs to be the name of a " + "feature of the same type and with the same encoder parameters. If text or sequence features are tied, " + "consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have " + "equal sized outputs. This is necessary when using the `sequence` combiner.", + ) + + def has_augmentation(self) -> bool: + return False + + +@DeveloperAPI +class ECDInputFeatureConfig(BaseFeatureConfig): + pass + + +@DeveloperAPI +class BaseOutputFeatureConfig(BaseFeatureConfig): + """Base output feature config class.""" + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + ) + + default_validation_metric: str = schema_utils.String( + default=None, + allow_none=True, + description="Internal only use parameter: default validation metric for output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list[str] = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + ) + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + ) + + input_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder.", + parameter_metadata=ParameterMetadata(internal_only=True), + ) + + num_classes: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Size of the input to the decoder.", + parameter_metadata=ParameterMetadata(internal_only=True), + ) + + +T = TypeVar("T", bound=BaseFeatureConfig) + + +class FeatureCollection(Generic[T], schema_utils.ListSerializable): + def __init__(self, features: list[T]): + self._features = features + self._name_to_feature = {f.name: f for f in features} + for k, v in self._name_to_feature.items(): + setattr(self, k, v) + + def to_list(self) -> list[dict[str, Any]]: + out_list = [] + for feature in self._features: + out_list.append(feature.to_dict()) + return out_list + + def items(self) -> Iterable[tuple[str, T]]: + return self._name_to_feature.items() + + def __iter__(self): + return iter(self._features) + + def __len__(self): + return len(self._features) + + def __getitem__(self, i) -> T: + if isinstance(i, str): + return self._name_to_feature[i] + else: + return self._features[i] + + +class FeatureList(schema_utils.SchemaField): + """A schema field that deserializes a list of dicts into a FeatureCollection. + + Each item is resolved via the inner TypeSelection's resolve() method. + """ + + def __init__( + self, + inner: schema_utils.TypeSelection, + min_length: int | None = None, + max_length: int | None = None, + equal: int | None = None, + metadata: dict | None = None, + ): + self.inner = inner + self.min_length = min_length + self.max_length = max_length + self.equal = equal + self.metadata = metadata or {} + + def _deserialize(self, value, attr, data, **kwargs) -> FeatureCollection: + if not isinstance(value, list): + raise ConfigValidationError(f"Expected a list of features for '{attr}', got {type(value).__name__}") + + # Validate length constraints + n = len(value) + if self.equal is not None and n != self.equal: + raise ConfigValidationError(f"Expected exactly {self.equal} feature(s) for '{attr}', got {n}") + if self.min_length is not None and n < self.min_length: + raise ConfigValidationError(f"Expected at least {self.min_length} feature(s) for '{attr}', got {n}") + if self.max_length is not None and n > self.max_length: + raise ConfigValidationError(f"Expected at most {self.max_length} feature(s) for '{attr}', got {n}") + + feature_list = [self.inner.resolve(item) for item in value] + return FeatureCollection(feature_list) + + def _jsonschema_type_mapping(self): + inner_schema = self.inner._jsonschema_type_mapping() or {} + result = {"type": "array", "items": inner_schema} + if self.min_length is not None: + result["minItems"] = self.min_length + if self.max_length is not None: + result["maxItems"] = self.max_length + if self.equal is not None: + result["minItems"] = self.equal + result["maxItems"] = self.equal + return result + + +class FeaturesTypeSelection(schema_utils.TypeSelection): + def __init__( + self, + *args, + min_length: int | None = 1, + max_length: int | None = None, + supplementary_metadata=None, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.min_length = min_length + self.max_length = max_length + self.supplementary_metadata = {} if supplementary_metadata is None else supplementary_metadata + + def get_list_field(self): + min_length = self.min_length + max_length = self.max_length + equal = None + if min_length == max_length: + min_length = None + max_length = None + equal = self.max_length + + return field( + metadata={ + "marshmallow_field": FeatureList( + self, + min_length=min_length, + max_length=max_length, + equal=equal, + metadata=self.supplementary_metadata, + ) + }, + ) + + +class ECDInputFeatureSelection(FeaturesTypeSelection): + def __init__(self): + super().__init__( + registry=ecd_input_config_registry, + description="Type of the input feature", + supplementary_metadata={"uniqueItemProperties": ["name"]}, + ) + + def _jsonschema_type_mapping(self): + return get_input_feature_jsonschema(MODEL_ECD) + + +class LLMInputFeatureSelection(FeaturesTypeSelection): + def __init__(self): + super().__init__(registry=llm_input_config_registry, description="Type of the input feature") + + def _jsonschema_type_mapping(self): + return get_input_feature_jsonschema(MODEL_LLM) + + +class ECDOutputFeatureSelection(FeaturesTypeSelection): + def __init__(self): + super().__init__(registry=ecd_output_config_registry, description="Type of the output feature") + + def _jsonschema_type_mapping(self): + return get_output_feature_jsonschema(MODEL_ECD) + + +class LLMOutputFeatureSelection(FeaturesTypeSelection): + def __init__(self): + # TODO(Arnav): Remove the hard check on max_length once we support multiple output features. + super().__init__(max_length=1, registry=llm_output_config_registry, description="Type of the output feature") + + def _jsonschema_type_mapping(self): + return get_output_feature_jsonschema(MODEL_LLM) diff --git a/ludwig/schema/features/binary_feature.py b/ludwig/schema/features/binary_feature.py new file mode 100644 index 0000000..d0d57ab --- /dev/null +++ b/ludwig/schema/features/binary_feature.py @@ -0,0 +1,141 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, BINARY_WEIGHTED_CROSS_ENTROPY, MODEL_ECD, ROC_AUC +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(BINARY) +class BinaryInputFeatureConfigMixin(LudwigBaseConfig): + """BinaryInputFeatureConfigMixin is a dataclass that configures the parameters used in both the binary input + feature and the binary global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=BINARY) + + +@DeveloperAPI +class BinaryInputFeatureConfig(BinaryInputFeatureConfigMixin, BaseInputFeatureConfig): + """BinaryInputFeatureConfig is a dataclass that configures the parameters used for a binary input feature.""" + + type: str = schema_utils.ProtectedString(BINARY) + + encoder: BaseEncoderConfig = None + + +@DeveloperAPI +@ecd_input_config_registry.register(BINARY) +class ECDBinaryInputFeatureConfig(BinaryInputFeatureConfig): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=BINARY, + default="passthrough", + ) + + +@DeveloperAPI +@output_mixin_registry.register(BINARY) +class BinaryOutputFeatureConfigMixin(LudwigBaseConfig): + """BinaryOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the binary output + feature and the binary global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = None + + loss: BaseLossConfig = LossDataclassField( + feature_type=BINARY, + default=BINARY_WEIGHTED_CROSS_ENTROPY, + ) + + +@DeveloperAPI +class BinaryOutputFeatureConfig(BinaryOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """BinaryOutputFeatureConfig is a dataclass that configures the parameters used for a binary output feature.""" + + type: str = schema_utils.ProtectedString(BINARY) + + calibration: bool = schema_utils.Boolean( + default=False, + description="Calibrate the model's output probabilities using temperature scaling.", + parameter_metadata=FEATURE_METADATA[BINARY]["calibration"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [ROC_AUC], + default=ROC_AUC, + description="Internal only use parameter: default validation metric for binary output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[BINARY]["dependencies"], + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="binary_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[BINARY]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[BINARY]["reduce_input"], + ) + + threshold: float = schema_utils.FloatRange( + default=0.5, + min=0, + max=1, + description="The threshold used to convert output probabilities to predictions. Predicted probabilities greater" + "than or equal to threshold are mapped to True.", + parameter_metadata=FEATURE_METADATA[BINARY]["threshold"], + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(BINARY) +class ECDBinaryOutputFeatureConfig(BinaryOutputFeatureConfig): + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=BINARY, + default="regressor", + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(BINARY) +class BinaryDefaultsConfig(BinaryInputFeatureConfigMixin, BinaryOutputFeatureConfigMixin): + # NOTE(travis): defaults use ECD input feature as it contains all the encoders + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=BINARY, + default="passthrough", + ) + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=BINARY, + default="regressor", + ) diff --git a/ludwig/schema/features/category_feature.py b/ludwig/schema/features/category_feature.py new file mode 100644 index 0000000..5949f75 --- /dev/null +++ b/ludwig/schema/features/category_feature.py @@ -0,0 +1,180 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ACCURACY, CATEGORY, CATEGORY_DISTRIBUTION, MODEL_ECD, MODEL_LLM, SOFTMAX_CROSS_ENTROPY +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + llm_output_config_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(CATEGORY) +class CategoryInputFeatureConfigMixin(LudwigBaseConfig): + """CategoryInputFeatureConfigMixin is a dataclass that configures the parameters used in both the category + input feature and the category global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=CATEGORY) + + +@DeveloperAPI +class CategoryInputFeatureConfig(CategoryInputFeatureConfigMixin, BaseInputFeatureConfig): + """CategoryInputFeatureConfig is a dataclass that configures the parameters used for a category input + feature.""" + + type: str = schema_utils.ProtectedString(CATEGORY) + + encoder: BaseEncoderConfig = None + + +@DeveloperAPI +@ecd_input_config_registry.register(CATEGORY) +class ECDCategoryInputFeatureConfig(CategoryInputFeatureConfig): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=CATEGORY, + default="dense", + ) + + +@DeveloperAPI +@output_mixin_registry.register(CATEGORY) +class CategoryOutputFeatureConfigMixin(LudwigBaseConfig): + """CategoryOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the category + output feature and the category global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = None + + loss: BaseLossConfig = LossDataclassField( + feature_type=CATEGORY, + default=SOFTMAX_CROSS_ENTROPY, + ) + + +@DeveloperAPI +class CategoryOutputFeatureConfig(CategoryOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """CategoryOutputFeatureConfig is a dataclass that configures the parameters used for a category output + feature.""" + + type: str = schema_utils.ProtectedString(CATEGORY) + + calibration: bool = schema_utils.Boolean( + default=False, + description="Calibrate the model's output probabilities using temperature scaling.", + parameter_metadata=FEATURE_METADATA[CATEGORY]["calibration"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [ACCURACY], + default=ACCURACY, + description="Internal only use parameter: default validation metric for category output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[CATEGORY]["dependencies"], + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="category_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[CATEGORY]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[CATEGORY]["reduce_input"], + ) + + top_k: int = schema_utils.PositiveInteger( + default=3, + description="Determines the parameter k, the number of categories to consider when computing the top_k " + "measure. It computes accuracy but considering as a match if the true category appears in the " + "first k predicted categories ranked by decoder's confidence.", + parameter_metadata=FEATURE_METADATA[CATEGORY]["top_k"], + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(CATEGORY) +class ECDCategoryOutputFeatureConfig(CategoryOutputFeatureConfig): + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=CATEGORY, + default="classifier", + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(CATEGORY_DISTRIBUTION) +class CategoryDistributionOutputFeatureConfig(CategoryOutputFeatureConfig): + """CategoryDistributionOutputFeatureConfig is a dataclass that configures the parameters used for a + category_distribution output feature.""" + + type: str = schema_utils.ProtectedString(CATEGORY_DISTRIBUTION) + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=CATEGORY, + default="classifier", + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="category_distribution_output") + + +@DeveloperAPI +@ecd_defaults_config_registry.register(CATEGORY) +class CategoryDefaultsConfig(CategoryInputFeatureConfigMixin, CategoryOutputFeatureConfigMixin): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=CATEGORY, + default="dense", + ) + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=CATEGORY, + default="classifier", + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(CATEGORY_DISTRIBUTION) +class CategoryDistributionDefaultsConfig(CategoryOutputFeatureConfigMixin): + pass + + +@DeveloperAPI +@llm_output_config_registry.register(CATEGORY) +class LLMCategoryOutputFeatureConfig(CategoryOutputFeatureConfig): + """LLMCategoryOutputFeatureConfig is a dataclass that configures the parameters used for a category output + feature when using the Ludwig Light Model.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="category_llm") + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_LLM, + feature_type=CATEGORY, + default="category_extractor", + ) diff --git a/ludwig/schema/features/date_feature.py b/ludwig/schema/features/date_feature.py new file mode 100644 index 0000000..22288cc --- /dev/null +++ b/ludwig/schema/features/date_feature.py @@ -0,0 +1,34 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DATE, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@ecd_defaults_config_registry.register(DATE) +@input_mixin_registry.register(DATE) +class DateInputFeatureConfigMixin(LudwigBaseConfig): + """DateInputFeatureConfigMixin is a dataclass that configures the parameters used in both the date input + feature and the date global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=DATE) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=DATE, + default="embed", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(DATE) +class DateInputFeatureConfig(DateInputFeatureConfigMixin, BaseInputFeatureConfig): + """DateInputFeature is a dataclass that configures the parameters used for a date input feature.""" + + type: str = schema_utils.ProtectedString(DATE) diff --git a/ludwig/schema/features/h3_feature.py b/ludwig/schema/features/h3_feature.py new file mode 100644 index 0000000..eb9ac55 --- /dev/null +++ b/ludwig/schema/features/h3_feature.py @@ -0,0 +1,34 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import H3, MODEL_ECD +from ludwig.schema import utils as schema_utils +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@ecd_defaults_config_registry.register(H3) +@input_mixin_registry.register(H3) +class H3InputFeatureConfigMixin(LudwigBaseConfig): + """H3InputFeatureConfigMixin is a dataclass that configures the parameters used in both the h3 input feature + and the h3 global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=H3) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=H3, + default="embed", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(H3) +class H3InputFeatureConfig(H3InputFeatureConfigMixin, BaseInputFeatureConfig): + """H3InputFeatureConfig is a dataclass that configures the parameters used for an h3 input feature.""" + + type: str = schema_utils.ProtectedString(H3) diff --git a/ludwig/schema/features/image_feature.py b/ludwig/schema/features/image_feature.py new file mode 100644 index 0000000..ee4136c --- /dev/null +++ b/ludwig/schema/features/image_feature.py @@ -0,0 +1,126 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import IMAGE, LOSS, MODEL_ECD, SOFTMAX_CROSS_ENTROPY +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.augmentation.base import BaseAugmentationConfig +from ludwig.schema.features.augmentation.image import RandomHorizontalFlipConfig, RandomRotateConfig +from ludwig.schema.features.augmentation.utils import AugmentationDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + +# Augmentation operations when augmentation is set to True +AUGMENTATION_DEFAULT_OPERATIONS = [ + RandomHorizontalFlipConfig(), + RandomRotateConfig(), +] + + +@DeveloperAPI +@ecd_defaults_config_registry.register(IMAGE) +@input_mixin_registry.register(IMAGE) +class ImageInputFeatureConfigMixin(LudwigBaseConfig): + """ImageInputFeatureConfigMixin is a dataclass that configures the parameters used in both the image input + feature and the image global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=IMAGE) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=IMAGE, + default="stacked_cnn", + ) + + augmentation: list[BaseAugmentationConfig] = AugmentationDataclassField( + feature_type=IMAGE, + default=False, + default_augmentations=AUGMENTATION_DEFAULT_OPERATIONS, + description="Augmentation operation configuration.", + ) + + def has_augmentation(self) -> bool: + # Check for None, False, and [] + return bool(self.augmentation) + + +@DeveloperAPI +@ecd_input_config_registry.register(IMAGE) +class ImageInputFeatureConfig(ImageInputFeatureConfigMixin, BaseInputFeatureConfig): + """ImageInputFeatureConfig is a dataclass that configures the parameters used for an image input feature.""" + + type: str = schema_utils.ProtectedString(IMAGE) + + +@DeveloperAPI +@output_mixin_registry.register(IMAGE) +class ImageOutputFeatureConfigMixin(LudwigBaseConfig): + """ImageOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the image output + feature and the image global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=IMAGE, + default="unet", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=IMAGE, + default=SOFTMAX_CROSS_ENTROPY, + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(IMAGE) +class ImageOutputFeatureConfig(ImageOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """ImageOutputFeatureConfig is a dataclass that configures the parameters used for an image output feature.""" + + type: str = schema_utils.ProtectedString(IMAGE) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[IMAGE]["dependencies"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [LOSS], + default=LOSS, + description="Internal only use parameter: default validation metric for image output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="image_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default=None, + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[IMAGE]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default=None, + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[IMAGE]["reduce_input"], + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(IMAGE) +class ImageDefaultsConfig(ImageInputFeatureConfigMixin, ImageOutputFeatureConfigMixin): + pass diff --git a/ludwig/schema/features/loss/__init__.py b/ludwig/schema/features/loss/__init__.py new file mode 100644 index 0000000..e7ee053 --- /dev/null +++ b/ludwig/schema/features/loss/__init__.py @@ -0,0 +1 @@ +from ludwig.schema.features.loss.loss import get_loss_classes, get_loss_cls, get_loss_schema_registry # noqa diff --git a/ludwig/schema/features/loss/loss.py b/ludwig/schema/features/loss/loss.py new file mode 100644 index 0000000..711e9c0 --- /dev/null +++ b/ludwig/schema/features/loss/loss.py @@ -0,0 +1,927 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + ANOMALY, + BINARY, + BINARY_WEIGHTED_CROSS_ENTROPY, + CATEGORY, + CORN, + DEEP_SAD, + DEEP_SVDD, + DICE_LOSS, + DROCC, + ENTMAX15_LOSS, + ENTROPIC_OPEN_SET, + FOCAL_LOSS, + HUBER, + IMAGE, + LOVASZ_SOFTMAX_LOSS, + MEAN_ABSOLUTE_ERROR, + MEAN_ABSOLUTE_PERCENTAGE_ERROR, + MEAN_SQUARED_ERROR, + NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY, + NT_XENT_LOSS, + NUMBER, + OBJECTOSPHERE, + POLY_LOSS, + ROOT_MEAN_SQUARED_ERROR, + ROOT_MEAN_SQUARED_PERCENTAGE_ERROR, + SEQUENCE, + SEQUENCE_SOFTMAX_CROSS_ENTROPY, + SET, + SIGMOID_CROSS_ENTROPY, + SOFTMAX_CROSS_ENTROPY, + SPARSEMAX_LOSS, + TEXT, + TIMESERIES, + VECTOR, +) +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import LOSS_METADATA +from ludwig.utils.registry import Registry + +ROBUST_LAMBDA_DESCRIPTION = ( + "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of " + "classes. Useful in case of noisy labels." +) + +CONFIDENCE_PENALTY_DESCRIPTION = ( + "Penalizes overconfident predictions (low entropy) by adding an additional term " + "that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` " + "term to the loss, where a is the value of this parameter. Useful in case of noisy labels." +) + +CLASS_WEIGHTS_DESCRIPTION = ( + "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. " + "The value can be a vector of weights, one for each class, that is multiplied to the " + "loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in " + "case of unbalanced class distribution. The ordering of the vector follows the category to integer ID " + "mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value " + "can be a dictionary with class strings as keys and weights as values, like " + "`{class_a: 0.5, class_b: 0.7, ...}`." +) + +CLASS_SIMILARITIES_DESCRIPTION = ( + "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of " + "classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows " + "the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too)." +) + +CLASS_SIMILARITIES_TEMPERATURE_DESCRIPTION = ( + "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of " + "that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be " + "provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more " + "tolerable than errors between really different classes." +) + + +@DeveloperAPI +class BaseLossConfig(schema_utils.LudwigBaseConfig): + """Base class for feature configs.""" + + type: str + + weight: float = 1.0 + + @classmethod + def name(cls) -> str: + return "[undefined]" + + +_loss_registry = Registry[type[BaseLossConfig]]() +_loss_feature_registry = Registry[dict[str, type[BaseLossConfig]]]() + + +@DeveloperAPI +def get_loss_schema_registry() -> Registry[type[BaseLossConfig]]: + return _loss_registry + + +@DeveloperAPI +def get_loss_cls(feature: str, name: str) -> type[BaseLossConfig]: + return _loss_feature_registry[feature][name] + + +@DeveloperAPI +def get_loss_classes(feature: str) -> dict[str, type[BaseLossConfig]]: + return _loss_feature_registry[feature] + + +def register_loss(features: str | list[str]): + if isinstance(features, str): + features = [features] + + def wrap(cls: type[BaseLossConfig]): + _loss_registry[cls.type] = cls + for feature in features: + feature_registry = _loss_feature_registry.get(feature, {}) + feature_registry[cls.type] = cls + _loss_feature_registry[feature] = feature_registry + return cls + + return wrap + + +@DeveloperAPI +@register_loss([NUMBER, TIMESERIES, VECTOR]) +class MSELossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + MEAN_SQUARED_ERROR, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["MSELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Mean Squared Error (MSE)" + + +@DeveloperAPI +@register_loss([NUMBER, TIMESERIES, VECTOR]) +class MAELossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + MEAN_ABSOLUTE_ERROR, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["MAELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Mean Absolute Error (MAE)" + + +@DeveloperAPI +@register_loss([NUMBER, TIMESERIES, VECTOR]) +class MAPELossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + MEAN_ABSOLUTE_PERCENTAGE_ERROR, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["MAELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Mean Absolute Percentage Error (MAPE)" + + +@DeveloperAPI +@register_loss([NUMBER]) +class RMSELossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + ROOT_MEAN_SQUARED_ERROR, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["RMSELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Root Mean Squared Error (RMSE)" + + +@DeveloperAPI +@register_loss([NUMBER]) +class RMSPELossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + ROOT_MEAN_SQUARED_PERCENTAGE_ERROR, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["RMSPELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Root Mean Squared Percentage Error (RMSPE)" + + +@DeveloperAPI +@register_loss([BINARY]) +class BWCEWLossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + BINARY_WEIGHTED_CROSS_ENTROPY, + description="Type of loss.", + ) + + positive_class_weight: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="Weight of the positive class.", + parameter_metadata=LOSS_METADATA["BWCEWLoss"]["positive_class_weight"], + ) + + robust_lambda: int = schema_utils.NonNegativeInteger( + default=0, + description=ROBUST_LAMBDA_DESCRIPTION, + parameter_metadata=LOSS_METADATA["BWCEWLoss"]["robust_lambda"], + ) + + confidence_penalty: float = schema_utils.NonNegativeFloat( + default=0, + description=CONFIDENCE_PENALTY_DESCRIPTION, + parameter_metadata=LOSS_METADATA["BWCEWLoss"]["confidence_penalty"], + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["BWCEWLoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Binary Weighted Cross Entropy (BWCE)" + + +@DeveloperAPI +@register_loss([CATEGORY, VECTOR, IMAGE]) +class SoftmaxCrossEntropyLossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + SOFTMAX_CROSS_ENTROPY, + description="Type of loss.", + ) + + class_weights: list[float] | dict | None = schema_utils.OneOfOptionsField( + default=None, + description=CLASS_WEIGHTS_DESCRIPTION, + field_options=[ + schema_utils.Dict(default=None, allow_none=True), + schema_utils.List(list_type=float, allow_none=False), + ], + parameter_metadata=LOSS_METADATA["SoftmaxCrossEntropyLoss"]["class_weights"], + ) + + robust_lambda: int = schema_utils.NonNegativeInteger( + default=0, + description=ROBUST_LAMBDA_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SoftmaxCrossEntropyLoss"]["robust_lambda"], + ) + + confidence_penalty: float = schema_utils.NonNegativeFloat( + default=0, + description=CONFIDENCE_PENALTY_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SoftmaxCrossEntropyLoss"]["confidence_penalty"], + ) + + class_similarities: list = schema_utils.List( + list, + default=None, + description=CLASS_SIMILARITIES_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SoftmaxCrossEntropyLoss"]["class_similarities"], + ) + + class_similarities_temperature: int = schema_utils.NonNegativeInteger( + default=0, + description=CLASS_SIMILARITIES_TEMPERATURE_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SoftmaxCrossEntropyLoss"]["class_similarities_temperature"], + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["SoftmaxCrossEntropyLoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Softmax Cross Entropy" + + +@DeveloperAPI +@register_loss([SEQUENCE, TEXT]) +class SequenceSoftmaxCrossEntropyLossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + SEQUENCE_SOFTMAX_CROSS_ENTROPY, + description="Type of loss.", + ) + + class_weights: list[float] | dict | None = schema_utils.OneOfOptionsField( + default=None, + description=CLASS_WEIGHTS_DESCRIPTION, + field_options=[ + schema_utils.Dict(default=None, allow_none=True), + schema_utils.List(list_type=float, allow_none=False), + ], + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["class_weights"], + ) + + robust_lambda: int = schema_utils.NonNegativeInteger( + default=0, + description=ROBUST_LAMBDA_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["robust_lambda"], + ) + + confidence_penalty: float = schema_utils.NonNegativeFloat( + default=0, + description=CONFIDENCE_PENALTY_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["confidence_penalty"], + ) + + class_similarities: list = schema_utils.List( + list, + default=None, + description=CLASS_SIMILARITIES_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["class_similarities"], + ) + + class_similarities_temperature: int = schema_utils.NonNegativeInteger( + default=0, + description=CLASS_SIMILARITIES_TEMPERATURE_DESCRIPTION, + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["class_similarities_temperature"], + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["weight"], + ) + + unique: bool = schema_utils.Boolean( + default=False, + description="If true, the loss is only computed for unique elements in the sequence.", + parameter_metadata=LOSS_METADATA["SequenceSoftmaxCrossEntropyLoss"]["unique"], + ) + + @classmethod + def name(self) -> str: + return "Sequence Softmax Cross Entropy" + + +@DeveloperAPI +@register_loss([SEQUENCE, TEXT]) +class NextTokenSoftmaxCrossEntropyLossConfig(SequenceSoftmaxCrossEntropyLossConfig): + type: str = schema_utils.ProtectedString( + NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY, + description="Type of loss.", + ) + + @classmethod + def name(self) -> str: + return "Next Token Softmax Cross Entropy" + + +@DeveloperAPI +@register_loss([SET]) +class SigmoidCrossEntropyLossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + SIGMOID_CROSS_ENTROPY, + description="Type of loss.", + ) + + class_weights: list[float] | dict | None = schema_utils.OneOfOptionsField( + default=None, + description=CLASS_WEIGHTS_DESCRIPTION, + field_options=[ + schema_utils.Dict(default=None, allow_none=True), + schema_utils.List(list_type=float, allow_none=False), + ], + parameter_metadata=LOSS_METADATA["SigmoidCrossEntropyLoss"]["class_weights"], + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["SigmoidCrossEntropyLoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Sigmoid Cross Entropy" + + +@DeveloperAPI +@register_loss([NUMBER, TIMESERIES, VECTOR]) +class HuberLossConfig(BaseLossConfig): + type: str = schema_utils.ProtectedString( + HUBER, + description=( + "Loss that combines advantages of both `mean_absolute_error` (MAE) and `mean_squared_error` (MSE). The " + "delta-scaled L1 region makes the loss less sensitive to outliers than MSE, while the L2 region provides " + "smoothness over MAE near 0. See [Huber loss](https://en.wikipedia.org/wiki/Huber_loss) for more details." + ), + ) + + delta: float = schema_utils.FloatRange( + default=1.0, + min=0, + min_inclusive=False, + description="Threshold at which to change between delta-scaled L1 and L2 loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["MSELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Huber Loss" + + +@DeveloperAPI +@register_loss([CATEGORY]) +class CORNLossConfig(BaseLossConfig): + """Conditional Ordinal Regression for Neural networks, used for ordered cateogry values. + + Source: + Xintong Shi, Wenzhi Cao, and Sebastian Raschka (2021). + Deep Neural Networks for Rank-Consistent Ordinal Regression Based On Conditional Probabilities. + Arxiv preprint; https://arxiv.org/abs/2111.08851 + """ + + type: str = schema_utils.ProtectedString( + CORN, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + parameter_metadata=LOSS_METADATA["MSELoss"]["weight"], + ) + + @classmethod + def name(self) -> str: + return "Conditional Ordinal Regression (CORN)" + + @property + def class_weights(self) -> int: + return 1.0 + + @property + def class_similarities_temperature(self) -> int: + return 0 + + +@DeveloperAPI +@register_loss([ANOMALY]) +class DeepSVDDLossConfig(BaseLossConfig): + """Deep Support Vector Data Description (Deep SVDD) loss for anomaly detection. + + Trains the encoder to map normal data into a compact hypersphere centred at c. + Hard-boundary objective: L = mean(||z - c||^2) for all training points. + Soft-boundary (nu > 0): L = R + (1/nu) * mean(max(0, ||z - c||^2 - R)) where + R is the nu-th quantile of distances (no gradient through R). + + Reference: Ruff et al., "Deep One-Class Classification", ICML 2018. + """ + + type: str = schema_utils.ProtectedString( + DEEP_SVDD, + description="Deep SVDD loss — pulls encoder outputs toward hypersphere center c.", + ) + + nu: float = schema_utils.FloatRange( + default=0.1, + min=0.0, + max=1.0, + min_inclusive=False, + description=( + "Fraction of training examples allowed outside the hypersphere (soft-boundary mode). " + "Set nu=0 for hard-boundary SVDD where all points are pulled toward c." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Deep SVDD" + + +@DeveloperAPI +@register_loss([ANOMALY]) +class DeepSADLossConfig(BaseLossConfig): + """Deep Semi-supervised Anomaly Detection (Deep SAD) loss. + + Extends Deep SVDD with labeled anomaly examples. Normal/unlabeled samples + (target != 1) are pulled toward center c; labeled anomalies (target == 1) + are pushed away via an inverted distance term weighted by eta. + + Reference: Ruff et al., "Deep Semi-Supervised Anomaly Detection", ICLR 2020. + """ + + type: str = schema_utils.ProtectedString( + DEEP_SAD, + description="Deep SAD loss — semi-supervised, labeled anomalies pushed away from center c.", + ) + + eta: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight for the labeled anomaly repulsion term.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Deep SAD" + + +@DeveloperAPI +@register_loss([ANOMALY]) +class DROCCLossConfig(BaseLossConfig): + """Deeply Robust One-Class Classification (DROCC) loss. + + Prevents hypersphere collapse (all representations converge to c) via + an adversarial perturbation regulariser. Recommended for expressive encoders + (e.g. transformers) that are prone to degenerate solutions. + + Reference: Goyal et al., "DROCC: Deep Robust One-Class Classification", ICML 2020. + """ + + type: str = schema_utils.ProtectedString( + DROCC, + description="DROCC loss — prevents collapse via adversarial score perturbations.", + ) + + perturbation_strength: float = schema_utils.NonNegativeFloat( + default=0.1, + description="Magnitude of adversarial perturbations. Typical range: 0.01–0.5.", + ) + + num_perturbation_steps: int = schema_utils.PositiveInteger( + default=5, + description="Gradient ascent steps for adversarial perturbation generation.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "DROCC" + + +@DeveloperAPI +@register_loss([BINARY, CATEGORY]) +class EntropicOpenSetLossConfig(BaseLossConfig): + """Entropic Open-Set Loss for open-set recognition. + + Combines standard cross-entropy for known-class samples with an entropy + maximisation term for background/unknown samples. This discourages the + network from making confident predictions on out-of-distribution inputs, + "curing" network agnostophobia. + + The background class (identified by ``background_class``) is treated as the + catch-all unknown category. Samples with that label contribute only the + entropic term; all other samples contribute cross-entropy as normal. + If ``background_class`` is None the loss reduces to standard cross-entropy. + + Reference: + Dhamija et al., "Reducing Network Agnostophobia", NeurIPS 2018. + https://arxiv.org/abs/1811.04110 + """ + + type: str = schema_utils.ProtectedString( + ENTROPIC_OPEN_SET, + description=( + "Entropic open-set loss — cross-entropy for known classes + entropy " + "maximisation for the background/unknown class." + ), + ) + + background_class: int = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Class index that represents 'unknown' or background samples. " + "Samples with this label receive the entropic penalty instead of " + "cross-entropy. Set to None to disable open-set behaviour and fall " + "back to standard cross-entropy." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Entropic Open-Set" + + +@DeveloperAPI +@register_loss([BINARY, CATEGORY]) +class ObjectosphereLossConfig(BaseLossConfig): + """Objectosphere Loss for open-set recognition. + + Extends the Entropic Open-Set Loss with a feature-magnitude constraint: + + * **Known samples**: standard cross-entropy + hinge term that pushes the + logit L2 norm above ``xi`` (large magnitude → confident, well-separated + representations). + * **Unknown/background samples**: entropy maximisation + magnitude + minimisation weighted by ``zeta`` (small magnitude → low-confidence, + "don't know" responses). + + The combined objective makes it easy to threshold on logit magnitude at + inference time: known-class inputs will have large norms; truly unknown + inputs will have small norms regardless of the argmax prediction. + + Reference: + Dhamija et al., "Reducing Network Agnostophobia", NeurIPS 2018. + https://arxiv.org/abs/1811.04110 + """ + + type: str = schema_utils.ProtectedString( + OBJECTOSPHERE, + description=( + "Objectosphere loss — cross-entropy + magnitude push for known classes, " + "entropy maximisation + magnitude suppression for unknowns." + ), + ) + + background_class: int = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Class index that represents 'unknown' or background samples. " + "Samples with this label receive the entropic + magnitude-suppression " + "penalty. Set to None to fall back to standard cross-entropy." + ), + ) + + xi: float = schema_utils.NonNegativeFloat( + default=10.0, + description=( + "Minimum desired logit L2 norm for known-class samples. " + "A hinge term max(0, xi - ||z||)² is added to push representations " + "of known inputs above this threshold. Typical values: 1–50." + ), + ) + + zeta: float = schema_utils.NonNegativeFloat( + default=0.1, + description=( + "Weight applied to the magnitude-suppression term for unknown samples. " + "Higher values more aggressively shrink logit norms for background inputs." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Objectosphere" + + +@DeveloperAPI +@register_loss([BINARY, CATEGORY, IMAGE]) +class FocalLossConfig(BaseLossConfig): + """Focal Loss for classification with class imbalance. + + Applies a modulating factor (1 - p_t)^gamma to the standard cross-entropy loss + so that easy examples contribute less to the gradient. + + Reference: Lin et al., "Focal Loss for Dense Object Detection", ICCV 2017. + """ + + type: str = schema_utils.ProtectedString( + FOCAL_LOSS, + description="Type of loss.", + ) + + alpha: float = schema_utils.FloatRange( + default=0.25, + min=0.0, + max=1.0, + description=( + "Weighting factor for the positive class in binary classification. " + "Balances the importance of positive/negative examples." + ), + ) + + gamma: float = schema_utils.NonNegativeFloat( + default=2.0, + description=( + "Focusing parameter that reduces the loss contribution from easy examples " + "and extends the range in which an example receives low loss. " + "gamma=0 reduces focal loss to standard cross-entropy." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Focal Loss" + + +@DeveloperAPI +@register_loss([IMAGE]) +class DiceLossConfig(BaseLossConfig): + """Dice Loss for image segmentation. + + Computes 1 minus the Dice coefficient between predicted soft masks and + one-hot ground-truth masks. + + Reference: Milletari et al., "V-Net", 3DV 2016. + """ + + type: str = schema_utils.ProtectedString( + DICE_LOSS, + description="Type of loss.", + ) + + smooth: float = schema_utils.NonNegativeFloat( + default=1.0, + description=( + "Laplace smoothing term added to numerator and denominator to prevent " + "division by zero when both prediction and target are empty." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Dice Loss" + + +@DeveloperAPI +@register_loss([IMAGE]) +class LovaszSoftmaxLossConfig(BaseLossConfig): + """Lovasz-Softmax Loss for multi-class semantic segmentation. + + Uses the Lovasz extension of submodular functions to construct a convex + surrogate for the per-class IoU loss. + + Reference: Berman et al., "The Lovasz-Softmax Loss", CVPR 2018. + """ + + type: str = schema_utils.ProtectedString( + LOVASZ_SOFTMAX_LOSS, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Lovász-Softmax Loss" + + +@DeveloperAPI +@register_loss([VECTOR]) +class NTXentLossConfig(BaseLossConfig): + """NT-Xent (Normalized Temperature-scaled Cross Entropy) contrastive loss (SimCLR). + + Given a batch of N vector representations, computes contrastive loss + assuming consecutive pairs (2i, 2i+1) are positive pairs. + + Reference: Chen et al., "A Simple Framework for Contrastive Learning", ICML 2020. + """ + + type: str = schema_utils.ProtectedString( + NT_XENT_LOSS, + description="Type of loss.", + ) + + temperature: float = schema_utils.FloatRange( + default=0.07, + min=0.0, + min_inclusive=False, + description=( + "Temperature parameter for scaling the cosine similarity scores. " + "Lower values make the distribution sharper, higher values softer." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "NT-Xent Loss" + + +@DeveloperAPI +@register_loss([CATEGORY]) +class PolyLossConfig(BaseLossConfig): + """PolyLoss for multi-class classification. + + Extends cross-entropy with a first-order polynomial correction term + epsilon * (1 - p_t) that upweights hard examples. + + Reference: Leng et al., "PolyLoss", ICLR 2022. + """ + + type: str = schema_utils.ProtectedString( + POLY_LOSS, + description="Type of loss.", + ) + + epsilon: float = schema_utils.FloatRange( + default=1.0, + min=0.0, + description=( + "Coefficient for the polynomial correction term. epsilon=0 reduces PolyLoss to standard cross-entropy." + ), + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Poly Loss" + + +@DeveloperAPI +@register_loss([CATEGORY, TEXT, SEQUENCE]) +class SparsemaxLossConfig(BaseLossConfig): + """Sparsemax Loss: a sparse alternative to softmax cross-entropy. + + The natural loss companion to the sparsemax activation. Assigns zero + gradient to classes outside the sparsemax support. + + Reference: Martins & Astudillo, "From Softmax to Sparsemax", ICML 2016. + """ + + type: str = schema_utils.ProtectedString( + SPARSEMAX_LOSS, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Sparsemax Loss" + + +@DeveloperAPI +@register_loss([CATEGORY, TEXT, SEQUENCE]) +class Entmax15LossConfig(BaseLossConfig): + """Entmax-1.5 Loss: a semi-sparse alternative to softmax cross-entropy. + + The Fenchel-conjugate loss of the alpha=1.5 entmax activation. Produces + moderately sparse probability distributions between softmax and sparsemax. + + Reference: Peters et al., "Sparse Sequence-to-Sequence Models", ACL 2019. + """ + + type: str = schema_utils.ProtectedString( + ENTMAX15_LOSS, + description="Type of loss.", + ) + + weight: float = schema_utils.NonNegativeFloat( + default=1.0, + description="Weight of the loss.", + ) + + @classmethod + def name(cls) -> str: + return "Entmax-1.5 Loss" diff --git a/ludwig/schema/features/loss/utils.py b/ludwig/schema/features/loss/utils.py new file mode 100644 index 0000000..5f911f8 --- /dev/null +++ b/ludwig/schema/features/loss/utils.py @@ -0,0 +1,45 @@ +from dataclasses import Field + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.loss import get_loss_classes, get_loss_cls + + +@DeveloperAPI +def get_loss_conds(feature_type: str): + """Returns a JSON schema of conditionals to validate against loss types for specific feature types.""" + conds = [] + for loss in get_loss_classes(feature_type): + loss_cls = get_loss_cls(feature_type, loss) + other_props = schema_utils.unload_jsonschema_from_config_class(loss_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + loss_cond = schema_utils.create_cond( + {"type": loss}, + other_props, + ) + conds.append(loss_cond) + return conds + + +@DeveloperAPI +def LossDataclassField(feature_type: str, default: str) -> Field: + loss_registry = get_loss_classes(feature_type) + + class LossSelection(schema_utils.TypeSelection): + def __init__(self): + super().__init__(registry=loss_registry, default_value=default) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return get_loss_cls(feature_type, key) + + def _jsonschema_type_mapping(self): + return { + "type": "object", + "properties": { + "type": {"type": "string", "enum": list(loss_registry.keys()), "default": default}, + }, + "title": "loss_options", + "allOf": get_loss_conds(feature_type), + } + + return LossSelection().get_default_field() diff --git a/ludwig/schema/features/number_feature.py b/ludwig/schema/features/number_feature.py new file mode 100644 index 0000000..34336b7 --- /dev/null +++ b/ludwig/schema/features/number_feature.py @@ -0,0 +1,136 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MEAN_SQUARED_ERROR, MODEL_ECD, NUMBER +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(NUMBER) +class NumberInputFeatureConfigMixin(LudwigBaseConfig): + """NumberInputFeatureConfigMixin is a dataclass that configures the parameters used in both the number input + feature and the number global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=NUMBER) + + +@DeveloperAPI +class NumberInputFeatureConfig(NumberInputFeatureConfigMixin, BaseInputFeatureConfig): + """NumberInputFeatureConfig is a dataclass that configures the parameters used for a number input feature.""" + + type: str = schema_utils.ProtectedString(NUMBER) + + encoder: BaseEncoderConfig = None + + +@DeveloperAPI +@ecd_input_config_registry.register(NUMBER) +class ECDNumberInputFeatureConfig(NumberInputFeatureConfig): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=NUMBER, + default="passthrough", + ) + + +@DeveloperAPI +@output_mixin_registry.register(NUMBER) +class NumberOutputFeatureConfigMixin(LudwigBaseConfig): + """NumberOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the number output + feature and the number global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = None + + loss: BaseLossConfig = LossDataclassField( + feature_type=NUMBER, + default=MEAN_SQUARED_ERROR, + ) + + +@DeveloperAPI +class NumberOutputFeatureConfig(NumberOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """NumberOutputFeatureConfig is a dataclass that configures the parameters used for a category output + feature.""" + + type: str = schema_utils.ProtectedString(NUMBER) + + clip: list[int] | tuple[int] = schema_utils.FloatRangeTupleDataclassField( + n=2, + default=None, + allow_none=True, + min=0, + max=999999999, + description="Clip the predicted output to the specified range.", + parameter_metadata=FEATURE_METADATA[NUMBER]["clip"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [MEAN_SQUARED_ERROR], + default=MEAN_SQUARED_ERROR, + description="Internal only use parameter: default validation metric for number output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[NUMBER]["dependencies"], + ) + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[NUMBER]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[NUMBER]["reduce_input"], + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="number_output") + + +@DeveloperAPI +@ecd_output_config_registry.register(NUMBER) +class ECDNumberOutputFeatureConfig(NumberOutputFeatureConfig): + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=NUMBER, + default="regressor", + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(NUMBER) +class NumberDefaultsConfig(NumberInputFeatureConfigMixin, NumberOutputFeatureConfigMixin): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=NUMBER, + default="passthrough", + ) + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=NUMBER, + default="regressor", + ) diff --git a/ludwig/schema/features/preprocessing/__init__.py b/ludwig/schema/features/preprocessing/__init__.py new file mode 100644 index 0000000..90440ec --- /dev/null +++ b/ludwig/schema/features/preprocessing/__init__.py @@ -0,0 +1,16 @@ +# Register all preprocessors +from ludwig.schema.features.preprocessing import ( # noqa + audio, + bag, + binary, + category, + date, + h3, + image, + number, + sequence, + set, + text, + timeseries, + vector, +) diff --git a/ludwig/schema/features/preprocessing/audio.py b/ludwig/schema/features/preprocessing/audio.py new file mode 100644 index 0000000..5dc42ae --- /dev/null +++ b/ludwig/schema/features/preprocessing/audio.py @@ -0,0 +1,140 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, BFILL, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_preprocessor(AUDIO) +class AudioPreprocessingConfig(BasePreprocessingConfig): + audio_file_length_limit_in_s: int = schema_utils.NonNegativeFloat( + default=7.5, + allow_none=False, + description="Float value that defines the maximum limit of the audio file in seconds. All files longer than " + "this limit are cut off. All files shorter than this limit are padded with padding_value", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["audio_file_length_limit_in_s"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=BFILL, + allow_none=False, + description="What strategy to follow when there's a missing value in an audio column", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["computed_fill_value"], + ) + + in_memory: bool = schema_utils.Boolean( + default=True, + description="Defines whether the audio dataset will reside in memory during the training process or will be " + "dynamically fetched from disk (useful for large datasets). In the latter case a training batch " + "of input audio will be fetched from disk each training iteration.", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["in_memory"], + ) + + padding_value: float = schema_utils.NonNegativeFloat( + default=0.0, + allow_none=False, + description="Float value that is used for padding.", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["padding_value"], + ) + + norm: str = schema_utils.StringOptions( + ["per_file"], + default=None, + allow_none=True, + description="Normalization strategy for the audio files. If None, no normalization is performed. If " + "per_file, z-norm is applied on a 'per file' level", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["norm"], + ) + + type: str = schema_utils.StringOptions( + ["fbank", "group_delay", "raw", "stft", "stft_phase"], + default="fbank", + description="Defines the type of audio feature to be used.", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["type"], + ) + + window_length_in_s: float = schema_utils.NonNegativeFloat( + default=0.04, + description="Defines the window length used for the short time Fourier transformation. This is only needed if " + "the audio_feature_type is 'raw'.", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["window_length_in_s"], + ) + + window_shift_in_s: float = schema_utils.NonNegativeFloat( + default=0.02, + description="Defines the window shift used for the short time Fourier transformation (also called " + "hop_length). This is only needed if the audio_feature_type is 'raw'. ", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["window_shift_in_s"], + ) + + num_fft_points: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="Defines the number of fft points used for the short time Fourier transformation", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["num_fft_points"], + ) + + window_type: str = schema_utils.StringOptions( + ["bartlett", "blackman", "hamming", "hann"], + default="hamming", + description="Defines the type window the signal is weighted before the short time Fourier transformation.", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["window_type"], + ) + + num_filter_bands: int = schema_utils.PositiveInteger( + default=80, + description="Defines the number of filters used in the filterbank. Only needed if audio_feature_type " + "is 'fbank'", + parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING]["num_filter_bands"], + ) + + mode: str = schema_utils.StringOptions( + ["eager", "lazy", "lazy_cached"], + default="lazy", + allow_none=False, + description="Preprocessing mode for audio features. 'eager' decodes all files during preprocessing " + "and stores tensors in the Parquet cache. 'lazy' stores file paths and decodes per batch during " + "training, keeping peak memory bounded to batch_size × clip_size. 'lazy_cached' behaves like " + "'lazy' on the first training epoch but writes decoded tensors to a numpy memmap alongside the " + "Parquet cache; subsequent epochs read from the memmap directly, eliminating decode overhead.", + ) + + prefetch_size: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of batches to prefetch in a background thread while the GPU processes the current " + "batch. None (default) selects automatically: 0 for 'eager' mode, 4 for 'lazy' and " + "'lazy_cached' (epoch 1). After the first epoch in 'lazy_cached' mode, prefetch is " + "automatically disabled since memmap reads are fast enough. Set to 0 to disable prefetch " + "entirely, or to a positive integer to override the automatic selection.", + ) + + lazy_cache_dir: str | None = schema_utils.String( + default=None, + allow_none=True, + description="Directory in which to cache audio files when the source data is in-memory (e.g. a " + "HuggingFace dataset). Only used when mode is 'lazy' or 'lazy_cached' and the input entries " + "are not already paths to existing files. When None, defaults to " + "~/.cache/ludwig/lazy_media//. " + "Has no effect when the input column already contains local file paths. " + "Note: this controls the file cache for in-memory sources; the decoded memmap for " + "'lazy_cached' mode is placed next to the Parquet cache instead.", + ) diff --git a/ludwig/schema/features/preprocessing/bag.py b/ludwig/schema/features/preprocessing/bag.py new file mode 100644 index 0000000..dfb2d2a --- /dev/null +++ b/ludwig/schema/features/preprocessing/bag.py @@ -0,0 +1,60 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BAG, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.utils import strings_utils +from ludwig.utils.tokenizers import tokenizer_registry + + +@DeveloperAPI +@register_preprocessor(BAG) +class BagPreprocessingConfig(BasePreprocessingConfig): + tokenizer: str = schema_utils.StringOptions( + tokenizer_registry.keys(), + default="space", + allow_none=False, + description="Defines how to transform the raw text content of the dataset column to a set of elements. The " + "default value space splits the string on spaces. Common options include: underscore (splits on " + "underscore), comma (splits on comma), json (decodes the string into a set or a list through a " + "JSON parser).", + parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING]["tokenizer"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a set column", + parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING]["computed_fill_value"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=10000, + allow_none=True, + description="The maximum number of most common tokens to be considered. If the data contains more than this " + "amount, the most infrequent tokens will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING]["most_common"], + ) diff --git a/ludwig/schema/features/preprocessing/base.py b/ludwig/schema/features/preprocessing/base.py new file mode 100644 index 0000000..a430c34 --- /dev/null +++ b/ludwig/schema/features/preprocessing/base.py @@ -0,0 +1,12 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils + + +@DeveloperAPI +class BasePreprocessingConfig(schema_utils.LudwigBaseConfig): + """Base class for input feature preprocessing. Not meant to be used directly. + + The dataclass format prevents arbitrary properties from being set. Consequently, in child classes, all properties + from the corresponding input feature class are copied over: check each class to check which attributes are different + from the preprocessing of each feature. + """ diff --git a/ludwig/schema/features/preprocessing/binary.py b/ludwig/schema/features/preprocessing/binary.py new file mode 100644 index 0000000..2ab7fd0 --- /dev/null +++ b/ludwig/schema/features/preprocessing/binary.py @@ -0,0 +1,83 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + BFILL, + BINARY, + DROP_ROW, + FFILL, + FILL_WITH_FALSE, + FILL_WITH_MODE, + FILL_WITH_TRUE, + PREPROCESSING, +) +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.utils import strings_utils + + +@DeveloperAPI +@register_preprocessor(BINARY) +class BinaryPreprocessingConfig(BasePreprocessingConfig): + """BinaryPreprocessingConfig is a dataclass that configures the parameters used for a binary input feature.""" + + missing_value_strategy: str = schema_utils.StringOptions( + [FILL_WITH_MODE, BFILL, FFILL, DROP_ROW, FILL_WITH_FALSE, FILL_WITH_TRUE], + default=FILL_WITH_FALSE, + allow_none=False, + description="What strategy to follow when there's a missing value in a binary column", + parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING]["missing_value_strategy"], + ) + + fallback_true_label: str = schema_utils.String( + default=None, + allow_none=True, + description="The label to interpret as 1 (True) when the binary feature doesn't have a " + "conventional boolean value", + parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING]["fallback_true_label"], + ) + + fill_value: int | float | str = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + field_options=[ + schema_utils.FloatRange(default=None, allow_none=True, min=0, max=1, description=""), + schema_utils.StringOptions(options=strings_utils.all_bool_strs(), default="Y", allow_none=False), + schema_utils.Boolean(default=True, description=""), + ], + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: int | float | str = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + field_options=[ + schema_utils.FloatRange(default=1.0, allow_none=False, min=0, max=1, description=""), + schema_utils.StringOptions(options=strings_utils.all_bool_strs(), default="Y", allow_none=False), + schema_utils.Boolean(default=True, description=""), + ], + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING]["computed_fill_value"], + ) + + +@DeveloperAPI +@register_preprocessor("binary_output") +class BinaryOutputPreprocessingConfig(BinaryPreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + [FILL_WITH_MODE, BFILL, FFILL, DROP_ROW, FILL_WITH_FALSE, FILL_WITH_TRUE], + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a binary output feature", + parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING]["missing_value_strategy"], + ) + + fallback_true_label: str = schema_utils.String( + default=None, + allow_none=True, + description="The label to interpret as 1 (True) when the binary feature doesn't have a " + "conventional boolean value", + parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING]["fallback_true_label"], + ) diff --git a/ludwig/schema/features/preprocessing/category.py b/ludwig/schema/features/preprocessing/category.py new file mode 100644 index 0000000..387e850 --- /dev/null +++ b/ludwig/schema/features/preprocessing/category.py @@ -0,0 +1,130 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import CATEGORY, DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA, PREPROCESSING_METADATA +from ludwig.utils import strings_utils + + +@DeveloperAPI +@register_preprocessor(CATEGORY) +class CategoryPreprocessingConfig(BasePreprocessingConfig): + """CategoryPreprocessingConfig is a dataclass that configures the parameters used for a category input + feature.""" + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a category column", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description=( + "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`" + ), + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["computed_fill_value"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="Whether the string has to be lowercased before being handled by the tokenizer.", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=10000, + allow_none=True, + description="The maximum number of most common tokens to be considered. if the data contains more than this " + "amount, the most infrequent tokens will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["most_common"], + ) + + cache_encoder_embeddings: bool = schema_utils.Boolean( + default=False, + description=( + "For fixed encoders, compute encoder embeddings in preprocessing to avoid this step at train time. " + "Can speed up the time taken per step during training, but will invalidate the preprocessed data " + "if the encoder type is changed." + ), + parameter_metadata=PREPROCESSING_METADATA["cache_encoder_embeddings"], + ) + + +@DeveloperAPI +@register_preprocessor("category_output") +class CategoryOutputPreprocessingConfig(CategoryPreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a category output feature", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["missing_value_strategy"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="Whether the string has to be lowercased before being handled by the tokenizer.", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=10000, + allow_none=True, + description="The maximum number of most common tokens to be considered. if the data contains more than this " + "amount, the most infrequent tokens will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["most_common"], + ) + + +@DeveloperAPI +@register_preprocessor("category_distribution_output") +class CategoryDistributionOutputPreprocessingConfig(BasePreprocessingConfig): + def __post_init__(self): + if self.vocab is None: + raise ConfigValidationError("`vocab` must be specified for `category_distribution` output feature.") + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a category output feature", + parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING]["missing_value_strategy"], + ) + + vocab: list[str] = schema_utils.List(default=None) + + +@DeveloperAPI +@register_preprocessor("category_llm") +class LLMCategoryOutputPreprocessingConfig(CategoryOutputPreprocessingConfig): + def __post_init__(self): + if self.vocab is None: + raise ConfigValidationError("`vocab` must be specified for `category_llm` output feature.") + if self.fallback_label is None: + raise ConfigValidationError("`fallback_label` must be specified for `category_llm` output feature.") + + vocab: list[str] = schema_utils.List( + default=None, + allow_none=False, + description="The list of labels that the model can predict.", + ) + + fallback_label: str = schema_utils.String( + default="", + allow_none=False, + description="The label to use when the model doesn't match any of the labels in the `labels` list.", + ) diff --git a/ludwig/schema/features/preprocessing/date.py b/ludwig/schema/features/preprocessing/date.py new file mode 100644 index 0000000..624ff9a --- /dev/null +++ b/ludwig/schema/features/preprocessing/date.py @@ -0,0 +1,41 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BFILL, DATE, DROP_ROW, FFILL, FILL_WITH_CONST, PREPROCESSING +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_preprocessor(DATE) +class DatePreprocessingConfig(BasePreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + [FILL_WITH_CONST, BFILL, FFILL, DROP_ROW], + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a date column", + parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default="", + allow_none=False, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default="", + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING]["computed_fill_value"], + ) + + datetime_format: str = schema_utils.String( + default=None, + allow_none=True, + description="This parameter can either be a datetime format string, or null, in which case the datetime " + "format will be inferred automatically.", + parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING]["datetime_format"], + ) diff --git a/ludwig/schema/features/preprocessing/h3.py b/ludwig/schema/features/preprocessing/h3.py new file mode 100644 index 0000000..7808dba --- /dev/null +++ b/ludwig/schema/features/preprocessing/h3.py @@ -0,0 +1,33 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import FILL_WITH_CONST, H3, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_preprocessor(H3) +class H3PreprocessingConfig(BasePreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in an h3 column", + parameter_metadata=FEATURE_METADATA[H3][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: int = schema_utils.PositiveInteger( + default=576495936675512319, + allow_none=False, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[H3][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: int = schema_utils.PositiveInteger( + default=576495936675512319, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[H3][PREPROCESSING]["computed_fill_value"], + ) diff --git a/ludwig/schema/features/preprocessing/image.py b/ludwig/schema/features/preprocessing/image.py new file mode 100644 index 0000000..d3e660b --- /dev/null +++ b/ludwig/schema/features/preprocessing/image.py @@ -0,0 +1,196 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BFILL, DROP_ROW, IMAGE, IMAGENET1K, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_preprocessor(IMAGE) +class ImagePreprocessingConfig(BasePreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=BFILL, + allow_none=False, + description="What strategy to follow when there's a missing value in an image column", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="The maximum number of most common tokens to be considered. If the data contains more than this " + "amount, the most infrequent tokens will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["computed_fill_value"], + ) + + height: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The image height in pixels. If this parameter is set, images will be resized to the specified " + "height using the resize_method parameter. If None, images will be resized to the size of the " + "first image in the dataset.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["height"], + ) + + width: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The image width in pixels. If this parameter is set, images will be resized to the specified " + "width using the resize_method parameter. If None, images will be resized to the size of the " + "first image in the dataset.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["width"], + ) + + num_channels: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of channels in the images. If specified, images will be read in the mode specified by the " + "number of channels. If not specified, the number of channels will be inferred from the image " + "format of the first valid image in the dataset.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["num_channels"], + ) + + resize_method: str = schema_utils.StringOptions( + ["crop_or_pad", "interpolate"], + default="interpolate", + allow_none=False, + description="The method to use for resizing images.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["resize_method"], + ) + + infer_image_num_channels: bool = schema_utils.Boolean( + default=True, + description="If true, then the number of channels in the dataset is inferred from a sample of the first image " + "in the dataset.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["infer_image_num_channels"], + ) + + infer_image_dimensions: bool = schema_utils.Boolean( + default=True, + description="If true, then the height and width of images in the dataset will be inferred from a sample of " + "the first image in the dataset. Each image that doesn't conform to these dimensions will be " + "resized according to resize_method. If set to false, then the height and width of images in the " + "dataset will be specified by the user.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["infer_image_dimensions"], + ) + + infer_image_max_height: int = schema_utils.PositiveInteger( + default=256, + allow_none=False, + description="If infer_image_dimensions is set, this is used as the maximum height of the images in " + "the dataset.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["infer_image_max_height"], + ) + + infer_image_max_width: int = schema_utils.PositiveInteger( + default=256, + allow_none=False, + description="If infer_image_dimensions is set, this is used as the maximum width of the images in the dataset.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["infer_image_max_width"], + ) + + infer_image_sample_size: int = schema_utils.PositiveInteger( + default=100, + allow_none=False, + description="The sample size used for inferring dimensions of images in infer_image_dimensions.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["infer_image_sample_size"], + ) + + standardize_image: str | None = schema_utils.StringOptions( + [IMAGENET1K], + default=None, + allow_none=True, + description="Standardize image by per channel mean centering and standard deviation scaling .", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["standardize_image"], + ) + + in_memory: bool = schema_utils.Boolean( + default=True, + description="Defines whether image dataset will reside in memory during the training process or will be " + "dynamically fetched from disk (useful for large datasets). In the latter case a training batch " + "of input images will be fetched from disk each training iteration.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["in_memory"], + ) + + num_processes: int = schema_utils.PositiveInteger( + default=1, + allow_none=False, + description="Specifies the number of processes to run for preprocessing images.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["num_processes"], + ) + + requires_equal_dimensions: bool = schema_utils.Boolean( + default=False, + description="If true, then width and height must be equal.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["requires_equal_dimensions"], + ) + + num_classes: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of channel classes in the images. If specified, this value will be validated " + "against the inferred number of classes. Use 2 to convert grayscale images to binary images.", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["num_classes"], + ) + + infer_image_num_classes: bool = schema_utils.Boolean( + default=False, + description="If true, then the number of channel classes in the dataset will be inferred from a sample of " + "the first image in the dataset. Each unique channel value will be mapped to a class and preprocessing will " + "create a masked image based on the channel classes. ", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["infer_image_num_classes"], + ) + + mode: str = schema_utils.StringOptions( + ["eager", "lazy", "lazy_cached"], + default="lazy", + allow_none=False, + description="Preprocessing mode for image features. 'eager' decodes all files during preprocessing " + "and stores tensors in the Parquet cache. 'lazy' stores file paths and decodes per batch during " + "training, keeping peak memory bounded to batch_size × image_size. 'lazy_cached' behaves like " + "'lazy' on the first training epoch but writes decoded tensors to a numpy memmap alongside the " + "Parquet cache; subsequent epochs read from the memmap directly, eliminating decode overhead. " + "Lazy mode is disabled automatically when a torchvision pretrained encoder is used.", + ) + + prefetch_size: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="Number of batches to prefetch in a background thread while the GPU processes the current " + "batch. None (default) selects automatically: 0 for 'eager' mode, 4 for 'lazy' and " + "'lazy_cached' (epoch 1). After the first epoch in 'lazy_cached' mode, prefetch is " + "automatically disabled since memmap reads are fast enough. Set to 0 to disable prefetch " + "entirely, or to a positive integer to override the automatic selection.", + ) + + lazy_cache_dir: str | None = schema_utils.String( + default=None, + allow_none=True, + description="Directory in which to cache image files when the source data is in-memory (e.g. a " + "HuggingFace dataset). Only used when mode is 'lazy' or 'lazy_cached' and the input entries " + "are not already paths to existing files. When None, defaults to " + "~/.cache/ludwig/lazy_media//. Has no effect when the input column already " + "contains local file paths.", + ) + + +@DeveloperAPI +@register_preprocessor("image_output") +class ImageOutputPreprocessingConfig(ImagePreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in an image column", + parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING]["missing_value_strategy"], + ) diff --git a/ludwig/schema/features/preprocessing/number.py b/ludwig/schema/features/preprocessing/number.py new file mode 100644 index 0000000..a9aa4c6 --- /dev/null +++ b/ludwig/schema/features/preprocessing/number.py @@ -0,0 +1,109 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + DROP_ROW, + FILL_WITH_CONST, + FILL_WITH_MEAN, + MISSING_VALUE_STRATEGY_OPTIONS, + NUMBER, + PREPROCESSING, +) +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_preprocessor(NUMBER) +class NumberPreprocessingConfig(BasePreprocessingConfig): + """NumberPreprocessingConfig is a dataclass that configures the parameters used for a number input feature.""" + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS + [FILL_WITH_MEAN], + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a number column", + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: float = schema_utils.FloatRange( + default=0.0, + allow_none=False, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: float = schema_utils.FloatRange( + default=0.0, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["computed_fill_value"], + ) + + normalization: str = schema_utils.StringOptions( + ["zscore", "minmax", "log1p", "iq"], + default="zscore", + allow_none=True, + description=( + "Normalization strategy to use for this number feature. If the value is `null` no normalization is " + "performed." + ), + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["normalization"], + ) + + outlier_strategy: str | None = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS + [FILL_WITH_MEAN, None], + default=None, + allow_none=True, + description=( + "Determines how outliers will be handled in the dataset. In most cases, replacing outliers with the " + "column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough " + "to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers " + "is to leave them in the data, which is the behavior when this parameter is left as `null`." + ), + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["outlier_strategy"], + ) + + outlier_threshold: float | None = schema_utils.FloatRange( + default=3.0, + allow_none=False, + min=0.0, + description=( + "Standard deviations from the mean past which a value is considered an outlier. The 3-sigma " + "rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 " + "standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations " + "of the mean (see: [68–95–99.7 rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule)). " + "As such anything farther away than that is highly likely to be an outlier, and may distort the learning " + "process by disproportionately affecting the model." + ), + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["outlier_threshold"], + ) + + computed_outlier_fill_value: float = schema_utils.FloatRange( + default=0.0, + allow_none=False, + description="The internally computed fill value to replace outliers with in case the " + "outlier_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["computed_outlier_fill_value"], + ) + + +@DeveloperAPI +@register_preprocessor("number_output") +class NumberOutputPreprocessingConfig(NumberPreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS + [FILL_WITH_MEAN], + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a number output feature", + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["missing_value_strategy"], + ) + + normalization: str = schema_utils.StringOptions( + ["zscore", "minmax", "log1p", "iq"], + default=None, + allow_none=True, + description="Normalization strategy to use for this number feature.", + parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING]["normalization"], + ) diff --git a/ludwig/schema/features/preprocessing/sequence.py b/ludwig/schema/features/preprocessing/sequence.py new file mode 100644 index 0000000..6a9e6de --- /dev/null +++ b/ludwig/schema/features/preprocessing/sequence.py @@ -0,0 +1,175 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, SEQUENCE +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA, PREPROCESSING_METADATA +from ludwig.utils import strings_utils + + +@DeveloperAPI +@register_preprocessor(SEQUENCE) +class SequencePreprocessingConfig(BasePreprocessingConfig): + tokenizer: str = schema_utils.String( + default="space", + allow_none=False, + description="Defines how to map from the raw string content of the dataset column to a sequence of elements.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["tokenizer"], + ) + + vocab_file: str = schema_utils.String( + default=None, + allow_none=True, + description="Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the " + "first string until \t or \n is considered a word.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["vocab_file"], + ) + + sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The desired length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated and sequences shorter than this value will be padded. If None, sequence length will be " + "inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["sequence_length"], + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=256, + allow_none=True, + description="The maximum length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence " + "length will be inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["max_sequence_length"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=20000, + allow_none=False, + description="The maximum number of most common tokens in the vocabulary. If the data contains more than this " + "amount, the most infrequent symbols will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["most_common"], + ) + + padding_symbol: str = schema_utils.String( + default=strings_utils.PADDING_SYMBOL, + allow_none=False, + description="The string used as a padding symbol. This special token is mapped to the integer ID 0 in the " + "vocabulary.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["padding_symbol"], + ) + + unknown_symbol: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The string used as an unknown placeholder. This special token is mapped to the integer ID 1 in " + "the vocabulary.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["unknown_symbol"], + ) + + padding: str = schema_utils.StringOptions( + ["left", "right"], + default="right", + allow_none=False, + description="The direction of the padding.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["padding"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["lowercase"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a text column", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["computed_fill_value"], + ) + + ngram_size: int = schema_utils.PositiveInteger( + default=2, + allow_none=False, + description="The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["ngram_size"], + ) + + cache_encoder_embeddings: bool = schema_utils.Boolean( + default=False, + description="Compute encoder embeddings in preprocessing, speeding up training time considerably.", + parameter_metadata=PREPROCESSING_METADATA["cache_encoder_embeddings"], + ) + + +@DeveloperAPI +@register_preprocessor("sequence_output") +class SequenceOutputPreprocessingConfig(SequencePreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a sequence output feature", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["missing_value_strategy"], + ) + + sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The desired length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated and sequences shorter than this value will be padded. If None, sequence length will be " + "inferred from the training dataset.", + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=256, + allow_none=True, + description="The maximum length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence " + "length will be inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["max_sequence_length"], + ) + + tokenizer: str = schema_utils.String( + default="space", + allow_none=False, + description="Defines how to map from the raw string content of the dataset column to a sequence of elements.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["tokenizer"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=20000, + allow_none=False, + description="The maximum number of most common tokens in the vocabulary. If the data contains more than this " + "amount, the most infrequent symbols will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["most_common"], + ) + + ngram_size: int = schema_utils.PositiveInteger( + default=2, + allow_none=False, + description="The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING]["ngram_size"], + ) diff --git a/ludwig/schema/features/preprocessing/set.py b/ludwig/schema/features/preprocessing/set.py new file mode 100644 index 0000000..7c4ab21 --- /dev/null +++ b/ludwig/schema/features/preprocessing/set.py @@ -0,0 +1,94 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, SET +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.utils import strings_utils + + +@DeveloperAPI +@register_preprocessor(SET) +class SetPreprocessingConfig(BasePreprocessingConfig): + tokenizer: str = schema_utils.String( + default="space", + allow_none=False, + description="Defines how to transform the raw text content of the dataset column to a set of elements. The " + "default value space splits the string on spaces. Common options include: underscore (splits on " + "underscore), comma (splits on comma), json (decodes the string into a set or a list through a " + "JSON parser).", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["tokenizer"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a set column", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["computed_fill_value"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=10000, + allow_none=True, + description="The maximum number of most common tokens to be considered. If the data contains more than this " + "amount, the most infrequent tokens will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["most_common"], + ) + + +@DeveloperAPI +@register_preprocessor("set_output") +class SetOutputPreprocessingConfig(SetPreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a set output feature", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["missing_value_strategy"], + ) + + tokenizer: str = schema_utils.String( + default="space", + allow_none=False, + description="Defines how to transform the raw text content of the dataset column to a set of elements. The " + "default value space splits the string on spaces. Common options include: underscore (splits on " + "underscore), comma (splits on comma), json (decodes the string into a set or a list through a " + "JSON parser).", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["tokenizer"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=10000, + allow_none=True, + description="The maximum number of most common tokens to be considered. If the data contains more than this " + "amount, the most infrequent tokens will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING]["most_common"], + ) diff --git a/ludwig/schema/features/preprocessing/text.py b/ludwig/schema/features/preprocessing/text.py new file mode 100644 index 0000000..e6f0a15 --- /dev/null +++ b/ludwig/schema/features/preprocessing/text.py @@ -0,0 +1,234 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, TEXT +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.llms.prompt import PromptConfig, PromptConfigField +from ludwig.schema.metadata import FEATURE_METADATA, PREPROCESSING_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.utils import strings_utils +from ludwig.utils.tokenizers import tokenizer_registry + + +@DeveloperAPI +class BaseTextPreprocessingConfig(BasePreprocessingConfig): + """TextPreprocessingConfig is a dataclass that configures the parameters used for a text input feature.""" + + pretrained_model_name_or_path: str = schema_utils.String( + default=None, + allow_none=True, + description="This can be either the name of a pretrained HuggingFace model or a path where it was downloaded.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["pretrained_model_name_or_path"], + ) + + tokenizer: str = schema_utils.StringOptions( + tokenizer_registry.keys(), + default="space_punct", + allow_none=False, + description="Defines how to map from the raw string content of the dataset column to a sequence of elements.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["tokenizer"], + ) + + vocab_file: str = schema_utils.String( + default=None, + allow_none=True, + description="Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the " + "first string until `\\t` or `\\n` is considered a word.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["vocab_file"], + ) + + sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The desired length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated and sequences shorter than this value will be padded. If None, sequence length will be " + "inferred from the training dataset.", + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=256, + allow_none=True, + description="The maximum length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence " + "length will be inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["max_sequence_length"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=20000, + allow_none=False, + description="The maximum number of most common tokens in the vocabulary. If the data contains more than this " + "amount, the most infrequent symbols will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["most_common"], + ) + + padding_symbol: str = schema_utils.String( + default=strings_utils.PADDING_SYMBOL, + allow_none=False, + description="The string used as the padding symbol for sequence features. Ignored for features using " + "huggingface encoders, which have their own vocabulary.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["padding_symbol"], + ) + + unknown_symbol: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The string used as the unknown symbol for sequence features. Ignored for features using " + "huggingface encoders, which have their own vocabulary.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["unknown_symbol"], + ) + + padding: str = schema_utils.StringOptions( + ["left", "right"], + default="right", + allow_none=False, + description="The direction of the padding.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["padding"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["lowercase"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a text column.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description=( + "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`." + ), + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default=strings_utils.UNKNOWN_SYMBOL, + allow_none=False, + description="The internally computed fill value to replace missing values with in case the " + "`missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["computed_fill_value"], + ) + + ngram_size: int = schema_utils.PositiveInteger( + default=2, + allow_none=False, + description="The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["ngram_size"], + ) + + cache_encoder_embeddings: bool = schema_utils.Boolean( + default=False, + description=( + "For pretrained encoders, compute encoder embeddings in preprocessing, " + "speeding up training time considerably. Only supported when `encoder.trainable=false`." + ), + parameter_metadata=PREPROCESSING_METADATA["cache_encoder_embeddings"], + ) + + compute_idf: bool = schema_utils.Boolean( + default=False, + parameter_metadata=INTERNAL_ONLY, + ) + + +@DeveloperAPI +@register_preprocessor(TEXT) +class TextPreprocessingConfig(BaseTextPreprocessingConfig): + """TextPreprocessingConfig is a dataclass that configures the parameters used for a text input feature.""" + + prompt: PromptConfig = PromptConfigField().get_default_field() + + +@DeveloperAPI +@register_preprocessor("text_llm_input") +class LLMTextInputPreprocessingConfig(BaseTextPreprocessingConfig): + """LLMs require the prompt to be provided at the top-level, not preprocessing.""" + + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The maximum length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence " + "length will be inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["max_sequence_length"], + ) + + +@DeveloperAPI +@register_preprocessor("text_output") +class TextOutputPreprocessingConfig(BaseTextPreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a text output feature.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["missing_value_strategy"], + ) + + sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The desired length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated and sequences shorter than this value will be padded. If None, sequence length will be " + "inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["sequence_length"], + ) + + max_sequence_length: int = schema_utils.PositiveInteger( + default=256, + allow_none=True, + description="The maximum length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence " + "length will be inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["max_sequence_length"], + ) + + tokenizer: str = schema_utils.StringOptions( + tokenizer_registry.keys(), + default="space_punct", + allow_none=False, + description="Defines how to map from the raw string content of the dataset column to a sequence of elements.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["tokenizer"], + ) + + lowercase: bool = schema_utils.Boolean( + default=False, + description="If true, converts the string to lowercase before tokenizing.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["lowercase"], + ) + + most_common: int = schema_utils.PositiveInteger( + default=20000, + allow_none=False, + description="The maximum number of most common tokens in the vocabulary. If the data contains more than this " + "amount, the most infrequent symbols will be treated as unknown.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["most_common"], + ) + + ngram_size: int = schema_utils.PositiveInteger( + default=2, + allow_none=False, + description="The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["ngram_size"], + ) + + +@DeveloperAPI +@register_preprocessor("text_llm_output") +class LLMTextOutputPreprocessingConfig(TextOutputPreprocessingConfig): + max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The maximum length (number of tokens) of the sequence. Sequences that are longer than this value " + "will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence " + "length will be inferred from the training dataset.", + parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING]["max_sequence_length"], + ) diff --git a/ludwig/schema/features/preprocessing/timeseries.py b/ludwig/schema/features/preprocessing/timeseries.py new file mode 100644 index 0000000..2644180 --- /dev/null +++ b/ludwig/schema/features/preprocessing/timeseries.py @@ -0,0 +1,118 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, TIMESERIES +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.utils.tokenizers import tokenizer_registry + + +class BaseTimeseriesPreprocessingConfig(BasePreprocessingConfig): + tokenizer: str = schema_utils.StringOptions( + tokenizer_registry.keys(), + default="space", + allow_none=False, + description="Defines how to map from the raw string content of the dataset column to a sequence of elements.", + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["tokenizer"], + ) + + timeseries_length_limit: int = schema_utils.PositiveInteger( + default=256, + allow_none=False, + description="Defines the maximum length of the timeseries. All timeseries longer than this limit are cut off.", + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["timeseries_length_limit"], + ) + + padding_value: float = schema_utils.NonNegativeFloat( + default=0.0, + allow_none=False, + description="Float value that is used for padding and replacing missing values within a row.", + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["padding_value"], + ) + + padding: str = schema_utils.StringOptions( + ["left", "right"], + default="right", + allow_none=False, + description="The direction of the padding.", + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["padding"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description=( + "What strategy to follow when there's a missing value in a column. Currently applies only to a row missing " + "in its entirety, not invididual elements within the row. For now, `NaN` values within a row are filled " + "using the `padding_value`." + ), + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default="", + allow_none=False, + description=( + "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`." + ), + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default="", + allow_none=False, + description=( + "The internally computed fill value to replace missing values with in case the " + "`missing_value_strategy` is `fill_with_mode` or `fill_with_mean`." + ), + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["computed_fill_value"], + ) + + +@DeveloperAPI +@register_preprocessor(TIMESERIES) +class TimeseriesPreprocessingConfig(BaseTimeseriesPreprocessingConfig): + window_size: int = schema_utils.NonNegativeInteger( + default=0, + allow_none=False, + description=( + "Optional lookback window size used to convert a column-major dataset (one observation per row) " + "into a row-major dataset (each row has a timeseries window of observations). Starting from a given " + "observation, a sliding window is taken going `window_size - 1` rows back to form the timeseries input " + "feature. If this value is left as 0, then it is assumed that the dataset has been provided in row-major " + "format (i.e., it has already been preprocessed such that each row is a timeseries window)." + ), + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when a row of data is missing.", + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["missing_value_strategy"], + ) + + +@DeveloperAPI +@register_preprocessor("timeseries_output") +class TimeseriesOutputPreprocessingConfig(BaseTimeseriesPreprocessingConfig): + horizon: int = schema_utils.NonNegativeInteger( + default=0, + allow_none=False, + description=( + "Optional forecasting horizon used to convert a column-major dataset (one observation per row) " + "into a row-major dataset (each row has a timeseries window of observations). Starting from a given " + "observation, a sliding window is token going `horizon` rows forward in time, excluding the observation " + "in the current row. If this value is left as 0, then it is assumed that the dataset has been provided in " + "row-major format (i.e., it has already been preprocessed such that each row is a timeseries window)." + ), + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when a row of data is missing.", + parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING]["missing_value_strategy"], + ) diff --git a/ludwig/schema/features/preprocessing/utils.py b/ludwig/schema/features/preprocessing/utils.py new file mode 100644 index 0000000..e95ffe9 --- /dev/null +++ b/ludwig/schema/features/preprocessing/utils.py @@ -0,0 +1,83 @@ +from dataclasses import field + +import pydantic + +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.utils.registry import Registry + +preprocessing_registry = Registry() + + +@DeveloperAPI +def register_preprocessor(name: str): + def wrap(preprocessing_config: BasePreprocessingConfig): + preprocessing_registry[name] = preprocessing_config + return preprocessing_config + + return wrap + + +@DeveloperAPI +def PreprocessingDataclassField(feature_type: str): + """Custom dataclass field that when used inside a dataclass will allow the user to specify a preprocessing + config. + + Returns: Initialized dataclass field that converts an untyped dict with params to a preprocessing config. + """ + + class PreprocessingConfigField(schema_utils.SchemaField): + """Custom field that deserializes a dict for a valid preprocessing config from the preprocessing_registry + and creates a corresponding JSON schema for external usage.""" + + def _deserialize(self, value, attr, data, **kwargs): + if value is None: + return None + if isinstance(value, dict): + if feature_type in preprocessing_registry: + pre = preprocessing_registry[feature_type] + try: + return pre.model_validate(value) + except (TypeError, ConfigValidationError) as error: + raise ConfigValidationError( + f"Invalid preprocessing params: {value}, see `{pre}` definition. Error: {error}" + ) + raise ConfigValidationError( + f"Invalid params for preprocessor: {value}, expect dict with at least a valid `type` attribute." + ) + raise ConfigValidationError("Field should be None or dict") + + def _jsonschema_type_mapping(self): + preprocessor_cls = preprocessing_registry[feature_type] + props = schema_utils.unload_jsonschema_from_config_class(preprocessor_cls)["properties"] + return { + "type": "object", + "properties": props, + "title": "preprocessing_options", + "additionalProperties": True, + } + + try: + preprocessor = preprocessing_registry[feature_type] + load_default = lambda: preprocessor.model_validate({}) + try: + dump_default = preprocessor.model_validate({}).to_dict() + except pydantic.ValidationError: + dump_default = {} + + return field( + metadata={ + "marshmallow_field": PreprocessingConfigField( + allow_none=False, + dump_default=dump_default, + load_default=load_default, + ) + }, + default_factory=load_default, + ) + except Exception as e: + raise ConfigValidationError( + f"Unsupported preprocessing type: {feature_type}. See preprocessing_registry. Details: {e}" + ) diff --git a/ludwig/schema/features/preprocessing/vector.py b/ludwig/schema/features/preprocessing/vector.py new file mode 100644 index 0000000..424c994 --- /dev/null +++ b/ludwig/schema/features/preprocessing/vector.py @@ -0,0 +1,61 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, VECTOR +from ludwig.schema import utils as schema_utils +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import register_preprocessor +from ludwig.schema.metadata import FEATURE_METADATA + + +@DeveloperAPI +@register_preprocessor(VECTOR) +class VectorPreprocessingConfig(BasePreprocessingConfig): + vector_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The size of the vector. If None, the vector size will be inferred from the data.", + parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING]["vector_size"], + ) + + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=FILL_WITH_CONST, + allow_none=False, + description="What strategy to follow when there's a missing value in a vector column", + parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING]["missing_value_strategy"], + ) + + fill_value: str = schema_utils.String( + default="", + allow_none=False, + pattern=r"^([0-9]+(\.[0-9]*)?\s*)*$", + description="The value to replace missing values with in case the missing_value_strategy is fill_with_const", + parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING]["fill_value"], + ) + + computed_fill_value: str = schema_utils.String( + default="", + allow_none=False, + pattern=r"^([0-9]+(\.[0-9]*)?\s*)*$", + description="The internally computed fill value to replace missing values with in case the " + "missing_value_strategy is fill_with_mode or fill_with_mean", + parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING]["computed_fill_value"], + ) + + +@DeveloperAPI +@register_preprocessor("vector_output") +class VectorOutputPreprocessingConfig(VectorPreprocessingConfig): + missing_value_strategy: str = schema_utils.StringOptions( + MISSING_VALUE_STRATEGY_OPTIONS, + default=DROP_ROW, + allow_none=False, + description="What strategy to follow when there's a missing value in a vector output feature", + parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING]["missing_value_strategy"], + ) + + vector_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The size of the vector. If None, the vector size will be inferred from the data.", + parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING]["vector_size"], + ) diff --git a/ludwig/schema/features/sequence_feature.py b/ludwig/schema/features/sequence_feature.py new file mode 100644 index 0000000..0a14077 --- /dev/null +++ b/ludwig/schema/features/sequence_feature.py @@ -0,0 +1,107 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import LOSS, MODEL_ECD, SEQUENCE, SEQUENCE_SOFTMAX_CROSS_ENTROPY +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(SEQUENCE) +class SequenceInputFeatureConfigMixin(LudwigBaseConfig): + """SequenceInputFeatureConfigMixin is a dataclass that configures the parameters used in both the sequence + input feature and the sequence global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=SEQUENCE) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=SEQUENCE, + default="embed", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(SEQUENCE) +class SequenceInputFeatureConfig(SequenceInputFeatureConfigMixin, BaseInputFeatureConfig): + """SequenceInputFeatureConfig is a dataclass that configures the parameters used for a sequence input + feature.""" + + type: str = schema_utils.ProtectedString(SEQUENCE) + + +@DeveloperAPI +@output_mixin_registry.register(SEQUENCE) +class SequenceOutputFeatureConfigMixin(LudwigBaseConfig): + """SequenceOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the sequence + output feature and the sequence global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=SEQUENCE, + default="generator", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=SEQUENCE, + default=SEQUENCE_SOFTMAX_CROSS_ENTROPY, + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(SEQUENCE) +class SequenceOutputFeatureConfig(SequenceOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """SequenceOutputFeatureConfig is a dataclass that configures the parameters used for a sequence output + feature.""" + + type: str = schema_utils.ProtectedString(SEQUENCE) + + default_validation_metric: str = schema_utils.StringOptions( + [LOSS], + default=LOSS, + description="Internal only use parameter: default validation metric for sequence output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[SEQUENCE]["dependencies"], + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="sequence_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[SEQUENCE]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[SEQUENCE]["reduce_input"], + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(SEQUENCE) +class SequenceDefaultsConfig(SequenceInputFeatureConfigMixin, SequenceOutputFeatureConfigMixin): + pass diff --git a/ludwig/schema/features/set_feature.py b/ludwig/schema/features/set_feature.py new file mode 100644 index 0000000..c971ef8 --- /dev/null +++ b/ludwig/schema/features/set_feature.py @@ -0,0 +1,114 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import JACCARD, MODEL_ECD, SET, SIGMOID_CROSS_ENTROPY +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(SET) +class SetInputFeatureConfigMixin(LudwigBaseConfig): + """SetInputFeatureConfigMixin is a dataclass that configures the parameters used in both the set input feature + and the set global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=SET) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=SET, + default="embed", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(SET) +class SetInputFeatureConfig(SetInputFeatureConfigMixin, BaseInputFeatureConfig): + """SetInputFeatureConfig is a dataclass that configures the parameters used for a set input feature.""" + + type: str = schema_utils.ProtectedString(SET) + + +@DeveloperAPI +@output_mixin_registry.register(SET) +class SetOutputFeatureConfigMixin(LudwigBaseConfig): + """SetOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the set output + feature and the set global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=SET, + default="classifier", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=SET, + default=SIGMOID_CROSS_ENTROPY, + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(SET) +class SetOutputFeatureConfig(SetOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """SetOutputFeatureConfig is a dataclass that configures the parameters used for a set output feature.""" + + type: str = schema_utils.ProtectedString(SET) + + default_validation_metric: str = schema_utils.StringOptions( + [JACCARD], + default=JACCARD, + description="Internal only use parameter: default validation metric for set output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[SET]["dependencies"], + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="set_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[SET]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[SET]["reduce_input"], + ) + + threshold: float = schema_utils.FloatRange( + default=0.5, + min=0, + max=1, + description="The threshold used to convert output probabilities to predictions. Tokens with predicted" + "probabilities greater than or equal to threshold are predicted to be in the output set (True).", + parameter_metadata=FEATURE_METADATA[SET]["threshold"], + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(SET) +class SetDefaultsConfig(SetInputFeatureConfigMixin, SetOutputFeatureConfigMixin): + pass diff --git a/ludwig/schema/features/text_feature.py b/ludwig/schema/features/text_feature.py new file mode 100644 index 0000000..1d2f6d3 --- /dev/null +++ b/ludwig/schema/features/text_feature.py @@ -0,0 +1,206 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + LOSS, + MODEL_ECD, + MODEL_LLM, + NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY, + SEQUENCE_SOFTMAX_CROSS_ENTROPY, + TEXT, +) +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + llm_defaults_config_registry, + llm_input_config_registry, + llm_output_config_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(TEXT) +class TextInputFeatureConfigMixin(LudwigBaseConfig): + """TextInputFeatureConfigMixin is a dataclass that configures the parameters used in both the text input + feature and the text global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=TEXT) + + +@DeveloperAPI +class TextInputFeatureConfig(TextInputFeatureConfigMixin, BaseInputFeatureConfig): + """TextInputFeatureConfig is a dataclass that configures the parameters used for a text input feature.""" + + type: str = schema_utils.ProtectedString(TEXT) + + encoder: BaseEncoderConfig = None + + +@DeveloperAPI +@ecd_input_config_registry.register(TEXT) +class ECDTextInputFeatureConfig(TextInputFeatureConfig): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=TEXT, + default="parallel_cnn", + ) + + +@DeveloperAPI +@llm_input_config_registry.register(TEXT) +class LLMTextInputFeatureConfig(TextInputFeatureConfig): + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="text_llm_input") + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_LLM, + feature_type=TEXT, + default="passthrough", + ) + + +@DeveloperAPI +@output_mixin_registry.register(TEXT) +class TextOutputFeatureConfigMixin(LudwigBaseConfig): + """TextOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the text output + feature and the text global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = None + + loss: BaseLossConfig = LossDataclassField( + feature_type=TEXT, + default=SEQUENCE_SOFTMAX_CROSS_ENTROPY, + ) + + +@DeveloperAPI +class TextOutputFeatureConfig(TextOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """TextOutputFeatureConfig is a dataclass that configures the parameters used for a text output feature.""" + + type: str = schema_utils.ProtectedString(TEXT) + + class_similarities: list = schema_utils.List( + list, + default=None, + description="If not null this parameter is a c x c matrix in the form of a list of lists that contains the " + "mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. ", + parameter_metadata=FEATURE_METADATA[TEXT]["class_similarities"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [LOSS], + default=LOSS, + description="Internal only use parameter: default validation metric for binary output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[TEXT]["dependencies"], + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="text_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[TEXT]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default="sum", + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[TEXT]["reduce_input"], + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(TEXT) +class ECDTextOutputFeatureConfig(TextOutputFeatureConfig): + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=TEXT, + default="generator", + ) + + +@DeveloperAPI +@llm_output_config_registry.register(TEXT) +class LLMTextOutputFeatureConfig(TextOutputFeatureConfig): + default_validation_metric: str = schema_utils.StringOptions( + [LOSS], + default=LOSS, + description="Internal only use parameter: default validation metric for text output feature for LLMs.", + parameter_metadata=INTERNAL_ONLY, + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="text_llm_output") + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_LLM, + feature_type=TEXT, + default="text_extractor", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=TEXT, + default=NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY, + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(TEXT) +class ECDTextDefaultsConfig(TextInputFeatureConfigMixin, TextOutputFeatureConfigMixin): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=TEXT, + default="parallel_cnn", + ) + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=TEXT, + default="generator", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=TEXT, + default=SEQUENCE_SOFTMAX_CROSS_ENTROPY, + ) + + +@DeveloperAPI +@llm_defaults_config_registry.register(TEXT) +class LLMTextDefaultsConfig(TextInputFeatureConfigMixin, TextOutputFeatureConfigMixin): + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_LLM, + feature_type=TEXT, + default="passthrough", + ) + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_LLM, + feature_type=TEXT, + default="text_extractor", + ) + + # TODO(Arnav): Refactor LossDataclassField to only accept loss types that are valid for the model + loss: BaseLossConfig = LossDataclassField( + feature_type=TEXT, + default=NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY, + ) diff --git a/ludwig/schema/features/timeseries_feature.py b/ludwig/schema/features/timeseries_feature.py new file mode 100644 index 0000000..b377083 --- /dev/null +++ b/ludwig/schema/features/timeseries_feature.py @@ -0,0 +1,113 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import HUBER, MEAN_SQUARED_ERROR, MODEL_ECD, TIMESERIES, VECTOR +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(TIMESERIES) +class TimeseriesInputFeatureConfigMixin(LudwigBaseConfig): + """TimeseriesInputFeatureConfigMixin is a dataclass that configures the parameters used in both the timeseries + input feature and the timeseries global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=TIMESERIES) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=TIMESERIES, + default="parallel_cnn", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(TIMESERIES) +class TimeseriesInputFeatureConfig(TimeseriesInputFeatureConfigMixin, BaseInputFeatureConfig): + """TimeseriesInputFeatureConfig is a dataclass that configures the parameters used for a timeseries input + feature.""" + + type: str = schema_utils.ProtectedString(TIMESERIES) + + +@DeveloperAPI +@output_mixin_registry.register(TIMESERIES) +class TimeseriesOutputFeatureConfigMixin(LudwigBaseConfig): + """TimeseriesOutputFeatureConfigMixin configures the parameters used in both the timeseries output feature and + the timeseries global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=TIMESERIES, + default="projector", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=TIMESERIES, + default=HUBER, + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(TIMESERIES) +class TimeseriesOutputFeatureConfig(BaseOutputFeatureConfig, TimeseriesOutputFeatureConfigMixin): + """TimeseriesOutputFeatureConfig configures the parameters used for a timeseries output feature.""" + + type: str = schema_utils.ProtectedString(TIMESERIES) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[VECTOR]["dependencies"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [MEAN_SQUARED_ERROR], + default=MEAN_SQUARED_ERROR, + description="Internal parameter.", + parameter_metadata=INTERNAL_ONLY, + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="timeseries_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default=None, + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[VECTOR]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default=None, + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[VECTOR]["reduce_input"], + ) + + horizon: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Internal parameter. Obtained from preprocessing", + parameter_metadata=INTERNAL_ONLY, + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(TIMESERIES) +class TimeseriesDefaultsConfig(TimeseriesInputFeatureConfigMixin, TimeseriesOutputFeatureConfigMixin): + pass diff --git a/ludwig/schema/features/utils.py b/ludwig/schema/features/utils.py new file mode 100644 index 0000000..621fa70 --- /dev/null +++ b/ludwig/schema/features/utils.py @@ -0,0 +1,143 @@ +from collections import defaultdict + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MODEL_ECD, MODEL_LLM +from ludwig.schema import utils as schema_utils +from ludwig.utils.registry import Registry + +input_config_registries = defaultdict(Registry) +output_config_registries = defaultdict(Registry) + +ecd_input_config_registry = input_config_registries[MODEL_ECD] +llm_input_config_registry = input_config_registries[MODEL_LLM] + +ecd_output_config_registry = output_config_registries[MODEL_ECD] +llm_output_config_registry = output_config_registries[MODEL_LLM] + +input_mixin_registry = Registry() +output_mixin_registry = Registry() +"""ECD models support the full range of feature parameters available in Ludwig, so any feature schema can be +registered into it. + +See `BinaryDefaultsConfig` for an example. +""" +ecd_defaults_config_registry = Registry() + +llm_defaults_config_registry = Registry() + + +def input_config_registry(model_type: str) -> Registry: + return input_config_registries[model_type] + + +def output_config_registry(model_type: str) -> Registry: + return output_config_registries[model_type] + + +@DeveloperAPI +def get_input_feature_cls(model_type: str, name: str): + # TODO(travis): not needed once we remove existing model config implementation + return input_config_registries[model_type][name] + + +@DeveloperAPI +def get_output_feature_cls(model_type: str, name: str): + # TODO(ksbrar): What is this? + return output_config_registries[model_type][name] + + +@DeveloperAPI +def get_input_feature_jsonschema(model_type: str): + """This function returns a JSON schema structured to only requires a `type` key and then conditionally applies + a corresponding input feature's field constraints. + + Returns: JSON Schema + """ + input_feature_types = sorted(list(input_config_registry(model_type).keys())) + schema = { + "type": "object", + "properties": { + "name": {"type": "string", "title": "name", "description": "Name of the input feature."}, + "type": { + "type": "string", + "enum": input_feature_types, + "title": "type", + "description": "Type of the input feature", + }, + "column": {"type": "string", "title": "column", "description": "Name of the column."}, + }, + "additionalProperties": True, + "allOf": get_input_feature_conds(model_type), + "required": ["name", "type"], + "title": "input_feature", + } + + return schema + + +@DeveloperAPI +def get_input_feature_conds(model_type: str): + """This function returns a list of if-then JSON clauses for each input feature type along with their properties + and constraints. + + Returns: List of JSON clauses + """ + input_feature_types = sorted(list(input_config_registry(model_type).keys())) + conds = [] + for feature_type in input_feature_types: + schema_cls = get_input_feature_cls(model_type, feature_type) + feature_schema = schema_utils.unload_jsonschema_from_config_class(schema_cls) + feature_props = feature_schema["properties"] + schema_utils.remove_duplicate_fields(feature_props) + + feature_cond = schema_utils.create_cond({"type": feature_type}, feature_props) + conds.append(feature_cond) + return conds + + +@DeveloperAPI +def get_output_feature_jsonschema(model_type: str): + """This function returns a JSON schema structured to only requires a `type` key and then conditionally applies + a corresponding output feature's field constraints. + + Returns: JSON Schema + """ + output_feature_types = sorted(list(output_config_registry(model_type).keys())) + schema = { + "type": "object", + "properties": { + "name": {"type": "string", "title": "name", "description": "Name of the output feature."}, + "type": { + "type": "string", + "enum": output_feature_types, + "title": "type", + "description": "Type of the output feature", + }, + "column": {"type": "string", "title": "column", "description": "Name of the column."}, + }, + "additionalProperties": True, + "allOf": get_output_feature_conds(model_type), + "required": ["name", "type"], + "title": "output_feature", + } + + return schema + + +@DeveloperAPI +def get_output_feature_conds(model_type: str): + """This function returns a list of if-then JSON clauses for each output feature type along with their + properties and constraints. + + Returns: List of JSON clauses + """ + output_feature_types = sorted(list(output_config_registry(model_type).keys())) + conds = [] + for feature_type in output_feature_types: + schema_cls = get_output_feature_cls(model_type, feature_type) + feature_schema = schema_utils.unload_jsonschema_from_config_class(schema_cls) + feature_props = feature_schema["properties"] + schema_utils.remove_duplicate_fields(feature_props) + feature_cond = schema_utils.create_cond({"type": feature_type}, feature_props) + conds.append(feature_cond) + return conds diff --git a/ludwig/schema/features/vector_feature.py b/ludwig/schema/features/vector_feature.py new file mode 100644 index 0000000..9574905 --- /dev/null +++ b/ludwig/schema/features/vector_feature.py @@ -0,0 +1,119 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MEAN_SQUARED_ERROR, MODEL_ECD, VECTOR +from ludwig.schema import utils as schema_utils +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import DecoderDataclassField +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import EncoderDataclassField +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.utils import LossDataclassField +from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField +from ludwig.schema.features.utils import ( + ecd_defaults_config_registry, + ecd_input_config_registry, + ecd_output_config_registry, + input_mixin_registry, + output_mixin_registry, +) +from ludwig.schema.metadata import FEATURE_METADATA +from ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY +from ludwig.schema.utils import LudwigBaseConfig + + +@DeveloperAPI +@input_mixin_registry.register(VECTOR) +class VectorInputFeatureConfigMixin(LudwigBaseConfig): + """VectorInputFeatureConfigMixin is a dataclass that configures the parameters used in both the vector input + feature and the vector global defaults section of the Ludwig Config.""" + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=VECTOR) + + encoder: BaseEncoderConfig = EncoderDataclassField( + MODEL_ECD, + feature_type=VECTOR, + default="dense", + ) + + +@DeveloperAPI +@ecd_input_config_registry.register(VECTOR) +class VectorInputFeatureConfig(VectorInputFeatureConfigMixin, BaseInputFeatureConfig): + """VectorInputFeatureConfig is a dataclass that configures the parameters used for a vector input feature.""" + + type: str = schema_utils.ProtectedString(VECTOR) + + +@DeveloperAPI +@output_mixin_registry.register(VECTOR) +class VectorOutputFeatureConfigMixin(LudwigBaseConfig): + """VectorOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the vector output + feature and the vector global defaults section of the Ludwig Config.""" + + decoder: BaseDecoderConfig = DecoderDataclassField( + MODEL_ECD, + feature_type=VECTOR, + default="projector", + ) + + loss: BaseLossConfig = LossDataclassField( + feature_type=VECTOR, + default=MEAN_SQUARED_ERROR, + ) + + +@DeveloperAPI +@ecd_output_config_registry.register(VECTOR) +class VectorOutputFeatureConfig(VectorOutputFeatureConfigMixin, BaseOutputFeatureConfig): + """VectorOutputFeatureConfig is a dataclass that configures the parameters used for a vector output feature.""" + + type: str = schema_utils.ProtectedString(VECTOR) + + dependencies: list = schema_utils.List( + default=[], + description="List of input features that this feature depends on.", + parameter_metadata=FEATURE_METADATA[VECTOR]["dependencies"], + ) + + default_validation_metric: str = schema_utils.StringOptions( + [MEAN_SQUARED_ERROR], + default=MEAN_SQUARED_ERROR, + description="Internal only use parameter: default validation metric for binary output feature.", + parameter_metadata=INTERNAL_ONLY, + ) + + preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type="vector_output") + + reduce_dependencies: str = schema_utils.ReductionOptions( + default=None, + description="How to reduce the dependencies of the output feature.", + parameter_metadata=FEATURE_METADATA[VECTOR]["reduce_dependencies"], + ) + + reduce_input: str = schema_utils.ReductionOptions( + default=None, + description="How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first " + "dimension (second if you count the batch dimension)", + parameter_metadata=FEATURE_METADATA[VECTOR]["reduce_input"], + ) + + softmax: bool = schema_utils.Boolean( + default=False, + description="Determines whether to apply a softmax at the end of the decoder. This is useful for predicting a " + "vector of values that sum up to 1 and can be interpreted as probabilities.", + parameter_metadata=FEATURE_METADATA[VECTOR]["softmax"], + ) + + vector_size: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The size of the vector. If None, the vector size will be inferred from the data.", + parameter_metadata=FEATURE_METADATA[VECTOR]["vector_size"], + ) + + +@DeveloperAPI +@ecd_defaults_config_registry.register(VECTOR) +class VectorDefaultsConfig(VectorInputFeatureConfigMixin, VectorOutputFeatureConfigMixin): + pass diff --git a/ludwig/schema/hyperopt/__init__.py b/ludwig/schema/hyperopt/__init__.py new file mode 100644 index 0000000..bc90b4e --- /dev/null +++ b/ludwig/schema/hyperopt/__init__.py @@ -0,0 +1,111 @@ +from abc import ABC + +import ludwig.schema.hyperopt.parameter # noqa: F401 +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import LOSS, TEST, TRAIN, VALIDATION +from ludwig.modules import metric_modules # noqa: F401 # Needed to ensure that the metric registry is populated. +from ludwig.modules.metric_registry import get_metric_registry +from ludwig.schema import utils as schema_utils +from ludwig.schema.hyperopt.executor import ExecutorConfig, ExecutorDataclassField +from ludwig.schema.hyperopt.search_algorithm import BaseSearchAlgorithmConfig, SearchAlgorithmDataclassField + + +@DeveloperAPI +class HyperoptConfig(schema_utils.LudwigBaseConfig, ABC): + """Basic hyperopt settings.""" + + output_feature: str = schema_utils.String( # TODO: make more restrictive + default="combined", + description=( + "The name of the output feature that we want to optimize the metric or loss of. Available values " + "are `combined` or the name of any output feature provided in the configuration. `combined` is a special " + "output feature that allows to optimize for the aggregated loss and metrics of all output features." + ), + ) + + goal: str = schema_utils.StringOptions( + options=["minimize", "maximize"], + default="minimize", + allow_none=False, + description=( + "Indicates if to minimize or maximize a metric or a loss of any of the output features on any of the " + "dataset splits. Available values are: minimize (default) or maximize." + ), + ) + + metric: str = schema_utils.StringOptions( + options=get_metric_registry().keys(), + default=LOSS, + allow_none=False, + description=( + "The metric that we want to optimize for. The default one is loss, but depending on the type of the " + "feature defined in output_feature, different metrics and losses are available. Check the metrics section " + "of the specific output feature type to figure out what metrics are available to use." + ), + ) + + split: str = schema_utils.StringOptions( + options=[TRAIN, VALIDATION, TEST], + default=VALIDATION, + allow_none=False, + description=( + "The split of data that we want to compute our metric on. By default it is the validation split, but " + "you have the flexibility to specify also train or test splits." + ), + ) + + eval_split: str = schema_utils.StringOptions( + options=[TRAIN, VALIDATION, TEST], + default=VALIDATION, + allow_none=False, + description=( + "The split of data that we want to run evaluation on. By default it is the validation split, but " + "you have the flexibility to specify also train or test splits." + ), + ) + + search_alg: BaseSearchAlgorithmConfig = SearchAlgorithmDataclassField( + description=( + "Specifies the algorithm to sample the defined parameters space. Candidate algorithms are those " + "found in [Ray Tune's Search Algorithms](https://docs.ray.io/en/latest/tune/api/suggestion.html)." + ) + ) + + executor: ExecutorConfig = ExecutorDataclassField( + description=( + "specifies how to execute the hyperparameter optimization. The execution could happen locally in a serial " + "manner or in parallel across multiple workers and with GPUs as well if available. The executor section " + "includes specification for work scheduling and the number of samples to generate." + ) + ) + + parameters: dict = schema_utils.Dict( + allow_none=False, + description=( + "This section consists of a set of hyperparameters to optimize. They are provided as keys (the names of " + "the parameters) and values associated with them (that define the search space). The values vary depending " + "on the type of the hyperparameter. Syntax for this section is based on [Ray Tune's Search Space " + "parameters](https://docs.ray.io/en/latest/tune/api/search_space.html)." + ), + ) + + +@DeveloperAPI +def get_hyperopt_jsonschema(): + props = schema_utils.unload_jsonschema_from_config_class(HyperoptConfig)["properties"] + + return { + "type": ["object", "null"], + "properties": props, + "title": "hyperopt_options", + "description": "Settings for hyperopt", + } + + +@DeveloperAPI +class HyperoptField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(HyperoptConfig, default_missing=True) + + def _jsonschema_type_mapping(self): + return get_hyperopt_jsonschema() diff --git a/ludwig/schema/hyperopt/executor.py b/ludwig/schema/hyperopt/executor.py new file mode 100644 index 0000000..b46dff3 --- /dev/null +++ b/ludwig/schema/hyperopt/executor.py @@ -0,0 +1,114 @@ +from dataclasses import field + +import pydantic + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import RAY +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.hyperopt.scheduler import BaseSchedulerConfig, SchedulerDataclassField + + +@DeveloperAPI +class ExecutorConfig(schema_utils.LudwigBaseConfig): + """Basic executor settings.""" + + type: str = schema_utils.ProtectedString(RAY) + + num_samples: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=( + "This parameter, along with the `space` specifications in the `parameters` section, controls how many " + "trials are generated." + ), + ) + + time_budget_s: int = schema_utils.PositiveInteger( + default=3600, allow_none=True, description="The number of seconds for the entire hyperopt run." + ) + + trial_driver_resources: dict[str, float] = schema_utils.Dict( + default=None, + description=( + "The resources reserved by each trial driver. This differs from cpu_resources_per_trial and " + "gpu_resources_per_trial because these resources are reserved for the driver, not its subsequent " + "workers. Only used when the trials themselves are on the Ray backend. Defaults to 1 CPU." + ), + ) + + cpu_resources_per_trial: int = schema_utils.PositiveInteger( + default=1, description="The number of CPU cores allocated to each trial" + ) + + gpu_resources_per_trial: int = schema_utils.NonNegativeInteger( + default=0, description="The number of GPU devices allocated to each trial" + ) + + kubernetes_namespace: str | None = schema_utils.String( + default=None, + allow_none=True, + description=( + "When running on Kubernetes, provide the namespace of the Ray cluster to sync results between " + "pods. See the Ray docs for more info." + ), + ) + + max_concurrent_trials: str | int | None = schema_utils.OneOfOptionsField( + default="auto", + allow_none=True, + description=("The maximum number of trials to train concurrently. Defaults to auto if not specified."), + field_options=[ + schema_utils.PositiveInteger( + default=1, allow_none=False, description="Manually set a number of concurrent trials." + ), + schema_utils.StringOptions( + options=["auto"], + default="auto", + allow_none=False, + description="Automatically set number of concurrent trials.", + ), + ], + ) + + scheduler: BaseSchedulerConfig = SchedulerDataclassField(description="") + + +@DeveloperAPI +def ExecutorDataclassField(description: str, default: dict = {}): + class ExecutorConfigField(schema_utils.SchemaField): + def _deserialize(self, value, attr, data, **kwargs): + if isinstance(value, dict): + try: + return ExecutorConfig.model_validate(value) + except (TypeError, ConfigValidationError): + raise ConfigValidationError(f"Invalid params for executor: {value}, see ExecutorConfig class.") + raise ConfigValidationError("Field should be dict") + + def _jsonschema_type_mapping(self): + return { + **schema_utils.unload_jsonschema_from_config_class(ExecutorConfig), + "title": "executor", + "description": description, + } + + if not isinstance(default, dict): + raise ConfigValidationError(f"Invalid default: `{default}`") + + load_default = lambda: ExecutorConfig.model_validate(default) + try: + dump_default = ExecutorConfig.model_validate(default).to_dict() + except pydantic.ValidationError: + dump_default = default if isinstance(default, dict) else {} + + return field( + metadata={ + "marshmallow_field": ExecutorConfigField( + allow_none=False, + load_default=load_default, + dump_default=dump_default, + metadata={"description": description, "parameter_metadata": None}, + ) + }, + default_factory=load_default, + ) diff --git a/ludwig/schema/hyperopt/parameter.py b/ludwig/schema/hyperopt/parameter.py new file mode 100644 index 0000000..9ec30f3 --- /dev/null +++ b/ludwig/schema/hyperopt/parameter.py @@ -0,0 +1,183 @@ +from pydantic.fields import FieldInfo + +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.hyperopt.utils import register_parameter_config + + +def quantization_number_field(dtype: type[float] | type[int] = float, default=None) -> FieldInfo: + description = ( + "Quantization number. Output values will be rounded to the nearest increment of `q` in range." + "Quantization makes the upper bound inclusive." + ) + if dtype is int: + field = schema_utils.Integer(default=default, allow_none=True, description=description) + else: + field = schema_utils.FloatRange(default=default, allow_none=True, description=description) + + return field + + +def log_base_field(default: float = 10) -> FieldInfo: + return schema_utils.FloatRange(default=default, description="Logarithmic base.") + + +@DeveloperAPI +@register_parameter_config("choice") +class ChoiceParameterConfig(schema_utils.LudwigBaseConfig): + """Config for a randomly sampled categorical search space.""" + + space: str = schema_utils.ProtectedString("choice") + + categories: list = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + description=( + "The list of values to use in creating the categorical space. The type of each value of the list is " + "general, i.e., they could be strings, integers, floats and anything else, even entire dictionaries." + ), + field_options=[ + schema_utils.List(list_type=float, allow_none=False, description="The list of floats to randomly sample."), + schema_utils.List(list_type=int, allow_none=False, description="The list of integers to randomly sample."), + schema_utils.List(list_type=str, allow_none=False, description="The list of strings to randomly sample."), + schema_utils.List( + list_type=list, + inner_type=dict, + allow_none=False, + description="The list of lists of configs to randomly sample.", + ), + schema_utils.DictList(allow_none=False, description="A list of nested config parameters to sample."), + ], + ) + + +@DeveloperAPI +@register_parameter_config("grid_search") +class GridSearchParameterConfig(schema_utils.LudwigBaseConfig): + """Config for a grid search space.""" + + space: str = schema_utils.ProtectedString("grid_search") + + values: list = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + description=( + "The list of values to use in creating the grid search space. The type of each value of the list is " + "general, i.e., they could be strings, integers, floats and anything else, even entire dictionaries." + ), + field_options=[ + schema_utils.List(list_type=float, allow_none=False, description="The list of floats to randomly sample."), + schema_utils.List(list_type=int, allow_none=False, description="The list of integers to randomly sample."), + schema_utils.List(list_type=str, allow_none=False, description="The list of strings to randomly sample."), + ], + ) + + +@DeveloperAPI +@register_parameter_config("uniform") +class UniformParameterConfig(schema_utils.LudwigBaseConfig): + """Config for a real-valued uniform search space.""" + + space: str = schema_utils.ProtectedString("uniform") + + lower: float = schema_utils.FloatRange(default=None, description="The minimum value the parameter can have.") + + upper: float = schema_utils.FloatRange(default=None, description="The maximum value the parameter can have.") + + +@DeveloperAPI +@register_parameter_config("quniform") +class QUniformParameterConfig(UniformParameterConfig): + """Config for a real-valued uniform search space with quantization.""" + + space: str = schema_utils.ProtectedString("quniform") + + q: float = quantization_number_field() + + +@DeveloperAPI +@register_parameter_config("loguniform") +class LogUniformParameterConfig(UniformParameterConfig): + """Config for a log-scaled real-valued uniform numeric search space.""" + + space: str = schema_utils.ProtectedString("loguniform") + + base: float = log_base_field() + + +@DeveloperAPI +@register_parameter_config("qloguniform") +class QLogUniformParameterConfig(UniformParameterConfig): + """Config for a log-scaled real-valued uniform search space with quantization.""" + + space: str = schema_utils.ProtectedString("qloguniform") + + q: float = quantization_number_field() + + base: float = log_base_field() + + +@DeveloperAPI +@register_parameter_config("randn") +class RandnParameterConfig(schema_utils.LudwigBaseConfig): + """Config for a Gaussian search space.""" + + space: str = schema_utils.ProtectedString("randn") + + mean: float = schema_utils.FloatRange(default=0.0, description="Mean of the normal distribution.") + + sd: float = schema_utils.FloatRange(default=1.0, description="Standard deviation of the normal distribution.") + + +@DeveloperAPI +@register_parameter_config("qrandn") +class QRandnParameterConfig(RandnParameterConfig): + """Config for a Gaussian search space with quantization.""" + + space: str = schema_utils.ProtectedString("qrandn") + + q: float = quantization_number_field() + + +@DeveloperAPI +@register_parameter_config("randint") +class RandintParameterConfig(schema_utils.LudwigBaseConfig): + """Config for an integer-valued uniform search space.""" + + space: str = schema_utils.ProtectedString("randint") + + lower: int = schema_utils.Integer(default=None, description="The minimum value the parameter can have.") + + upper: int = schema_utils.Integer(default=None, description="The maximum value the parameter can have.") + + +@DeveloperAPI +@register_parameter_config("qrandint") +class QRandintParameterConfig(RandintParameterConfig): + """Config for an integer-valued uniform search space with quantization.""" + + space: str = schema_utils.ProtectedString("qrandint") + + q: int = quantization_number_field(dtype=int) + + +@DeveloperAPI +@register_parameter_config("lograndint") +class LogRandintParameterConfig(RandintParameterConfig): + """Config for an log-scaled integer-valued search space.""" + + space: str = schema_utils.ProtectedString("lograndint") + + base: float = log_base_field() + + +@DeveloperAPI +@register_parameter_config("qlograndint") +class QLogRandintParameterConfig(RandintParameterConfig): + """Config for an log-scaled integer-valued search space with quantization.""" + + space: str = schema_utils.ProtectedString("qlograndint") + + q: int = quantization_number_field(dtype=int) + + base: float = log_base_field() diff --git a/ludwig/schema/hyperopt/scheduler.py b/ludwig/schema/hyperopt/scheduler.py new file mode 100644 index 0000000..aa3fbe7 --- /dev/null +++ b/ludwig/schema/hyperopt/scheduler.py @@ -0,0 +1,560 @@ +from abc import ABC +from collections.abc import Callable +from dataclasses import field +from importlib import import_module + +import pydantic + +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.hyperopt import utils as hyperopt_utils + +# ---------------------------------------------------------------------------------------------------------------------- +# To prevent direct dependency on ray import, the following static key stores are duplicated: + +# from ray.tune.schedulers import SCHEDULER_IMPORT +# https://github.com/ray-project/ray/blob/137a1b12c3b31a3622fa5f721a05a64e9b559b05/python/ray/tune/schedulers/__init__.py#L28 + +# from ray.tune.result import DEFAULT_RESULT_KEYS +# Taken from https://github.com/ray-project/ray/blob/137a1b12c3b31a3622fa5f721a05a64e9b559b05/python/ray/tune/result.py +TRAINING_ITERATION = "training_iteration" +TIME_TOTAL_S = "time_total_s" +TIMESTEPS_TOTAL = "timesteps_total" +MEAN_ACCURACY = "mean_accuracy" +MEAN_LOSS = "mean_loss" +DEFAULT_RESULT_KEYS = (TRAINING_ITERATION, TIME_TOTAL_S, TIMESTEPS_TOTAL, MEAN_ACCURACY, MEAN_LOSS) + +# from ray.tune.result import DEFAULT_METRIC +RAY_TUNE_DESULT_DEFAULT_METRIC = "_metric" +# ---------------------------------------------------------------------------------------------------------------------- + + +# Field aliases to cut down on code reuse: +@DeveloperAPI +def metric_alias(default=None): + return schema_utils.StringOptions( + options=list(DEFAULT_RESULT_KEYS) + [RAY_TUNE_DESULT_DEFAULT_METRIC], + default=default, + allow_none=default is None, + description=( + "The training result objective value attribute. Stopping procedures will use this attribute. If None but a " + "mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default." + ), + ) + + +@DeveloperAPI +def time_attr_alias(default=TRAINING_ITERATION): + return schema_utils.StringOptions( + options=list(DEFAULT_RESULT_KEYS), + default=default, + allow_none=False, + description=( + "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as" + " training_iteration as a measure of progress, the only requirement is that the attribute should increase " + "monotonically." + ), + ) + + +@DeveloperAPI +def max_t_alias(default=100): + return schema_utils.PositiveInteger( + default=default, + description=( + "max time units per trial. Trials will be stopped after max_t time units (determined by time_attr) have " + "passed." + ), + ) + + +@DeveloperAPI +class BaseSchedulerConfig(schema_utils.LudwigBaseConfig, ABC): + """Base class for schedulers. + + Not meant to be used directly. + """ + + type: str + """Name corresponding to a scheduler in `ludwig.schema.hyperopt.scheduler.scheduler_registry`. + + Technically mutable, but attempting to load a derived scheduler with `type` set to a mismatched value will result in + a `ValidationError`. + """ + + time_attr: str = time_attr_alias() + + metric: str | None = metric_alias() + + mode: str | None = schema_utils.StringOptions( + options=["min", "max"], + default=None, + allow_none=True, + description=( + "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute." + ), + ) + + def dependencies_installed(self): + """Some search algorithms require additional packages to be installed, check that they are available.""" + missing_packages = [] + missing_installs = [] + for package_name, install_name in hyperopt_utils.get_scheduler_dependencies(self.type): + try: + import_module(package_name) + except ImportError: + missing_packages.append(package_name) + missing_installs.append(install_name) + + if missing_packages: + missing_packages = ", ".join(missing_packages) + missing_installs = " ".join(missing_installs) + raise ImportError( + f"Some packages needed to use hyperopt scheduler {self.type} are not installed: " + f"{missing_packages}. To add these dependencies, run `pip install {missing_installs}`. For more " + "details, please refer to Ray Tune documentation for this scheduler." + ) + return True + + +@DeveloperAPI +class BaseHyperbandSchedulerConfig(BaseSchedulerConfig): + max_t: int = max_t_alias() + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("async_hyperband") +@hyperopt_utils.register_scheduler_config("asynchyperband") +@hyperopt_utils.register_scheduler_config("asha") +class AsyncHyperbandSchedulerConfig(BaseHyperbandSchedulerConfig): + """Asynchronous hyperband (ASHA) scheduler settings.""" + + type: str = schema_utils.ProtectedString("async_hyperband") + + max_t: int = max_t_alias() + + grace_period: int = schema_utils.PositiveInteger( + default=1, + description=( + "Only stop trials at least this old in time. The units are the same as the attribute named by `time_attr`." + ), + ) + + reduction_factor: int = schema_utils.NonNegativeFloat( + default=4, description=("Used to set halving rate and amount. This is simply a unit-less scalar.") + ) + + brackets: int = schema_utils.PositiveInteger( + default=1, + description=( + "Number of brackets. Each bracket has a different halving rate, specified by the reduction factor." + ), + ) + + stop_last_trials: bool = schema_utils.Boolean( + default=True, description="Whether to terminate the trials after reaching `max_t`." + ) + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("hyperband") +class HyperbandSchedulerConfig(BaseHyperbandSchedulerConfig): + """Standard hyperband scheduler settings.""" + + type: str = schema_utils.ProtectedString("hyperband") + + max_t: int = max_t_alias(default=81) + + reduction_factor: int = schema_utils.NonNegativeFloat( + default=3, description=("Used to set halving rate and amount. This is simply a unit-less scalar.") + ) + + stop_last_trials: bool = schema_utils.Boolean( + default=True, description=("Whether to terminate the trials after reaching max_t. Defaults to True.") + ) + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("median_stopping_rule") +@hyperopt_utils.register_scheduler_config("medianstoppingrule") +class MedianStoppingRuleSchedulerConfig(BaseSchedulerConfig): + """Median Stopping Rule scheduler settings.""" + + type: str = schema_utils.ProtectedString("median_stopping_rule") + + time_attr: str = time_attr_alias(TIME_TOTAL_S) + + grace_period: float = schema_utils.NonNegativeFloat( + default=60.0, + description=( + "Only stop trials at least this old in time. The mean will only be computed from this time onwards. The " + "units are the same as the attribute named by `time_attr`." + ), + ) + + min_samples_required: int = schema_utils.PositiveInteger( + default=3, description=("Minimum number of trials to compute median over.") + ) + + min_time_slice: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "Each trial runs at least this long before yielding (assuming it isn't stopped). Note: trials ONLY yield " + "if there are not enough samples to evaluate performance for the current result AND there are other " + "trials waiting to run. The units are the same as the attribute named by `time_attr`." + ), + ) + + hard_stop: bool = schema_utils.Boolean( + default=True, + description=( + "If False, pauses trials instead of stopping them. When all other trials are complete, paused trials will " + "be resumed and allowed to run FIFO." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("pbt") +class PopulationBasedTrainingSchedulerConfig(BaseSchedulerConfig): + """Population Based Training scheduler settings.""" + + type: str = schema_utils.ProtectedString("pbt") + + time_attr: str = time_attr_alias(TIME_TOTAL_S) + + perturbation_interval: float = schema_utils.NonNegativeFloat( + default=60.0, + description=( + "Models will be considered for perturbation at this interval of `time_attr`. Note that perturbation incurs " + "checkpoint overhead, so you shouldn't set this to be too frequent." + ), + ) + + burn_in_period: float = schema_utils.NonNegativeFloat( + default=60.0, + description=( + "Models will not be considered for perturbation before this interval of time_attr has passed. This " + "guarantees that models are trained for at least a certain amount of time or timesteps before being " + "perturbed." + ), + ) + + hyperparam_mutations: dict | None = schema_utils.Dict( + default=None, + description=( + "Hyperparams to mutate. The format is as follows: for each key, either a list, function, or a tune search " + "space object (`tune.loguniform`, tune.uniform, etc.) can be provided. A list specifies an allowed set of " + "categorical values. A function or tune search space object specifies the distribution of a continuous " + "parameter. You must use `tune.choice`, `tune.uniform`, `tune.loguniform`, etc.. Arbitrary " + "`tune.sample_from` objects are not supported. A key can also hold a dict for nested hyperparameters. You " + "must specify at least one of `hyperparam_mutations` or `custom_explore_fn`. Tune will sample the search " + "space provided by `hyperparam_mutations` for the initial hyperparameter values if the corresponding " + "hyperparameters are not present in a trial's initial config." + ), + ) + + quantile_fraction: float = schema_utils.FloatRange( + default=0.25, + allow_none=False, + min=0, + max=0.5, + description=( + "Parameters are transferred from the top `quantile_fraction` fraction of trials to the bottom " + "`quantile_fraction` fraction. Needs to be between 0 and 0.5. Setting it to 0 essentially implies doing " + "no exploitation at all." + ), + ) + + resample_probability: float = schema_utils.NonNegativeFloat( + default=0.25, + description=( + "The probability of resampling from the original distribution when applying `hyperparam_mutations`. If " + "not resampled, the value will be perturbed by a factor chosen from `perturbation_factors` if continuous, " + "or changed to an adjacent value if discrete." + ), + ) + + perturbation_factors: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(1.2, 0.8), + allow_none=False, + max=None, + description=("Scaling factors to choose between when mutating a continuous hyperparameter."), + ) + + # TODO: Add schema support for Callable + custom_explore_fn: str | Callable = schema_utils.String( + default=None, + allow_none=True, + description=( + "You can also specify a custom exploration function. This function is invoked as `f(config)` after " + "built-in perturbations from `hyperparam_mutations` are applied, and should return config updated as " + "needed. You must specify at least one of `hyperparam_mutations` or `custom_explore_fn`." + ), + ) + + log_config: bool = schema_utils.Boolean( + default=True, + description=( + "Whether to log the ray config of each model to `local_dir` at each exploit. Allows config schedule to be " + "reconstructed." + ), + ) + + require_attrs: bool = schema_utils.Boolean( + default=True, + description=( + "Whether to require `time_attr` and metric to appear in result for every iteration. If True, error will " + "be raised if these values are not present in trial result." + ), + ) + + synch: bool = schema_utils.Boolean( + default=False, + description=( + "If False, will use asynchronous implementation of PBT. Trial perturbations occur every " + "`perturbation_interval` for each trial independently. If True, will use synchronous implementation of " + "PBT. Perturbations will occur only after all trials are synced at the same `time_attr` every " + "`perturbation_interval`. Defaults to False. See Appendix A.1 here https://arxiv.org/pdf/1711.09846.pdf." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("pbt_replay") +class PopulationBasedTrainingReplaySchedulerConfig(BaseSchedulerConfig): + """Population Based Training Replay scheduler settings.""" + + type: str = schema_utils.ProtectedString("pbt_replay") + + # TODO: This should technically be a required paremeter. Do we need to add support for required params? + policy_file: str = schema_utils.String( + default=None, + allow_none=True, + description=( + "The PBT policy file. Usually this is stored in `~/ray_results/experiment_name/pbt_policy_xxx.txt` where " + "`xxx` is the trial ID." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("pb2", dependencies=[("sklearn", "scikit-learn"), ("GPy", "GPy")]) +class PopulationBasedBanditsSchedulerConfig(BaseSchedulerConfig): + """Population Based Bandits (PB2) scheduler settings.""" + + type: str = schema_utils.ProtectedString("pb2") + + time_attr: str = time_attr_alias(TIME_TOTAL_S) + + perturbation_interval: float = schema_utils.NonNegativeFloat( + default=60.0, + description=( + "Models will be considered for perturbation at this interval of `time_attr`. Note that perturbation " + "incurs checkpoint overhead, so you shouldn't set this to be too frequent." + ), + ) + + hyperparam_bounds: dict | None = schema_utils.Dict( + default=None, + description=( + "Hyperparameters to mutate. The format is as follows: for each key, enter a list of the form [min, max] " + "representing the minimum and maximum possible hyperparameter values." + ), + ) + + quantile_fraction: float = schema_utils.FloatRange( + default=0.25, + allow_none=False, + min=0, + max=0.5, + description=( + "Parameters are transferred from the top `quantile_fraction` fraction of trials to the bottom " + "`quantile_fraction` fraction. Needs to be between 0 and 0.5. Setting it to 0 essentially implies doing " + "no exploitation at all." + ), + ) + + log_config: bool = schema_utils.Boolean( + default=True, + description=( + "Whether to log the ray config of each model to `local_dir` at each exploit. Allows config schedule to be " + "reconstructed." + ), + ) + + require_attrs: bool = schema_utils.Boolean( + default=True, + description=( + "Whether to require `time_attr` and metric to appear in result for every iteration. If True, error will " + "be raised if these values are not present in trial result." + ), + ) + + synch: bool = schema_utils.Boolean( + default=False, + description=( + "If False, will use asynchronous implementation of PBT. Trial perturbations occur every " + "`perturbation_interval` for each trial independently. If True, will use synchronous implementation of " + "PBT. Perturbations will occur only after all trials are synced at the same `time_attr` every " + "`perturbation_interval`. Defaults to False. See Appendix A.1 here https://arxiv.org/pdf/1711.09846.pdf." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("hb_bohb") +class BOHBSchedulerConfig(BaseHyperbandSchedulerConfig): + """Hyperband for BOHB (hb_bohb) scheduler settings.""" + + type: str = schema_utils.ProtectedString("hb_bohb") + + max_t: int = max_t_alias(default=81) + + reduction_factor: int = schema_utils.NonNegativeFloat( + default=3, description=("Used to set halving rate and amount. This is simply a unit-less scalar.") + ) + + stop_last_trials: bool = schema_utils.Boolean( + default=True, description=("Whether to terminate the trials after reaching `max_t`. Defaults to True.") + ) + + +# TODO: Double-check support for this +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("fifo") +class FIFOSchedulerConfig(BaseSchedulerConfig): + """FIFO trial scheduler settings.""" + + type: str = schema_utils.ProtectedString("fifo") + + +# TODO: Double-check support for this as well as whether Callable args work properly +@DeveloperAPI +@hyperopt_utils.register_scheduler_config("resource_changing") +class ResourceChangingSchedulerConfig(BaseSchedulerConfig): + """Resource changing scheduler settings.""" + + type: str = schema_utils.ProtectedString("resource_changing") + + base_scheduler: str | None | Callable = schema_utils.String( + default=None, + allow_none=True, + description=("The scheduler to provide decisions about trials. If None, a default FIFOScheduler will be used."), + ) + + resources_allocation_function: str | Callable = schema_utils.String( + default=None, + allow_none=True, + description=( + "The callable used to change live trial resource requiements during tuning. This callable will be called on" + " each trial as it finishes one step of training. The callable must take four arguments: `TrialRunner`, " + "current `Trial`, current result `dict` and the `ResourceChangingScheduler` calling it. The callable must " + "return a `PlacementGroupFactory`, `Resources`, `dict` or None (signifying no need for an update). If " + "`resources_allocation_function` is None, no resource requirements will be changed at any time. By " + " default, `DistributeResources` will be used, distributing available CPUs and GPUs over all running " + "trials in a robust way, without any prioritization." + ), + ) + + +@DeveloperAPI +def get_scheduler_conds(): + """Returns a JSON schema of conditionals to validate against scheduler types defined in + `ludwig.schema.hyperopt.scheduler_registry`.""" + conds = [] + for scheduler_config in hyperopt_utils.scheduler_config_registry: + scheduler_cls = hyperopt_utils.scheduler_config_registry[scheduler_config] + other_props = schema_utils.unload_jsonschema_from_config_class(scheduler_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + preproc_cond = schema_utils.create_cond( + {"type": scheduler_config}, + other_props, + ) + conds.append(preproc_cond) + return conds + + +@DeveloperAPI +def SchedulerDataclassField(default={"type": "fifo"}, description="Hyperopt scheduler settings."): + """Custom dataclass field that when used inside of a dataclass will allow any scheduler in + `ludwig.schema.hyperopt.scheduler.scheduler_registry`. Sets default scheduler to 'fifo'. + + Args: + default: Dict specifying a scheduler with a `type` field and its associated parameters. Will attempt to + use `type` to load scheduler from registry with given params. (default: {"type": "fifo"}). + + Returns: + Initialized dataclass field that converts untyped dicts with params to scheduler dataclass instances. + """ + + class SchedulerConfigField(schema_utils.SchemaField): + """Custom field that deserializes a dict to a valid scheduler from + `ludwig.schema.hyperopt.scheduler_registry` and creates a corresponding `oneOf` JSON schema for external + usage.""" + + def _deserialize(self, value, attr, data, **kwargs): + if value is None: + return None + if isinstance(value, dict): + if "type" in value and value["type"] in hyperopt_utils.scheduler_config_registry: + scheduler_config_cls = hyperopt_utils.scheduler_config_registry[value["type"].lower()] + try: + return scheduler_config_cls.model_validate(value) + except (TypeError, ConfigValidationError) as e: + raise ConfigValidationError( + f"Invalid params for scheduler: {value}, see `{opt}` definition. Error: {e}" + ) + raise ConfigValidationError( + f"Invalid params for scheduler: {value}, expect dict with at least a valid `type` attribute." + ) + raise ConfigValidationError("Field should be None or dict") + + def _jsonschema_type_mapping(self): + # Note that this uses the same conditional pattern as combiners: + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": list(hyperopt_utils.scheduler_config_registry.keys()), + "default": default["type"], + "description": "The type of scheduler to use during hyperopt", + }, + }, + "title": "scheduler_options", + "allOf": get_scheduler_conds(), + "required": ["type"], + "description": description, + } + + if ( + not isinstance(default, dict) + or "type" not in default + or default["type"] not in hyperopt_utils.scheduler_config_registry + ): + raise ConfigValidationError(f"Invalid default: `{default}`") + try: + opt = hyperopt_utils.scheduler_config_registry[default["type"].lower()] + load_default = lambda: opt.model_validate(default) + try: + dump_default = opt.model_validate(default).to_dict() + except pydantic.ValidationError: + dump_default = default if isinstance(default, dict) else {} + + return field( + metadata={ + "marshmallow_field": SchedulerConfigField( + allow_none=False, + dump_default=dump_default, + load_default=load_default, + metadata={"description": description}, + ) + }, + default_factory=load_default, + ) + except Exception as e: + raise ConfigValidationError( + f"Unsupported scheduler type: {default['type']}. See scheduler_config_registry. Details: {e}" + ) diff --git a/ludwig/schema/hyperopt/search_algorithm.py b/ludwig/schema/hyperopt/search_algorithm.py new file mode 100644 index 0000000..1670556 --- /dev/null +++ b/ludwig/schema/hyperopt/search_algorithm.py @@ -0,0 +1,532 @@ +from dataclasses import field +from importlib.util import find_spec +from typing import Any + +import pydantic + +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.hyperopt import utils as hyperopt_utils + + +def points_to_evaluate_field(description: str | None = None): + return schema_utils.DictList( + description=description + or ( + "Initial parameter suggestions to be run first. This is for when you already have some good parameters " + "you want to run first to help the algorithm make better suggestions for future parameters. Needs to be " + "a list of dicts containing the configurations." + ), + ) + + +def evaluated_rewards_field(description: str | None = None): + return schema_utils.List( + description=description + or ( + "If you have previously evaluated the parameters passed in as points_to_evaluate you can avoid re-running " + "those trials by passing in the reward attributes as a list so the optimiser can be told the results " + "without needing to re-compute the trial. Must be the same length as `points_to_evaluate`." + ) + ) + + +@DeveloperAPI +class BaseSearchAlgorithmConfig(schema_utils.LudwigBaseConfig): + """Basic search algorithm settings.""" + + type: str = schema_utils.String(default="variant_generator", description="The search algorithm to use.") + + def set_random_state(self, ludwig_random_state: int) -> None: + """Overwrite the config random state. + + Search algorithms refer to random state by different names, however we want to overwrite unset random states + with the Ludwig random state. This method uses a registry of random state field names to provide a single + interface across all search algorithms. + """ + rs_field = hyperopt_utils.get_search_algorithm_random_state_field(self.type) + if rs_field is not None and self.__getattribute__(rs_field) is None: + self.__setattr__(rs_field, ludwig_random_state) + + def dependencies_installed(self) -> bool: + """Some search algorithms require additional packages to be installed, check that they are available.""" + missing_packages = [] + missing_installs = [] + for package_name, install_name in hyperopt_utils.get_search_algorithm_dependencies(self.type): + if find_spec(package_name) is None: + missing_packages.append(package_name) + missing_installs.append(install_name) + + if missing_packages: + missing_packages = ", ".join(missing_packages) + missing_installs = " ".join(missing_installs) + raise ImportError( + f"Some packages needed to use hyperopt search algorithm {self.type} are not installed: " + f"{missing_packages}. To add these dependencies, run `pip install {missing_installs}`. For more " + "details, please refer to Ray Tune documentation for this search algorithm." + ) + return True + + +@DeveloperAPI +def SearchAlgorithmDataclassField(description: str = "", default: dict = {"type": "variant_generator"}): + class SearchAlgorithmConfigField(schema_utils.SchemaField): + def _deserialize(self, value, attr, data, **kwargs): + if isinstance(value, dict): + try: + return BaseSearchAlgorithmConfig.model_validate(value) + except (TypeError, ConfigValidationError): + raise ConfigValidationError( + f"Invalid params for scheduler: {value}, see SearchAlgorithmConfig class." + ) + raise ConfigValidationError("Field should be dict") + + def _jsonschema_type_mapping(self): + return { + # **schema_utils.unload_jsonschema_from_config_class(BaseSearchAlgorithmConfig), + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": list(hyperopt_utils.search_algorithm_config_registry.keys()), + "default": default["type"], + "description": "The type of scheduler to use during hyperopt", + }, + }, + "title": "search_algorithm_options", + "required": ["type"], + "description": description, + } + + if not isinstance(default, dict): + raise ConfigValidationError(f"Invalid default: `{default}`") + + load_default = lambda: BaseSearchAlgorithmConfig.model_validate(default) + try: + dump_default = BaseSearchAlgorithmConfig.model_validate(default).to_dict() + except pydantic.ValidationError: + dump_default = default if isinstance(default, dict) else {} + + return field( + metadata={ + "marshmallow_field": SearchAlgorithmConfigField( + allow_none=False, + load_default=load_default, + dump_default=dump_default, + metadata={"description": description, "parameter_metadata": None}, + ) + }, + default_factory=load_default, + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config("random", random_state_field="random_state") +@hyperopt_utils.register_search_algorithm_config("variant_generator", random_state_field="random_state") +class BasicVariantSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.StringOptions(options=["random", "variant_generator"], default="random", allow_none=False) + + points_to_evaluate: list[dict] | None = schema_utils.DictList( + description=( + "Initial parameter suggestions to be run first. This is for when you already have some good parameters " + "you want to run first to help the algorithm make better suggestions for future parameters. Needs to be " + "a list of dicts containing the configurations." + ) + ) + + max_concurrent: int = schema_utils.NonNegativeInteger( + default=0, description="Maximum number of concurrently running trials. If 0 (default), no maximum is enforced." + ) + + constant_grid_search: bool = schema_utils.Boolean( + default=False, + description=( + "If this is set to True, Ray Tune will first try to sample random values and keep them constant over grid " + "search parameters. If this is set to False (default), Ray Tune will sample new random parameters in each " + "grid search condition." + ), + ) + + random_state: int = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Seed or numpy random generator to use for reproducible results. If None (default), will use the global " + "numpy random generator (np.random). Please note that full reproducibility cannot be guaranteed in a " + "distributed environment." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "ax", dependencies=[("ax", "ax-platform"), ("sqlalchemy", "sqlalchemy")] +) +class AxSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("ax") + + space: list[dict] | None = schema_utils.DictList( + description=( + r"Parameters in the experiment search space. Required elements in the dictionaries are: \“name\” (name of " + r"this parameter, string), \“type\” (type of the parameter: \“range\”, \“fixed\”, or \“choice\”, string), " + r"\“bounds\” for range parameters (list of two values, lower bound first), \“values\” for choice " + r"parameters (list of values), and \“value\” for fixed parameters (single value)." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + parameter_constraints: list | None = schema_utils.List( + description=r"Parameter constraints, such as \“x3 >= x4\” or \“x3 + x4 >= 2\”." + ) + + outcome_constraints: list | None = schema_utils.List( + description=r"Outcome constraints of form \“metric_name >= bound\”, like \“m1 <= 3.\”" + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "bayesopt", random_state_field="random_state", dependencies=[("bayes_opt", "bayesian-optimization")] +) +class BayesOptSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("bayesopt") + + space: dict | None = schema_utils.Dict( + description=( + "Continuous search space. Parameters will be sampled from this space which will be used to run trials" + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + utility_kwargs: dict | None = schema_utils.Dict( + description=( + "Parameters to define the utility function. The default value is a dictionary with three keys: " + "- kind: ucb (Upper Confidence Bound) - kappa: 2.576 - xi: 0.0" + ) + ) + + random_state: int = schema_utils.Integer(default=None, allow_none=True, description="Used to initialize BayesOpt.") + + random_search_steps: int = schema_utils.Integer( + default=10, + description=( + "Number of initial random searches. This is necessary to avoid initial local overfitting of " + "the Bayesian process." + ), + ) + + verbose: int = schema_utils.IntegerOptions( + options=[0, 1, 2], default=0, description="The level of verbosity. `0` is least verbose, `2` is most verbose." + ) + + patience: int = schema_utils.NonNegativeInteger( + default=5, description="Number of epochs to wait for a change in the top models." + ) + + skip_duplicate: bool = schema_utils.Boolean( + default=True, + description=( + "If False, the optimizer will allow duplicate points to be registered. This behavior may be desired in " + "high noise situations where repeatedly probing the same point will give different answers. In other " + "situations, the acquisition may occasionaly generate a duplicate point." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config("blendsearch", dependencies=[("flaml", "flaml[blendsearch]")]) +class BlendsearchSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("blendsearch") + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "bohb", random_state_field="seed", dependencies=[("hpbandster", "hpbandster"), ("ConfigSpace", "ConfigSpace")] +) +class BOHBSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("bohb") + + space: dict | None = schema_utils.Dict( + description=( + "Continuous ConfigSpace search space. Parameters will be sampled from this space which will be used " + "to run trials." + ) + ) + + bohb_config: dict | None = schema_utils.Dict(description="configuration for HpBandSter BOHB algorithm") + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + seed: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Optional random seed to initialize the random number generator. Setting this should lead to identical " + "initial configurations at each run." + ), + ) + + max_concurrent: int = schema_utils.Integer( + default=0, + description=( + "Number of maximum concurrent trials. If this Searcher is used in a `ConcurrencyLimiter`, the " + "`max_concurrent` value passed to it will override the value passed here. Set to <= 0 for no limit on " + "concurrency." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config("cfo", dependencies=[("flaml", "flaml")]) +class CFOSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("cfo") + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "dragonfly", random_state_field="random_state_seed", dependencies=[("dragonfly", "dragonfly-opt")] +) +class DragonflySAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("dragonfly") + + optimizer: str | None = schema_utils.StringOptions( + options=["random", "bandit", "genetic"], + default=None, + allow_none=True, + description=( + "Optimizer provided from dragonfly. Choose an optimiser that extends `BlackboxOptimiser`. If this is a " + "string, `domain` must be set and `optimizer` must be one of [random, bandit, genetic]." + ), + ) + + domain: str | None = schema_utils.StringOptions( + options=["cartesian", "euclidean"], + default=None, + allow_none=True, + description=( + "Optional domain. Should only be set if you don't pass an optimizer as the `optimizer` argument. If set, " + "has to be one of `[cartesian, euclidean]`." + ), + ) + + space: list[dict] | None = schema_utils.DictList( + description=( + "Search space. Should only be set if you don't pass an optimizer as the `optimizer` argument. Defines the " + "search space and requires a `domain` to be set. Can be automatically converted from the `param_space` " + "dict passed to `tune.Tuner()`." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + evaluated_rewards: list | None = evaluated_rewards_field() + + random_state_seed: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Seed for reproducible results. Defaults to None. Please note that setting this to a value will change " + "global random state for `numpy` on initalization and loading from checkpoint." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "hebo", random_state_field="random_state_seed", dependencies=[("hebo", "HEBO")] +) +class HEBOSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("hebo") + + space: list[dict] | None = schema_utils.DictList( + description="A dict mapping parameter names to Tune search spaces or a HEBO DesignSpace object." + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + evaluated_rewards: list | None = evaluated_rewards_field() + + random_state_seed: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Seed for reproducible results. Defaults to None. Please note that setting this to a value will change " + "global random state for `numpy` on initalization and loading from checkpoint." + ), + ) + + max_concurrent: int = schema_utils.NonNegativeInteger( + default=8, + description=( + "Number of maximum concurrent trials. If this Searcher is used in a `ConcurrencyLimiter`, the " + "`max_concurrent` value passed to it will override the value passed here." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "hyperopt", random_state_field="random_state_seed", dependencies=[("hyperopt", "hyperopt")] +) +class HyperoptSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("hyperopt") + + space: list[dict] | None = schema_utils.DictList( + description=( + "HyperOpt configuration. Parameters will be sampled from this configuration and will be used to override " + "parameters generated in the variant generation process." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + n_initial_points: int = schema_utils.PositiveInteger( + default=20, + description=( + "The number of random evaluations of the objective function before starting to approximate it with tree " + "parzen estimators. Defaults to 20." + ), + ) + + random_state_seed: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description=("Seed for reproducible results. Defaults to None."), + ) + + gamma: float = schema_utils.FloatRange( + min=0.0, + max=1.0, + default=0.25, + description=( + "The split to use in TPE. TPE models two splits of the evaluated hyperparameters: the top performing " + "`gamma` percent, and the remaining examples. For more details, see [Making a Science of Model Search: " + "Hyperparameter Optimization in Hundreds of Dimensions for Vision Architectures.]" + "(http://proceedings.mlr.press/v28/bergstra13.pdf)." + ), + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config("nevergrad", dependencies=[("nevergrad", "nevergrad")]) +class NevergradSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("nevergrad") + + # TODO: Add a registry mapping string names to nevergrad optimizers + # optimizer: Optional[str] = None + + # TODO: Add schemas for nevergrad optimizer kwargs + optimizer_kwargs: dict | None = schema_utils.Dict(description="Kwargs passed in when instantiating the optimizer.") + + space: list[dict] | None = schema_utils.DictList( + description=( + "Nevergrad parametrization to be passed to optimizer on instantiation, or list of parameter names if you " + "passed an optimizer object." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config( + "optuna", random_state_field="seed", dependencies=[("optuna", "optuna")] +) +class OptunaSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("optuna") + + space: dict | None = schema_utils.Dict( + description=( + "Hyperparameter search space definition for Optuna's sampler. This can be either a dict with parameter " + "names as keys and optuna.distributions as values, or a Callable - in which case, it should be a " + "define-by-run function using optuna.trial to obtain the hyperparameter values. The function should " + "return either a dict of constant values with names as keys, or None. For more information, see " + "[the Optuna docs]" + "(https://optuna.readthedocs.io/en/stable/tutorial/10_key_features/002_configurations.html)." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + # TODO: Add a registry of Optuna samplers schemas + # sampler = None + + seed: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description=( + "Seed to initialize sampler with. This parameter is only used when `sampler=None`. In all other cases, " + "the sampler you pass should be initialized with the seed already." + ), + ) + + evaluated_rewards: list | None = evaluated_rewards_field() + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config("skopt", dependencies=[("skopt", "scikit-optimize")]) +class SkoptSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("skopt") + + optimizer: Any | None = None + + space: dict | None = schema_utils.Dict( + description=( + "A dict mapping parameter names to valid parameters, i.e. tuples for numerical parameters and lists " + "for categorical parameters. If you passed an optimizer instance as the optimizer argument, this should " + "be a list of parameter names instead." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + evaluated_rewards: list | None = evaluated_rewards_field( + description=( + "If you have previously evaluated the parameters passed in as points_to_evaluate you can avoid " + "re-running those trials by passing in the reward attributes as a list so the optimiser can be told the " + "results without needing to re-compute the trial. Must be the same length as points_to_evaluate. (See " + "tune/examples/skopt_example.py)" + ) + ) + + convert_to_python: bool = schema_utils.Boolean( + default=True, + description="SkOpt outputs numpy primitives (e.g. `np.int64`) instead of Python types. If this setting is set " + "to `True`, the values will be converted to Python primitives.", + ) + + +@DeveloperAPI +@hyperopt_utils.register_search_algorithm_config("zoopt", dependencies=[("zoopt", "zoopt")]) +class ZooptSAConfig(BaseSearchAlgorithmConfig): + type: str = schema_utils.ProtectedString("zoopt") + + algo: str = schema_utils.ProtectedString( + pstring="asracos", + description="To specify an algorithm in zoopt you want to use. Only support ASRacos currently.", + ) + + budget: int | None = schema_utils.PositiveInteger( + default=None, allow_none=True, description="Optional. Number of samples." + ) + + dim_dict: dict | None = schema_utils.Dict( + description=( + "Dimension dictionary. For continuous dimensions: (continuous, search_range, precision); For discrete " + "dimensions: (discrete, search_range, has_order); For grid dimensions: (grid, grid_list). More details " + "can be found in zoopt package." + ) + ) + + points_to_evaluate: list[dict] | None = points_to_evaluate_field() + + parallel_num: int = schema_utils.PositiveInteger( + default=1, + description=( + "How many workers to parallel. Note that initial phase may start less workers than this number. More " + "details can be found in zoopt package." + ), + ) diff --git a/ludwig/schema/hyperopt/utils.py b/ludwig/schema/hyperopt/utils.py new file mode 100644 index 0000000..07dedd8 --- /dev/null +++ b/ludwig/schema/hyperopt/utils.py @@ -0,0 +1,170 @@ +from collections.abc import Callable + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.registry import Registry + +parameter_config_registry = Registry() +scheduler_config_registry = Registry() +scheduler_dependencies_registry = Registry() +search_algorithm_config_registry = Registry() +search_algorithm_dependencies_registry = Registry() +search_algorithm_random_state_field_registry = Registry() + + +@DeveloperAPI +def get_parameter_cls(name: str) -> type["BaseParameterConfig"]: # noqa: F821 + """Get a registered hyperopt parameter config class by name. + + Args: + name: the name of a parameter config class registered in `ludwig.schema.hyperopt.parameter` + + Returns: + A parameter config class from `ludwig.schema.hyperopt.parameter` + """ + return parameter_config_registry[name] + + +@DeveloperAPI +def get_scheduler_cls(name: str) -> type["BaseSchedulerConfig"]: # noqa: F821 + """Get a registered hyperopt scheduler config class by name. + + Args: + name: the name of a scheduler config class registered in `ludwig.schema.hyperopt.scheduler` + + Returns: + A scheduler config class from `ludwig.schema.hyperopt.scheduler` + """ + return search_algorithm_config_registry[name] + + +@DeveloperAPI +def get_scheduler_dependencies(name: str) -> list[str]: + """Get the list of dependencies for a registered hyperopt scheduler. + + Args: + name: the name of a scheduler config class registered in `ludwig.schema.hyperopt.scheduler` + + Returns: + The list of imports needed to use the scheduler + """ + return scheduler_dependencies_registry[name] + + +@DeveloperAPI +def get_search_algorithm_cls(name: str) -> type["BaseSearchAlgorithmConfig"]: # noqa: F821 + """Get a registered hyperopt search algorithm config class by name. + + Args: + name: the name of a search algorithm config class registered in `ludwig.schema.hyperopt.search_algorithm` + + Returns: + A scheduler config class from `ludwig.schema.hyperopt.search_algorithm` + """ + return search_algorithm_config_registry[name] + + +@DeveloperAPI +def get_search_algorithm_dependencies(name: str) -> list[str]: + """Get the list of dependencies for a registered hyperopt search algorithm. + + Args: + name: the name of a search algorithm config class registered in `ludwig.schema.hyperopt.search_algorithm` + + Returns: + The list of imports needed to use the search algorithm + """ + return search_algorithm_dependencies_registry[name] + + +@DeveloperAPI +def get_search_algorithm_random_state_field(name: str): + """Get the field name of the random state for a registered hyperopt search algorithm. + + Args: + name: the name of a search algorithm config class registered in `ludwig.schema.hyperopt.search_algorithm` + + Returns: + The name of the random state field in the config + """ + return search_algorithm_random_state_field_registry[name] + + +@DeveloperAPI +def register_parameter_config(name: str) -> Callable: + """Register a parameter config class by name. + + Args: + name: the name to register the parameter class under, does not need to correspond to the value of `space` + + Returns: + Wrapper function to decorate a `BaseParameterConfig` subclass + """ + + def wrap(cls: type["BaseParameterConfig"]) -> type["BaseParameterConfig"]: # noqa: F821 + """Add a parameter config class to the registry. + + Args: + cls: a subclass of `BaseParameterConfig` + + Returns: + `cls` unaltered + """ + parameter_config_registry[name] = cls + return cls + + return wrap + + +@DeveloperAPI +def register_scheduler_config(name: str, dependencies: list[tuple[str]] | None = None): + """Register a scheduler config class by name. + + Args: + name: the name to scheduler the parameter class under, does not need to correspond to the value of `type` + dependencies: the list of scheduler dependency package name/install name pairs, e.g. + `("sklearn", "scikit-learn")` + + Returns: + Wrapper function to decorate a `BaseSchedulerConfig` subclass + """ + + def wrap(scheduler_config: type["BaseSchedulerConfig"]) -> type["BaseSchedulerConfig"]: # noqa: F821 + """Add a parameter config class to the registry. + + Args: + cls: a subclass of `BaseParameterConfig` + + Returns: + `cls` unaltered + """ + scheduler_config_registry[name] = scheduler_config + scheduler_dependencies_registry[name] = dependencies if dependencies is not None else [] + return scheduler_config + + return wrap + + +# TODO: create a search alg metadata class to register in place of individual metadata args +@DeveloperAPI +def register_search_algorithm_config( + name: str, random_state_field: str | None = None, dependencies: list[tuple[str, str]] | None = None +) -> Callable: + """Register a search algorithm config class by name. + + Args: + name: the name to register the search algorithm class under, does not need to correspond to the value of `type` + random_state_field: the name of the random state in this search algorithm + dependencies: the list of search algorithm dependency package name/install name pairs, e.g. + `("sklearn", "scikit-learn")` + + Returns: + Wrapper function to decorate a `BaseSearchAlgorithmConfig` subclass + """ + + def wrap(cls: type["BaseSearchAlgorithmConfig"]) -> type["BaseSearchAlgorithmConfig"]: # noqa: F821 + search_algorithm_config_registry[name] = cls + search_algorithm_dependencies_registry[name] = dependencies if dependencies is not None else [] + search_algorithm_random_state_field_registry[name] = random_state_field + return cls + + return wrap diff --git a/ludwig/schema/jsonschema.py b/ludwig/schema/jsonschema.py new file mode 100644 index 0000000..f397378 --- /dev/null +++ b/ludwig/schema/jsonschema.py @@ -0,0 +1,35 @@ +"""JSON Schema generation for Ludwig config classes. + +Uses pydantic's model_json_schema() under the hood, replacing the previous marshmallow-based converter. +""" + + +def marshmallow_schema_to_jsonschema_dict(schema_instance): + """Backward-compatible JSON schema generation. + + Previously converted marshmallow schemas. Now uses pydantic's model_json_schema(). + The schema_instance can be either: + - A pydantic model class (LudwigBaseConfig subclass) + - Legacy: called with a marshmallow Schema instance (raises helpful error) + """ + from ludwig.schema.utils import LudwigBaseConfig + + if isinstance(schema_instance, type) and issubclass(schema_instance, LudwigBaseConfig): + cls = schema_instance + elif isinstance(schema_instance, LudwigBaseConfig): + cls = type(schema_instance) + else: + raise TypeError( + f"Expected a Ludwig config class, got {type(schema_instance)}. " + "Marshmallow schemas are no longer supported. Use pydantic BaseModel subclasses." + ) + + schema_dict = cls.model_json_schema() + name = cls.__name__ + + # Wrap in definitions format for backward compat + return { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": {name: schema_dict}, + "$ref": f"#/definitions/{name}", + } diff --git a/ludwig/schema/llms/__init__.py b/ludwig/schema/llms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/schema/llms/base_model.py b/ludwig/schema/llms/base_model.py new file mode 100644 index 0000000..5ff177e --- /dev/null +++ b/ludwig/schema/llms/base_model.py @@ -0,0 +1,158 @@ +import logging +import os +from dataclasses import field + +from transformers import AutoConfig + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BASE_MODEL +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import LLM_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json + +logger = logging.getLogger(__name__) + +# Maps a preset LLM name to the full slash-delimited HF path. If the user chooses a preset LLM, the preset LLM name is +# replaced with the full slash-delimited HF path using this map, after JSON validation but before config object +# initialization. +MODEL_PRESETS = { + # Bloom + "bloomz-3b": "bigscience/bloomz-3b", + "bloomz-7b1": "bigscience/bloomz-7b1", + # CodeLlama + "codellama-7b": "codellama/CodeLlama-7b-hf", + "codellama-13b": "codellama/CodeLlama-13b-hf", + "codellama-34b": "codellama/CodeLlama-34b-hf", + "codellama-7b-instruct": "codellama/CodeLlama-7b-instruct-hf", + "codellama-13b-instruct": "codellama/CodeLlama-13b-instruct-hf", + "codellama-34b-instruct": "codellama/CodeLlama-34b-instruct-hf", + # GPT Neo and GPT J + "gpt-neo-2.7B": "EleutherAI/gpt-neo-2.7B", + "gpt-j-6b": "EleutherAI/gpt-j-6b", + # LLama-2 + "llama-2-7b": "meta-llama/Llama-2-7b-hf", + "llama-2-13b": "meta-llama/Llama-2-13b-hf", + "llama-2-70b": "meta-llama/Llama-2-70b-hf", + "llama-2-7b-chat": "meta-llama/Llama-2-7b-chat-hf", + "llama-2-13b-chat": "meta-llama/Llama-2-13b-chat-hf", + "llama-2-70b-chat": "meta-llama/Llama-2-70b-chat-hf", + # Mistral + "mistral-7b": "mistralai/Mistral-7B-v0.1", + "mistral-7b-instruct": "mistralai/Mistral-7B-Instruct-v0.1", + # Mixtral + "mixtral-8x7b": "mistralai/Mixtral-8x7B-v0.1", + "mixtral-8x7b-instruct": "mistralai/Mixtral-8x7B-Instruct-v0.1", + # OPT + "opt-350m": "facebook/opt-350m", + "opt-1.3b": "facebook/opt-1.3b", + "opt-6.7b": "facebook/opt-6.7b", + # Pythia + "pythia-2.8b": "EleutherAI/pythia-2.8b", + "pythia-12b": "EleutherAI/pythia-12b", + # Vicuna + "vicuna-7b": "lmsys/vicuna-7b-v1.3", + "vicuna-13b": "lmsys/vicuna-13b-v1.3", + # Zephyr + "zephyr-7b-alpha": "HuggingFaceH4/zephyr-7b-alpha", + "zephyr-7b-beta": "HuggingFaceH4/zephyr-7b-beta", + # Phi + "phi-1": "microsoft/phi-1", + "phi-1_5": "microsoft/phi-1_5", + "phi-2": "microsoft/phi-2", +} + + +@DeveloperAPI +def BaseModelDataclassField(): + description = ( + "Base pretrained model to use. This can be one of the presets defined by Ludwig, a fully qualified " + "name of a pretrained model from the HuggingFace Hub, or a path to a directory containing a " + "pretrained model." + ) + + def validate(model_name: str): + """Validates and upgrades the given model name to its full path, if applicable. + + If the name exists in `MODEL_PRESETS`, returns the corresponding value from the dict; otherwise checks if the + given name (which should be a full path) exists locally or in the transformers library. + """ + if isinstance(model_name, str): + if model_name in MODEL_PRESETS: + return MODEL_PRESETS[model_name] + if os.path.isdir(model_name): + return model_name + try: + AutoConfig.from_pretrained(model_name, trust_remote_code=False) + return model_name + except ValueError: + # Model requires custom code (trust_remote_code). Allow it through validation + # since the user must explicitly set trust_remote_code: true in their config + # for it to actually load at training time. + logger.warning( + f"Model `{model_name}` uses custom code on HuggingFace. Make sure to set " + f"`trust_remote_code: true` in your config to use this model." + ) + return model_name + except OSError: + raise ConfigValidationError( + f"Specified base model `{model_name}` could not be loaded. If this is a private repository, make " + f"sure to set HUGGING_FACE_HUB_TOKEN in your environment. Check that {model_name} is a valid " + "pretrained CausalLM listed on huggingface or a valid local directory containing the weights for a " + "pretrained CausalLM from huggingface. See: " + "https://huggingface.co/models?pipeline_tag=text-generation&sort=downloads for a full list." + ) + raise ConfigValidationError( + f"`base_model` should be a string, instead given: {model_name}. This can be a preset or any pretrained " + "CausalLM on huggingface. See: https://huggingface.co/models?pipeline_tag=text-generation&sort=downloads" + ) + + class BaseModelField(schema_utils.SchemaField): + def _serialize(self, value, attr, obj, **kwargs): + if isinstance(value, str): + return value + raise ConfigValidationError(f"Value to serialize is not a string: {value}") + + def _deserialize(self, value, attr, obj, **kwargs): + return validate(value) + + def _jsonschema_type_mapping(self): + return { + "anyOf": [ + { + "type": "string", + "enum": list(MODEL_PRESETS.keys()), + "description": ( + "Pick from a set of popular LLMs of different sizes across a variety of architecture types." + ), + "title": "preset", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA[BASE_MODEL]["_anyOf"]["preset"]), + }, + { + "type": "string", + "description": "Enter the full path to a huggingface LLM.", + "title": "custom", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA[BASE_MODEL]["_anyOf"]["custom"]), + }, + ], + "description": description, + "title": "base_model_options", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA[BASE_MODEL]["_meta"]), + } + + return field( + metadata={ + "marshmallow_field": BaseModelField( + required=True, + allow_none=False, + validate=validate, + metadata={ # TODO: extra metadata dict probably unnecessary, but currently a widespread pattern + "description": description, + "parameter_metadata": convert_metadata_to_json(LLM_METADATA[BASE_MODEL]["_meta"]), + }, + ), + }, + # TODO: This is an unfortunate side-effect of dataclass init order - you cannot have non-default fields follow + # default fields, so we have to give `base_model` a fake default of `None`. + default=None, + ) diff --git a/ludwig/schema/llms/generation.py b/ludwig/schema/llms/generation.py new file mode 100644 index 0000000..2fa74cf --- /dev/null +++ b/ludwig/schema/llms/generation.py @@ -0,0 +1,334 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import LLM_METADATA + + +@DeveloperAPI +class LLMGenerationConfig(schema_utils.LudwigBaseConfig): + """Parameters for LLM Generation Config. + + Should match the parameters in + https://huggingface.co/docs/transformers/v4.28.0/en/main_classes/text_generation#transformers.GenerationConfig + """ + + # Parameters that control the length of the output + + max_new_tokens: int | None = schema_utils.PositiveInteger( + default=32, + allow_none=True, + description="The maximum number of new tokens to generate, ignoring the number of tokens in the input prompt. " + "If not set, this is dynamically determined by Ludwig based on either the `max_sequence_length` of the ouput " + "feature, the global_max_sequence_length specified in preprocessing (if specified), or the " + "maximum context length supported by the model (in the order specified).", + parameter_metadata=LLM_METADATA["generation"]["max_new_tokens"], + ) + + min_new_tokens: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="The minimum number of new tokens to generate, ignoring the number of tokens in the input prompt.", + parameter_metadata=LLM_METADATA["generation"]["min_new_tokens"], + ) + + max_length: int = schema_utils.PositiveInteger( + default=32, + allow_none=True, + description="The maximum length the generated tokens can have. Corresponds to the length of the input prompt " + "+ max_new_tokens. Its effect is overridden by max_new_tokens, if also set.", + parameter_metadata=LLM_METADATA["generation"]["max_length"], + ) + + min_length: int = schema_utils.NonNegativeInteger( + default=0, + allow_none=True, + description="The minimum length of the sequence to be generated. Corresponds to the length of the " + "input prompt + min_new_tokens. Its effect is overridden by min_new_tokens, if also set.", + parameter_metadata=LLM_METADATA["generation"]["min_length"], + ) + + early_stopping: bool | str | None = schema_utils.Boolean( + default=False, + description="Controls the stopping condition for beam-based methods, like beam-search. It accepts the following" + " values: True, where the generation stops as soon as there are num_beams complete candidates; False, where an " + "heuristic is applied and the generation stops when is it very unlikely to find better candidates; `never`, " + "where the beam search procedure only stops when there cannot be better candidates (canonical beam search " + "algorithm)", + ) + + max_time: float | None = schema_utils.FloatRange( + default=None, + min=None, + max=None, + allow_none=True, + description="The maximum amount of time you allow the computation to run for in seconds. generation will still" + " finish the current pass after allocated time has been passed. ", + ) + + # Parameters that control the generation strategy used + + do_sample: bool | None = schema_utils.Boolean( + default=True, + description="Whether or not to use sampling ; use greedy decoding otherwise.", + parameter_metadata=LLM_METADATA["generation"]["do_sample"], + ) + + num_beams: int | None = schema_utils.PositiveInteger( + default=1, + allow_none=True, + description="Number of beams for beam search. 1 means no beam search and is the default value." + " The beam search strategy generates the translation word by word from left-to-right while keeping a fixed" + " number (beam) of active candidates at each time step during token generation. By increasing the beam size," + " the translation performance can increase at the expense of significantly reducing the decoder speed.", + parameter_metadata=LLM_METADATA["generation"]["num_beams"], + ) + + num_beam_groups: int | None = schema_utils.PositiveInteger( + default=1, + allow_none=True, + description="Number of groups to divide num_beams into in order to ensure diversity among different groups of " + "beams. 1 means no group beam search.", + ) + + penalty_alpha: float | None = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="The values balance the model confidence and the degeneration penalty in contrastive " + " search decoding.", + ) + + use_cache: bool | None = schema_utils.Boolean( + default=True, + description="Whether or not the model should use the past last key/values attentions (if applicable to the " + "model) to speed up decoding.", + parameter_metadata=LLM_METADATA["generation"]["use_cache"], + ) + + prompt_lookup_num_tokens: int | None = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="The number of tokens to consider as a candidate from the prompt for prompt lookup decoding, " + " an alternate way of performing assisted generation. If set to 0, the prompt lookup decoding is not used.", + parameter_metadata=LLM_METADATA["generation"]["prompt_lookup_num_tokens"], + ) + + # Parameters for manipulation of the model output logits + + temperature: float | None = schema_utils.NonNegativeFloat( + default=0.1, + allow_none=True, + description="Temperature is used to control the randomness of predictions." + " A high temperature value (closer to 1) makes the output more diverse and random, while a lower temperature" + " (closer to 0) makes the model's responses more deterministic and focused on the most likely outcome." + " In other words, temperature adjusts the probability distribution from which the model picks the next token.", + parameter_metadata=LLM_METADATA["generation"]["temperature"], + ) + + top_k: int | None = schema_utils.PositiveInteger( + default=50, + allow_none=True, + description="The number of highest probability vocabulary tokens to keep for top-k-filtering.", + parameter_metadata=LLM_METADATA["generation"]["top_k"], + ) + + top_p: float | None = schema_utils.FloatRange( + default=1.0, + min=0.0, + max=1.0, + allow_none=True, + description="If set to float < 1, only the most probable tokens with probabilities that add up to " + "top_p or higher are kept for generation.", + parameter_metadata=LLM_METADATA["generation"]["top_p"], + ) + + typical_p: float | None = schema_utils.FloatRange( + default=1.0, + min=0.0, + max=1.0, + allow_none=True, + description="Local typicality measures how similar the conditional probability of predicting a target token " + "next is to the expected conditional probability of predicting a random token next, given the partial text " + "already generated. If set to float < 1, the smallest set of the most locally typical tokens with " + "probabilities that add up to typical_p or higher are kept for generation.", + ) + + epsilon_cutoff: float | None = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=1.0, + allow_none=True, + description="If set to float strictly between 0 and 1, only tokens with a conditional probability greater " + "than epsilon_cutoff will be sampled. In the paper, suggested values range from 3e-4 to 9e-4, depending on the" + " size of the model.", + ) + + eta_cutoff: float | None = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=1.0, + allow_none=True, + description="Eta sampling is a hybrid of locally typical sampling and epsilon sampling. If set to float " + "strictly between 0 and 1, a token is only considered if it is greater than either eta_cutoff or " + "sqrt(eta_cutoff) * exp(-entropy(softmax(next_token_logits))). The latter term is intuitively the expected next" + " token probability, scaled by sqrt(eta_cutoff). In the paper, suggested values range from 3e-4 to 2e-3, " + "depending on the size of the model.", + ) + + diversity_penalty: float | None = schema_utils.NonNegativeFloat( + default=0.0, + allow_none=True, + description="The value used to control the diversity of the generated text. The higher the value, the more " + "diverse the text will be. If set to 0, no diversity is enforced." + "This value is subtracted from a beam(s) score if it generates a token same as any beam from other group at a" + "particular time. Note that diversity_penalty is only effective if group beam search is enabled.", + ) + + repetition_penalty: float | None = schema_utils.NonNegativeFloat( + default=1.0, + allow_none=True, + description="The parameter for repetition penalty. 1.0 means no penalty. " + "See [this paper](https://arxiv.org/pdf/1909.05858.pdf) for more details.", + ) + + encoder_repetition_penalty: float | None = schema_utils.NonNegativeFloat( + default=1.0, + allow_none=True, + description="The paramater for encoder_repetition_penalty. An exponential penalty on sequences that are not" + " in the original input. 1.0 means no penalty.", + ) + + length_penalty: float | None = schema_utils.Float( + default=1.0, + allow_none=True, + description="Exponential penalty to the length that is used with beam-based generation. It is applied as an " + "exponent to the sequence length, which in turn is used to divide the score of the sequence. Since the score is" + " the log likelihood of the sequence (i.e. negative), length_penalty > 0.0 promotes longer sequences, while " + "length_penalty < 0.0 encourages shorter sequences.", + ) + + no_repeat_ngram_size: int | None = schema_utils.NonNegativeInteger( + default=0, + allow_none=True, + description="If set to int > 0, all ngrams of that size can only occur once.", + ) + + bad_words_ids: list[list[int]] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of token ids that are not allowed to be generated. In order to get the tokens of the words " + "that should not appear in the generated text, use tokenizer(bad_word, add_prefix_space=True).input_ids.", + ) + + force_words_ids: list[list[int]] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of token ids that are forced to be generated by the model. In order to get the tokens of the" + " words that should appear in the generated text, use tokenizer(force_word, add_prefix_space=True).input_ids.", + ) + + renormalize_logits: bool | None = schema_utils.Boolean( + default=False, + description="Whether to renormalize the logits after temperature and top_k/top_p filtering.", + ) + + # TODO(This needs to be defined based on the Constraint class) + # constraints: + + forced_bos_token_id: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description="The id of the token to force as the first generated token after the decoder_start_token_id." + "Useful for multilingual models like mBART where the first generated token needs to be the target language" + "token.", + ) + + forced_eos_token_id: int | list[int] | None = schema_utils.Integer( + default=None, + allow_none=True, + description="The id of the token to force as the last generated token when max_length is reached. Optionally, " + "use a list to set multiple end-of-sequence tokens.", + ) + + remove_invalid_values: bool | None = schema_utils.Boolean( + default=False, + description="Whether to remove possible nan and inf outputs of the model to prevent the generation method to " + "crash. Note that using remove_invalid_values can slow down generation.", + ) + + exponential_decay_length_penalty: tuple[int, float] | None = schema_utils.FloatRange( + default=None, + min=0.0, + max=1.0, + allow_none=True, + description="This Tuple adds an exponentially increasing length penalty, after a certain amount of tokens have " + "been generated. The tuple shall consist of: (start_index, decay_factor) where start_index indicates where " + "penalty starts and decay_factor represents the factor of exponential decay", + ) + + suppress_tokens: list[int] | None = schema_utils.List( + list_type=int, + default=None, + allow_none=True, + description="A list of tokens that will be suppressed at generation. The SupressTokens logit processor will set" + " their log probs to -inf so that they are not sampled.", + ) + + begin_suppress_tokens: list[int] | None = schema_utils.List( + list_type=int, + default=None, + allow_none=True, + description="A list of tokens that will be suppressed at the beginning of the generation. The " + "SupressBeginTokens logit processor will set their log probs to -inf so that they are not sampled.", + ) + + forced_decoder_ids: list[list[int]] | None = schema_utils.List( + default=None, + allow_none=True, + description="A list of forced decoder ids. The ForcedDecoderIds logit processor will set the log probs of all " + "tokens that are not in the list to -inf so that they are not sampled.", + ) + + sequence_bias: dict[tuple[int], float] | None = schema_utils.Dict( + default=None, + allow_none=True, + description="A dictionary of token ids to bias the generation towards. The SequenceBias logit processor will " + "add the bias to the log probs of the tokens in the dictionary. Positive biases increase the odds of the " + "sequence being selected, while negative biases do the opposite. ", + ) + + guidance_scale: float | None = schema_utils.FloatRange( + default=None, + min=0.0, + allow_none=True, + description="The guidance scale for classifier free guidance (CFG). CFG is enabled by setting guidance_scale >" + " 1. Higher guidance scale encourages the model to generate samples that are more closely linked to the input" + " prompt, usually at the expense of poorer quality.", + ) + + # Special tokens that can be used at generation time + + pad_token_id: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description="The id of the padding token. If not set, the padding token id of the tokenizer is used.", + ) + + bos_token_id: int | None = schema_utils.Integer( + default=None, + allow_none=True, + description="The id of the beginning of sentence token. If not set, the bos token id of the tokenizer is used.", + ) + + eos_token_id: int | list[int] | None = schema_utils.Integer( + default=None, + allow_none=True, + description="The id of the end of sentence token. If not set, the eos token id of the tokenizer is used.", + ) + + +@DeveloperAPI +class LLMGenerationConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(LLMGenerationConfig) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(LLMGenerationConfig) diff --git a/ludwig/schema/llms/model_parameters.py b/ludwig/schema/llms/model_parameters.py new file mode 100644 index 0000000..f634c96 --- /dev/null +++ b/ludwig/schema/llms/model_parameters.py @@ -0,0 +1,93 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils + + +@DeveloperAPI +class RoPEScalingConfig(schema_utils.LudwigBaseConfig): + """Dynamic RoPE-scaling (rotary position embeddings) to extend the context length of LLM like LLaMA, GPT-NeoX, + or Falcon. + + This parameter is a dictionary containing the scaling configuration for the RoPE embeddings. Currently supports + three scaling strategies: linear and dynamic. Their scaling factor must be an float greater than 1. The expected + format is {'rope_type': strategy name, 'factor': scaling factor} + """ + + def __post_init__(self): + # Both parameters must be set, or none. + if not self.rope_type: + raise ConfigValidationError( + f"`rope_scaling`'s `rope_type` field must be one of ['linear', 'dynamic'], got {self.rope_type}" + ) + + if not self.factor: + raise ConfigValidationError( + f"When using `rope_scaling`, `factor` must be specified and be > 1. Got {self.factor}." + ) + + rope_type: str | None = schema_utils.StringOptions( + options=["linear", "dynamic"], + default=None, + allow_none=True, + description="Currently supports two strategies: linear and dynamic scaling.", + ) + + factor: float | None = schema_utils.FloatRange( + default=None, + allow_none=True, + min=1.0, + min_inclusive=False, + description="The scaling factor for RoPE embeddings.", + ) + + +@DeveloperAPI +class RoPEScalingConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(RoPEScalingConfig, default_missing=True) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(RoPEScalingConfig, title="rope_scaling") + + +@DeveloperAPI +class ModelParametersConfig(schema_utils.LudwigBaseConfig): + rope_scaling: RoPEScalingConfig = RoPEScalingConfigField().get_default_field() + + neftune_noise_alpha: int | None = schema_utils.IntegerRange( + default=0, + min=0, + allow_none=True, + description="The alpha parameter for the embedding noise, which controls the amount of noise added to the " + "embeddings. The higher the value, the more noise is added. This is based on the paper NEFTune: Noisy " + "Embeddings Improve Instruction Finetuning. Paper: https://arxiv.org/pdf/2310.05914.pdf. Default: 0." + "Suggested values: 5, 10", + ) + + def to_dict(self): + config = {} + if self.rope_scaling: + config["rope_scaling"] = self.rope_scaling.to_dict() + if self.neftune_noise_alpha: + config["neftune_noise_alpha"] = self.neftune_noise_alpha + return config + + +@DeveloperAPI +class ModelParametersConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(ModelParametersConfig, default_missing=True) + + def _jsonschema_type_mapping(self): + return { + "oneOf": [ + {"type": "null", "title": "disabled", "description": "Skip configurable model parameters."}, + { + **schema_utils.unload_jsonschema_from_config_class(ModelParametersConfig), + "title": "enabled", + "description": "Set model parameters options.", + }, + ], + "title": "Model Parameters", + "description": "Configurable model parameters for LLMs.", + } diff --git a/ludwig/schema/llms/peft.py b/ludwig/schema/llms/peft.py new file mode 100644 index 0000000..b2f001b --- /dev/null +++ b/ludwig/schema/llms/peft.py @@ -0,0 +1,1532 @@ +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import LLM_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json +from ludwig.utils.registry import Registry + +if TYPE_CHECKING: + from peft import PeftConfig + + +adapter_registry = Registry() + + +@DeveloperAPI +def register_adapter(name: str): + def wrap(config: BaseAdapterConfig): + adapter_registry[name] = config + return config + + return wrap + + +@DeveloperAPI +class LoraPostprocessorConfig(schema_utils.LudwigBaseConfig): + """This Dataclass is a schema for the nested postprocessing config under adapter of type "lora".""" + + merge_adapter_into_base_model: bool = schema_utils.Boolean( + default=False, + description="""Instructs whether or not the fine-tuned LoRA weights are to be merged into the base LLM model so +that the complete fine-tuned model is available to be used and/or persisted, and then reused upon loading as a single +model (rather than having to load base and fine-tuned models separately).""", + ) + progressbar: bool = schema_utils.Boolean( + default=False, + description="Instructs whether or not to show a progress bar indicating the unload and merge process.", + ) + + +@DeveloperAPI +class LoraPostprocessorConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(LoraPostprocessorConfig) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(LoraPostprocessorConfig, title="LoraPostprocessor") + + +@DeveloperAPI +class BaseAdapterConfig(schema_utils.LudwigBaseConfig, ABC): + type: str + + pretrained_adapter_weights: str | None = schema_utils.String( + default=None, description="Path to pretrained weights.", allow_none=True + ) + + postprocessor: LoraPostprocessorConfig = LoraPostprocessorConfigField().get_default_field() + + @abstractmethod + def to_config(self, **kwargs) -> "PeftConfig": + pass + + +@DeveloperAPI +class EvaSubConfig(schema_utils.LudwigBaseConfig): + """Configuration for EVA (Explained Variance Adaptation) LoRA initialization. + + EVA initializes LoRA based on the SVD of layer input activations, achieving state-of-the-art + performance by adapting the adapter directions to the actual data distribution. + Paper: https://arxiv.org/abs/2410.07170 + """ + + rho: float = schema_utils.NonNegativeFloat( + default=2.0, + description="Scaling factor for EVA. Controls how strongly activations influence the initialization.", + ) + tau: float = schema_utils.FloatRange( + default=0.99, + min=0.0, + max=1.0, + description="Momentum for running statistics in EVA.", + ) + use_label_mask: bool = schema_utils.Boolean( + default=True, + description="Whether to mask padding/ignore tokens when computing activation statistics.", + ) + label_mask_value: int = schema_utils.Integer( + default=-100, + description="Token id to mask out (usually the ignore_index for cross-entropy loss).", + ) + whiten: bool = schema_utils.Boolean( + default=False, + description="Whether to whiten activations before computing SVD.", + ) + adjust_scaling_factors: bool = schema_utils.Boolean( + default=True, + description="Adjust LoRA scaling factors after EVA initialization to preserve pre-trained model outputs.", + ) + + +@DeveloperAPI +class EvaSubConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(EvaSubConfig, allow_none=True, default_missing=True) + + def _jsonschema_type_mapping(self): + inner = schema_utils.unload_jsonschema_from_config_class(EvaSubConfig, title="EvaConfig") + return {"oneOf": [inner, {"type": "null"}]} + + +@DeveloperAPI +class LoftQSubConfig(schema_utils.LudwigBaseConfig): + """Configuration for LoftQ quantization-aware LoRA initialization. + + LoftQ simultaneously quantizes the backbone weights and initializes LoRA adapters + to minimize the quantization error. Requires `init_lora_weights='loftq'`. + Paper: https://arxiv.org/abs/2310.08659 + """ + + loftq_bits: int = schema_utils.IntegerOptions( + options=[2, 4, 8], + default=4, + description="Number of bits for LoftQ quantization (2, 4, or 8).", + ) + loftq_iter: int = schema_utils.PositiveInteger( + default=1, + description="Number of LoftQ iterations. More iterations improve approximation quality.", + ) + + +@DeveloperAPI +class LoftQSubConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(LoftQSubConfig, allow_none=True, default_missing=True) + + def _jsonschema_type_mapping(self): + inner = schema_utils.unload_jsonschema_from_config_class(LoftQSubConfig, title="LoftQConfig") + return {"oneOf": [inner, {"type": "null"}]} + + +@DeveloperAPI +@register_adapter(name="lora") +class LoraConfig(BaseAdapterConfig): + def __post_init__(self): + if self.alpha is None: + self.alpha = self.r * 2 + if self.init_lora_weights == "loftq" and self.loftq_config is None: + raise ConfigValidationError( + "`loftq_config` must be set when `init_lora_weights` is 'loftq'. " + "Example: loftq_config: {loftq_bits: 4, loftq_iter: 1}" + ) + if self.init_lora_weights == "eva" and self.eva_config is None: + raise ConfigValidationError( + "`eva_config` must be set when `init_lora_weights` is 'eva'. Example: eva_config: {rho: 2.0}" + ) + + type: str = schema_utils.ProtectedString( + "lora", + description=LLM_METADATA["adapter"]["lora"]["type"].long_description, + ) + + r: int = schema_utils.PositiveInteger( + default=8, + description="Lora attention dimension.", + parameter_metadata=LLM_METADATA["adapter"]["lora"]["r"], + ) + + alpha: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The alpha parameter for Lora scaling. Defaults to `2 * r`.", + parameter_metadata=LLM_METADATA["adapter"]["lora"]["alpha"], + ) + + dropout: float = schema_utils.NonNegativeFloat( + default=0.05, + description="The dropout probability for Lora layers.", + parameter_metadata=LLM_METADATA["adapter"]["lora"]["dropout"], + ) + + # TODO(travis): figure out why calling this `bias` doesn't work + bias_type: str = schema_utils.StringOptions( + options=["none", "all", "lora_only"], + default="none", + description="Bias type for Lora.", + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description=( + "List of module names or regex expression of the module names to replace with LoRA. " + "For example, ['q', 'v'] or '.*decoder.*(SelfAttention|EncDecAttention).*(q|v)$'. " + "Defaults to targeting the query and value matrices of all self-attention and encoder-decoder attention " + "layers." + ), + parameter_metadata=LLM_METADATA["adapter"]["lora"]["target_modules"], + ) + + use_rslora: bool = schema_utils.Boolean( + default=False, + description=( + "When set to True, uses Rank-Stabilized LoRA which sets the adapter scaling factor to " + "lora_alpha/math.sqrt(r), since it was proven to work better. Otherwise, it will use the original " + "default value of lora_alpha/r. Paper: https://arxiv.org/abs/2312.03732." + ), + parameter_metadata=LLM_METADATA["adapter"]["lora"]["use_rslora"], + ) + + use_dora: bool = schema_utils.Boolean( + default=False, + description=( + "Enable 'Weight-Decomposed Low-Rank Adaptation' (DoRA). This technique decomposes the updates of the " + "weights into two parts, magnitude and direction. Direction is handled by normal LoRA, whereas the " + "magnitude is handled by a separate learnable parameter. This can improve the performance of LoRA, " + "especially at low ranks. Right now, DoRA only supports non-quantized linear layers. DoRA introduces a " + "bigger overhead than pure LoRA, so it is recommended to merge weights for inference. For more " + "information, see https://arxiv.org/abs/2402.09353" + ), + parameter_metadata=LLM_METADATA["adapter"]["lora"]["use_dora"], + ) + + loraplus_lr_ratio: float | None = schema_utils.Float( + default=None, + allow_none=True, + description=( + "LoRA+ learning rate ratio (Hayou et al., ICML 2024). When set, the B matrices use " + "lr * loraplus_lr_ratio while A matrices use the base lr. Typical values: 2-16. " + "Provides 1-2%% accuracy gain and up to 2x speedup over standard LoRA. " + "Paper: https://arxiv.org/abs/2402.12354" + ), + ) + + init_lora_weights: str | bool = schema_utils.StringOptions( + options=["default", "gaussian", "eva", "olora", "pissa", "corda", "loftq", "orthogonal"], + default="default", + allow_none=False, + description=( + "Initialization strategy for LoRA weight matrices. " + "'default' uses the standard Kaiming uniform init (A) and zeros (B). " + "'gaussian' uses Gaussian init for A. " + "'pissa' (Principal Singular values and Singular vectors Adaptation) initializes using SVD of the " + "pretrained weight, converging faster and often outperforming standard LoRA. " + "Paper: https://arxiv.org/abs/2404.02948. " + "'eva' (Explained Variance Adaptation) initializes from the SVD of layer input activations — " + "requires `eva_config` to be set. Paper: https://arxiv.org/abs/2410.07170. " + "'corda' (Context-Oriented Decomposition Adaptation) combines PiSSA and full fine-tuning signals, " + "converging faster than PiSSA. Paper: https://arxiv.org/abs/2406.05223. " + "'olora' (Orthonormal LoRA) uses QR decomposition for better conditioning. " + "'loftq' (LoftQ) jointly quantizes base weights and initializes LoRA — " + "requires `loftq_config` to be set. Paper: https://arxiv.org/abs/2310.08659. " + "'orthogonal' uses orthogonal initialization." + ), + ) + + eva_config: EvaSubConfig | None = EvaSubConfigField().get_default_field() + + loftq_config: LoftQSubConfig | None = LoftQSubConfigField().get_default_field() + + rank_pattern: dict | None = schema_utils.Dict( + default=None, + allow_none=True, + description=( + "Per-layer rank overrides as a mapping of layer name (or regex) to rank integer. " + "Overrides the global `r` for matched layers. Useful for LoRA-XS style configurations " + "where different layers benefit from different ranks. " + "Example: {'model.layers.0.self_attn.q_proj': 4, 'model.layers.0.self_attn.v_proj': 2}" + ), + ) + + alpha_pattern: dict | None = schema_utils.Dict( + default=None, + allow_none=True, + description=( + "Per-layer alpha (scaling) overrides as a mapping of layer name (or regex) to float. " + "Overrides the global `alpha` for matched layers." + ), + ) + + layer_replication: list | None = schema_utils.List( + default=None, + allow_none=True, + description=( + "Layer replication configuration as a list of [start, end] index pairs. Enables depth-wise " + "parameter efficiency by sharing LoRA weights across layer ranges. " + "Example: [[0, 4], [2, 5]] creates two overlapping groups." + ), + ) + + def to_config(self, task_type: str | None = None, **kwargs) -> "PeftConfig": + from peft import LoraConfig as _LoraConfig + + init_weights = self.init_lora_weights + if init_weights == "default": + init_weights = True + + eva_config = None + loftq_config = None + if init_weights == "eva" and self.eva_config is not None: + from peft import EvaConfig as _EvaConfig + + eva_config = _EvaConfig( + rho=self.eva_config.rho, + tau=self.eva_config.tau, + use_label_mask=self.eva_config.use_label_mask, + label_mask_value=self.eva_config.label_mask_value, + whiten=self.eva_config.whiten, + adjust_scaling_factors=self.eva_config.adjust_scaling_factors, + ) + elif init_weights == "loftq" and self.loftq_config is not None: + from peft import LoftQConfig as _LoftQConfig + + loftq_config = _LoftQConfig( + loftq_bits=self.loftq_config.loftq_bits, + loftq_iter=self.loftq_config.loftq_iter, + ) + + layer_replication = None + if self.layer_replication is not None: + layer_replication = [tuple(pair) for pair in self.layer_replication] + + return _LoraConfig( + r=self.r, + lora_alpha=self.alpha, + lora_dropout=self.dropout, + bias=self.bias_type, + target_modules=self.target_modules, + task_type=task_type, + use_rslora=self.use_rslora, + use_dora=self.use_dora, + init_lora_weights=init_weights, + eva_config=eva_config, + loftq_config=loftq_config, + rank_pattern=self.rank_pattern or {}, + alpha_pattern=self.alpha_pattern or {}, + layer_replication=layer_replication, + ) + + @classmethod + def name(cls) -> str: + return "LoRA" + + @classmethod + def description(cls) -> str: + return LLM_METADATA["adapter"]["lora"]["type"].long_description + + +@DeveloperAPI +class BasePromptLearningConfig(BaseAdapterConfig): + """Config for prompt learning adapters. Not meant to be used directly. + + Adapted from https://github.com/huggingface/peft/blob/main/src/peft/utils/config.py (PromptLearningConfig) + """ + + num_virtual_tokens: int = schema_utils.PositiveInteger( + default=8, + description="Number of virtual tokens to add to the prompt. Virtual tokens are used to control the behavior of " + " the model during inference. ", + parameter_metadata=LLM_METADATA["adapter"]["prompt_learning"]["num_virtual_tokens"], + ) + + token_dim: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The hidden embedding dimension of the base transformer model.", + ) + + num_transformer_submodules: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The number of transformer submodules in the base transformer model.", + ) + + num_attention_heads: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The number of attention heads in the base transformer model.", + ) + + num_layers: int | None = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="The number of layers in the base transformer model.", + ) + + +# TODO(travis): fix text generation when using prompt tuning: +# RuntimeError: shape '[-1, 17]' is invalid for input of size 9 +# @DeveloperAPI +# @register_adapter("prompt_tuning") +# class PromptTuningConfig(BasePromptLearningConfig): +# """Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/prompt_tuning.py.""" + +# def __post_init__(self): +# if self.prompt_tuning_init == "TEXT" and not self.prompt_tuning_init_text: +# raise ConfigValidationError( +# "Must provide `prompt_tuning_init_text` when `prompt_tuning_init` is set to `TEXT`." +# ) + +"""# type: str = schema_utils.ProtectedString("prompt_tuning")""" # Quotes allow mypy to run without syntax errors. + +# prompt_tuning_init: str = schema_utils.StringOptions( +# ["RANDOM", "TEXT"], +# default="RANDOM", +# description="The type of initialization to use for the prompt embedding. ", +# parameter_metadata=LLM_METADATA["adapter"]["prompt_tuning"]["prompt_tuning_init"], +# ) + +# prompt_tuning_init_text: str = schema_utils.String( +# default="", +# description="The text to use to initialize the prompt embedding.", +# parameter_metadata=LLM_METADATA["adapter"]["prompt_tuning"]["prompt_tuning_init_text"], +# ) + +# def to_config(self, **kwargs) -> "PeftConfig": +# from peft import PromptTuningConfig as _PromptTuningConfig + +# return _PromptTuningConfig( +# num_virtual_tokens=self.num_virtual_tokens, +# token_dim=self.token_dim, +# num_transformer_submodules=self.num_transformer_submodules, +# num_attention_heads=self.num_attention_heads, +# num_layers=self.num_layers, +# prompt_tuning_init=self.prompt_tuning_init, +# prompt_tuning_init_text=self.prompt_tuning_init_text, +# **kwargs +# ) + + +# TODO(travis): fix prefix tuning and p-tuning to work with distributed training +# @DeveloperAPI +# @register_adapter("prefix_tuning") +# class PrefixTuningConfig(BasePromptLearningConfig): +# """Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/prefix_tuning.py.""" + +"""# type: str = schema_utils.ProtectedString("prefix_tuning")""" # Quotes allow mypy to run without syntax errors. + +# encoder_hidden_size: Optional[int] = schema_utils.Integer( +# default=None, +# allow_none=True, +# description="The hidden embedding dimension of the prompt encoder.", +# ) + +# prefix_projection: bool = schema_utils.Boolean( +# default=False, +# description="Whether to use a projection layer in the prompt encoder to project the prefix tokens", +# ) + +# def to_config(self, task_type: str = None, **kwargs) -> "PeftConfig": +# from peft import PrefixTuningConfig as _PrefixTuningConfig + +# return _PrefixTuningConfig( +# num_virtual_tokens=self.num_virtual_tokens, +# token_dim=self.token_dim, +# num_transformer_submodules=self.num_transformer_submodules, +# num_attention_heads=self.num_attention_heads, +# num_layers=self.num_layers, +# encoder_hidden_size=self.encoder_hidden_size, +# prefix_projection=self.prefix_projection, +# task_type=task_type, +# ) + + +# @DeveloperAPI +# @register_adapter("p_tuning") +# class PTuningConfig(BasePromptLearningConfig): +"""# type: str = schema_utils.ProtectedString("p_tuning")""" # Quotes allow mypy to run without syntax errors. + +# encoder_reparameterization_type: str = schema_utils.StringOptions( +# ["MLP", "LSTM"], +# default="MLP", +# allow_none=False, +# description="The type of reparameterization to use for the prompt encoder.", +# ) + +# encoder_hidden_size: Optional[int] = schema_utils.PositiveInteger( +# default=None, +# allow_none=True, +# description="The hidden embedding dimension of the prompt encoder.", +# ) + +# encoder_num_layers: Optional[int] = schema_utils.PositiveInteger( +# default=2, +# allow_none=True, +# description="The number of layers in the prompt encoder.", +# ) + +# encoder_dropout: Optional[float] = schema_utils.FloatRange( +# default=0.0, +# min=0.0, +# max=1.0, +# description="The dropout probability for the prompt encoder.", +# ) + +# def to_config(self, task_type: str = None, **kwargs) -> "PeftConfig": +# from peft import PromptEncoderConfig as _PromptEncoderConfig + +# return _PromptEncoderConfig( +# num_virtual_tokens=self.num_virtual_tokens, +# token_dim=self.token_dim, +# num_transformer_submodules=self.num_transformer_submodules, +# num_attention_heads=self.num_attention_heads, +# num_layers=self.num_layers, +# encoder_reparameterization_type=self.encoder_reparameterization_type, +# encoder_hidden_size=self.encoder_hidden_size, +# encoder_num_layers=self.encoder_num_layers, +# encoder_dropout=self.encoder_dropout, +# task_type=task_type, +# ) + + +@DeveloperAPI +@register_adapter("adalora") +class AdaloraConfig(LoraConfig): + """Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/adalora.py.""" + + type: str = schema_utils.ProtectedString( + "adalora", + description=LLM_METADATA["adapter"]["adalora"]["type"].long_description, + ) + + target_r: int = schema_utils.PositiveInteger( + default=8, + description="Target Lora Matrix Dimension. The target average rank of incremental matrix.", + ) + + init_r: int = schema_utils.PositiveInteger( + default=12, + description="Initial Lora Matrix Dimension. The initial rank for each incremental matrix.", + ) + + tinit: int = schema_utils.NonNegativeInteger( + default=0, + description="The steps of initial fine-tuning warmup.", + ) + + tfinal: int = schema_utils.NonNegativeInteger( + default=0, + description="The steps of final fine-tuning warmup.", + ) + + delta_t: int = schema_utils.NonNegativeInteger( + default=1, + description="The time internval between two budget allocations. The step interval of rank allocation.", + ) + + beta1: float = schema_utils.FloatRange( + default=0.85, + min=0.0, + max=1.0, + description="The hyperparameter of EMA for sensitivity smoothing.", + ) + + beta2: float = schema_utils.FloatRange( + default=0.85, + min=0.0, + max=1.0, + description=" The hyperparameter of EMA for undertainty quantification.", + ) + + orth_reg_weight: float = schema_utils.FloatRange( + default=0.5, + min=0.0, + max=1.0, + description="The coefficient of orthogonality regularization.", + ) + + total_step: int = schema_utils.PositiveInteger( + default=10000, + allow_none=False, + description="The total training steps for AdaLoRA rank allocation scheduling. " + "Must be a positive integer (required by peft >= 0.14).", + ) + + rank_pattern: dict | None = schema_utils.Dict( + default=None, + allow_none=True, + description="The allocated rank for each weight matrix by RankAllocator.", + ) + + def to_config(self, **kwargs) -> "PeftConfig": + from peft import AdaLoraConfig as _AdaLoraConfig + + return _AdaLoraConfig( + r=self.r, + lora_alpha=self.alpha, + lora_dropout=self.dropout, + bias=self.bias_type, + target_r=self.target_r, + init_r=self.init_r, + tinit=self.tinit, + tfinal=self.tfinal, + deltaT=self.delta_t, + beta1=self.beta1, + beta2=self.beta2, + orth_reg_weight=self.orth_reg_weight, + total_step=self.total_step, + rank_pattern=self.rank_pattern, + ) + + @classmethod + def name(cls) -> str: + return "AdaLoRA" + + @classmethod + def description(cls) -> str: + return LLM_METADATA["adapter"]["adalora"]["type"].long_description + + +@DeveloperAPI +# TODO: 02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix +# "TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')" +# (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938). +# +# @register_adapter("adaption_prompt") +class AdaptionPromptConfig(BaseAdapterConfig): + """Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/adaption_prompt/config.py.""" + + def __post_init__(self): + if not self.adapter_len: + raise ConfigValidationError( + "`adapter_len` must be set to a value greater than 0 when finetuning is enabled and the adapter" + "type is `adaption_prompt`. This is the length of the adaption prompt to insert." + ) + + if not self.adapter_layers: + raise ConfigValidationError( + "`adapter_layers` must be set to a value greater than 0 when finetuning is enabled and the adapter" + "type is `adaption_prompt`. This is the number of adapter layers to insert." + ) + + type: str = schema_utils.ProtectedString( + "adaption_prompt", + description=LLM_METADATA["adapter"]["adaption_prompt"]["type"].long_description, + ) + + adapter_len: int = schema_utils.PositiveInteger( + default=4, + description="Number of adapter tokens to insert.", + parameter_metadata=LLM_METADATA["adapter"]["adaption_prompt"]["adapter_len"], + ) + + adapter_layers: int = schema_utils.PositiveInteger( + default=1, + allow_none=False, + description="Number of adapter layers to insert (from the top).", + parameter_metadata=LLM_METADATA["adapter"]["adaption_prompt"]["adapter_layers"], + ) + + def to_config(self, task_type: str | None = None, **kwargs) -> "PeftConfig": + from peft import AdaptionPromptConfig as _AdaptionPromptConfig + + return _AdaptionPromptConfig( + adapter_len=self.adapter_len, + adapter_layers=self.adapter_layers, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "Adaption Prompt" + + @classmethod + def description(cls) -> str: + return LLM_METADATA["adapter"]["adaption_prompt"]["type"].long_description + + +@DeveloperAPI +@register_adapter("ia3") +class IA3Config(BaseAdapterConfig): + type: str = schema_utils.ProtectedString( + "ia3", + description=LLM_METADATA["adapter"]["ia3"]["type"].long_description, + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="The names of the modules to apply (IA)^3 to.", + parameter_metadata=LLM_METADATA["adapter"]["ia3"]["target_modules"], + ) + + feedforward_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description=( + "The names of the modules to be treated as feedforward modules, as in the original paper. These modules " + "will have (IA)^3 vectors multiplied to the input, instead of the output. feedforward_modules must be a " + "name or a subset of names present in target_modules." + ), + parameter_metadata=LLM_METADATA["adapter"]["ia3"]["feedforward_modules"], + ) + + fan_in_fan_out: bool = schema_utils.Boolean( + default=False, + description=( + "Set this to True if the layer to replace stores weight like (fan_in, fan_out). For example, gpt-2 uses " + "Conv1D which stores weights like (fan_in, fan_out) and hence this should be set to True. " + ), + parameter_metadata=LLM_METADATA["adapter"]["ia3"]["fan_in_fan_out"], + ) + + modules_to_save: list[str] | None = schema_utils.List( + list_type=str, + default=None, + allow_none=True, + description=( + "List of modules apart from (IA)^3 layers to be set as trainable and saved in the final checkpoint." + ), + parameter_metadata=LLM_METADATA["adapter"]["ia3"]["modules_to_save"], + ) + + init_ia3_weights: bool = schema_utils.Boolean( + default=True, + description="Whether to initialize the vectors in the (IA)^3 layers, defaults to True.", + parameter_metadata=LLM_METADATA["adapter"]["ia3"]["init_ia3_weights"], + ) + + def to_config(self, task_type: str | None = None, **kwargs) -> "PeftConfig": + from peft import IA3Config as _IA3Config + + return _IA3Config( + target_modules=self.target_modules, + feedforward_modules=self.feedforward_modules, + fan_in_fan_out=self.fan_in_fan_out, + modules_to_save=self.modules_to_save, + init_ia3_weights=self.init_ia3_weights, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "IA3" + + @classmethod + def description(cls) -> str: + return LLM_METADATA["adapter"]["ia3"]["type"].long_description + + +@DeveloperAPI +@register_adapter(name="vera") +class VeraAdapterConfig(BaseAdapterConfig): + """VeRA: Vector-based Random Matrix Adaptation (ICLR 2024). + + Uses shared frozen random matrices with trained scaling vectors. 10x fewer trainable + parameters than LoRA, useful for extreme parameter efficiency and multi-tenant serving. + """ + + type: str = schema_utils.ProtectedString("vera", description="VeRA adapter.") + + r: int = schema_utils.PositiveInteger(default=256, description="VeRA rank dimension.") + + target_modules: list[str] | None = schema_utils.List( + default=None, allow_none=True, description="List of module names to apply VeRA to." + ) + + projection_prng_key: int = schema_utils.NonNegativeInteger( + default=0, description="PRNG key for shared random projection matrices." + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import VeraConfig as _VeraConfig + + return _VeraConfig( + r=self.r, + target_modules=self.target_modules, + projection_prng_key=self.projection_prng_key, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "VeRA" + + @classmethod + def description(cls) -> str: + return "Vector-based Random Matrix Adaptation. Shares frozen random matrices across layers; only small scaling vectors are trained, giving 10× fewer parameters than LoRA at the same rank." + + +@DeveloperAPI +@register_adapter(name="loha") +class LoHaAdapterConfig(BaseAdapterConfig): + """LoHa: Low-Rank Hadamard Product Adaptation. + + Uses Hadamard product of two low-rank matrices for parameter-efficient fine-tuning. + Can capture more complex weight updates than LoRA at the same rank. + """ + + type: str = schema_utils.ProtectedString("loha", description="LoHa adapter.") + + r: int = schema_utils.PositiveInteger(default=8, description="LoHa rank dimension.") + + alpha: float = schema_utils.Float(default=8, description="Scaling factor for LoHa.") + + target_modules: list[str] | None = schema_utils.List( + default=None, allow_none=True, description="List of module names to apply LoHa to." + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import LoHaConfig as _LoHaConfig + + return _LoHaConfig(r=self.r, alpha=self.alpha, target_modules=self.target_modules, task_type=task_type) + + @classmethod + def name(cls) -> str: + return "LoHa" + + @classmethod + def description(cls) -> str: + return "Low-Rank Hadamard Product Adaptation. Uses a Hadamard product of two low-rank matrices to capture more complex weight updates than LoRA at the same rank." + + +@DeveloperAPI +@register_adapter(name="lokr") +class LoKrAdapterConfig(BaseAdapterConfig): + """LoKr: Low-Rank Kronecker Product Adaptation. + + Uses Kronecker product decomposition for efficient weight updates. + """ + + type: str = schema_utils.ProtectedString("lokr", description="LoKr adapter.") + + r: int = schema_utils.PositiveInteger(default=8, description="LoKr rank dimension.") + + alpha: float = schema_utils.Float(default=8, description="Scaling factor for LoKr.") + + target_modules: list[str] | None = schema_utils.List( + default=None, allow_none=True, description="List of module names to apply LoKr to." + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import LoKrConfig as _LoKrConfig + + return _LoKrConfig(r=self.r, alpha=self.alpha, target_modules=self.target_modules, task_type=task_type) + + @classmethod + def name(cls) -> str: + return "LoKr" + + @classmethod + def description(cls) -> str: + return "Low-Rank Kronecker Product Adaptation. Uses Kronecker product decomposition for efficient weight updates with a different inductive bias than LoRA." + + +@DeveloperAPI +@register_adapter(name="fourierft") +class FourierFTAdapterConfig(BaseAdapterConfig): + """FourierFT: Frequency-domain fine-tuning. + + Learns weight updates in the Fourier frequency domain, providing a different + inductive bias than spatial methods like LoRA. + """ + + type: str = schema_utils.ProtectedString("fourierft", description="FourierFT adapter.") + + n_frequency: int = schema_utils.PositiveInteger(default=1000, description="Number of frequency components.") + + scaling: float = schema_utils.Float(default=150.0, description="Scaling factor for FourierFT.") + + target_modules: list[str] | None = schema_utils.List( + default=None, allow_none=True, description="List of module names to apply FourierFT to." + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import FourierFTConfig as _FourierFTConfig + + return _FourierFTConfig( + n_frequency=self.n_frequency, + scaling=self.scaling, + target_modules=self.target_modules, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "FourierFT" + + @classmethod + def description(cls) -> str: + return "Frequency-domain fine-tuning. Learns weight updates in the Fourier frequency domain, providing a complementary inductive bias to spatial methods like LoRA." + + +@DeveloperAPI +@register_adapter(name="boft") +class BOFTAdapterConfig(BaseAdapterConfig): + """BOFT: Butterfly Orthogonal Fine-Tuning. + + Uses butterfly factorization to learn orthogonal transformations, preserving + the pre-trained model's hyperspherical energy while adapting to new tasks. + """ + + type: str = schema_utils.ProtectedString("boft", description="BOFT adapter.") + + boft_block_size: int = schema_utils.PositiveInteger( + default=4, description="Block size for butterfly factorization." + ) + + boft_n_butterfly_factor: int = schema_utils.PositiveInteger(default=1, description="Number of butterfly factors.") + + boft_dropout: float = schema_utils.NonNegativeFloat(default=0.05, description="Dropout for BOFT layers.") + + target_modules: list[str] | None = schema_utils.List( + default=None, allow_none=True, description="List of module names to apply BOFT to." + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import BOFTConfig as _BOFTConfig + + return _BOFTConfig( + boft_block_size=self.boft_block_size, + boft_n_butterfly_factor=self.boft_n_butterfly_factor, + boft_dropout=self.boft_dropout, + target_modules=self.target_modules, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "BOFT" + + @classmethod + def description(cls) -> str: + return "Butterfly Orthogonal Fine-Tuning. Learns orthogonal transformations via butterfly factorization, preserving the pre-trained model's geometry while adapting to new tasks." + + +@DeveloperAPI +@register_adapter(name="tinylora") +class TinyLoraAdapterConfig(BaseAdapterConfig): + """TinyLoRA: Extreme parameter-efficient fine-tuning via SVD projection. + + Uses SVD decomposition of frozen weights and projects a tiny trainable vector through fixed random tensors. + Enables fine-tuning in as few as 13 parameters. Ideal for extremely constrained hardware or edge deployment. + Paper: https://arxiv.org/abs/2602.04118 + """ + + type: str = schema_utils.ProtectedString("tinylora", description="TinyLoRA adapter (LoRA-XS equivalent).") + + r: int = schema_utils.PositiveInteger( + default=2, + description="SVD rank for the frozen U, Sigma, V decomposition. The paper recommends r=2.", + ) + + u: int = schema_utils.PositiveInteger( + default=64, + description=( + "Trainable vector dimension per group. Controls the expressivity of the adaptation. " + "Can be as low as 1–13 for extreme parameter efficiency." + ), + ) + + weight_tying: float = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=1.0, + description=( + "Degree of weight tying across target modules (0.0 = no sharing, 1.0 = full sharing). " + "Sharing trainable vectors across modules further reduces parameter count." + ), + ) + + projection_seed: int = schema_utils.NonNegativeInteger( + default=42, + description="Random seed for generating the fixed projection matrices.", + ) + + save_projection: bool = schema_utils.Boolean( + default=True, + description="Whether to save the projection tensors in the state dict.", + ) + + tinylora_dropout: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Dropout probability for TinyLoRA layers.", + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of module names or regex to apply TinyLoRA to.", + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import TinyLoraConfig as _TinyLoraConfig + + return _TinyLoraConfig( + r=self.r, + u=self.u, + weight_tying=self.weight_tying, + projection_seed=self.projection_seed, + save_projection=self.save_projection, + tinylora_dropout=self.tinylora_dropout, + target_modules=self.target_modules, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "TinyLoRA" + + @classmethod + def description(cls) -> str: + return "TinyLoRA: extreme parameter-efficient fine-tuning via SVD projection (LoRA-XS variant)." + + +@DeveloperAPI +@register_adapter(name="c3a") +class C3AAdapterConfig(BaseAdapterConfig): + """C3A: Contextual / Conditional / Compositional Adapter. + + Uses block-diagonal matrices for structured parameter efficiency. + Enables context-aware adapter routing and multi-task modularity. + """ + + type: str = schema_utils.ProtectedString("c3a", description="C3A adapter.") + + block_size: int = schema_utils.PositiveInteger( + default=256, + description=( + "Block size for C3A, must be divisible by both the input size and the output size of each target layer. " + "Setting this to the GCD of all target layer dimensions is a safe default. " + "Larger block sizes mean fewer parameters." + ), + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of module names or regex to apply C3A to.", + ) + + bias_type: str = schema_utils.StringOptions( + options=["none", "all", "c3a_only"], + default="none", + description="Bias type for C3A. 'none' trains no biases; 'all' or 'c3a_only' trains the adapter biases.", + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import C3AConfig as _C3AConfig + + return _C3AConfig( + block_size=self.block_size, + target_modules=self.target_modules, + bias=self.bias_type, + block_size_pattern={}, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "C3A" + + @classmethod + def description(cls) -> str: + return "C3A: context-aware block-diagonal adapter for multi-task and compositional fine-tuning." + + +@DeveloperAPI +@register_adapter(name="oft") +class OFTAdapterConfig(BaseAdapterConfig): + """OFT: Orthogonal Fine-Tuning. + + Applies orthogonal transformations to the weight matrices, preserving the hyperspherical energy + of the pre-trained model while adapting to new tasks. Particularly effective for maintaining + output diversity and preventing catastrophic forgetting. + Paper: https://arxiv.org/abs/2306.07280 + """ + + type: str = schema_utils.ProtectedString("oft", description="OFT adapter.") + + r: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "OFT rank. When 0, the block size (`oft_block_size`) controls the granularity instead. " + "Cannot be set simultaneously with `oft_block_size`." + ), + ) + + oft_block_size: int = schema_utils.PositiveInteger( + default=32, + description="Block size for the butterfly factorization of the orthogonal transform.", + ) + + module_dropout: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Probability of randomly zeroing an OFT block during training.", + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of module names or regex to apply OFT to.", + ) + + coft: bool = schema_utils.Boolean( + default=False, + description="Whether to use Constrained OFT (COFT), which enforces the constraint ||I - R^T R||_F <= eps.", + ) + + eps: float = schema_utils.NonNegativeFloat( + default=6e-5, + description="Constraint strength for COFT (only used when `coft=True`).", + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import OFTConfig as _OFTConfig + + return _OFTConfig( + r=self.r, + oft_block_size=self.oft_block_size, + module_dropout=self.module_dropout, + target_modules=self.target_modules, + coft=self.coft, + eps=self.eps, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "OFT" + + @classmethod + def description(cls) -> str: + return "OFT: Orthogonal Fine-Tuning that preserves hyperspherical energy of the pre-trained model." + + +@DeveloperAPI +@register_adapter(name="hra") +class HRAAdapterConfig(BaseAdapterConfig): + """HRA: Householder Reflection Adaptation. + + Parameterizes weight updates as products of Householder reflections, which are orthogonal by construction. + Provides stronger expressivity than OFT with fewer hyperparameters. + Paper: https://arxiv.org/abs/2405.17484 + """ + + type: str = schema_utils.ProtectedString("hra", description="HRA adapter.") + + r: int = schema_utils.PositiveInteger( + default=8, + description="Number of Householder reflections (rank). More reflections = more expressive adaptation.", + ) + + apply_GS: bool = schema_utils.Boolean( + default=False, + description=( + "Whether to apply Gram-Schmidt orthogonalization to the Householder vectors. " + "Improves numerical stability at the cost of a small overhead." + ), + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of module names or regex to apply HRA to.", + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import HRAConfig as _HRAConfig + + return _HRAConfig( + r=self.r, + apply_GS=self.apply_GS, + target_modules=self.target_modules, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "HRA" + + @classmethod + def description(cls) -> str: + return "HRA: Householder Reflection Adaptation — orthogonal updates via Householder reflections." + + +@DeveloperAPI +@register_adapter(name="waveft") +class WaveFTAdapterConfig(BaseAdapterConfig): + """WaveFT: Wavelet-domain Fine-Tuning. + + Learns weight updates in the wavelet frequency domain using discrete wavelet transforms. + Provides a different inductive bias from spatial methods like LoRA, often benefiting + tasks with structured or periodic patterns. + Paper: https://arxiv.org/abs/2411.09295 + """ + + type: str = schema_utils.ProtectedString("waveft", description="WaveFT adapter.") + + n_frequency: int = schema_utils.PositiveInteger( + default=2592, + description="Number of wavelet frequency components to learn. Fewer = more parameter efficient.", + ) + + scaling: float = schema_utils.NonNegativeFloat( + default=25.0, + description="Scaling factor applied to the wavelet-domain updates.", + ) + + wavelet_family: str = schema_utils.StringOptions( + options=["db1", "db2", "db3", "haar", "sym2", "coif1"], + default="db1", + description=( + "Wavelet family to use for the discrete wavelet transform. " + "'db1'/'haar' are simplest; higher-order Daubechies ('db2', 'db3') capture smoother features." + ), + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of module names or regex to apply WaveFT to.", + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import WaveFTConfig as _WaveFTConfig + + return _WaveFTConfig( + n_frequency=self.n_frequency, + scaling=self.scaling, + wavelet_family=self.wavelet_family, + target_modules=self.target_modules, + n_frequency_pattern={}, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "WaveFT" + + @classmethod + def description(cls) -> str: + return "WaveFT: Wavelet-domain fine-tuning with structured frequency-domain weight updates." + + +@DeveloperAPI +@register_adapter(name="ln_tuning") +class LNTuningAdapterConfig(BaseAdapterConfig): + """LN-Tuning: Layer Normalization Tuning. + + Fine-tunes only the layer normalization parameters (weight and bias) of the model. + Extremely parameter-efficient — often only ~0.1% of total parameters — while surprisingly + effective for domain adaptation tasks. + """ + + type: str = schema_utils.ProtectedString("ln_tuning", description="LN-Tuning adapter.") + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description=( + "List of layer norm module names or regex to tune. " + "Defaults to all LayerNorm / RMSNorm modules in the model." + ), + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import LNTuningConfig as _LNTuningConfig + + return _LNTuningConfig( + target_modules=self.target_modules, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "LN-Tuning" + + @classmethod + def description(cls) -> str: + return "LN-Tuning: tunes only the layer normalization parameters for ultra-lightweight adaptation." + + +@DeveloperAPI +@register_adapter(name="vblora") +class VBLoRAAdapterConfig(BaseAdapterConfig): + """VBLoRA: Vector Bank LoRA. + + Represents LoRA weight matrices as a sparse combination of shared vectors from a global bank. + Achieves significant parameter compression by reusing vectors across layers. + Paper: https://arxiv.org/abs/2405.15179 + """ + + type: str = schema_utils.ProtectedString("vblora", description="VBLoRA adapter.") + + r: int = schema_utils.PositiveInteger( + default=4, + description="LoRA rank dimension. Controls the bottleneck size of each adaptation.", + ) + + num_vectors: int = schema_utils.PositiveInteger( + default=256, + description="Number of vectors in the global vector bank shared across all layers.", + ) + + vector_length: int = schema_utils.PositiveInteger( + default=256, + description="Length (dimension) of each vector in the bank. Usually set to the hidden size or head dim.", + ) + + topk: int = schema_utils.PositiveInteger( + default=2, + description=( + "Number of top-k vectors selected from the bank for each LoRA matrix column. " + "Higher k increases expressivity but also parameter count." + ), + ) + + vblora_dropout: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Dropout probability for VBLoRA layers.", + ) + + save_only_topk_weights: bool = schema_utils.Boolean( + default=False, + description="Whether to save only the top-k selection logits rather than the full bank weights.", + ) + + target_modules: list[str] | None = schema_utils.List( + default=None, + allow_none=True, + description="List of module names or regex to apply VBLoRA to.", + ) + + def to_config(self, task_type: str | None = None, **kwargs): + from peft import VBLoRAConfig as _VBLoRAConfig + + return _VBLoRAConfig( + r=self.r, + num_vectors=self.num_vectors, + vector_length=self.vector_length, + topk=self.topk, + vblora_dropout=self.vblora_dropout, + save_only_topk_weights=self.save_only_topk_weights, + target_modules=self.target_modules, + task_type=task_type, + ) + + @classmethod + def name(cls) -> str: + return "VBLoRA" + + @classmethod + def description(cls) -> str: + return "VBLoRA: Vector Bank LoRA that shares vectors across layers for extreme compression." + + +@DeveloperAPI +def get_adapter_conds(): + conds = [] + for adapter_type, adapter_cls in adapter_registry.items(): + other_props = schema_utils.unload_jsonschema_from_config_class(adapter_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + preproc_cond = schema_utils.create_cond( + {"type": adapter_type}, + other_props, + ) + conds.append(preproc_cond) + return conds + + +@DeveloperAPI +def AdapterDataclassField(default: str | None = None): + description = "Whether to use parameter-efficient fine-tuning" + + class AdapterSelection(schema_utils.TypeSelection): + def __init__(self): + super().__init__( + registry=adapter_registry, + default_value=default, + description=description, + parameter_metadata=None, + allow_str_value=True, + allow_none=True, + ) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return adapter_registry[key] + + @staticmethod + def _jsonschema_type_mapping(): + return { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": list(adapter_registry.keys()), + "description": "The type of PEFT adapter to use during fine-tuning", + }, + }, + "title": "Perform parameter efficient fine-tuning", + "allOf": get_adapter_conds(), + "required": ["type"], + "description": "The type of PEFT adapter to use during fine-tuning", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA["adapter"]["_oneOf"]["allOf"]), + }, + { + "type": "null", + "title": "adapter_null_option", + "description": "Disable the adapter.", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA["adapter"]["_oneOf"]["none"]), + }, + ], + "title": "adapter_options", + "description": "Whether to use parameter-efficient fine-tuning", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA["adapter"]["_meta"]), + "default": default, + } + + return AdapterSelection().get_default_field() + + +# ================================================================================================ +# Multi-adapter support +# ================================================================================================ +# +# The singular `adapter:` field above supports the common case: one adapter attached to one +# base model. The `adapters:` field below supports the multi-adapter case: several named +# adapters attached to the same base model, switchable at runtime via PEFT's `set_adapter()` +# and optionally merged via `add_weighted_adapter()` using combination types like TIES and +# DARE (Yadav et al., NeurIPS 2023 / Yu et al., ICML 2024). +# +# `adapter:` and `adapters:` are mutually exclusive — a config must use one form or the other. +# Back-compat: existing configs that set `adapter:` continue to work unchanged. + + +@DeveloperAPI +class MergeAdaptersConfig(schema_utils.LudwigBaseConfig): + """Optional weighted merge over a subset of the named adapters. + + Produces a new adapter registered under ``name`` by combining ``sources`` with the + matching ``weights`` under ``combination_type``. The merged adapter is added to the + model alongside the sources; pick it as ``active`` to make it the default at + inference time. + """ + + name: str = schema_utils.String( + default="merged", + description="Name to register the merged adapter under.", + ) + + sources: list | None = schema_utils.List( + default=None, + allow_none=True, + description="Names of the adapters to merge. Each name must appear in the `adapters` map.", + ) + + weights: list | None = schema_utils.List( + default=None, + allow_none=True, + description=( + "Per-source weights; must have the same length as `sources`. If null, all weights default to 1.0." + ), + ) + + combination_type: str = schema_utils.StringOptions( + options=["linear", "svd", "ties", "dare_linear", "dare_ties", "magnitude_prune"], + default="linear", + allow_none=False, + description=( + "PEFT weighted-merge combination type. 'linear' is a plain weighted sum. " + "'ties' (Yadav et al., NeurIPS 2023) resolves sign conflicts across source " + "deltas before merging. 'dare_linear' / 'dare_ties' (Yu et al., ICML 2024) " + "prune a fraction `density` of deltas before merging for smaller footprints." + ), + ) + + density: float = schema_utils.FloatRange( + default=0.5, + min=0.0, + max=1.0, + description=( + "Fraction of weight deltas kept when `combination_type` is 'ties', " + "'dare_linear', 'dare_ties', or 'magnitude_prune'. Ignored for 'linear' / 'svd'." + ), + ) + + +@DeveloperAPI +class MergeAdaptersConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(MergeAdaptersConfig, allow_none=True, default_missing=True) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(MergeAdaptersConfig, title="MergeAdapters") + + +@DeveloperAPI +class NamedAdaptersConfig(schema_utils.LudwigBaseConfig): + """Configuration for multiple named PEFT adapters on the same base model.""" + + adapters: dict | None = schema_utils.Dict( + default=None, + allow_none=False, + description=( + "Mapping of adapter name -> adapter config. Each value is a regular adapter " + "config (e.g. ``{type: lora, r: 8}``) identical to what the singular " + "`adapter:` field accepts. Adapter names must be unique; PEFT will register " + "each one on the model and the first-listed adapter becomes the active one " + "unless `active` is set." + ), + ) + + active: str | None = schema_utils.String( + default=None, + allow_none=True, + description=( + "Name of the adapter to activate after all adapters are registered. " + "If null, the first entry in `adapters` is used. Set this to a merged adapter " + "name from `merge:` to activate the merged adapter at inference time." + ), + ) + + merge: MergeAdaptersConfig | None = MergeAdaptersConfigField().get_default_field() + + +@DeveloperAPI +class NamedAdaptersConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(NamedAdaptersConfig, allow_none=True, default_missing=True) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(NamedAdaptersConfig, title="NamedAdapters") + + +@DeveloperAPI +def NamedAdaptersDataclassField(): + return NamedAdaptersConfigField().get_default_field() diff --git a/ludwig/schema/llms/prompt.py b/ludwig/schema/llms/prompt.py new file mode 100644 index 0000000..240ab3b --- /dev/null +++ b/ludwig/schema/llms/prompt.py @@ -0,0 +1,106 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import SEMANTIC +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import LLM_METADATA + + +@DeveloperAPI +class RetrievalConfig(schema_utils.LudwigBaseConfig): + """This Dataclass is a schema for the nested retrieval config under prompt.""" + + def __post_init__(self): + # TODO: have a dynamically loaded schema based on the selection of the type param + # https://github.com/ludwig-ai/ludwig/pull/3351#discussion_r1181910954 + # Ensure k is non-zero if we're using a retrieval strategy + if self.type is not None and self.k == 0: + self.k = 1 + + if self.type is None and self.k != 0: + raise ConfigValidationError("k must be 0 if retrieval type is None.") + elif self.type is not None and self.k <= 0: + raise ConfigValidationError("k must be greater than 0 if retrieval type is not None.") + + if self.type is None and self.model_name is not None: + raise ConfigValidationError("model_name must be None if retrieval type is None.") + elif self.type == SEMANTIC and self.model_name is None: + raise ConfigValidationError(f"model_name must not be None if retrieval type is '{SEMANTIC}'.") + + type: str = schema_utils.String( + default=None, + allow_none=True, + description=( + "The type of retrieval to use for the prompt. If `None`, then no retrieval is used, and the task " + "is framed as a zero-shot learning problem. If not `None` (e.g. either 'random' or 'semantic'), then " + "samples are retrieved from an index of the training set and used to augment the input to the model " + "in a few-shot learning setting." + ), + parameter_metadata=LLM_METADATA["prompt"]["retrieval"]["type"], + ) + + index_name: str = schema_utils.String( + default=None, + allow_none=True, + description="The name of the index to use for the prompt. Indices are stored in the ludwig cache by default.", + parameter_metadata=LLM_METADATA["prompt"]["retrieval"]["index_name"], + ) + + model_name: str = schema_utils.String( + default=None, + allow_none=True, + description="The model used to generate the embeddings used to retrieve samples to inject in the prompt.", + parameter_metadata=LLM_METADATA["prompt"]["retrieval"]["model_name"], + ) + + k: int = schema_utils.NonNegativeInteger( + default=0, + description="The number of samples to retrieve.", + parameter_metadata=LLM_METADATA["prompt"]["retrieval"]["k"], + ) + + +@DeveloperAPI +class RetrievalConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(RetrievalConfig) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(RetrievalConfig, title="Retrieval") + + +@DeveloperAPI +class PromptConfig(schema_utils.LudwigBaseConfig): + """This Dataclass is a schema for the nested prompt config under preprocessing.""" + + template: str = schema_utils.String( + default=None, + allow_none=True, + description=( + "The template to use for the prompt. Must contain at least one of the columns from the input dataset " + "or `__sample__` as a variable surrounded in curly brackets {} to indicate where to insert the " + "current feature. Multiple columns can be inserted, e.g.: `The {color} {animal} jumped over " + "the {size} {object}`, where every term in curly brackets is a column in the dataset. If a `task` " + "is specified, then the template must also contain the `__task__` variable. If `retrieval` is specified, " + "then the template must also contain the `__context__` variable. If no template is provided, then a " + "default will be used based on the retrieval settings, and a task must be set in the config." + ), + parameter_metadata=LLM_METADATA["prompt"]["template"], + ) + + task: str = schema_utils.String( + default=None, + allow_none=True, + description="The task to use for the prompt. Required if `template` is not set.", + parameter_metadata=LLM_METADATA["prompt"]["task"], + ) + + retrieval: RetrievalConfig = RetrievalConfigField().get_default_field() + + +@DeveloperAPI +class PromptConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(PromptConfig) + + def _jsonschema_type_mapping(self): + return schema_utils.unload_jsonschema_from_config_class(PromptConfig) diff --git a/ludwig/schema/llms/quantization.py b/ludwig/schema/llms/quantization.py new file mode 100644 index 0000000..0b1a5b0 --- /dev/null +++ b/ludwig/schema/llms/quantization.py @@ -0,0 +1,172 @@ +import warnings + +from transformers import BitsAndBytesConfig + +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import LLM_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json + +warnings.filterwarnings( + action="ignore", + category=UserWarning, + module="bitsandbytes.cuda_setup.main", +) + + +# Valid torchao modes. Keep in sync with `ludwig/utils/quantization.py::quantize_model`. +_TORCHAO_MODES = ["int4_weight_only", "int8_weight_only", "int8_dynamic", "float8"] + + +@DeveloperAPI +class QuantizationConfig(schema_utils.LudwigBaseConfig): + backend: str = schema_utils.StringOptions( + options=["bitsandbytes", "torchao"], + default="bitsandbytes", + allow_none=False, + description=( + "Quantization backend. 'bitsandbytes' (default) applies 4-bit / 8-bit quantization at " + "model load time via the bitsandbytes library — the existing QLoRA fine-tuning path. " + "'torchao' applies PyTorch-native quantization via torchao after model load, and can " + "additionally run quantization-aware training (QAT) when `qat: true` is set." + ), + ) + + mode: str | None = schema_utils.StringOptions( + options=_TORCHAO_MODES, + default=None, + allow_none=True, + description=( + "torchao-only quantization mode. Ignored when `backend` is 'bitsandbytes'. " + "'int4_weight_only' and 'int8_weight_only' quantize only the weight matrices (activations " + "stay in fp16/bf16). 'int8_dynamic' quantizes activations to int8 dynamically per-forward. " + "'float8' stores weights in fp8 (useful on H100+)." + ), + ) + + qat: bool = schema_utils.Boolean( + default=False, + description=( + "torchao-only. When true, inserts fake-quant observers into the model before training " + "(QAT). The model is trained in the target low-precision numerical regime, then converted " + "to actually-quantized weights at save time. Ignored when `backend` is 'bitsandbytes'." + ), + ) + + bits: int = schema_utils.IntegerOptions( + options=[4, 8], + default=4, + description="The quantization level to apply to weights on load.", + parameter_metadata=LLM_METADATA["quantization"]["bits"], + ) + + llm_int8_threshold: float = schema_utils.NonNegativeFloat( + default=6.0, + description=( + "This corresponds to the outlier threshold for outlier detection as described in `LLM.int8() : 8-bit " + "Matrix Multiplication for Transformers at Scale` paper: https://arxiv.org/abs/2208.07339. Any hidden " + "states value that is above this threshold will be considered an outlier and the operation on those " + "values will be done in fp16. Values are usually normally distributed, that is, most values are in the " + "range [-3.5, 3.5], but there are some exceptional systematic outliers that are very differently " + "distributed for large models. These outliers are often in the interval [-60, -6] or [6, 60]. Int8 " + "quantization works well for values of magnitude ~5, but beyond that, there is a significant performance " + "penalty. A good default threshold is 6, but a lower threshold might be needed for more unstable models " + "(small models, fine-tuning)." + ), + ) + + llm_int8_has_fp16_weight: bool = schema_utils.Boolean( + default=False, + description=( + "This flag runs LLM.int8() with 16-bit main weights. This is useful for fine-tuning as the weights do " + "not have to be converted back and forth for the backward pass." + ), + ) + + bnb_4bit_compute_dtype: str = schema_utils.StringOptions( + options=["float32", "float16", "bfloat16"], + default="float16", + description=( + "This sets the computational type which might be different than the input type. For example, inputs " + "might be fp32, but computation can be set to bf16 for speedups." + ), + ) + + bnb_4bit_use_double_quant: bool = schema_utils.Boolean( + default=True, + description=( + "This flag is used for nested quantization where the quantization constants from the first quantization " + "are quantized again." + ), + ) + + bnb_4bit_quant_type: str = schema_utils.StringOptions( + options=["fp4", "nf4"], + default="nf4", + description="This sets the quantization data type in the bnb.nn.Linear4Bit layers.", + ) + + def to_bitsandbytes(self) -> BitsAndBytesConfig: + return BitsAndBytesConfig( + load_in_4bit=self.bits == 4, + load_in_8bit=self.bits == 8, + llm_int8_threshold=self.llm_int8_threshold, + llm_int8_has_fp16_weight=self.llm_int8_has_fp16_weight, + bnb_4bit_compute_dtype=self.bnb_4bit_compute_dtype, + bnb_4bit_use_double_quant=self.bnb_4bit_use_double_quant, + bnb_4bit_quant_type=self.bnb_4bit_quant_type, + ) + + def validate_backend(self) -> None: + """Cross-validate backend-specific fields. + + Called from :class:`~ludwig.schema.model_types.llm.LLMModelConfig.__post_init__` + rather than from this class's own ``__post_init__`` because errors raised from a + nested config's post-init get wrapped into a generic "Invalid params" message by + the parent's :class:`NestedConfigField`, losing the specific reason. + """ + if self.backend == "torchao": + if self.mode is None: + raise ConfigValidationError( + "`quantization.mode` is required when `quantization.backend` is 'torchao'. " + f"Options: {_TORCHAO_MODES}." + ) + else: # bitsandbytes + if self.mode is not None: + raise ConfigValidationError( + "`quantization.mode` is only supported for `backend: torchao`. Remove the " + "field or switch the backend to 'torchao'." + ) + if self.qat: + raise ConfigValidationError( + "`quantization.qat: true` is only supported for `backend: torchao`. " + "The bitsandbytes backend does not implement quantization-aware training." + ) + + +@DeveloperAPI +class QuantizationConfigField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(QuantizationConfig, default_missing=True) + + def _jsonschema_type_mapping(self): + return { + "oneOf": [ + { + "type": "null", + "title": "disabled", + "description": "Disable quantization.", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA["quantization"]["_oneOf"]["none"]), + }, + { + **schema_utils.unload_jsonschema_from_config_class(QuantizationConfig), + "title": "enabled", + "description": "Set quantization options.", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA["quantization"]["_oneOf"]["object"]), + }, + ], + "title": "quantization", + "description": "Set quantization options.", + "parameter_metadata": convert_metadata_to_json(LLM_METADATA["quantization"]["_meta"]), + } diff --git a/ludwig/schema/lr_scheduler.py b/ludwig/schema/lr_scheduler.py new file mode 100644 index 0000000..2af1b4a --- /dev/null +++ b/ludwig/schema/lr_scheduler.py @@ -0,0 +1,298 @@ +from abc import ABC +from dataclasses import field + +import pydantic + +import ludwig.schema.utils as schema_utils +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import LOSS, MODEL_ECD, TRAINING +from ludwig.error import ConfigValidationError +from ludwig.schema.metadata import TRAINER_METADATA + + +@DeveloperAPI +class LRSchedulerConfig(schema_utils.LudwigBaseConfig, ABC): + """Configuration for learning rate scheduler parameters.""" + + decay: str = schema_utils.StringOptions( + options=["linear", "exponential", "cosine", "one_cycle", "inverse_sqrt", "polynomial", "wsd"], + default=None, + allow_none=True, + description=( + "Learning rate decay schedule. Options: 'linear', 'exponential', 'cosine', 'one_cycle', " + "'inverse_sqrt', 'polynomial', 'wsd'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["decay"], + ) + + decay_rate: float = schema_utils.FloatRange( + default=0.96, + min=0, + max=1, + description="Decay per epoch (%): Factor to decrease the Learning rate.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["decay_rate"], + ) + + decay_steps: int = schema_utils.PositiveInteger( + default=10000, + description="The number of steps to take in the exponential learning rate decay.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["decay_steps"], + ) + + staircase: bool = schema_utils.Boolean( + default=False, + description="Decays the learning rate at discrete intervals.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["staircase"], + ) + + reduce_on_plateau: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "How many times to reduce the learning rate when the algorithm hits a plateau (i.e. the performance on the " + "training set does not improve)" + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["reduce_on_plateau"], + ) + + reduce_on_plateau_patience: int = schema_utils.NonNegativeInteger( + default=10, + description=( + "How many evaluation steps have to pass before the learning rate reduces when `reduce_on_plateau > 0`." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["reduce_on_plateau_patience"], + ) + + reduce_on_plateau_rate: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description="Rate at which we reduce the learning rate when `reduce_on_plateau > 0`.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["reduce_on_plateau_rate"], + ) + + warmup_evaluations: int = schema_utils.NonNegativeFloat( + default=0, + description="Number of evaluation steps to warmup the learning rate for.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["warmup_evaluations"], + ) + + warmup_fraction: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Fraction of total training steps to warmup the learning rate for.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["warmup_fraction"], + ) + + reduce_eval_metric: str = schema_utils.String( + default=LOSS, + allow_none=False, + description=("Metric plateau used to trigger when we reduce the learning rate when `reduce_on_plateau > 0`."), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["reduce_eval_metric"], + ) + + reduce_eval_split: str = schema_utils.String( + default=TRAINING, + allow_none=False, + description=("Which dataset split to listen on for reducing the learning rate when `reduce_on_plateau > 0`."), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["reduce_eval_split"], + ) + + # Parameters for CosineAnnealingWarmRestarts scheduler + + t_0: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of steps before the first restart for cosine annealing decay. If not specified, it" + " will be set to `steps_per_checkpoint`.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["t_0"], + ) + + t_mult: int = schema_utils.PositiveInteger( + default=1, + description="Period multiplier after each restart for cosine annealing decay. Defaults to 1, i.e.," + " restart every `t_0` steps. If set to a larger value, the period between restarts increases by that" + " multiplier. For e.g., if t_mult is 2, then the periods would be: t_0, 2*t_0, 2^2*t_0, 2^3*t_0, etc.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["t_mult"], + ) + + eta_min: float = schema_utils.FloatRange( + default=0, + min=0, + max=1, + description="Minimum learning rate allowed for cosine annealing decay. Default: 0.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["eta_min"], + ) + + # Parameters for OneCycleLR scheduler + + max_lr: float = schema_utils.Float( + default=None, + allow_none=True, + description=( + "Maximum learning rate for the OneCycleLR scheduler. If None, defaults to the optimizer's base " + "learning rate. Used only when decay='one_cycle'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["max_lr"], + ) + + pct_start: float = schema_utils.FloatRange( + default=0.3, + min=0, + max=1, + description=( + "Fraction of training steps spent increasing the learning rate in the OneCycleLR scheduler. " + "Used only when decay='one_cycle'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["pct_start"], + ) + + div_factor: float = schema_utils.Float( + default=25.0, + allow_none=False, + description=( + "Determines the initial learning rate (initial_lr = max_lr / div_factor) for OneCycleLR. " + "Used only when decay='one_cycle'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["div_factor"], + ) + + final_div_factor: float = schema_utils.Float( + default=1e4, + allow_none=False, + description=( + "Determines the minimum learning rate (min_lr = initial_lr / final_div_factor) for OneCycleLR. " + "Used only when decay='one_cycle'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["final_div_factor"], + ) + + # Parameters for InverseSqrtLR scheduler + + inverse_sqrt_warmup_steps: int = schema_utils.PositiveInteger( + default=4000, + description=( + "Number of warmup steps for the inverse square root scheduler. After warmup, the LR decays as " + "1/sqrt(step). This is the classic Transformer schedule from Vaswani et al. (2017). " + "Used only when decay='inverse_sqrt'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["inverse_sqrt_warmup_steps"], + ) + + # Parameters for Polynomial Decay scheduler + + polynomial_power: float = schema_utils.Float( + default=1.0, + allow_none=False, + description=( + "Power of the polynomial decay. power=1.0 gives linear decay; higher values give more concave " + "decay curves. Used only when decay='polynomial'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["polynomial_power"], + ) + + polynomial_end_lr: float = schema_utils.Float( + default=0.0, + allow_none=False, + description=( + "Final (minimum) learning rate at the end of polynomial decay. Used only when decay='polynomial'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["polynomial_end_lr"], + ) + + # Parameters for Warmup-Stable-Decay (WSD) scheduler + + wsd_warmup_fraction: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description=( + "Fraction of total training steps spent in the linear warmup phase of the WSD scheduler. " + "Used only when decay='wsd'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["wsd_warmup_fraction"], + ) + + wsd_stable_fraction: float = schema_utils.FloatRange( + default=0.8, + min=0, + max=1, + description=( + "Fraction of total training steps spent in the constant LR phase of the WSD scheduler. " + "Used only when decay='wsd'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["wsd_stable_fraction"], + ) + + wsd_decay_fraction: float = schema_utils.FloatRange( + default=0.1, + min=0, + max=1, + description=( + "Fraction of total training steps spent in the decay phase of the WSD scheduler. " + "wsd_warmup_fraction + wsd_stable_fraction + wsd_decay_fraction should sum to 1. " + "Used only when decay='wsd'." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scheduler"]["wsd_decay_fraction"], + ) + + +# TODO(travis): too much boilerplate here, we should find a way to abstract all this and only require specifying the +# minimal amount needed for the new config object. +@DeveloperAPI +def LRSchedulerDataclassField(description: str, default: dict | None = None): + """Returns custom dataclass field for `LRSchedulerConfig`. Allows `None` by default. + + Args: + description: Description of the dataclass field + default: dict that specifies param values that will be loaded by its schema class (default: None). + """ + allow_none = True + default = default or {} + + class LRSchedulerConfigField(schema_utils.SchemaField): + """Custom field class for learning rate scheduler. + + Deserializes a dict to a valid instance of `LRSchedulerConfig` and creates a corresponding JSON schema for + external usage. + """ + + def _deserialize(self, value, attr, data, **kwargs): + if value is None: + return value + if isinstance(value, dict): + try: + return LRSchedulerConfig.model_validate(value) + except (TypeError, ConfigValidationError) as e: + raise ConfigValidationError( + f"Invalid params for learning rate scheduler: {value}, see LRSchedulerConfig class. Error: {e}" + ) + raise ConfigValidationError("Field should be None or dict") + + def _jsonschema_type_mapping(self): + return { + **schema_utils.unload_jsonschema_from_config_class(LRSchedulerConfig), + "title": "learning_rate_scheduler_options", + "description": description, + } + + if not isinstance(default, dict): + raise ConfigValidationError(f"Invalid default: `{default}`") + + load_default = lambda: LRSchedulerConfig.model_validate(default) + try: + dump_default = LRSchedulerConfig.model_validate(default).to_dict() + except pydantic.ValidationError: + dump_default = default if isinstance(default, dict) else {} + + return field( + metadata={ + "marshmallow_field": LRSchedulerConfigField( + allow_none=allow_none, + load_default=load_default, + dump_default=dump_default, + metadata={ + "description": description, + }, + ) + }, + default_factory=load_default, + ) diff --git a/ludwig/schema/metadata/__init__.py b/ludwig/schema/metadata/__init__.py new file mode 100644 index 0000000..7e9f4a4 --- /dev/null +++ b/ludwig/schema/metadata/__init__.py @@ -0,0 +1,39 @@ +import os +from typing import Any + +import yaml + +from ludwig.schema.metadata.parameter_metadata import ParameterMetadata + +_PATH_HERE = os.path.abspath(os.path.dirname(__file__)) +_CONFIG_DIR = os.path.join(_PATH_HERE, "configs") + + +def _to_metadata(d: dict[str, Any]) -> ParameterMetadata | dict[str, Any]: + is_nested = False + for k, v in list(d.items()): + if isinstance(v, dict): + d[k] = _to_metadata(v) + is_nested = True + + if is_nested: + return d + + return ParameterMetadata.from_dict(d) + + +def _load(fname: str) -> dict[str, Any]: + with open(os.path.join(_CONFIG_DIR, fname)) as f: + return _to_metadata(yaml.safe_load(f)) + + +COMMON_METADATA = _load("common.yaml") +COMBINER_METADATA = _load("combiners.yaml") +DECODER_METADATA = _load("decoders.yaml") +ENCODER_METADATA = _load("encoders.yaml") +FEATURE_METADATA = _load("features.yaml") +PREPROCESSING_METADATA = _load("preprocessing.yaml") +TRAINER_METADATA = _load("trainer.yaml") +OPTIMIZER_METADATA = _load("optimizers.yaml") +LOSS_METADATA = _load("loss.yaml") +LLM_METADATA = _load("llm.yaml") diff --git a/ludwig/schema/metadata/configs/combiners.yaml b/ludwig/schema/metadata/configs/combiners.yaml new file mode 100644 index 0000000..2d9b2c1 --- /dev/null +++ b/ludwig/schema/metadata/configs/combiners.yaml @@ -0,0 +1,1639 @@ +comparator: + type: + short_description: Used for recommendation problems, features associated with distinct entities, output depends on entity-level comparison. + long_description: + The comparator combiner compares the hidden representation of two entities defined by lists of + features. It assumes all outputs from encoders are tensors of size `b x h` where `b` is the batch + size and `h` is the hidden dimension, which can be different for each input. If the input tensors + have a different shape, it automatically flattens them. It then concatenates the representations + of each entity and projects them both to vectors of size `output_size`. Finally, it compares the + two entity representations by dot product, element-wise multiplication, absolute difference and + bilinear product. It returns the final `b x h` tensor where `h` is the size of the concatenation + of the four comparisons. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + entity_1: + literature_references: + - https://ludwig.ai/0.6/configuration/combiner/#comparator-combiner + ui_display_name: Entity 1 + expected_impact: 3 + entity_2: + literature_references: + - https://ludwig.ai/0.6/configuration/combiner/#comparator-combiner + ui_display_name: Entity 2 + expected_impact: 3 + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + ui_display_name: null + expected_impact: 1 + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 3 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 15 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: "TRUE" + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +concat: + type: + short_description: Concatenates outputs of all encoders and passes concatenated representation through stack of fully connected layers. + long_description: + The concat combiner assumes all outputs from encoders are tensors of size `b x h` where `b` is + the batch size and `h` is the hidden dimension, which can differ for each input. It + concatenates along the `h` dimension, and then (optionally) passes the concatenated tensor + through a stack of fully connected layers. It returns the final `b x h` tensor where `h` is the + size of the last fully connected layer or the sum of the sizes of the `h` of all inputs in the + case there are no fully connected layers. If there is only a single input feature and no fully + connected layers, the output of the input feature encoder is passed through the combiner + unchanged. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + flatten_inputs: + ui_display_name: null + expected_impact: 1 + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + ui_display_name: null + expected_impact: 1 + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 3 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 16 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + residual: + ui_display_name: null + expected_impact: 1 + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: "TRUE" + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +project_aggregate: + type: + short_description: Projects the encoder outputs to a common size then takes the average. + long_description: + The project aggregate combiner projects the input vectors to a common size + and then aggregates them by taking the average across all the vectors. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + ui_display_name: null + expected_impact: 1 + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 3 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 17 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + projection_size: + ui_display_name: null + expected_impact: 1 + residual: + ui_display_name: null + expected_impact: 1 + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: "TRUE" + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +sequence: + type: + short_description: Stacks a sequence concat combiner with a sequence encoder. + long_description: + The sequence combiner stacks a sequence concat combiner with a sequence encoder. All the + considerations about input tensor ranks described for the sequence concat combiner apply also in + this case, but the main difference is that this combiner uses the `b x s x h` output of the + sequence concat combiner, where `b` is the batch size, `s` is the sequence length and `h` is the + sum of the hidden dimensions of all input features, as input for any of the sequence encoders + described in the sequence features encoders section. All considerations on the shape of + the outputs for the sequence encoders also apply to the sequence combiner. + encoder: + ui_display_name: null + expected_impact: 3 + main_sequence_feature: + ui_display_name: null + expected_impact: 3 + reduce_output: + ui_display_name: null + expected_impact: 1 +sequence_concat: + type: + short_description: Concatenates the outputs of multiple sequence features. + long_description: + The sequence_concat combiner assumes at least one output from the encoders is a tensor of size + `b x s x h` where `b` is the batch size, `s` is the length of the sequence and `h` is the hidden + dimension. A sequence-like (sequence, text or time series) input feature can be specified with + the `main_sequence_feature` parameter which takes the name of sequence-like input feature as its + value. If no `main_sequence_feature` is specified, the combiner will look through all the + features in the order they are defined in the configuration and will look for a feature with a + rank 3 tensor output (sequence, text or time series). If it cannot find one it will raise an + exception, otherwise the output of that feature will be used for concatenating the other features + along the sequence `s` dimension. + + If there are other input features with a rank 3 output tensor, the combiner will concatenate + them alongside the s dimension, which means that all of them must have identical s dimension, + otherwise a dimension mismatch error will be returned thrown during training when a datapoint + with two sequential features of different lengths are provided. + + Other features that have a b x h rank 2 tensor output will be replicated s times and + concatenated to the s dimension. The final output is a b x s x h' tensor where h' is the size of + the concatenation of the h dimensions of all input features. + main_sequence_feature: + ui_display_name: null + expected_impact: 3 + reduce_output: + ui_display_name: null + expected_impact: 1 +tabnet: + type: + short_description: Tabnet is specifically tailored for high performance on tabular data. + long_description: + The tabnet combiner implements the TabNet model, which uses attention and sparsity to achieve + high performance on tabular data. It assumes all outputs from encoders are tensors of size b x h + where b is the batch size and h is the hidden dimension, which can be different for each input. + If the input tensors have a different shape, it automatically flattens them. It returns the + final b x h' tensor where h' is the user-specified output size. + literature_references: + - https://arxiv.org/abs/1908.07442 + compute_tier: 1 + bn_epsilon: + default_value_reasoning: + Default value found in popular ML packages like Keras + and Tensorflow. + description_implications: + An epsilon is added to the denominator of the batch + normalization operation so that the function converges. Setting the epsilon + to 0 is inadvisable. + example_value: + - 1.0e-05 + expected_impact: 1 + literature_references: + - "[Keras example](https://keras.io/api/layers/normalization_layers/batch_normalization/)" + suggested_values: 1e-3-1e-9 + suggested_values_reasoning: Common epsilon choices + ui_display_name: Batch Normalization Epsilon + bn_momentum: + description_implications: + "Higher values result in faster updates, but more + sensitivity to noise in the dataset. Lower values result in slower updates. + + + If momentum is set to 0, moving statistics will not be updated during + training. This is likely to cause variance between train and test performance, + and is not recommended." + example_value: + - 0.05 + literature_references: + - "TabNet Paper: https://arxiv.org/abs/1908.07442" + - "Torch Batch Norm: https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html" + other_information: + "`bn_momentum` is only used if `norm`: `batch`. For other + values of `norm` it has no effect. + + + `bn_momentum` is different from optimizer momentum. Batch norm moving + estimate statistics are updated according to the rule: + + x_hat = (1 - momentum) * x_hat + momentum * x_t, + + where x_hat is the estimated statistic and x_t is the new observed value." + suggested_values: 0.01-0.2 + ui_display_name: Batch Norm Momentum + expected_impact: 1 + bn_virtual_bs: + default_value_reasoning: Paper default. + description_implications: + Virtual Batch Normalization is a normalization method + that extends batch normalization. Regular batch normalization causes the + output of a neural network for an input example to be highly dependent + on several other inputs in the same minibatch. To avoid this problem + in virtual batch normalization (VBN), each example is normalized based + on the statistics collected on a reference batch of examples that are + chosen once and fixed at the start of training, and on itself. The reference + batch is normalized using only its own statistics. VBN is computationally + expensive because it requires running forward propagation on two minibatches + of data, so the authors use it only in the generator network. A higher + virtual batch size could improve normalization, but it also causes training + to run slower since each batch will be sampled multiple times. + expected_impact: 1 + literature_references: + - https://paperswithcode.com/method/virtual-batch-normalization + ui_display_name: "Ghost Normalization: Virtual batch size" + dropout: + default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1908.07442). + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + entmax_alpha: + ui_display_name: null + expected_impact: 1 + entmax_mode: + ui_display_name: null + expected_impact: 1 + num_shared_blocks: + ui_display_name: null + expected_impact: 1 + num_steps: + ui_display_name: null + expected_impact: 1 + num_total_blocks: + ui_display_name: null + expected_impact: 1 + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 18 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + relaxation_factor: + ui_display_name: null + expected_impact: 1 + size: + ui_display_name: null + expected_impact: 3 + sparsity: + ui_display_name: null + expected_impact: 1 +tabtransformer: + type: + short_description: Projects and concatenates features, then passes them through a transformer. + long_description: + The tabtransformer combiner combines input features in the following sequence of operations. + Except for binary and number features, the combiner projects features to an embedding size. + These features are concatenated as if they were a sequence and passed through a transformer. + After the transformer, the number and binary features are concatenated (which are of size 1) and + then concatenated with the output of the transformer and is passed to a stack of fully connected + layers (from TabTransformer Tabular Data Modeling Using Contextual Embeddings). It assumes all + outputs from encoders are tensors of size `b x h` where `b` is the batch size and `h` is the + hidden dimension, which can be different for each input. If the input tensors have a different + shape, it automatically flattens them. It then projects each input tensor to the same hidden / + embedding size and encodes them with a stack of Transformer layers. Finally, the transformer + combiner applies a reduction to the outputs of the Transformer stack, followed by the above + concatenation and optional fully connected layers. The output is a `b x h` tensor where `h` is the + size of the last fully connected layer or the hidden / embedding size, or a `b x n x h` where `n` + is the number of input features and `h` is the hidden / embedding size if no reduction is applied. + literature_references: + - https://arxiv.org/abs/2012.06678 + compute_tier: 2 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1706.03762). + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embed_input_feature_name: + default_value_reasoning: + Though the ideal embedding size depends on the task + and dataset, setting the feature embedding size equal to the hidden size + and adding feature embeddings to hidden representations ('add') is a good + starting point. + description_implications: + Input feature name embeddings have been shown to + improve performance of deep learning methods on tabular data. Feature + name embeddings play a similar role to positional embeddings in a language + model, allowing the network to learn conditional dependencies between + input features. + example_value: + - 64 + literature_references: + - "TabTransformer: Tabular Data Modeling Using Contextual Embeddings" + other_information: + Must be an integer, 'add', or null. If an integer, specifies + the embedding size for input feature names. Input feature name embeddings + will be concatenated to hidden representations. Must be less than or equal + to hidden_size. If 'add', input feature names use embeddings the same + size as hidden_size, and are added (element-wise) to the hidden representations. + If null, input feature embeddings are not used. + related_parameters: + - hidden_size + ui_display_name: Embed Input Feature Name + expected_impact: 3 + fc_activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + example_value: + - relu + expected_impact: 1 + literature_references: + - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html + related_parameters: + - activation, activation_function, conv_activation, recurrent_activation + suggested_values: relu, alternatively leakyRelu or elu + suggested_values_reasoning: + The default value will work well in the majority + of the cases + ui_display_name: FC Activation + fc_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: FC Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + fc_residual: + ui_display_name: null + expected_impact: 1 + hidden_size: + default_value_reasoning: Not too big, not too small. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 2 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + ui_display_name: null + expected_impact: 1 + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 3 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_heads: + default_value_reasoning: + "The middle value explored in the original TabTransformer + paper. Source: https://arxiv.org/pdf/2012.06678.pdf" + description_implications: + Increasing the number of attention heads can increase + model performance at the cost of additional compute and memory. + example_value: + - 8 + expected_impact: 1 + literature_references: + - https://arxiv.org/pdf/2012.06678.pdf + suggested_values: 16 + suggested_values_reasoning: + If your model is underperforming, increasing the + number of attention heads can improve its ability to correlate items in + a sequence. + ui_display_name: Number of attention heads + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data. For + many data types, one layer is sufficient. + description_implications: + "The ideal number of transformer layers depends + on the length and complexity of input sequences, as well as the task. + + + For more complex tasks, and higher number of transformer layers may be + useful. However, too many layers will increase memory and slow training + while providing diminishing returns of model performance." + example_value: + - 1 + expected_impact: 1 + suggested_values: 1 - 12 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Transformer Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 19 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + reduce_output: + ui_display_name: null + expected_impact: 1 + transformer_output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 2 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 20 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Transformer Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: "TRUE" + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +transformer: + type: + short_description: The transformer combiner combines input features using a stack of Transformer blocks. + long_description: + The transformer combiner combines input features using a stack of Transformer blocks (from + Attention Is All You Need). It assumes all outputs from encoders are tensors of size `b x h` + where `b` is the batch size and `h` is the hidden dimension, which can be different for each + input. If the input tensors have a different shape, it automatically flattens them. It then + projects each input tensor to the same hidden / embedding size and encodes them with a stack of + Transformer layers. Finally, the transformer combiner applies a reduction to the outputs of the + Transformer stack, followed by optional fully connected layers. The output is a `b x h` tensor + where `h` is the size of the last fully connected layer or the hidden / embedding size, or a + `b x n x h` where `n` is the number of input features and `h` is the hidden / embedding size if + no reduction is applied. + literature_references: + - https://arxiv.org/abs/1706.03762 + compute_tier: 2 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1706.03762). + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + fc_activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + example_value: + - relu + expected_impact: 1 + literature_references: + - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html + related_parameters: + - activation, activation_function, conv_activation, recurrent_activation + suggested_values: relu, alternatively leakyRelu or elu + suggested_values_reasoning: + The default value will work well in the majority + of the cases + ui_display_name: FC Activation + fc_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: FC Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + fc_residual: + ui_display_name: null + hidden_size: + default_value_reasoning: Not too big, not too small. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 2 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + ui_display_name: null + expected_impact: 1 + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 3 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_heads: + ui_display_name: null + expected_impact: 1 + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data. For + many data types, one layer is sufficient. + description_implications: + "The ideal number of transformer layers depends + on the length and complexity of input sequences, as well as the task. + + + For more complex tasks, and higher number of transformer layers may be + useful. However, too many layers will increase memory and slow training + while providing diminishing returns of model performance." + example_value: + - 1 + expected_impact: 1 + suggested_values: 1 - 12 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Transformer Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 21 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + reduce_output: + ui_display_name: null + expected_impact: 1 + transformer_output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 2 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 22 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Transformer Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: "TRUE" + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer diff --git a/ludwig/schema/metadata/configs/common.yaml b/ludwig/schema/metadata/configs/common.yaml new file mode 100644 index 0000000..d419b51 --- /dev/null +++ b/ludwig/schema/metadata/configs/common.yaml @@ -0,0 +1,348 @@ +activation: + default_value_reasoning: The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: relu + suggested_values_reasoning: ReLU will work well in the majority of the cases + ui_display_name: Activation +bias_initializer: + default_value_reasoning: It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer +dropout: + default_value_reasoning: Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout +fc_layers: + default_value_reasoning: By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers +flatten_inputs: + ui_display_name: null + expected_impact: 1 +norm: + default_value_reasoning: While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values_reasoning: Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type +norm_params: + ui_display_name: null + expected_impact: 1 +num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 3 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers +output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 16 - 1024 + suggested_values_reasoning: Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size +residual: + ui_display_name: null + expected_impact: 1 +use_bias: + default_value_reasoning: "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: "TRUE" + ui_display_name: Use Bias +weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +embedding_initializer: + default_value_reasoning: According to https://arxiv.org/abs/1711.09160, choice + of embedding initialization is not important as long as the variance is + kept reasonably low. + description_implications: + According to https://arxiv.org/abs/1711.09160, choice + of embedding initialization is not important as long as the variance is + kept reasonably low. + example_value: + - kaiming + expected_impact: 1 + literature_references: + - https://arxiv.org/abs/1711.09160 + suggested_values: kaiming + suggested_values_reasoning: https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326 + ui_display_name: Embedding Initialization +embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size +embeddings_on_cpu: + default_value_reasoning: By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU +embeddings_trainable: + default_value_reasoning: + If trained from scratch, embedding vectors are typically + learned alongside the rest of the model. + description_implications: + Typically this value is only set to False if pre-trained + embeddings are uploaded. Even then, it is reasonable to leave it as True + in order to fine-tune the embeddings. + expected_impact: 1 + related_parameters: + - embedding_size, representation, pretrained_embeddings + ui_display_name: (under Embeddings header) Trainable? +pretrained_embeddings: + default_value_reasoning: Embeddings are commonly trained from scratch, or + incorporated as part of a pre-trained model package. + description_implications: If pretrained embeddings are specified, then the + model may have a head start in its representation of various input entities. + example_value: + - ~/Downloads/glove.6B.100d.txt + expected_impact: 0 + related_parameters: + - embedding_size, embeddings_trainable + ui_display_name: Pretrained embeddings path +max_sequence_length: + default_value_reasoning: Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null +vocab: + default_value_reasoning: Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed +vocab_size: + internal_only: true + ui_display_name: Not displayed +representation: + default_value_reasoning: Trainable, randomly initialized embedding vectors + often lead to more subtle representations of input entities than one-hot + vectors. + description_implications: If set to sparse, the representations for input + entities are fixed as one-hot vectors. This leads to less flexible representations + for input entities, but could lead to faster training since there are + less learnable parameters. + expected_impact: 1 + other_information: "" + related_parameters: + - embedding_size, embeddings_trainable, pretrained_embeddings + ui_display_name: Representation approach +reduce_output: + default_value_reasoning: Sums the tensors along the sequence dimension. + description_implications: "\"last\", \"sum\", \"mean\", and \"max\" are the\ + \ fastest and most memory-efficient operations\u2013 they result in tensors\ + \ that are the same-size as a single item in the input sequence. However,\ + \ these are simple aggregation operations, therefore some information\ + \ may be lost. \n\n\"concat\" concatenates each tensor together, creating\ + \ a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves\ + \ this information, but can be very memory-intensive and should only be\ + \ applied if the sequence length and/or tensor size is small. \n\n\"attention\"\ + \ takes a weighted sum of the items in the sequence, where the weights\ + \ for each item in the sequence are determined by the model on-the-fly\ + \ based on the features of the item itself. This is both slower and and\ + \ more memory-intensive than the other operations; however, it can also\ + \ provide a richer \"global\" representation of the sequence." + expected_impact: 1 + related_parameters: + - max_sequence_length + suggested_values: '"attention". This and the default covers 95% of use cases.' + suggested_values_reasoning: If you would like better performance and are not + compute/memory-constrained, attention-based reduction can potentially + provide a richer global representation than the default, but note that attention + reduction does not work with `cache_encoder_embeddings=true`. + ui_display_name: Sequence Reducer diff --git a/ludwig/schema/metadata/configs/decoders.yaml b/ludwig/schema/metadata/configs/decoders.yaml new file mode 100644 index 0000000..0d89aff --- /dev/null +++ b/ludwig/schema/metadata/configs/decoders.yaml @@ -0,0 +1,703 @@ +BaseDecoder: + type: + expected_impact: 1 + fc_layers: + expected_impact: 1 + num_fc_layers: + expected_impact: 3 + fc_output_size: + expected_impact: 3 + fc_use_bias: + expected_impact: 1 + fc_weights_initializer: + expected_impact: 1 + fc_bias_initializer: + expected_impact: 1 + fc_norm: + expected_impact: 2 + fc_norm_params: + expected_impact: 1 + fc_activation: + expected_impact: 2 + fc_dropout: + expected_impact: 3 +Classifier: + type: + short_description: + Projects combiner output to a vector the size of the number of available classes. + long_description: + The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a + projection into a vector of size of the number of available classes, followed by a sigmoid. + expected_impact: 0 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + num_classes: + other_information: Internal Only + internal_only: true + ui_display_name: Not Displayed + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: true + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +MLPClassifier: + type: + short_description: + Multi-layer perceptron classifier decoder. + long_description: + The MLPClassifier decoder stacks one or more fully-connected hidden layers + (each of configurable size, activation, and dropout) before a final linear + projection to class logits. Use this when the combiner output benefits from + additional non-linear transformation before the classification head. When + num_fc_layers=0 it is equivalent to the standard Classifier decoder. Supports + optional temperature-scaling calibration (Guo et al., ICML 2017) and MC Dropout + uncertainty estimation (Gal & Ghahramani, ICML 2016). + expected_impact: 2 + num_fc_layers: + expected_impact: 3 + ui_display_name: Number of Hidden Layers + output_size: + expected_impact: 3 + ui_display_name: Hidden Layer Size + activation: + expected_impact: 2 + ui_display_name: Activation + dropout: + expected_impact: 3 + ui_display_name: Dropout + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + num_classes: + other_information: Internal Only + internal_only: true + ui_display_name: Not Displayed + use_bias: + expected_impact: 1 + ui_display_name: Use Bias + weights_initializer: + expected_impact: 1 + ui_display_name: Layer Weights Initializer + bias_initializer: + expected_impact: 1 + ui_display_name: Bias Initializer + calibration: + expected_impact: 2 + ui_display_name: Calibration Method + mc_dropout_samples: + expected_impact: 2 + ui_display_name: MC Dropout Samples +Projector: + type: + short_description: + Projects combiner output into an output vector. + long_description: + The Projector decoder is a (potentially empty) stack of fully connected layers, followed by a + projection into a tensor of the vector size (optionally followed by a softmax in the case of + multi-class classification). + expected_impact: 0 + activation: + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + clip: + ui_display_name: null + expected_impact: 1 + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: true + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +Regressor: + type: + short_description: + Projects combiner output to a single number. + long_description: + The regressor decoder is a (potentially empty) stack of fully connected layers, followed by a + projection to a single number. + expected_impact: 0 + activation: + ui_display_name: null + expected_impact: 2 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: true + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster." + - "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +PassthroughDecoder: + type: + short_description: + Provides the raw input from the combiner. + long_description: + The passthrough decoder simply returns the raw output coming from the combiner. + expected_impact: 0 + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed +SequenceGeneratorDecoder: + type: + short_description: + Generates a sequence by sampling from the model. + long_description: + The generator decoder is a (potentially empty) stack of fully connected layers, followed by an + RNN that generates outputs feeding on its own previous predictions and generates a tensor of + size `b x s' x c`, where `b` is the batch size, `s'` is the length of the generated sequence and + `c` is the number of classes, followed by a softmax_cross_entropy. During training teacher + forcing is adopted, meaning the list of targets is provided as both inputs and outputs (shifted + by 1), while at evaluation time greedy decoding (generating one token at a time and feeding it + as input for the next step) is performed by beam search, using a beam of 1 by default. In + general a generator expects a `b x h` shaped input tensor, where `h` is a hidden dimension. The + `h` vectors are (after an optional stack of fully connected layers) fed into the rnn generator. + One exception is when the generator uses attention, as in that case the expected size of the + input tensor is `b x s x h`, which is the output of a sequence, text or time series input + feature without reduced outputs or the output of a sequence-based combiner. If a `b x h` input + is provided to a generator decoder using an RNN with attention instead, an error will be raised + during model building. + expected_impact: 0 + cell_type: + ui_display_name: null + expected_impact: 3 + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + max_sequence_length: + expected_impact: 3 + ui_display_name: null + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data and + task. For many data types, one layer is sufficient. + description_implications: + Increasing the number of layers may improve model + performance for longer sequences or more complex tasks. + example_value: + - 1 + expected_impact: 3 + suggested_values: 1-3 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Recurrent Layers + reduce_input: + description_implications: + "\u201Clast\u201D: Reduces tensor by taking the\ + \ last non-zero element per sequence in the sequence dimension.\n\u201C\ + sum\u201D: Reduces tensor by summing across the sequence dimension.\n\u201C\ + mean\u201D: Reduces tensor by taking the mean of the sequence dimension.\n\ + \u201Cavg\u201D: synonym for \u201Cmean\u201D.\n\u201Cmax\u201D: Reduces\ + \ tensor by taking the maximum value of the last dimension across the\ + \ sequence dimension.\n\u201Cconcat\u201D: Reduces tensor by concatenating\ + \ the second and last dimension.\n\u201Cattention\u201D: Reduces tensor\ + \ by summing across the sequence dimension after applying feedforward\ + \ attention.\n\u201Cnone\u201D: no reduction." + expected_impact: 2 + ui_display_name: Combiner Reduce Mode + vocab_size: + ui_display_name: Not displayed +SequenceTaggerDecoder: + type: + short_description: + Used for classifying each element of an input sequence. + long_description: + The tagger decoder is a (potentially empty) stack of fully connected layers, + followed by a projection into a tensor of size `b x s x c`, where `b` is the batch size, + `s` is the length of the sequence and `c` is the number of classes, followed by a + `softmax_cross_entropy`. + + This decoder requires its input to be shaped as `b x s x h`, where `h` is + a hidden dimension, which is the output of a sequence, text or time series input feature without + reduced outputs or the output of a sequence-based combiner. This can be done by ensuring that + at least one of the sequence, text or time series input feature's encoders has `reduce_output` set to + `None`. This will prevent a `b x h` input from being provided to this decoder and an error + from being raised during model building. + + The tagger decoder also requires the `reduce_input` parameter of the output feature to be set to `None`. + If this is not set, Ludwig will automatically override the value by setting it to None and log a warning. + expected_impact: 0 + attention_embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + Increasing the embedding size may cause the model + to train more slowly, but the higher dimensionality can also improve overall + quality. + expected_impact: 2 + suggested_values: 128 - 2048 + suggested_values_reasoning: + Try models with smaller or larger embedding sizes + to observe relative impact. + ui_display_name: Attention Embedding Size + attention_num_heads: + ui_display_name: null + expected_impact: 1 + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + max_sequence_length: + expected_impact: 3 + ui_display_name: null + use_attention: + ui_display_name: null + expected_impact: 1 + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab_size: + ui_display_name: Not displayed + internal_only: true +UNetDecoder: + type: + short_description: The UNet decoder convolutional and up-conv layers + long_description: + Stacks of two 2D convolutional layers with optional normalization + and relu activation, preceeded by an up-conv layer in all but the + final level of the decoder. + compute_tier: 1 + conv_norm: + expected_impact: 2 + ui_display_name: Convolutional Normalization + height: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + num_channels: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + num_classes: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + width: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + num_stages: + short_description: Number of up-sampling stages in the UNet decoder. + long_description: + Controls how many encoder/decoder stage pairs are built. The input + image dimensions must be divisible by 2^num_stages. Larger values + allow the network to capture features at more spatial scales but + require more memory and compute. + expected_impact: 2 + ui_display_name: Number of Stages +SegFormerDecoder: + type: + short_description: Lightweight MLP decoder head for semantic segmentation. + long_description: + A simple all-MLP decoder head inspired by the SegFormer architecture + (Xie et al., NeurIPS 2021). Takes a single feature map from the combiner, + projects it through MLP layers, and bilinearly upsamples to the target + output resolution. Efficient and well-suited for transformer encoder + backbones. + compute_tier: 1 + hidden_size: + expected_impact: 2 + ui_display_name: MLP Hidden Size + dropout: + expected_impact: 1 + ui_display_name: Dropout + height: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + width: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + num_classes: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + num_channels: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED +FPNDecoder: + type: + short_description: Feature Pyramid Network decoder for multi-scale segmentation. + long_description: + A Feature Pyramid Network (FPN) decoder (Lin et al., CVPR 2017) that builds + a multi-scale feature pyramid using lateral connections and a top-down pathway. + All pyramid levels are upsampled to the finest scale and merged, then a 1x1 + convolution produces the final segmentation logits. Well-suited for detecting + and segmenting objects at multiple scales. + compute_tier: 2 + num_channels: + short_description: Number of channels in each FPN level. + long_description: + All feature maps in the pyramid are projected to this many channels via 1x1 + convolutions before the top-down merging pass. Larger values increase model + capacity but also memory and compute cost. + expected_impact: 2 + ui_display_name: FPN Channels + num_levels: + short_description: Number of pyramid levels to build. + long_description: + Controls how many lateral plus top-down stages are applied. More levels capture + coarser-scale context but require more parameters. Typical range is 2-5. + expected_impact: 2 + ui_display_name: Number of FPN Levels + height: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + width: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed + num_classes: + default_value_reasoning: + Computed internally, automatically, based on image data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED +AnomalyDecoder: + type: + short_description: + Computes squared distance to hypersphere center as anomaly score. + long_description: + The AnomalyDecoder computes ||z - c||^2 as the anomaly score, where z is the + encoder/combiner output and c is the hypersphere center initialized after the + first epoch as the mean of all encoder outputs. Implements the geometric core + of Deep SVDD (Ruff et al., ICML 2018). + expected_impact: 0 + input_size: + other_information: Internal Only + internal_only: true + related_parameters: + - "No" + ui_display_name: Not Displayed diff --git a/ludwig/schema/metadata/configs/encoders.yaml b/ludwig/schema/metadata/configs/encoders.yaml new file mode 100644 index 0000000..2bf87fa --- /dev/null +++ b/ludwig/schema/metadata/configs/encoders.yaml @@ -0,0 +1,9224 @@ +BaseEncoder: + skip: + internal_only: true + other_information: Internal Only + ui_display_name: Not Displayed +HFEncoder: + trainable: + default_value_reasoning: + Trainable is disabled by default to make the model useful for generating fast baselines, which can be + further sped up by setting `preprocessing.cache_encoder_embeddings`. In many cases strong performance + can be achieved without adjusting the weights of the pretrained model, but for best performance we + recommend setting this to true. + description_implications: + "Ludwig currently supports two variations on fine-tuning, configured via the trainable encoder parameter: + (1) modifying the weights of the pretrained encoder to adapt them to the downstream task (trainable=true), + or (2) keeping the pretrained encoder weights fixed and training a stack of dense layers that sit + downstream as the combiner and decoder modules (trainable=false, default). This is sometimes distinguished + as transfer learning. Allowing the weights to be modified by setting trainable=true can significantly + improve performance on the downstream task, but will take significantly longer to train (due to the + additional backward passes over the pretrained model parameters). Additionally, more care needs to be + taken when selecting hyperparameters when trainable=true to prevent + [catastrophic forgettng](https://en.wikipedia.org/wiki/Catastrophic_interference), whereby the + model forgets all of the valuable information it learned during pretraining." + expected_impact: 3 + literature_references: + - http://d2l.ai/chapter_computer-vision/fine-tuning.html" + related_parameters: + - use_pretrained, pretrained_model, saved_weights_in_checkpoint + suggested_values: + - false + suggested_values_reasoning: + Freezing the weights (i.e. `trainable = False`) + is only worth trying if you are loading in pretrained weights. In that + case, check to see if your model is overfitting. If so, freezing the weights + (and therefore reducing model complexity) may be beneficial. + ui_display_name: Trainable + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + reduce_output: + default_value_reasoning: Sums the tensors along the sequence dimension. + description_implications: + "\"last\", \"sum\", \"mean\", and \"max\" are the\ + \ fastest and most memory-efficient operations\u2013 they result in tensors\ + \ that are the same-size as a single item in the input sequence. However,\ + \ these are simple aggregation operations, therefore some information\ + \ may be lost. \n\n\"concat\" concatenates each tensor together, creating\ + \ a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves\ + \ this information, but can be very memory-intensive and should only be\ + \ applied if the sequence length and/or tensor size is small. \n\n\"attention\"\ + \ takes a weighted sum of the items in the sequence, where the weights\ + \ for each item in the sequence are determined by the model on-the-fly\ + \ based on the features of the item itself. This is both slower and and\ + \ more memory-intensive than the other operations; however, it can also\ + \ provide a richer \"global\" representation of the sequence." + expected_impact: 1 + related_parameters: + - max_sequence_length + suggested_values: '"attention". This and the default covers 95% of use cases.' + suggested_values_reasoning: + If you would like better performance and are not + compute/memory-constrained, attention-based reduction can potentially + provide a richer global representation than the default. + ui_display_name: Sequence Reducer +ALBERT: + type: + short_description: Similar to BERT with lower memory footprint and faster training. + long_description: + The `albert` encoder loads a pretrained [ALBERT](https://arxiv.org/abs/1909.11942) (default `albert-base-v2`) model + using the Hugging Face transformers package. Albert is similar to BERT, with significantly lower memory usage and + somewhat faster training time:. + compute_tier: 2 + attention_probs_dropout_prob: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - hidden_dropout_prob, classifier_dropout_prob + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_probs_dropout_prob + bos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Beginning-of-Sentence Token Id + expected_impact: 1 + classifier_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - hidden_dropout_prob, attention_probs_dropout_prob + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: classifier_dropout_prob + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 1 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + eos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: End-of-Sentence Token Id + expected_impact: 1 + hidden_act: + default_value_reasoning: Taken from huggingface. + description_implications: + Changing this activation function will only affect + the feed-forward layers of the transformer. + example_value: + - relu + expected_impact: 1 + literature_references: + - "[Hugging face docs for ALBERT config](https://huggingface.co/docs/transformers/model_doc/albert#transformers.AlbertConfig.hidden_act)\n\ + \r\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + suggested_values: gelu + suggested_values_reasoning: Taken from huggingface defaults. + ui_display_name: Hidden Layer Activation + hidden_dropout_prob: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "attention_probs_dropout_prob, + + classifier_dropout_prob" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: hidden_dropout_prob + hidden_size: + default_value_reasoning: Huggingface default. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 1 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + inner_group_num: + ui_display_name: null + expected_impact: 1 + intermediate_size: + ui_display_name: null + expected_impact: 1 + layer_norm_eps: + ui_display_name: null + expected_impact: 1 + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 1 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_attention_heads: + ui_display_name: null + expected_impact: 1 + num_hidden_groups: + ui_display_name: null + expected_impact: 1 + num_hidden_layers: + ui_display_name: null + expected_impact: 1 + pad_token_id: + ui_display_name: null + expected_impact: 1 + position_embedding_type: + ui_display_name: null + expected_impact: 1 + pretrained_kwargs: + default_value_reasoning: These arguments typically don't need to be specified. + expected_impact: 1 + related_parameters: + - pretrained_model_name_or_path + suggested_values: Default + ui_display_name: null + pretrained_model_name_or_path: + default_value_reasoning: + The default model is the canonical model for this + model architecture, and is therefore a good starting point for most use + cases. + description_implications: + "There are two factors to consider when choosing\ + \ a pre-trained model: (1) size, and (2) task similarity. \n\nThe larger\ + \ the model, the more subtle its comprehension of inputs can become. However,\ + \ larger models are also more compute and memory-intensive to train.\n\ + \nModels pretrained on highly-related source tasks are more likely to\ + \ be successful on the target task. Consider searching the HuggingFace\ + \ model repository for models trained on similar tasks." + expected_impact: 2 + literature_references: + - https://arxiv.org/abs/1909.11942 + related_parameters: + - use_pretrained, trainable, pretrained_kwargs + suggested_values: albert-large-v2, albert-base-chinese + suggested_values_reasoning: + "If you would like better performance and are + not compute/memory-constrained, increasing model capacity can potentially + provide a richer representation than the default. The suggested value + upsizes the model while maintaining the same model architecture. + + + Language models trained on general corpora typically generalize well. + Consider deviating from the default only if the text in the dataset originates + from another domain (e.g. languages other than English)." + ui_display_name: Pretrained model + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + type_vocab_size: + ui_display_name: null + expected_impact: 1 + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +AutoTransformer: + type: + short_description: Automatically retrieves the architecture from the provided model name/path. + long_description: + The `auto_transformer` encoder automatically instantiates the model architecture for the specified + `pretrained_model_name_or_path`. Unlike the other HF encoders, `auto_transformer` does not provide a default value for + `pretrained_model_name_or_path`, this is its only mandatory parameter. See the Hugging Face + [AutoModels documentation](https://huggingface.co/docs/transformers/model_doc/auto) for more details. + literature_references: + - https://huggingface.co/docs/transformers/model_doc/auto + compute_tier: 2 + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + expected_impact: 1 + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 3 + reduce_output: + ui_display_name: null + expected_impact: 1 + trainable: + expected_impact: 3 + ui_display_name: null + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +BERT: + type: + short_description: Bidirectional transformer great for language modeling. + long_description: + The bert encoder loads a pretrained BERT (default bert-base-uncased) model using the Hugging + Face transformers package. BERT is a bidirectional transformer pretrained using a combination of + masked language modeling objective and next sentence prediction on a large corpus comprising the + Toronto Book Corpus and Wikipedia. + literature_references: + - https://arxiv.org/abs/1810.04805 + compute_tier: 2 + attention_probs_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - hidden_dropout_prob, classifier_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_probs_dropout_prob + classifier_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - hidden_dropout_prob, attention_probs_dropout_prob + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: classifier_dropout + gradient_checkpointing: + ui_display_name: null + expected_impact: 1 + hidden_act: + default_value_reasoning: Taken from huggingface. + description_implications: + Changing this activation function will only affect + the feed-forward layers of the transformer. + example_value: + - relu + expected_impact: 1 + literature_references: + - "[Huggingface docs for BERT config](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertConfig.hidden_act)\n\ + \r\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + suggested_values: gelu + suggested_values_reasoning: Taken from huggingface defaults. + ui_display_name: Hidden Layer Activation + hidden_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - attention_probs_dropout_prob, classifier_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: hidden_dropout_prob + hidden_size: + default_value_reasoning: Huggingface default. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 1 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + intermediate_size: + ui_display_name: null + expected_impact: 1 + layer_norm_eps: + ui_display_name: null + expected_impact: 1 + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 2 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_attention_heads: + ui_display_name: null + expected_impact: 1 + num_hidden_layers: + ui_display_name: null + expected_impact: 1 + pad_token_id: + ui_display_name: null + expected_impact: 1 + position_embedding_type: + ui_display_name: null + expected_impact: 1 + pretrained_kwargs: + ui_display_name: null + expected_impact: 1 + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + type_vocab_size: + ui_display_name: null + expected_impact: 1 + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +ModernBERT: + type: + short_description: Modern BERT with Flash Attention 2, RoPE, and 8192 token context. + long_description: + The modernbert encoder loads a pretrained ModernBERT (default answerdotai/ModernBERT-base) + model using the Hugging Face transformers package. ModernBERT is the first major architectural + upgrade to BERT, incorporating Flash Attention 2, Rotary Position Embeddings (RoPE), GeGLU + activations, unpadding for efficiency, and alternating local/global attention. It supports up + to 8192 tokens (vs BERT's 512). + literature_references: + - https://arxiv.org/abs/2412.13663 + compute_tier: 2 + hidden_act: + default_value_reasoning: Taken from huggingface. + description_implications: + Changing this activation function will only affect + the feed-forward layers of the transformer. + expected_impact: 1 + suggested_values: gelu + suggested_values_reasoning: Taken from huggingface defaults. + ui_display_name: Hidden Layer Activation + hidden_dropout_prob: + default_value_reasoning: ModernBERT uses no dropout by default. + expected_impact: 2 + ui_display_name: hidden_dropout_prob + hidden_size: + default_value_reasoning: Huggingface default for ModernBERT-base. + expected_impact: 1 + ui_display_name: Hidden Size + initializer_range: + expected_impact: 1 + ui_display_name: null + intermediate_size: + ui_display_name: null + expected_impact: 1 + layer_norm_eps: + ui_display_name: null + expected_impact: 1 + max_position_embeddings: + default_value_reasoning: ModernBERT supports up to 8192 tokens. + expected_impact: 2 + ui_display_name: Max Position Embeddings + max_sequence_length: + internal_only: true + ui_display_name: null + num_attention_heads: + ui_display_name: null + expected_impact: 1 + num_hidden_layers: + ui_display_name: null + expected_impact: 1 + pad_token_id: + ui_display_name: null + expected_impact: 1 + pretrained_kwargs: + ui_display_name: null + expected_impact: 1 + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + internal_only: true + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained model. + expected_impact: 3 + ui_display_name: Use Pretrained + vocab: + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +BagEmbedWeighted: + type: + short_description: Transforms feature to vector, maps to sparse or dense embeddings, then aggregates. + long_description: + The embed weighted encoder first transforms the element frequency vector to sparse integer + lists, which are then mapped to either dense or sparse embeddings (one-hot encodings). Lastly, + embeddings are aggregated as a weighted sum where each embedding is multiplied by its respective + element's frequency. Inputs are of size b while outputs are of size b x h where b is the batch + size and h is the dimensionality of the embeddings. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + default_value_reasoning: + If trained from scratch, embedding vectors are typically + learned alongside the rest of the model. + description_implications: + Typically this value is only set to False if pre-trained + embeddings are uploaded. Even then, it is reasonable to leave it as True + in order to fine-tune the embeddings. + expected_impact: 1 + related_parameters: + - embedding_size, representation, pretrained_embeddings + ui_display_name: (under Embeddings header) Trainable? + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + force_embedding_size: + default_value_reasoning: + It is not often the case that the user has a strict + need for using an embedding size that should be larger than the vocabulary + size. + description_implications: + Should only be True if the user has a strict need + for using an embedding size that should be larger than the vocabulary + size. For example, there may be size requirements across multiple features + imposed by downstream modules like the ComparatorCombiner. + expected_impact: 1 + related_parameters: + - embedding_size + suggested_values: + - false + suggested_values_reasoning: True for advanced usage only. + ui_display_name: Force Embedding Size + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + pretrained_embeddings: + default_value_reasoning: + Embeddings are commonly trained from scratch, or + incorporated as part of a pre-trained model package. + description_implications: + If pretrained embeddings are specified, then the + model may have a head start in its representation of various input entities. + example_value: + - ~/Downloads/glove.6B.100d.txt + expected_impact: 0 + related_parameters: + - embedding_size, embeddings_trainable + ui_display_name: Pretrained embeddings path + representation: + default_value_reasoning: + Trainable, randomly initialized embedding vectors + often lead to more subtle representations of input entities than one-hot + vectors. + description_implications: + If set to sparse, the representations for input + entities are fixed as one-hot vectors. This leads to less flexible representations + for input entities, but could lead to faster training since there are + less learnable parameters. + expected_impact: 1 + other_information: "" + related_parameters: + - embedding_size, embeddings_trainable, pretrained_embeddings + ui_display_name: Representation approach + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from published [literature](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +CTRL: + type: + short_description: Language model trained to condition on control codes that govern style, content and task-specific behavior. + long_description: + The `ctrl` encoder loads a pretrained [CTRL](https://arxiv.org/abs/1909.05858) (default `ctrl`) model using the Hugging + Face transformers package. CTRL is a conditional transformer language model trained to condition on control codes that + govern style, content, and task-specific behavior. + literature_references: + - https://arxiv.org/abs/1909.05858 + compute_tier: 2 + attn_pdrop: + ui_display_name: null + dff: + ui_display_name: null + embd_pdrop: + ui_display_name: null + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + layer_norm_epsilon: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + n_ctx: + ui_display_name: null + n_embd: + ui_display_name: null + n_head: + ui_display_name: null + n_layer: + ui_display_name: null + n_positions: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + resid_pdrop: + ui_display_name: null + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +CamemBERT: + type: + short_description: Language model trained on large French text corpus. + long_description: + The `camembert` encoder loads a pretrained [CamemBERT](https://arxiv.org/abs/1911.03894) + (default `jplu/tf-camembert-base`) model using the Hugging Face transformers package. CamemBERT is pre-trained on a + large French language web-crawled text corpus. + literature_references: + - https://arxiv.org/abs/1911.03894 + compute_tier: 2 + attention_probs_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - classifier_dropout, hidden_dropout_prob + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_probs_dropout_prob + classifier_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - attention_probs_dropout_prob, hidden_dropout_prob + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: classifier_dropout + gradient_checkpointing: + ui_display_name: null + hidden_act: + default_value_reasoning: Taken from huggingface. + description_implications: + Changing this activation function will only affect + the feed-forward layers of the transformer. + example_value: + - relu + expected_impact: 1 + literature_references: + - "[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + suggested_values: gelu + suggested_values_reasoning: Taken from huggingface defaults. + ui_display_name: Hidden Layer Activation + hidden_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "attention_probs_dropout_prob, \nclassifier_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: hidden_dropout_prob + hidden_size: + default_value_reasoning: Huggingface default. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 1 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + intermediate_size: + ui_display_name: null + layer_norm_eps: + ui_display_name: null + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 2 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_attention_heads: + ui_display_name: null + num_hidden_layers: + ui_display_name: null + pad_token_id: + ui_display_name: null + position_embedding_type: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + type_vocab_size: + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +CategoricalEmbed: + type: + short_description: Maps the categorical feature to a dense embedding. + long_description: + The dense encoder maps to a dense embedding and is returned as outputs of size `b x h`, + where `b` is the batch size and `h` is the dimensionality of the embeddings. + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_initializer: + default_value_reasoning: + According to https://arxiv.org/abs/1711.09160, choice + of embedding initialization is not important as long as the variance is + kept reasonably low. + description_implications: + According to https://arxiv.org/abs/1711.09160, choice + of embedding initialization is not important as long as the variance is + kept reasonably low. + example_value: + - kaiming + expected_impact: 1 + literature_references: + - https://arxiv.org/abs/1711.09160 + suggested_values: kaiming + suggested_values_reasoning: https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326 + ui_display_name: Embedding Initialization + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed +CategoricalSparse: + type: + short_description: Maps the categorical feature to a sparse embedding. + long_description: + The sparse encoder maps to a sparse embedding (one-hot encodings) and is returned as outputs of + size `b x h`, where `b` is the batch size and `h` is the dimensionality of the embeddings. + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_initializer: + default_value_reasoning: + According to https://arxiv.org/abs/1711.09160, choice + of embedding initialization is not important as long as the variance is + kept reasonably low. + description_implications: + According to https://arxiv.org/abs/1711.09160, choice + of embedding initialization is not important as long as the variance is + kept reasonably low. + example_value: + - kaiming + expected_impact: 1 + literature_references: + - https://arxiv.org/abs/1711.09161 + suggested_values: kaiming + suggested_values_reasoning: https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/327 + ui_display_name: Embedding Initialization + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed +DateEmbed: + type: + short_description: Embeds the date elements passes them through fully connected layers. + long_description: + The Embed encoder passes the year through a fully connected layer of one neuron and embeds all + other elements for the date, concatenates them and passes the concatenated representation + through fully connected layers. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +DateWave: + type: + short_description: Embeds the date elements by taking the cosine of their value before passing through fully connected layers. + long_description: + The Wave encoder passes the year through a fully connected layer of one neuron and represents + all other elements for the date by taking the cosine of their value with a different period (12 + for months, 31 for days, etc.), concatenates them and passes the concatenated representation + through fully connected layers. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +DenseEncoder: + type: + short_description: Passes the raw numerical values through fully connected layers. + long_description: + The dense encoder passes the raw numerical values through fully connected layers. In this case + the inputs of size `b` are transformed to size `b x h`. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + input_size: + internal_only: true + other_information: Internal Only + related_parameters: + - "No" + ui_display_name: Not Displayed + fc_layers: + ui_display_name: null + expected_impact: 1 + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data. For + many data types, one layer is sufficient. + description_implications: + "Increasing the number of layers may improve model + performance by allowing the model to synthesize learned features derived + from the original input. If the input is simple, ex. a category with a + few options, increasing the number of layers has no benefit. For more + complex inputs, additional layers add more 'processing power' to extract + useful information from the input. + + + However, more layers will increase training time and may reduce accuracy + due to overfitting." + example_value: + - 1 + expected_impact: 3 + other_information: + If you have multiple input features, varying the number + of layers in the combiner or output feature decoder will have more impact. + related_parameters: + - layers + suggested_values: 1-3 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + use_bias: + ui_display_name: null + expected_impact: 1 + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +DistilBERT: + type: + short_description: A distilled version of BERT base that is 40% smaller and 60% faster with 95% of performance preserved. + long_description: + The `distilbert` encoder loads a pretrained [DistilBERT](https://medium.com/huggingface/distilbert-8cf3380435b5) + (default `distilbert-base-uncased`) model using the Hugging Face transformers package. DistilBERT is a small, fast, cheap and light + Transformer model trained by distilling BERT base. It has 40% less parameters than + bert-base-uncased, runs 60% faster while preserving over 95% of BERT’s performances as measured + on the GLUE language understanding benchmark. + compute_tier: 2 + activation: + default_value_reasoning: + This is the default activation function used in the + Distillbert huggingface implementation + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + attention_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout, qa_dropout, seq_classif_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_dropout + dim: + ui_display_name: null + dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "attention_dropout, + + qa_dropout, + + seq_classif_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout + hidden_dim: + ui_display_name: null + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 2 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + n_heads: + ui_display_name: null + n_layers: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + qa_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout, attention_dropout, seq_classif_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: qa_dropout + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + seq_classif_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "dropout, + + attention_dropout, + + qa_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: seq_classif_dropout + sinusoidal_pos_embds: + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +ELECTRA: + type: + short_description: Transformer encoder that can be used to encode a sequence of tokens with little compute + long_description: + The `electra`` encoder loads a pretrained [ELECTRA](https://openreview.net/pdf?id=r1xMH1BtvB) model using the Hugging Face transformers package. + ELECTRA is a new pretraining approach which trains two transformer models the generator and the + discriminator. The generator’s role is to replace tokens in a sequence, and is therefore trained + as a masked language model. The discriminator, which is the model we’re interested in, tries to + identify which tokens were replaced by the generator in the sequence. + literature_references: + - https://openreview.net/pdf?id=r1xMH1BtvB + compute_tier: 2 + attention_probs_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - hidden_dropout_prob, classifier_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_probs_dropout_prob + classifier_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - hidden_dropout_prob, attention_probs_dropout_prob + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: classifier_dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 1 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + hidden_act: + default_value_reasoning: Taken from huggingface. + description_implications: + Changing this activation function will only affect + the feed-forward layers of the transformer. + example_value: + - relu + expected_impact: 1 + literature_references: + - "[Huggingface docs for ELECTRA config](https://huggingface.co/docs/transformers/model_doc/electra#transformers.ElectraConfig.hidden_act) + + + [Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + suggested_values: gelu + suggested_values_reasoning: Taken from huggingface defaults. + ui_display_name: Hidden Layer Activation + hidden_dropout_prob: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "attention_probs_dropout_prob, + + classifier_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: hidden_dropout_prob + hidden_size: + default_value_reasoning: Huggingface default. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 1 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + intermediate_size: + ui_display_name: null + layer_norm_eps: + ui_display_name: null + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 2 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_attention_heads: + ui_display_name: null + num_hidden_layers: + ui_display_name: null + position_embedding_type: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + type_vocab_size: + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +FlauBERT: + type: + short_description: Language model with BERT related architecture trained on large French text corpus. + long_description: + The `flaubert`` encoder loads a pretrained [FlauBERT](https://arxiv.org/abs/1912.05372) (default `jplu/tf-flaubert-base-uncased``) model + using the Hugging Face transformers package. FlauBERT has an architecture similar to BERT and is + pre-trained on a large French language corpus. + literature_references: + - https://arxiv.org/abs/1912.05372 + compute_tier: 2 + asm: + ui_display_name: null + expected_impact: 1 + attention_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_dropout + bos_index: + ui_display_name: null + expected_impact: 1 + causal: + ui_display_name: null + expected_impact: 1 + dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - attention_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout + emb_dim: + ui_display_name: null + expected_impact: 1 + embed_init_std: + ui_display_name: null + expected_impact: 1 + eos_index: + ui_display_name: null + expected_impact: 1 + gelu_activation: + ui_display_name: null + expected_impact: 1 + init_std: + ui_display_name: null + expected_impact: 1 + is_encoder: + ui_display_name: null + expected_impact: 1 + lang_id: + ui_display_name: null + expected_impact: 1 + layer_norm_eps: + ui_display_name: null + expected_impact: 1 + layerdrop: + ui_display_name: null + expected_impact: 1 + mask_index: + ui_display_name: null + expected_impact: 1 + mask_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Mask Token ID + expected_impact: 1 + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 1 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + n_heads: + ui_display_name: null + expected_impact: 1 + n_langs: + default_value_reasoning: Default value used in pre-trained HF encoder. + expected_impact: 1 + ui_display_name: Number of Languages + n_layers: + ui_display_name: null + expected_impact: 1 + pad_index: + ui_display_name: null + expected_impact: 1 + pre_norm: + ui_display_name: null + expected_impact: 1 + pretrained_kwargs: + ui_display_name: null + expected_impact: 1 + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + sinusoidal_embeddings: + ui_display_name: null + expected_impact: 1 + trainable: + ui_display_name: null + expected_impact: 3 + unk_index: + ui_display_name: null + expected_impact: 1 + use_lang_emb: + ui_display_name: null + expected_impact: 1 + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +GPT2: + type: + short_description: GPT-2 is a pre-trained language model used for NLP tasks like generation, summarization, and translation. + long_description: The `gpt2` encoder loads a pretrained + [GPT-2](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) + (default `gpt2`) model using the Hugging Face transformers package. GPT-2 is a causal (unidirectional) transformer pretrained using language + modeling on a very large corpus of ~40 GB of text data. + literature_references: + - https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf + compute_tier: 3 + activation_function: + ui_display_name: null + attn_pdrop: + ui_display_name: null + embd_pdrop: + ui_display_name: null + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + layer_norm_epsilon: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + n_ctx: + ui_display_name: null + n_embd: + ui_display_name: null + n_head: + ui_display_name: null + n_inner: + ui_display_name: null + n_layer: + ui_display_name: null + n_positions: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + resid_pdrop: + ui_display_name: null + scale_attn_weights: + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +GPT: + type: + short_description: GPT is a pre-trained language model used for NLP tasks like generation, summarization, and translation. + long_description: The `gpt` encoder loads a pretrained + [GPT](https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf) + (default `openai-gpt`) model using the Hugging Face transformers package. + GPT is a causal (unidirectional) transformer pre-trained using language modeling on a large corpus with long range dependencies, the Toronto Book Corpus. + literature_references: + - https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf + compute_tier: 2 + afn: + ui_display_name: null + attn_pdrop: + ui_display_name: null + embd_pdrop: + ui_display_name: null + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + layer_norm_epsilon: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + n_ctx: + ui_display_name: null + n_embd: + ui_display_name: null + n_head: + ui_display_name: null + n_layer: + ui_display_name: null + n_positions: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + resid_pdrop: + ui_display_name: null + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +H3Embed: + type: + short_description: Encodes each H3 component with embeddings then takes a sum and passes them through fully connected layers. + long_description: + The Embed encoder encodes each component of the H3 representation (mode, edge, resolution, + base cell and children cells) with embeddings. Children cells with value 0 will be masked out. + After the embedding, all embeddings are summed and optionally passed through a stack of fully + connected layers. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + reduce_output: + default_value_reasoning: Sums the tensors along the sequence dimension. + description_implications: + "\"last\", \"sum\", \"mean\", and \"max\" are the\ + \ fastest and most memory-efficient operations\u2013 they result in tensors\ + \ that are the same-size as a single item in the input sequence. However,\ + \ these are simple aggregation operations, therefore some information\ + \ may be lost. \n\n\"concat\" concatenates each tensor together, creating\ + \ a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves\ + \ this information, but can be very memory-intensive and should only be\ + \ applied if the sequence length and/or tensor size is small. \n\n\"attention\"\ + \ takes a weighted sum of the items in the sequence, where the weights\ + \ for each item in the sequence are determined by the model on-the-fly\ + \ based on the features of the item itself. This is both slower and and\ + \ more memory-intensive than the other operations; however, it can also\ + \ provide a richer \"global\" representation of the sequence." + expected_impact: 1 + related_parameters: + - max_sequence_length + suggested_values: '"attention". This and the default covers 95% of use cases.' + suggested_values_reasoning: + If you would like better performance and are not + compute/memory-constrained, attention-based reduction can potentially + provide a richer global representation than the default. + ui_display_name: Sequence Reducer + use_bias: + ui_display_name: null + expected_impact: 1 + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +H3RNN: + type: + short_description: Encodes each H3 component with embeddings then passes them through an RNN encoder. + long_description: + The RNN encoder encodes each component of the H3 representation (mode, edge, resolution, + base cell and children cells) with embeddings. Children cells with value 0 will be masked out. + After the embedding, all embeddings are passed through an RNN encoder. The intuition behind this + is that, starting from the base cell, the sequence of children cells can be seen as a sequence + encoding the path in the tree of all H3 hexes. + activation: + ui_display_name: null + expected_impact: 1 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 2 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + bidirectional: + default_value_reasoning: + For short sequences, it is reasonable to use a vanilla + RNN. + description_implications: + Setting bidirectional to True may increase the compute + and memory requirements of the model, but may also increase model performance + on long sequences. + expected_impact: 0 + literature_references: + - https://devopedia.org/bidirectional-rnn#:~:text=RNN%20has%20the%20limitation%20that,forward%20and%20reverse%20time%20order. + related_parameters: + - cell_type, activation, recurrent_activation, use_bias + suggested_values: + - true + suggested_values_reasoning: + "RNNs can sometimes suffer from catastrophic forgetting + (source: https://en.wikipedia.org/wiki/Catastrophic_interference ) on + long sequences. Allowing the RNN to read from both the beginning and end + of the sequence can improve its representation at each timestep." + ui_display_name: Bidirectional + cell_type: + default_value_reasoning: + The LSTM cell has proven to be the most performant + of the three cells. + description_implications: + "There are two reasons to consider other cell types: + (1) compute costs and (2) catastrophic forgetting (source: https://en.wikipedia.org/wiki/Catastrophic_interference + ). RNNs have marginally less compute costs, but are prone to catastrophic + forgetting." + expected_impact: 3 + related_parameters: + - "bidirectional + + activation + + recurrent_activation + + use_bias" + ui_display_name: Cell Type + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - recurrent_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + hidden_size: + default_value_reasoning: + H3 values numbers, so a small RNN dimensionality + is likely sufficient. + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 2 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data. For + many data types, one layer is sufficient. + description_implications: + Increasing the number of layers may improve model + performance for longer sequences or more complex tasks. + example_value: + - 1 + expected_impact: 3 + other_information: + If you have multiple input features, varying the number + of layers in the combiner or output feature decoder will have more impact. + related_parameters: + - layers + suggested_values: 1-3 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Recurrent Layers + recurrent_activation: + default_value_reasoning: sigmoid' is commonly used + expected_impact: 1 + other_information: + I don't think that this parameter is used anywhere in the + code base. It's being passed down but not used in the actual RNN forwarding + functions. + suggested_values: sigmoid, ReLu, tanh + ui_display_name: null + recurrent_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Recurrent Dropout + recurrent_initializer: + ui_display_name: null + expected_impact: 1 + reduce_output: + ui_display_name: null + expected_impact: 1 + unit_forget_bias: + ui_display_name: null + expected_impact: 1 + use_bias: + ui_display_name: null + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +H3WeightedSum: + type: + short_description: Encodes each H3 component with embeddings then takes a weighted sum. + long_description: + The Weighted Sum encoder encodes each component of the H3 representation (mode, edge, + resolution, base cell and children cells) with embeddings. Children cells with value 0 will be + masked out. After the embedding, all embeddings are summed with a weighted sum (with learned + weights) and optionally passed through a stack of fully connected layers. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + should_softmax: + ui_display_name: null + expected_impact: 1 + use_bias: + ui_display_name: null + expected_impact: 1 + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +Longformer: + type: + short_description: Transformer optimized for longer text inputs. + long_description: + The `longformer` encoder loads a pretrained [Longformer](https://arxiv.org/pdf/2004.05150.pdf) + (default `allenai/longformer-base-4096`) model using the Hugging Face transformers package. Longformer is a good choice + for longer text, as it supports sequences up to 4096 tokens long. + literature_references: + - https://arxiv.org/pdf/2004.05150.pdf + compute_tier: 2 + attention_window: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_tokens: + ui_display_name: null + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + "An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words or positions, + which can have a large vocbulary size. Ideally, after an embedding is + trained, it captures some of the semantics of the input by placing semantically + similar inputs close together in the embedding space. + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality." + expected_impact: 2 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + type_vocab_size: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + sep_token_id: + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +MLPMixer: + type: + short_description: Image encoder which applies fully connected layers to different patches of the image. + long_description: + MLP-Mixer divides the image into equal-sized patches, applying fully connected layers to each + patch to compute per-patch representations (tokens) and combining the representations with + fully-connected mixer layers. + compute_tier: 1 + avg_pool: + ui_display_name: null + channel_dim: + ui_display_name: null + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embed_size: + ui_display_name: null + height: + internal_only: true + ui_display_name: null + num_channels: + ui_display_name: null + num_layers: + default_value_reasoning: + The ideal number of layers depends on the size and + complexity of the input images. The default value is used in the paper + and tested on several image datasets. + description_implications: + Increasing the number of layers may improve model + performance for larger images or more complex image tasks. + example_value: + - 8 + expected_impact: 3 + literature_references: + - "MLP-Mixer: An all-MLP Architecture for Vision + + https://arxiv.org/abs/2105.01601" + suggested_values: 4 - 32 + suggested_values_reasoning: + Values from 8 - 32 are tested in the paper. It + is possible that fewer layers will be sufficient for some tasks. + ui_display_name: Number of Layers + patch_size: + default_value_reasoning: Taken from MLP-Mixer paper. + description_implications: + "The implications of the image patch size for this\ + \ layer depend on other factors, such as the true resolution of the incoming\ + \ image dataset. If the patch size is kept consistent but a higher resolution\ + \ image is used as input, then the resulting chunked sequence of tokens\ + \ will be longer than it would have been if the input resolution was lower.\ + \ \n\nThe original MLP-Mixer paper also notes that there is a tradeoff\ + \ with respect to the projection units learned by a model. In their findings,\ + \ a 32x32 patch size model learned very structured low frequency projection\ + \ units, while the equivalent 16x16 model learned high frequencies and\ + \ showed no clear structure." + expected_impact: 2 + literature_references: + - "[MLP Mixer paper](https://arxiv.org/pdf/2105.01601.pdf)" + suggested_values: + - 16 + - 32 + suggested_values_reasoning: + 16 and 32 are the values used in the original + MLP Mixer paper + ui_display_name: Patch Size + token_size: + ui_display_name: null + width: + internal_only: true + ui_display_name: null +MT5: + type: + short_description: MT5 is a multilingual variant of T5 useful for multilingual NLP use cases. + long_description: + The `mt5` encoder loads a pretrained [MT5](https://arxiv.org/abs/2010.11934) (default `google/mt5-base`) model using the + Hugging Face transformers package. MT5 is a multilingual variant of T5 trained on a dataset of 101 languages. + compute_tier: 2 + d_ff: + default_value_reasoning: Default value matches the pre-trained encoder. + description_implications: + If using a pre-trained encoder, this parameter will + be automatically derived from the pre-trained model. + expected_impact: 1 + ui_display_name: Dimensionality of Feed-Forward Layer + d_kv: + ui_display_name: null + d_model: + ui_display_name: null + decoder_start_token_id: + ui_display_name: null + dropout_rate: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout_rate + eos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: End-of-Sentence Token Id + feed_forward_proj: + ui_display_name: null + initializer_factor: + ui_display_name: null + is_encoder_decoder: + ui_display_name: null + layer_norm_epsilon: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_decoder_layers: + ui_display_name: null + num_heads: + ui_display_name: null + num_layers: + default_value_reasoning: + The default value matches the number of layers in + the default pretrained encoder. + description_implications: + "The ideal number of transformer layers depends + on the length and complexity of input sequences, as well as the task. + + + If using a pre-trained encoder, this parameter will be automatically derived + from the pre-trained model." + example_value: + - 8 + expected_impact: 3 + related_parameters: + - pretrained_model_or_path + suggested_values: 1 - 12 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Transformer Layers + pad_token_id: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + relative_attention_num_buckets: + ui_display_name: null + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + tie_word_embeddings: + default_value_reasoning: + Keeping the word embeddings separate ensures maximum + modeling flexibility. + description_implications: + The main tradeoff between True and False values + is in compute costs and model flexibility. If set to False, the model + will require more memory, but may be more flexible. If set to True, the + opposite is true. + example_value: + - true + expected_impact: 2 + suggested_values: + - true + suggested_values_reasoning: + "If set to True, then the word embeddings will + be shared between the encoder and decoder. There are two main reasons + to set this value to True: (1) saving compute resources. Word embedding + tables can be very large and using a single table between the encoder + and decoder can cut one's memory usage in half. (2) If the domain of + the generated text is highly similar to the input text. For example, if + training a Question and Answering (QA) text model, where both the questions + and answers are in the same language, the word embeddings used by the + encoder are likely usable by the decoder and vice-versa. On the other + hand, if training a translation model between two languages, the word + embeddings are not likely to be shareable by both model components." + ui_display_name: null + tokenizer_class: + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_cache: + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +ParallelCNN: + type: + short_description: Default option for processing sequence, audio, and text data types. + long_description: + The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch + size and s is the length of the sequence) into a sequence of embeddings, then it passes the + embedding through a number of parallel 1d convolutional layers with different filter size (by + default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. + This single vector concatenating the outputs of the parallel convolutional layers is then passed + through a stack of fully connected layers and returned as a b x h tensor where h is the output + size of the last fully connected layer. + compute_tier: 1 + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + filter_size: + ui_display_name: null + expected_impact: 2 + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + pool_function: + ui_display_name: Pooling function + expected_impact: 1 + pool_size: + ui_display_name: null + expected_impact: 1 + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + should_embed: + internal_only: true + ui_display_name: Not displayed + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +PassthroughEncoder: + type: + short_description: Passes the raw input through to the combiner. + long_description: + The passthrough encoder simply returns the raw numerical values coming from the input + placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is + the batch size. + input_size: + internal_only: true + other_information: Internal Only + related_parameters: + - "No" + ui_display_name: Not Displayed +BinaryPassthroughEncoder: + type: + short_description: Passes the raw input through to the combiner. + long_description: + The passthrough encoder simply returns the raw numerical values coming from the input + placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is + the batch size. + input_size: + internal_only: true + other_information: Internal Only + related_parameters: + - "No" + ui_display_name: Not Displayed +CategoricalPassthroughEncoder: + type: + short_description: Passes the raw input through to the combiner. + long_description: + The passthrough encoder simply returns the raw numerical values coming from the input + placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is + the batch size. + input_size: + internal_only: true + other_information: Internal Only + related_parameters: + - "No" + ui_display_name: Not Displayed +DeBERTa: + type: + short_description: Improved version of BERT and RoBERTa, achieving good baseline performance on many tasks. + long_description: + The [DeBERTa](https://arxiv.org/abs/2006.03654) encoder improves the BERT and RoBERTa models using + disentangled attention and enhanced mask decoder. With those two improvements, DeBERTa out performs RoBERTa + on a majority of NLU tasks with 80GB training data. + + In [DeBERTa V3](https://arxiv.org/abs/2111.09543), the authors further improved the efficiency of DeBERTa + using ELECTRA-Style pre-training with Gradient Disentangled Embedding Sharing. Compared to DeBERTa, + the V3 version significantly improves the model performance on downstream tasks. + compute_tier: 2 + literature_references: + - https://arxiv.org/abs/2006.03654 + - https://arxiv.org/abs/2111.09543 + pretrained_model_name_or_path: + default_value_reasoning: + The default model was selected based on the benchmarking work done by IBM's + [model recycling](https://ibm.github.io/model-recycling/microsoft_deberta-v3-base_table.html) project. + In that study, the selected model ranked first among all variants of the `microsoft/deberta-v3-base` + architecture on an evaluation across 36 different datasets. + description_implications: + Considerations when selecting a pretrained model version include number of parameters (how long the model + will take to fine-tuning / perform inference), general model performance on various benchmarks, and + specific model performance on the task you wish to fine-tune it on. + expected_impact: 2 + related_parameters: + - use_pretrained, trainable, pretrained_kwargs + ui_display_name: Pretrained model +RoBERTa: + type: + short_description: BERT based model that has higher accuracy and is easier parallelize due to larger mini-batches. + long_description: + The `roberta` encoder loads a pretrained [RoBERTa](https://arxiv.org/abs/1907.11692) (default `roberta-base`) model + using the Hugging Face transformers package. Replication of BERT pretraining which may match or exceed the performance + of BERT. RoBERTa builds on BERT and modifies key hyperparameters, removing the + next-sentence pretraining objective and training with much larger mini-batches and learning + rates. + literature_references: + - https://arxiv.org/abs/1907.11692 + compute_tier: 2 + bos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Beginning-of-Sentence Token Id + eos_token_id: + default_value_reasoning: + example_value: + - Default value used in pre-trained HF encoder. + expected_impact: 1 + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + pad_token_id: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +SequenceEmbed: + type: + short_description: Maps each element of the sequence to an embedding. + long_description: + The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h` + tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding + size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each + element of the batch. + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +SequencePassthrough: + type: + short_description: Transforms sequence values to a floats then reduces to obtain a vector for each element. + long_description: + The passthrough encoder simply transforms each input value into a float value and adds a + dimension to the input tensor, creating a b x s x 1 tensor where b is the batch size and s is + the length of the sequence. The tensor is reduced along the s dimension to obtain a single + vector of size h for each element of the batch. + encoding_size: + default_value_reasoning: + The default `reduce_output` method does not use this + parameter, so by default this parameter is not set. + description_implications: + This parameter must be equal to the size of the + input. Otherwise, an error will occur. + example_value: + - 128 + expected_impact: 1 + related_parameters: + - reduce_output + suggested_values_reasoning: NONE + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + reduce_output: + ui_display_name: null + expected_impact: 1 +SetSparseEncoder: + type: + short_description: Maps raw values to sparse integer lists, then maps to dense/sparse embeddings, then reduces to final vector. + long_description: + The Embed encoder takes the raw binary values coming from the input placeholders and transforms + them to sparse integer lists, then they are mapped to either dense or sparse embeddings (one-hot + encodings), finally they are reduced on the sequence dimension and returned as an aggregated + embedding vector. Inputs are of size b while outputs are of size b x h where b is the batch size + and h is the dimensionality of the embeddings. + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + representation: + ui_display_name: null + expected_impact: 1 + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +Stacked2DCNN: + type: + short_description: Stack of 2D convolutional layers followed by an optional stack of fully connected layers. + long_description: + Stack of 2D convolutional layers with optional normalization, dropout, and down-sampling + pooling layers, followed by an optional stack of fully connected layers. + compute_tier: 1 + conv_activation: + expected_impact: 1 + ui_display_name: Convolutional Activation + conv_bias: + expected_impact: 1 + ui_display_name: Convolutional Bias + conv_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "conv_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Convolutional Dropout + conv_norm: + expected_impact: 2 + ui_display_name: Convolutional Normalization + conv_norm_params: + expected_impact: 1 + ui_display_name: Convolutional Normalization Parameters + dilation: + expected_impact: 1 + ui_display_name: Dilation + fc_activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + example_value: + - relu + expected_impact: 1 + literature_references: + - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html + related_parameters: + - activation, activation_function, conv_activation, recurrent_activation + suggested_values: relu, alternatively leakyRelu or elu + suggested_values_reasoning: + The default value will work well in the majority + of the cases + ui_display_name: FC Activation + fc_bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + fc_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "conv_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: FC Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + fc_norm: + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. See Torch's documentation on batch normalization or for layer see + Torch's documentation on layer normalization. + expected_impact: 2 + related_parameters: + - fc_norm_params + suggested_values: batch + ui_display_name: Fully Connected Normalization + fc_norm_params: + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + expected_impact: 2 + related_parameters: + - fc_norm + suggested_values: Depends on the type of `norm` set. + ui_display_name: Fully Connected Normalization Parameters + fc_use_bias: + expected_impact: 1 + ui_display_name: FC Use Bias + fc_weights_initializer: + expected_impact: 1 + ui_display_name: FC Weights Initializer + groups: + expected_impact: 1 + ui_display_name: Groups + height: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + kernel_size: + expected_impact: 1 + ui_display_name: Kernel Size + num_channels: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + ui_display_name: NOT DISPLAYED + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + out_channels: + expected_impact: 2 + ui_display_name: Number of Output Channels + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + padding: + default_value_reasoning: + When padding is set to 'valid' like in the default + case, no padding is added. As a default value putting in the raw image + is the goal here. + description_implications: + By increasing the amount of padding, you can increase + the accuracy of the image analysis for certain circumstances. + example_value: + - "'same'" + expected_impact: 1 + literature_references: + - https://www.geeksforgeeks.org/cnn-introduction-to-padding/ + related_parameters: + - "padding_mode, + + resize method" + suggested_values: + "Same' padding if images are of different dimensions. \n\ + Specific [h, w] entries can be valuable on a per dataset basis." + suggested_values_reasoning: + If your images already have padding, there is + no need to add padding, so the default is fine. If your images come in + different dimensions, then 'same' padding can help pad the images to standardized + dimensions. For certain images, adding padding to the edges can help the + CNN process the images better which can improve model performance. This + depends on the images however. + ui_display_name: Padding + padding_mode: + expected_impact: 1 + ui_display_name: Padding Mode + pool_dilation: + expected_impact: 1 + ui_display_name: Pool Dilation + pool_kernel_size: + expected_impact: 1 + ui_display_name: Pool Kernel Size + pool_padding: + expected_impact: 1 + ui_display_name: Pool Padding + pool_stride: + expected_impact: 1 + ui_display_name: Pool Stride + stride: + expected_impact: 1 + ui_display_name: Stride + width: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED +StackedCNN: + type: + short_description: Maps inputs to embeddings then passes them through a stack of 1d convolutional layers. + long_description: + The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch + size and s is the length of the sequence) into a sequence of embeddings, then it passes the + embedding through a stack of 1d convolutional layers with different filter size (by default 6 + layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a + flatten operation. This single flatten vector is then passed through a stack of fully connected + layers and returned as a b x h tensor where h is the output size of the last fully connected + layer. + compute_tier: 1 + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dilation_rate: + default_value_reasoning: + The standard discrete convolution is the same as + a 1-dilated convolution. + description_implications: + Higher dilation rates increase the effective size + of the convolutional filter. Dilated convolution may improve performance + if the data is very correlated locally and also contains long-term dependencies. + example_value: + - 2 + expected_impact: 1 + other_information: Dilated convolution is also known as atrous convolution. + related_parameters: + - filter_size + suggested_values: 1-3 + suggested_values_reasoning: + The dilation rate is a factor which increases + the spacing between elements of the convolutional filter + ui_display_name: Dilation Rate + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_filters: + ui_display_name: null + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + padding: + ui_display_name: null + pool_function: + ui_display_name: null + expected_impact: 1 + pool_padding: + ui_display_name: null + expected_impact: 1 + pool_size: + ui_display_name: null + expected_impact: 1 + pool_strides: + ui_display_name: null + expected_impact: 1 + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + should_embed: + internal_only: true + ui_display_name: Not displayed + strides: + default_value_reasoning: + In general, it makes sense to have a smaller stride + that fits the input. Imagining the simple 2D image as our input, two pixels + next to eachother are strongly correlated while pixels that are further + apart will have a comparatively weaker correlation. Consequently, a higher + stride may cause significant information loss. + description_implications: + Changing the stride of a convolutional layer is + one form of downsampling (another being pooling). In the case of a large + stride, significant amounts of information is thrown away as the filter + convolves over its input. This should be usually avoided but may be desirable + in cases in which the user has some deep knowledge of the filter or of + the rest of the model architecture that makes it comfortable to allow + a higher level compression in the output feature map of this layer. + example_value: + - 1 + expected_impact: 2 + literature_references: + - "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html) + + + [machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/) + + + [crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + related_parameters: + - pool_strides, default_strides, default_pool_strides, block_strides + suggested_values: 1-2 + suggested_values_reasoning: + In general, points that are closer to eachother + in the input feature space will be more strongly correlated to eachother, + so it is a good idea to select a stride that captures these neighboring + relationships. + ui_display_name: Stride + use_bias: + ui_display_name: null + expected_impact: 1 + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +StackedCNNRNN: + type: + short_description: Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack. + long_description: + The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch + size and s is the length of the sequence) into a sequence of embeddings, then it passes the + embedding through a stack of convolutional layers (by default 2), that is followed by a stack of + recurrent layers (by default 1), followed by a reduce operation that by default only returns the + last output, but can perform other reduce functions. + compute_tier: 1 + activation: + ui_display_name: null + expected_impact: 2 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + bidirectional: + ui_display_name: null + expected_impact: 0 + cell_type: + ui_display_name: null + expected_impact: 3 + conv_activation: + ui_display_name: null + expected_impact: 1 + conv_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "conv_dropout, + + dropout, + + recurrent_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Convolutional Dropout + dilation_rate: + default_value_reasoning: + The standard discrete convolution is the same as + a 1-dilated convolution. + description_implications: + Higher dilation rates increase the effective size + of the convolutional filter. Dilated convolution may improve performance + if the data is very correlated locally and also contains long-term dependencies. + example_value: + - 2 + expected_impact: 1 + other_information: Dilated convolution is also known as atrous convolution. + related_parameters: + - filter_size + suggested_values: 1-3 + suggested_values_reasoning: + The dilation rate is a factor which increases + the spacing between elements of the convolutional filter + ui_display_name: Dilation Rate + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "conv_dropout, + + dropout, + + recurrent_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + example_value: + - relu + expected_impact: 1 + literature_references: + - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html + related_parameters: + - activation, activation_function, conv_activation, recurrent_activation + suggested_values: relu, alternatively leakyRelu or elu + suggested_values_reasoning: + The default value will work well in the majority + of the cases + ui_display_name: FC Activation + fc_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "conv_dropout, + + dropout, + + recurrent_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: FC Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + filter_size: + ui_display_name: null + expected_impact: 2 + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_filters: + ui_display_name: null + num_rec_layers: + ui_display_name: null + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + padding: + ui_display_name: null + pool_function: + ui_display_name: null + expected_impact: 1 + pool_padding: + ui_display_name: null + expected_impact: 1 + pool_size: + ui_display_name: null + expected_impact: 1 + pool_strides: + ui_display_name: null + expected_impact: 1 + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + recurrent_activation: + default_value_reasoning: sigmoid' is commonly used + expected_impact: 1 + other_information: + I don't think that this parameter is used anywhere in the + code base. It's being passed down but not used in the actual RNN forwarding + functions. + suggested_values: sigmoid, ReLu, tanh + ui_display_name: null + recurrent_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "conv_dropout, + + dropout, + + recurrent_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Recurrent Dropout + recurrent_initializer: + ui_display_name: null + expected_impact: 1 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + should_embed: + internal_only: true + ui_display_name: Not displayed + state_size: + ui_display_name: null + expected_impact: 3 + strides: + default_value_reasoning: + In general, it makes sense to have a smaller stride + that fits the input. Imagining the simple 2D image as our input, two pixels + next to eachother are strongly correlated while pixels that are further + apart will have a comparatively weaker correlation. Consequently, a higher + stride may cause significant information loss. + description_implications: + Changing the stride of a convolutional layer is + one form of downsampling (another being pooling). In the case of a large + stride, significant amounts of information is thrown away as the filter + convolves over its input. This should be usually avoided but may be desirable + in cases in which the user has some deep knowledge of the filter or of + the rest of the model architecture that makes it comfortable to allow + a higher level compression in the output feature map of this layer. + example_value: + - 1 + expected_impact: 2 + literature_references: + - "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html) + + + [machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/) + + + [crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + related_parameters: + - pool_strides, default_strides, default_pool_strides, block_strides + suggested_values: 1-2 + suggested_values_reasoning: + In general, points that are closer to eachother + in the input feature space will be more strongly correlated to eachother, + so it is a good idea to select a stride that captures these neighboring + relationships. + ui_display_name: Stride + unit_forget_bias: + ui_display_name: null + expected_impact: 1 + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +StackedParallelCNN: + type: + short_description: Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers. + long_description: + The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN + encoders where each layer of the stack is composed of parallel convolutional layers. It works by + first mapping the input integer sequence b x s (where b is the batch size and s is the length of + the sequence) into a sequence of embeddings, then it passes the embedding through a stack of + several parallel 1d convolutional layers with different filter size, followed by an optional + final pool and by a flatten operation. This single flattened vector is then passed through a + stack of fully connected layers and returned as a b x h tensor where h is the output size of the + last fully connected layer. + compute_tier: 1 + activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + expected_impact: 2 + suggested_values: + The default value will work well in the majority of the + cases + ui_display_name: Activation + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + filter_size: + ui_display_name: null + expected_impact: 2 + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_filters: + ui_display_name: null + num_stacked_layers: + description_implications: + While superceded by `stacked_layers`, this can directly + change the depth of the current stack of parallel convolutional layers. + example_value: + - 1 + expected_impact: 1 + related_parameters: + - stacked_layers + ui_display_name: Number of Stacked Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + pool_function: + ui_display_name: null + expected_impact: 1 + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + should_embed: + internal_only: true + ui_display_name: Not displayed + stacked_layers: + ui_display_name: null + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +StackedRNN: + type: + short_description: Utilizes a stack of recurrent layers followed by a reduce operation. + long_description: + The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch + size and s is the length of the sequence) into a sequence of embeddings, then it passes the + embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce + operation that by default only returns the last output, but can perform other reduce functions. + compute_tier: 1 + activation: + ui_display_name: null + expected_impact: 2 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + bidirectional: + ui_display_name: null + expected_impact: 0 + cell_type: + ui_display_name: null + expected_impact: 3 + dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "dropout, + + recurrent_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + example_value: + - relu + expected_impact: 1 + literature_references: + - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html + related_parameters: + - activation, activation_function, conv_activation, recurrent_activation + suggested_values: relu, alternatively leakyRelu or elu + suggested_values_reasoning: + The default value will work well in the majority + of the cases + ui_display_name: FC Activation + fc_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout, recurrent_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: FC Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + + + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data. For + many data types, one layer is sufficient. + description_implications: + Increasing the number of layers may improve model + performance for longer sequences or more complex tasks. + example_value: + - 1 + expected_impact: 3 + suggested_values: 1-3 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Recurrent Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + recurrent_activation: + default_value_reasoning: sigmoid' is commonly used + expected_impact: 1 + other_information: + I don't think that this parameter is used anywhere in the + code base. It's being passed down but not used in the actual RNN forwarding + functions. + suggested_values: sigmoid, ReLu, tanh + ui_display_name: null + recurrent_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - "dropout, + + recurrent_dropout, + + fc_dropout" + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Recurrent Dropout + recurrent_initializer: + ui_display_name: null + expected_impact: 1 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + should_embed: + internal_only: true + ui_display_name: Not displayed + state_size: + ui_display_name: null + expected_impact: 3 + unit_forget_bias: + ui_display_name: null + expected_impact: 1 + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +StackedTransformer: + type: + short_description: Stack of transformer blocks with optional stack of fully connected layers. + long_description: + The transformer encoder implements a stack of transformer blocks, replicating the architecture + introduced in the Attention is all you need paper, and adds am optional stack of fully connected + layers at the end. + literature_references: + - https://arxiv.org/abs/1706.03762 + compute_tier: 2 + bias_initializer: + default_value_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. + description_implications: + It's rare to see any performance gains from choosing + a different bias initialization. Some practitioners like to use a small + constant value such as 0.01 for all biases to ensure that all ReLU units + are activated in the beginning and have some effect on the gradient. However, + it's still an open question as to whether this provides consistent improvement. + expected_impact: 1 + literature_references: + - https://cs231n.github.io/neural-networks-2/ + related_parameters: + - weights_initializer + suggested_values: zeros + suggested_values_reasoning: + It is possible and common to initialize the biases + to be zero, since the asymmetry breaking is provided by the small random + numbers in the weights. For ReLU non-linearities, some people like to + use small constant value such as 0.01 for all biases because this ensures + that all ReLU units fire in the beginning and therefore obtain and propagate + some gradient. However, it is not clear if this provides a consistent + improvement (in fact some results seem to indicate that this performs + worse) and it is more common to simply use 0 bias initialization. + ui_display_name: Bias Initializer + dropout: + default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1908.07442). + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 3 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - fc_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: Dropout + embedding_size: + default_value_reasoning: Not too big, not too small. + description_implications: + 'An embedding is a relatively low-dimensional space + that is used to translate high-dimensional vectors like words, which can + have a large vocbulary size. Ideally, after an embedding is trained, it + captures some of the semantics of the input by placing semantically similar + inputs close together in the embedding space. + + + In most cases, the embedding size is chosen empirically, by trial and + error. From https://www.amazon.com/dp/1098115783, "one rule of thumb is + to use the fourth root of the total number of unique categorical elements + while another is that the embedding dimension should be approximately + 1.6 times the square root of the number of unique elements in the category, + and no less than 600." + + + Increasing the embedding size may cause the model to train more slowly, + but the higher dimensionality can also improve overall quality.' + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture + suggested_values: 1.6 * sqrt(vocab_size) + suggested_values_reasoning: + Rule of thumb suggested by a deep learning textbook. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Embedding Size + embeddings_on_cpu: + default_value_reasoning: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. + description_implications: + By default embeddings matrices are stored on GPU + memory if a GPU is used, as it allows for faster access. However, in some + cases when the vocabulary size is very large, the full embedding matrix + may be really big and unwieldy to have in GPU memory. This parameter forces + the placement of the embedding matrix in regular memory and the CPU is + used to access them. This may slow down training due to additional data + transfer between CPU and GPU memory, but can lead to healthier GPU memory + resource usage. + expected_impact: 1 + suggested_values: + - false + suggested_values_reasoning: + If GPU memory is not a constraint, having embeddings + stored and accessed within the GPU is faster. + ui_display_name: Embeddings on CPU + embeddings_trainable: + ui_display_name: null + expected_impact: 1 + fc_activation: + default_value_reasoning: + The Rectified Linear Units (ReLU) function is the + standard activation function used for adding non-linearity. It is simple, + fast, and empirically works well (https://arxiv.org/abs/1803.08375). + description_implications: + Changing the activation functions has an impact + on the computational load of the model and might require further hypterparameter + tuning + example_value: + - relu + expected_impact: 1 + literature_references: + - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html + related_parameters: + - activation, activation_function, conv_activation, recurrent_activation + suggested_values: relu, alternatively leakyRelu or elu + suggested_values_reasoning: + The default value will work well in the majority + of the cases + ui_display_name: FC Activation + fc_dropout: + default_value_reasoning: + Dropout can cause training to become less stable. + Consider start with a dropout-free baseline, and add dropout gradually + in subsequent experiments. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: FC Dropout + fc_layers: + default_value_reasoning: + By default the stack is built by using num_fc_layers, + output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, + activation, dropout. When a list of dictionaries is provided, the stack + is built following the parameters of each dict for building each layer. + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a big anough amount of data is provided, + but also makes the model more computationally expensive and potentially + more prone to overfitting. + example_value: + - dropout: 0.1 + output_size: 128 + - norm: layer + output_size: 64 + expected_impact: 1 + related_parameters: + - output_size + - use_bias + - weights_initializer + - bias_initializer + - norm + - norm_params + - activation + - dropout + suggested_values_reasoning: + It is easier to define a stack of fully connected + layers by just specifying num_fc_layers, output_size and the other individual + parameters. It will create a stack of layers with identical properties. + Use this parameter only if you need a fine grained level of control of + each individual layer in the stack. + ui_display_name: Fully Connected Layers + hidden_size: + default_value_reasoning: Taken from literature (https://arxiv.org/abs/1706.03762) + description_implications: + Increasing the hidden size makes the model larger + and slower to train, increases the model's capacity to capture more complexity. + It also increases the chance of overfitting. + expected_impact: 2 + suggested_values: 10 - 2048 + suggested_values_reasoning: + Increasing the hidden size makes sense if the + model is underfitting. It's useful to train both smaller and larger models + to see how model capacity affects performance. This should only be explored + after the architecture of the model has been settled. + ui_display_name: Hidden Size + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes and the positional embedding matrix are + computed accurately. + internal_only: true + ui_display_name: null + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer bells + and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher learning + rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to layers + deep in the network when weights are updated. For example, batch normalization + standardizes the inputs to a layer for each mini-batch. Try out different + normalizations to see if that helps with training stability + ui_display_name: Normalization Type + norm_params: + default_value_reasoning: + The default parameters that come with Torch's implementation + of these normalization types are a trusted starting point. + description_implications: + There are a variety of ways a certain set of parameters + specificed could influence performance here. Broadly speaking the different + values passed in here allow for different levels of smoothness to be observed + in the learning curves. Since setting this parameters depends on the type + of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) + for more information on the parameters to set for batch normalization, + and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) + for more information on the parameters to set for layer normalization. + example_value: + - affine: false + momentum: 0.2 + num_features: 100 + expected_impact: 1 + literature_references: + - "For BatchNorm2d: https://arxiv.org/abs/1502.03167 + For LayerNorm: https://arxiv.org/abs/1607.06450" + related_parameters: + - "`norm`" + suggested_values: Depends on the type of `norm` set. + suggested_values_reasoning: "NO" + ui_display_name: Normalization Parameters + num_fc_layers: + default_value_reasoning: + The encoder already has learnable parameters.Sometimes + the default is 1 for modules where the FC stack is used for shape management, + or the only source of learnable parameters. + description_implications: + Increasing num_fc_layers will increase the capacity + of the model. The model will be slower to train, and there's a higher + risk of overfitting. + example_value: + - 1 + expected_impact: 1 + other_information: + Not all modules that have fc_layers also have an accompanying + num_fc_layers parameter. Where both are present, fc_layers takes precedent + over num_fc_layers. Specifying num_fc_layers alone uses fully connected + layers that are configured by the defaults in FCStack. + related_parameters: + - fc_layers + suggested_values: 0-1 + suggested_values_reasoning: + The full model likely contains many learnable + parameters. Consider starting with very few, or without any additional + fully connected layers and add them if you observe evidence of limited + model capacity. Sometimes the default is 1 for modules where the FC stack + is used for shape management, or the only source of learnable parameters. + ui_display_name: Number of Fully Connected Layers + num_heads: + ui_display_name: null + num_layers: + default_value_reasoning: + The ideal number of layers depends on the data. For + many data types, one layer is sufficient. + description_implications: + "The ideal number of transformer layers depends + on the length and complexity of input sequences, as well as the task. + + + For more complex tasks, and higher number of transformer layers may be + useful. However, too many layers will increase memory and slow training + while providing diminishing returns of model performance." + example_value: + - 1 + expected_impact: 3 + suggested_values: 1 - 12 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Transformer Layers + output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 3 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Output Size + pretrained_embeddings: + ui_display_name: null + expected_impact: 0 + reduce_output: + ui_display_name: null + expected_impact: 1 + representation: + ui_display_name: null + expected_impact: 1 + should_embed: + internal_only: true + ui_display_name: Not displayed + transformer_output_size: + default_value_reasoning: A modest value, not too small, not too large. + description_implications: + If there are fully connected layers in this module, + increasing the output size of each fully connected layer will increase + the capacity of the model. However, the model may be slower to train, + and there's a higher risk of overfitting. If it seems like the model could + use even more capacity, consider increasing the number of fully connected + layers, or explore other architectures. + expected_impact: 2 + other_information: + If num_fc_layers=0 and fc_layers=None, and there are no + fully connected layers defined on the module, then this parameter may + have no effect on the module's final output shape. + related_parameters: + - num_fc_layers, fc_layers + suggested_values: 10 - 1024 + suggested_values_reasoning: + Increasing the output size increases the capacity + of the model. If this seems to have a positive effect, then it could be + worth increasing the number of layers, or trying a different architecture + with a larger capacity. + ui_display_name: Transformer Output Size + use_bias: + default_value_reasoning: + "Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to use bias terms. + + + Batch Normalization, however, adds a trainable shift parameter which is + added to the activation. When Batch Normalization is used in a layer, + bias terms are redundant and may be removed." + description_implications: + Bias terms may improve model accuracy, and don't + have much impact in terms of memory or training speed. For most models + it is reasonable to leave this parameter set to True. + example_value: + - true + expected_impact: 1 + other_information: + If fc_layers is not specified, or use_bias is not specified + for individual layers, the value of use_bias will be used as the default + for all layers. + related_parameters: + - bias_initializer, fc_layers + suggested_values: + - true + ui_display_name: Use Bias + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + weights_initializer: + default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf). + description_implications: + The method you choose to initialize layer weights + during training can have a big impact on performance as well as the reproducibility + of your final model between runs. As an example, if you were to randomly + initialize weights you would risk non-reproducibility (and possibly general + training performance), but sticking with constant values for initialization + might significantly increase the time needed for model convergence. Generally, + choosing one of the probabilistic approaches strikes a balance between + the two extremes, and the literature kicked off by the landmark [*Xavier + et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) + provides a few good options. See this nice discussion from [Weights and + Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) + for more information. + expected_impact: 1 + literature_references: + - "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster. + + + Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + suggested_values: xavier_uniform + suggested_values_reasoning: + Changing the weights initialization scheme is + something to consider if a model is having trouble with convergence, or + otherwise it is something to experiment with after other factors are considered. + The default choice (`xavier_uniform`) is a suitable starting point for + most tasks. + ui_display_name: Layer Weights Initializer +T5: + type: + short_description: Text-to-text approach transformer with good transfer performance on multiple tasks. + long_description: + The `t5` encoder loads a pretrained [T5](https://arxiv.org/pdf/1910.10683.pdf) (default `t5-small`) model using the + Hugging Face transformers package. T5 (Text-to-Text Transfer Transformer) is pre-trained on a huge text dataset crawled + from the web and shows good transfer performance on multiple tasks. + compute_tier: 2 + d_ff: + default_value_reasoning: Default value matches the pre-trained encoder. + description_implications: + If using a pre-trained encoder, this parameter will + be automatically derived from the pre-trained model. + expected_impact: 1 + ui_display_name: Dimensionality of Feed-Forward Layer + d_kv: + ui_display_name: null + d_model: + ui_display_name: null + dropout_rate: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout_rate + feed_forward_proj: + ui_display_name: null + initializer_factor: + ui_display_name: null + layer_norm_eps: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + num_decoder_layers: + ui_display_name: null + num_heads: + ui_display_name: null + num_layers: + default_value_reasoning: + The default value matches the number of layers in + the default pretrained encoder. + description_implications: + "The ideal number of transformer layers depends + on the length and complexity of input sequences, as well as the task. + + + If using a pre-trained model, this parameter will be automatically derived + from the pre-trained model." + example_value: + - 6 + expected_impact: 2 + related_parameters: + - pretrained_model_or_path + suggested_values: 1 - 12 + suggested_values_reasoning: + Increasing the number of layers may improve encoder + performance. However, more layers will increase training time and may + cause overfitting. Small numbers of layers usually work best. + ui_display_name: Number of Transformer Layers + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + relative_attention_num_buckets: + ui_display_name: null + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +TransformerXL: + type: + short_description: Transformer architecture that introduces the notion of recurrence to the deep self-attention network. + long_description: + The `transformer_xl` encoder loads a pretrained [Transformer-XL](https://arxiv.org/abs/1901.02860) + (default `transfo-xl-wt103`) model using the Hugging Face transformers package. Adds novel positional encoding scheme + which improves understanding and generation of long-form text up to thousands of tokens. Transformer-XL is a causal (uni-directional) + transformer with relative positioning (sinusoïdal) embeddings which can reuse previously + computed hidden-states to attend to longer context (memory). This model also uses adaptive + softmax inputs and outputs (tied). + compute_tier: 2 + adaptive: + default_value_reasoning: Huggingface default. + description_implications: + Adaptive softmax is a speedup technique for computing + probability distributions over words. For text with large vocabulary, + adaptive softmax improves both training speed. + expected_impact: 1 + related_parameters: + - vocab_size + ui_display_name: Adaptive Softmax + attn_type: + ui_display_name: null + clamp_len: + ui_display_name: null + cutoffs: + ui_display_name: null + d_embed: + ui_display_name: null + d_head: + ui_display_name: null + d_inner: + ui_display_name: null + d_model: + ui_display_name: null + div_val: + ui_display_name: null + dropatt: + ui_display_name: null + dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout + eos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: End-of-Sequence Token Id + init: + ui_display_name: null + init_range: + ui_display_name: null + init_std: + ui_display_name: null + layer_norm_epsilon: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + mem_len: + ui_display_name: null + n_head: + ui_display_name: null + n_layer: + ui_display_name: null + pre_lnorm: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + proj_init_std: + ui_display_name: null + proj_share_all_but_first: + ui_display_name: null + reduce_output: + ui_display_name: null + expected_impact: 1 + same_length: + ui_display_name: null + sample_softmax: + ui_display_name: null + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + untie_r: + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +TVAlexNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVBaseEncoder: + model_cache_dir: + ui_display_name: Model Cache Directory + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: Saved Weights in Checkpoint + trainable: + default_value_reasoning: By default, model components are trainable. + description_implications: + The tradeoff when using `trainable` is between speed + and flexibility. If False, less weights are subject to change and the + model will therefore train faster. However, the representations output + by this component are fixed for each input. + expected_impact: 3 + literature_references: + - "https://www.ibm.com/cloud/learn/overfitting + + + http://d2l.ai/chapter_computer-vision/fine-tuning.html" + related_parameters: + - use_pretrained, pretrained_model, saved_weights_in_checkpoint + suggested_values: + - false + suggested_values_reasoning: + Freezing the weights (i.e. `trainable = False`) + is only worth trying if you are loading in pretrained weights. In that + case, check to see if your model is overfitting. If so, freezing the weights + (and therefore reducing model complexity) may be beneficial. + ui_display_name: Trainable + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained +TVConvNeXtEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVDenseNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVEfficientNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVGoogLeNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVInceptionV3Encoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVMaxVitEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVMNASNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVMobileNetV2Encoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVMobileNetV3Encoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVRegNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVResNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVResNeXtEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVShuffleNetV2Encoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVSqueezeNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVSwinTransformerEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVViTEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVVGGEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +TVWideResNetEncoder: + model_variant: + ui_display_name: Model Variant + type: + ui_display_name: Type +XLM: + type: + short_description: XLM is pre-trained by cross-language modeling. + long_description: + The `xlm` encoder loads a pretrained [XLM](https://arxiv.org/abs/1901.07291) (default `xlm-mlm-en-2048`) model using the + Hugging Face transformers package. Pre-trained by cross-language modeling. + compute_tier: 2 + asm: + ui_display_name: null + attention_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: attention_dropout + bos_index: + ui_display_name: null + bos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Beginning-of-Sentence Token Id + causal: + ui_display_name: null + dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - attention_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout + emb_dim: + ui_display_name: null + embed_init_std: + ui_display_name: null + end_n_top: + ui_display_name: null + eos_index: + ui_display_name: null + gelu_activation: + ui_display_name: null + expected_impact: 1 + init_std: + ui_display_name: null + is_encoder: + ui_display_name: null + lang_id: + ui_display_name: null + layer_norm_eps: + ui_display_name: null + mask_index: + ui_display_name: null + mask_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Mask Token ID + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 2 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + n_heads: + ui_display_name: null + n_langs: + default_value_reasoning: Default value used in pre-trained HF encoder. + expected_impact: 1 + ui_display_name: Number of Languages + n_layers: + ui_display_name: null + pad_index: + ui_display_name: null + pad_token_id: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + sinusoidal_embeddings: + ui_display_name: null + start_n_top: + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + unk_index: + ui_display_name: null + use_lang_emb: + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +XLMRoBERTa: + type: + short_description: XLM-RoBERTa a large multi-lingual language model trained on 2.5TB of filtered CommonCrawl data. + long_description: + The `xlmroberta` encoder loads a pretrained [XLM-RoBERTa](https://arxiv.org/abs/1911.02116) + (default `jplu/tf-xlm-reoberta-base`) model using the Hugging Face transformers package. XLM-RoBERTa is a multi-language + model similar to BERT, trained on 100 languages. XLM-RoBERTa is based on Facebook’s RoBERTa model + released in 2019. It is a large multi-lingual language model, trained on 2.5TB of filtered + CommonCrawl data. + compute_tier: 2 + add_pooling_layer: + ui_display_name: null + bos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Beginning-of-Sentence Token Id + eos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: End-of-Sentence Token Id + max_position_embeddings: + default_value_reasoning: Taken from huggingface. + description_implications: + The size of the position embeddings table. This typically coincides with the + maximum sequence length this model might ever be used with. Typically set this + to something large just in case (e.g. 512, 1024, 2048). + expected_impact: 1 + suggested_values: 512 + suggested_values_reasoning: + Out of the box value based on published literature. + Try models with smaller or larger embedding sizes to observe relative + impact. + ui_display_name: Max Position Embeddings + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + pad_token_id: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + type_vocab_size: + ui_display_name: null + expected_impact: 1 + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +XLNet: + type: + short_description: XLNet is a transformer that outperforms BERT on a variety of benchmarks. + long_description: + The `xlnet` encoder loads a pretrained [XLNet](https://arxiv.org/abs/1906.08237) (default `xlnet-base-cased`) model + using the Hugging Face transformers package. XLnet is an extension of the Transformer-XL model pre-trained using + an autoregressive method to learn bidirectional contexts by maximizing the expected likelihood + over all permutations of the input sequence factorization order. XLNet outperforms BERT on a + variety of benchmarks. + compute_tier: 2 + attn_type: + ui_display_name: null + bi_data: + ui_display_name: null + bos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Beginning-of-Sentence Token Id + clamp_len: + ui_display_name: null + d_inner: + ui_display_name: null + d_model: + ui_display_name: null + dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 2 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - summary_last_dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: dropout + end_n_top: + ui_display_name: null + eos_token_id: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: End-of-Sequence Token Id + ff_activation: + ui_display_name: null + expected_impact: 1 + initializer_range: + description_implications: + There is an ideal value for this variable that doesn't + lead to the outputs of these matrices to vanish or explode + example_value: + - 0.02 + expected_impact: 1 + other_information: Must be greater than 0 + related_parameters: + - weights_initializer + suggested_values: 0.01-0.05 + suggested_values_reasoning: + Large values will likely lead to very large outputs. + Small values will lead to vanishing outputs. + ui_display_name: null + layer_norm_eps: + ui_display_name: null + max_sequence_length: + default_value_reasoning: + Sets the maximum sequence length of the expected + inputs, so input/output shapes are computed accurately. + internal_only: true + ui_display_name: null + mem_len: + ui_display_name: null + n_head: + ui_display_name: null + n_layer: + ui_display_name: null + pad_token_id: + ui_display_name: null + pretrained_kwargs: + ui_display_name: null + pretrained_model_name_or_path: + ui_display_name: null + expected_impact: 2 + reduce_output: + ui_display_name: null + expected_impact: 1 + reuse_len: + ui_display_name: null + same_length: + ui_display_name: null + saved_weights_in_checkpoint: + default_value_reasoning: + The weights of the encoder are not necessarily saved + in the checkpoint. The user has to save them first. + description_implications: + The memory footprint for some of these encoders + can be large. + internal_only: true + related_parameters: + - skip_save_model + suggested_values: + - false + suggested_values_reasoning: + Some of these encoders are large, so it might + be better to load them as needed, especially if 1. they're not used frequently + 2. the user doesn't have a lot of storage. + ui_display_name: null + start_n_top: + ui_display_name: null + summary_activation: + default_value_reasoning: Default value used in pre-trained HF encoder. + ui_display_name: Summary Activation Function + expected_impact: 1 + summary_last_dropout: + default_value_reasoning: Huggingface default. + description_implications: + "Dropout is a computationally cheap regularization\ + \ method where during training, some neurons are randomly ignored or \u201C\ + dropped out\u201D. Increasing dropout has the effect of making the training\ + \ process more noisy and lowering overall network capacity, but it can\ + \ be an effective regularization method to reduce overfitting and improve\ + \ generalization." + example_value: + - 0.2 + expected_impact: 1 + literature_references: + - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html + related_parameters: + - dropout + suggested_values: 0.05 - 0.8 + suggested_values_reasoning: + Tuning dropout is really something to be done + when all of the big choices about architecture have been settled. Consider + starting with 0.5 and adjusting the dropout depending on observed model + performance. + ui_display_name: summary_last_dropout + summary_type: + ui_display_name: null + summary_use_proj: + ui_display_name: null + trainable: + expected_impact: 3 + ui_display_name: null + untie_r: + ui_display_name: null + use_mems_eval: + ui_display_name: null + use_mems_train: + ui_display_name: null + use_pretrained: + default_value_reasoning: + By default, the model is initialized as a pretrained + model. + description_implications: + Pretrained models have typically already learned + features that are difficult to learn from scratch. They are particularly + beneficial when training on small amounts of data. + expected_impact: 3 + literature_references: + - https://machinelearningmastery.com/transfer-learning-for-deep-learning/ + related_parameters: + - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs + suggested_values: + - false + suggested_values_reasoning: + If you have a large amount of data and/or you + have data that differs from the typical distribution, then it might be + worth training the model from scratch. + ui_display_name: Use Pretrained + vocab: + default_value_reasoning: + Computed and passed along internally according to + preprocessing settings. + example_value: + - a + - b + - c + internal_only: true + ui_display_name: Not Displayed + vocab_size: + internal_only: true + ui_display_name: Not displayed +conv_params: + num_conv_layers: + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a large amount of data is provided, but + also makes the model more computationally expensive and potentially more + prone to overfitting. + expected_impact: 3 + related_parameters: + - conv_layers + ui_display_name: Number of Convolutional Layers + conv_layers: + description_implications: + The more layers that are specified the deeper and + higher capacity the model will be. This makes it possible to potentially + achieve better performance when a large amount of data is provided, but + also makes the model more computationally expensive and potentially more + prone to overfitting. + expected_impact: 1 + related_parameters: + - num_conv_layers + ui_display_name: Convolutional Layers + pool_function: + default_value_reasoning: + "Within a given sliding window (e.g. a \"patch\"\ + \ of a 3-channel image), the maximum value for each channel is kept. All\ + \ other values in the patch are discarded. Repeat this step for every\ + \ patch and you have a more compact representation of the image. \n\n\ + Intuitively, each patch encodes the features from a particular part of\ + \ an image, and it is more informative to look at the most prominent features\ + \ of an image than the average of all of them." + description_implications: + Both average and max pooling can achieve strong + performance. + expected_impact: 1 + literature_references: + - "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html + + + https://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + suggested_values: Default + suggested_values_reasoning: "No" + ui_display_name: Pooling function + pool_size: + ui_display_name: null + expected_impact: 1 + num_filters: + ui_display_name: null + filter_size: + ui_display_name: null + expected_impact: 2 +UNetEncoder: + type: + short_description: The UNet encoder convolutional and max pool layers + long_description: + Stacks of two 2D convolutional layers with optional normalization + and relu activation, followed by a max pool layer in all but the + final level of the encoder. + compute_tier: 1 + conv_norm: + expected_impact: 2 + ui_display_name: Convolutional Normalization + height: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + num_channels: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED + width: + default_value_reasoning: + Computed internally, automatically, based on image + data preprocessing. + internal_only: true + ui_display_name: NOT DISPLAYED +TimmEncoder: + model_name: + ui_display_name: Model Name + use_pretrained: + ui_display_name: Use Pretrained + saved_weights_in_checkpoint: + internal_only: true + ui_display_name: Saved Weights in Checkpoint + trainable: + ui_display_name: Trainable +TimmCAFormerEncoder: + model_name: + ui_display_name: Model Name +TimmConvFormerEncoder: + model_name: + ui_display_name: Model Name +TimmPoolFormerEncoder: + model_name: + ui_display_name: Model Name +TimmConvNeXtV2Encoder: + model_name: + ui_display_name: Model Name +MambaEncoder: + type: + short_description: State space model encoder for sequences. + long_description: + The Mamba encoder uses selective state space model (SSM) layers for sequence encoding. + It provides an alternative to Transformer attention that scales linearly with sequence length + instead of quadratically. Requires the mamba_ssm package for full Mamba support. Without it, + a simplified SSM-inspired encoder using gated convolutions is used as a fallback. + literature_references: + - https://arxiv.org/abs/2312.00752 + compute_tier: 2 + d_model: + ui_display_name: Model Dimension + n_layers: + ui_display_name: Number of Layers + d_state: + ui_display_name: State Dimension + d_conv: + ui_display_name: Convolution Width + expand_factor: + ui_display_name: Expansion Factor diff --git a/ludwig/schema/metadata/configs/features.yaml b/ludwig/schema/metadata/configs/features.yaml new file mode 100644 index 0000000..15007a4 --- /dev/null +++ b/ludwig/schema/metadata/configs/features.yaml @@ -0,0 +1,1035 @@ +audio: + preprocessing: + audio_file_length_limit_in_s: + ui_display_name: null + expected_impact: 2 + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + ui_display_name: Fill Value + expected_impact: 2 + in_memory: + ui_display_name: null + expected_impact: 1 + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + expected_impact: 3 + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + norm: + default_value_reasoning: + While batch normalization and layer normalization + usually lead to improvements, it can be useful to start with fewer + bells and whistles. + description_implications: + Normalization helps stabilize the learning process + and can have a regularizing effect that can help with generalization. + It's often suggested that with normalization, you can use a higher + learning rate. + example_value: + - batch + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/ + related_parameters: + - norm_params + suggested_values: '"batch" or "layer"' + suggested_values_reasoning: + Normalization tries to solve "internal covariate + shift" that comes from the changing distributions of the inputs to + layers deep in the network when weights are updated. For example, + batch normalization standardizes the inputs to a layer for each mini-batch. + Try out different normalizations to see if that helps with training + stability + ui_display_name: Normalization Type + num_fft_points: + ui_display_name: null + expected_impact: 1 + num_filter_bands: + literature_references: + - "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + related_parameters: + - window_length_in_s + - type + - window_shift_in_s + ui_display_name: Type + expected_impact: 1 + padding_value: + ui_display_name: null + expected_impact: 1 + type: + default_value_reasoning: + The default type fbank is set based on values + that we have tested and determined to be a good starting point for + audio feature preprocessing. This is not to say that it is the best + way to process every audio feature, it is just a good starting place + that performs well in general. + description_implications: + The different type of audio you select hear + will determine how your audio feature is preprocessed and transformed + into trainable data for the model. + example_value: + - stft + expected_impact: 3 + literature_references: + - "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + other_information: + Audio feature preprocessing depends heavily on the + type of audio data you are dealing with. The type of audio preprocessing + you will want to use will be dictated by the audio data you are dealing + with. + related_parameters: + - audio_file_length_limit_in_s + - norm + - padding_value + - in_memory + ui_display_name: Type + window_length_in_s: + literature_references: + - "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + related_parameters: + - window_shift_in_s + - type + - num_filter_bands + ui_display_name: Window Length in Seconds + expected_impact: 2 + window_shift_in_s: + literature_references: + - "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + related_parameters: + - window_length_in_s + - type + - num_filter_bands + ui_display_name: Window Shift in Seconds + expected_impact: 2 + window_type: + ui_display_name: null + expected_impact: 2 +bag: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + ui_display_name: Fill Value + expected_impact: 2 + lowercase: + ui_display_name: null + expected_impact: 2 + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + expected_impact: 3 + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + most_common: + default_value_reasoning: + If there are more than 10000 unique categories + in the data, it is likely that they will follow a long-tailed distribution + and the least common ones may not provide a lot of information + description_implications: + A smaller number will reduce the vocabulary, + making the embedding matrix smaller and reduce the memory footprint, + but will also collapse more tokens into the rare one, so the model + may perform worse when rare tokens appear in the data + example_value: + - 10000 + expected_impact: 2 + other_information: Specifying a vocab_file overrides this parameter + related_parameters: + - vocab_file, pretrained_embeddings + suggested_values: + A value that covers at least 95% of the tokens in the + data + suggested_values_reasoning: + Depending on the data distribution and how + important rare tokens are, 90%, 95% or 99% of the number of tokens + will leave out only very rare tokens that should not influence performance + substantially + ui_display_name: Most common (vocabulary size) + tokenizer: + ui_display_name: null + expected_impact: 3 +binary: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fallback_true_label: + description_implications: + Modeling performance should not be affected, + but the semantics of some binary metrics may change like for "false + positives", "false negatives", etc. if the true label is pinned to + the other value. + expected_impact: 2 + ui_display_name: Fallback True Label + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + calibration: + expected_impact: 3 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 + threshold: + expected_impact: 3 +category: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + lowercase: + ui_display_name: null + expected_impact: 2 + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + most_common: + default_value_reasoning: + If there are more than 10000 unique categories + in the data, it is likely that they will follow a long-tailed distribution + and the least common ones may not provide a lot of information + description_implications: + A smaller number will reduce the vocabulary, + making the embedding matrix smaller and reduce the memory footprint, + but will also collapse more tokens into the rare one, so the model + may perform worse when rare tokens appear in the data + example_value: + - 10000 + expected_impact: 2 + other_information: Specifying a vocab_file overrides this parameter + related_parameters: + - vocab_file, pretrained_embeddings + suggested_values: + A value that covers at least 95% of the tokens in the + data + suggested_values_reasoning: + Depending on the data distribution and how + important rare tokens are, 90%, 95% or 99% of the number of tokens + will leave out only very rare tokens that should not influence performance + substantially + ui_display_name: Most common (vocabulary size) + calibration: + expected_impact: 3 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 + top_k: + expected_impact: 3 +date: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + datetime_format: + default_value_reasoning: + Ludwig will try to infer the date format automatically, + but a specific format can be provided. The date string spec is the + same as the one described in python's datetime. + description_implications: + If Ludwig has trouble parsing dates, it could + be useful to specify an explicit format that Ludwig should parse date + feature values as. This could also serve as a form of normalization, + for example, if not all datetimes have the same granularity (some + have days, some have times), then the common format (i.e. %d %m %Y) + serves as a truncator. + example_value: + - "%d %b %Y" + expected_impact: 2 + suggested_values_reasoning: Have Ludwig figure out the date format automatically. + ui_display_name: Datetime format + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 +h3: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 +image: + # TODO: review metadata generated by Copilot + augmentation: + auto_augmentation_method: + default_value_reasoning: Trivial augment is computationally more efficient than the other methods. + description_implications: + Type of auto-augmentation method to apply to batch of images to improve model generalization + example_value: + "trivial_augment" + expected_impact: 1 + ui_display_name: Auto Augmentation Method + max_brightness: + default_value_reasoning: The default value of 3.0. + description_implications: + The maximum factor by which the brightness of + the image will be randomly changed. + example_value: + - 3.9 + expected_impact: 1 + ui_display_name: Maximum Brightness + min_brightness: + default_value_reasoning: The default value of 0.1. + description_implications: + The minimum brightness factor to apply to the + image. + example_value: + - 0.5 + expected_impact: 1 + ui_display_name: Minimum Brightness + max_contrast: + default_value_reasoning: The default value of 3.0 + description_implications: + The maximum factor by which the contrast of + the image will be randomly changed. + example_value: + - 3.0 + expected_impact: 1 + ui_display_name: Maximum Contrast + min_contrast: + default_value_reasoning: The default value of 0.1. + description_implications: + The minimum contrast factor to apply to the + image. + example_value: + - 0.1 + expected_impact: 1 + ui_display_name: Minimum contrast + kernel_size: + default_value_reasoning: The default value is 3. + description_implications: The kernel size is the size of the filter + matrix. A larger kernel size will result in a blurrier image, while a + smaller kernel size will result in less blurring. + example_value: + - 3 + expected_impact: 2 + suggested_values: + - 3 + - 5 + - 7 + suggested_values_reasoning: + The default value is 3, which is a common + value for image processing + ui_display_name: Kernel Size + rotation_degree: + default_value_reasoning: The default value of 15 means that the + image will be randomly rotated between -15 to +15 degrees. + description_implications: The degree of rotation to apply to the image. + expected_impact: 1 + ui_display_name: Rotation Degree + type: + description_implications: The type of augmentation to perform on the + image. + expected_impact: 1 + ui_display_name: Type + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + height: + ui_display_name: null + expected_impact: 2 + in_memory: + ui_display_name: null + expected_impact: 1 + infer_image_dimensions: + ui_display_name: null + expected_impact: 1 + infer_image_max_height: + ui_display_name: null + expected_impact: 1 + infer_image_max_width: + ui_display_name: null + expected_impact: 1 + infer_image_num_channels: + ui_display_name: null + expected_impact: 1 + infer_image_sample_size: + ui_display_name: null + expected_impact: 1 + infer_image_num_classes: + ui_display_name: null + expected_impact: 1 + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + num_channels: + ui_display_name: null + expected_impact: 2 + num_classes: + ui_display_name: null + expected_impact: 2 + num_processes: + ui_display_name: null + expected_impact: 2 + resize_method: + default_value_reasoning: + Interpolation may stretch or squish the image, + but it does not remove content or change the statistical distribution + of image values so it is more appropriate for most tasks. + description_implications: + "interpolation will not change the content of + the image, but it will change the aspect ratio. + + + crop_or_pad will preserve the aspect ratio of the image, but may remove + some content (in the case of cropping)." + expected_impact: 1 + related_parameters: + - height, width + ui_display_name: Resize Method + standardize_image: + ui_display_name: null + expected_impact: 1 + width: + ui_display_name: null + expected_impact: 2 + requires_equal_dimensions: + ui_display_name: null + expected_impact: 1 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 +number: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + computed_outlier_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + outlier_strategy: + default_value_reasoning: + Outlier definitions and how to handle them are very task-specific, so we leave + this feature disabled by default and ask the user to choose the strategy that works best for them. + description_implications: + Determines how outliers will be handled in the dataset. In most cases replacing outliers with the + column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough + to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers + is to leave them in the data, which is the behavior when this parameter is left as `null`. + related_parameters: + - outlier_threshold + suggested_values: fill_with_mean + ui_display_name: Outlier Strategy + expected_impact: 3 + outlier_threshold: + default_value_reasoning: + The definition of an outlier is often dataset and task dependent, but 2 or 3 standard deviations from + the mean is a common heuristic. + description_implications: + "Determines the threshold past which a number will be considered an outlier in the dataset. The 3-sigma + rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 + standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations + of the mean (see: 68–95–99.7 rule). As such anything farther away than that is highly likely to be an + outlier, and may distort the learning process by disproportionately affecting the model." + related_parameters: + - outlier_strategy + literature_references: + - https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule + suggested_values: 2 - 3 + ui_display_name: Outlier Threshold + expected_impact: 2 + normalization: + default_value_reasoning: + Z-score normalization helps improve the training stability and convergence + of neural networks by rescaling the numeric input features to have a mean + of 0 and a standard deviation of 1, reducing the variability and distribution + of the data. This improves neural network training. + description_implications: + The goal of normalization is to transform features + to be on a similar scale. Normalization can be a form of feature smoothing + that improves the performance and training stability of the model. + Normalizations may result in different effects on the semantics of + your number features. The best normalization technique is one that + empirically works well, so try new ideas if you think they'll work + well on your feature distribution. + expected_impact: 3 + literature_references: + - https://developers.google.com/machine-learning/data-prep/transform/normalization + suggested_values: zscore + suggested_values_reasoning: + "Z-score is a variation of scaling that represents\ + \ the number of standard deviations away from the mean. You would\ + \ use z-score to ensure your feature distributions have mean = 0 and\ + \ std = 1. It\u2019s useful when there are a few outliers, but not\ + \ so extreme that you need clipping." + ui_display_name: Normalization + clip: + expected_impact: 2 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 +sequence: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + lowercase: + ui_display_name: null + expected_impact: 2 + sequence_length: + default_value_reasoning: + The default value is `None`. Which means that the sequence length will be inferred from the dataset, + which may save you compute resources on datasets with short sequence samples. + description_implications: + A larger sequence length keeps more information + from the data, but also makes it more computationally expensive (more + memory and longer training time). A smaller sequence length keeps + less information from the data, but also makes it less computationally + expensive (less memory and shorter training time). + expected_impact: 3 + related_parameters: + - max_sequence_length + suggested_values: + If tying the weights of multiple sequence encoders together, + this parameter may need to be set to ensure that all sequence features have the same sequence length. + ui_display_name: Sequence Length + max_sequence_length: + default_value_reasoning: + The default value is 256. Every sequence will + be truncated to this length. + description_implications: + A larger sequence length keeps more information + from the data, but also makes it more computationally expensive (more + memory and longer training time). A smaller sequence length keeps + less information from the data, but also makes it less computationally + expensive (less memory and shorter training time). + expected_impact: 3 + related_parameters: + - vocab_size, embedding_size + suggested_values: + Use the lowest value that covers most of your input + data. Only increase the value if crucial parts of the input data are + truncated. + ui_display_name: Maximum Sequence Length + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + most_common: + default_value_reasoning: + If there are more than 10000 unique categories + in the data, it is likely that they will follow a long-tailed distribution + and the least common ones may not provide a lot of information + description_implications: + A smaller number will reduce the vocabulary, + making the embedding matrix smaller and reduce the memory footprint, + but will also collapse more tokens into the rare one, so the model + may perform worse when rare tokens appear in the data + example_value: + - 10000 + expected_impact: 2 + other_information: Specifying a vocab_file overrides this parameter + related_parameters: + - vocab_file, pretrained_embeddings + suggested_values: + A value that covers at least 95% of the tokens in the + data + suggested_values_reasoning: + Depending on the data distribution and how + important rare tokens are, 90%, 95% or 99% of the number of tokens + will leave out only very rare tokens that should not influence performance + substantially + ui_display_name: Most common (vocabulary size) + ngram_size: + default_value_reasoning: Size of the n-gram when using the `ngram` tokenizer. + example_value: + - 3 + ui_display_name: n-gram size + expected_impact: 2 + padding: + ui_display_name: null + expected_impact: 1 + padding_symbol: + ui_display_name: null + expected_impact: 1 + tokenizer: + ui_display_name: null + expected_impact: 3 + unknown_symbol: + ui_display_name: null + expected_impact: 1 + vocab_file: + default_value_reasoning: + The vocabulary can be parsed automatically from + the incoming input features. + description_implications: + It can be useful to specify your own vocabulary + list if the vocabulary is very large, there's no out of the box tokenizer + that fits your data, or if there are several uncommon or infrequently + occurring tokens that we want to guarantee to be a part of the vocabulary, + rather than treated as an unknown. + expected_impact: 0 + ui_display_name: Vocab File + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 +set: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + lowercase: + ui_display_name: null + expected_impact: 2 + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + expected_impact: 3 + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + most_common: + default_value_reasoning: + If there are more than 10000 unique categories + in the data, it is likely that they will follow a long-tailed distribution + and the least common ones may not provide a lot of information + description_implications: + A smaller number will reduce the vocabulary, + making the embedding matrix smaller and reduce the memory footprint, + but will also collapse more tokens into the rare one, so the model + may perform worse when rare tokens appear in the data + example_value: + - 10000 + expected_impact: 2 + other_information: Specifying a vocab_file overrides this parameter + related_parameters: + - vocab_file, pretrained_embeddings + suggested_values: + A value that covers at least 95% of the tokens in the + data + suggested_values_reasoning: + Depending on the data distribution and how + important rare tokens are, 90%, 95% or 99% of the number of tokens + will leave out only very rare tokens that should not influence performance + substantially + ui_display_name: Most common (vocabulary size) + tokenizer: + ui_display_name: null + expected_impact: 3 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 + threshold: + expected_impact: 3 +text: + preprocessing: + computed_fill_value: + example_value: + - Depends on dtype + internal_only: true + related_parameters: + - missing_value_strategy, fill_value + ui_display_name: DOCSTRING ONLY + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + lowercase: + default_value_reasoning: + Reading the text in lowercase enables the model + to treat capitalized and lowercase words as the same, effectively + increasing the number of data points per word. + description_implications: + If you set lowercase to False, then capitalized + words are seen as completely separate entities than lowercase words. + example_value: + - true + expected_impact: 2 + related_parameters: + - vocab_size + suggested_values: "TRUE" + suggested_values_reasoning: + If there is a strong reason to treat capitalized + words and lowercased words differently, then set this to False. Otherwise, + it is preferable to bucket the words and make the model case-insensitive. + ui_display_name: Convert to lowercase + sequence_length: + default_value_reasoning: + The default value is `None`. Which means that the sequence length will be inferred from the dataset, + which may save you compute resources on datasets with short text samples. + description_implications: + A larger sequence length keeps more information + from the data, but also makes it more computationally expensive (more + memory and longer training time). A smaller sequence length keeps + less information from the data, but also makes it less computationally + expensive (less memory and shorter training time). + expected_impact: 3 + related_parameters: + - max_sequence_length + suggested_values: + If tying the weights of multiple text encoders together, + this parameter may need to be set to ensure that all text features have the same sequence length. + ui_display_name: Sequence Length + max_sequence_length: + default_value_reasoning: + The default value is 256. Every sequence will + be truncated to this length. + description_implications: + A larger sequence length keeps more information + from the data, but also makes it more computationally expensive (more + memory and longer training time). A smaller sequence length keeps + less information from the data, but also makes it less computationally + expensive (less memory and shorter training time). + expected_impact: 3 + related_parameters: + - vocab_size, embedding_size + suggested_values: + Use the lowest value that covers most of your input + data. Only increase the value if crucial parts of the input data are + truncated. + ui_display_name: Maximum Sequence Length + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + most_common: + default_value_reasoning: + If there are more than 10000 unique categories + in the data, it is likely that they will follow a long-tailed distribution + and the least common ones may not provide a lot of information + description_implications: + A smaller number will reduce the vocabulary, + making the embedding matrix smaller and reduce the memory footprint, + but will also collapse more tokens into the rare one, so the model + may perform worse when rare tokens appear in the data + example_value: + - 10000 + expected_impact: 2 + other_information: Specifying a vocab_file overrides this parameter + related_parameters: + - vocab_file, pretrained_embeddings + suggested_values: + A value that covers at least 95% of the tokens in the + data + suggested_values_reasoning: + Depending on the data distribution and how + important rare tokens are, 90%, 95% or 99% of the number of tokens + will leave out only very rare tokens that should not influence performance + substantially + ui_display_name: Most common (vocabulary size) + ngram_size: + default_value_reasoning: Size of the n-gram when using the `ngram` tokenizer. + example_value: + - 3 + ui_display_name: n-gram size + expected_impact: 2 + padding: + default_value_reasoning: + We usually want to add padding to the end of + a text sequence to fill in any remaining space as opposed to the beggining + so we set the default to right. + description_implications: + If you pad to the left, the encoded vector will + have leading padding tokens as opposed to trailing padding tokens. + This could matter based on the type of text input you are expecting. + expected_impact: 1 + related_parameters: + - "padding_symbol, + + max_sequence_length" + suggested_values: "'right'" + suggested_values_reasoning: + right padding is the usual way to add padding + to a text sequence + ui_display_name: Padding + padding_symbol: + ui_display_name: null + expected_impact: 1 + pretrained_model_name_or_path: + internal_only: true + ui_display_name: null + expected_impact: 0 + tokenizer: + default_value_reasoning: + 'The default tokenizer is `space_punct`, an abbreviation + of "Space punctuation". This tokenizer creates sub-words by dividing + the text on whitespace and punctuation characters. For example: The + text `''hello world!isn''t this great?''` would be transformed to + `[''hello'', ''world'', ''!'', ''isn'', "''", ''t'', ''this'', ''great'', + ''?'']`. This is the default value because it is a fast tokenizer + that works reasonably well.' + description_implications: + Choosing a tokenizer can be difficult. The primary + thing to check is that the tokenizer you have selected is compatible + with the language(s) in your text data. This means either selecting + a tokenizer that is language-specific (i.e. `french_tokenize` if working + with French text) or general enough that its tokenizations are language-agnostic + (i.e. `space_punct`). + example_value: + - space_punct + expected_impact: 3 + literature_references: + - https://huggingface.co/course/chapter2/4?fw=pt + related_parameters: + - vocab_file, pretrained_model_name_or_path + suggested_values: sentencepiece + suggested_values_reasoning: + "SentencePiece is a tokenizer developed by + Google which utilizes Byte-Pair Encoding (BPE), which strikes a good + balance between character-level and word-level tokenization (more + info on BPE here: https://towardsdatascience.com/byte-pair-encoding-the-dark-horse-of-modern-nlp-eb36c7df4f10 + ). This tokenizer is language-agnostic and more sophisticated than + the default." + ui_display_name: Tokenizer + unknown_symbol: + ui_display_name: null + expected_impact: 1 + vocab_file: + default_value_reasoning: + The vocabulary can be parsed automatically from + the incoming input features. + description_implications: + It can be useful to specify your own vocabulary + list if the vocabulary is very large, there's no out of the box tokenizer + that fits your data, or if there are several uncommon or infrequently + occurring tokens that we want to guarantee to be a part of the vocabulary, + rather than treated as an unknown. + expected_impact: 0 + ui_display_name: Vocab File + class_similarities: + expected_impact: 1 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 +timeseries: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + expected_impact: 3 + padding: + ui_display_name: null + expected_impact: 1 + padding_value: + ui_display_name: null + expected_impact: 1 + timeseries_length_limit: + ui_display_name: null + expected_impact: 2 + tokenizer: + ui_display_name: null + expected_impact: 3 +vector: + preprocessing: + computed_fill_value: + internal_only: true + ui_display_name: null + fill_value: + expected_impact: 2 + ui_display_name: Fill Value + missing_value_strategy: + default_value_reasoning: + The default `fill_with_const` replaces missing + values with the value specified by `fill_value`. + description_implications: + Determines how missing values will be handled + in the dataset. Not all strategies are valid for all datatypes. For + example, `fill_with_mean` is applicable to continuous numerical data. + Note that choosing to drop rows with missing values could result in + losing information, especially if there is a high proportion of missing + values in the dataset. + expected_impact: 3 + related_parameters: + - fill_value + ui_display_name: Missing Value Strategy + vector_size: + ui_display_name: null + expected_impact: 3 + dependencies: + expected_impact: 1 + reduce_dependencies: + expected_impact: 1 + reduce_input: + expected_impact: 1 + softmax: + expected_impact: 3 + vector_size: + expected_impact: 3 diff --git a/ludwig/schema/metadata/configs/llm.yaml b/ludwig/schema/metadata/configs/llm.yaml new file mode 100644 index 0000000..d89ca74 --- /dev/null +++ b/ludwig/schema/metadata/configs/llm.yaml @@ -0,0 +1,246 @@ +base_model: + _anyOf: + preset: + ui_display_name: Preset + expected_impact: 3 + custom: + ui_display_name: Custom + expected_impact: 3 + _meta: + ui_display_name: Model Name + expected_impact: 3 + ui_component_type: radio_string_combined + short_description: This can be one of the presets or a fully qualified name of a pretrained model from the HuggingFace Hub +generation: + temperature: + ui_display_name: Temperature + default_value_reasoning: + Increasing the temperature will allow the model to generate more diverse sequences, + but will also increase the likelihood of generating nonsense. As such, we recommend setting this value to + something closer to 0 for classification tasks, and something closer to 1 for text generation tasks where the + goal is to generate novel text. + expected_impact: 3 + max_new_tokens: + ui_display_name: Max New Tokens + default_value_reasoning: + Increasing the maximum number of new tokens will allow the model + to generate longer sequences, but because inference time scales linearly with the sequence length, + longer sequences will be much slower to generate. For classification tasks, it's generally better to + use a smaller number of new tokens, while for text generation tasks, it's generally better to use a larger + number of new tokens. + expected_impact: 3 + num_beams: + ui_display_name: Number of Beams + default_value_reasoning: + Increasing the number of beams will allow the model to generate more diverse sequences, + but will also increase inference time. Some backends (like DeepSpeed) also do not support beam search. + As such, we recommend leaving this as 1 in most cases, unless you're finding the quality of the generated + sequences to be lacking. + expected_impact: 2 + top_k: + ui_display_name: Top K + expected_impact: 2 + top_p: + ui_display_name: Top P + expected_impact: 2 + max_length: + ui_display_name: Max Length + expected_impact: 2 + min_length: + ui_display_name: Min Length + expected_impact: 2 + min_new_tokens: + ui_display_name: Min New Tokens + expected_impact: 2 + do_sample: + ui_display_name: Do Sample + expected_impact: 2 + use_cache: + ui_display_name: Use Cache + expected_impact: 2 + prompt_lookup_num_tokens: + ui_display_name: Prompt Lookup Num Tokens + expected_impact: 2 +prompt: + retrieval: + type: + ui_display_name: Type + expected_impact: 3 + index_name: + ui_display_name: Index Name + expected_impact: 2 + model_name: + ui_display_name: Model Name + expected_impact: 2 + k: + ui_display_name: Top K + expected_impact: 2 + task: + ui_display_name: Task + ui_component_type: textarea + expected_impact: 3 + template: + ui_display_name: Template + ui_component_type: textarea + expected_impact: 3 +adapter: + _oneOf: + allOf: + ui_display_name: Perform parameter efficient fine-tuning + expected_impact: 3 + none: + ui_display_name: Disabled + expected_impact: 3 + _meta: + expected_impact: 3 + ui_component_type: radio_string_combined + lora: + type: + long_description: | + LoRA is a simple, yet effective, method for parameter-efficient fine-tuning of pretrained language models. + It works by adding a small number of trainable parameters to the model, which are used to adapt the + pretrained parameters to the downstream task. This allows the model to be fine-tuned with a much smaller + number of training examples, and can even be used to fine-tune models on tasks that have no training data + available at all. + r: + ui_display_name: R + expected_impact: 3 + alpha: + ui_display_name: Alpha + expected_impact: 1 + dropout: + ui_display_name: Dropout + expected_impact: 2 + target_modules: + ui_display_name: Target Modules + expected_impact: 2 + use_rslora: + ui_display_name: Enable RSLora + expected_impact: 2 + use_dora: + ui_display_name: Enable DoRa + expected_impact: 2 + init_lora_weights: + ui_display_name: LoRA Initializer + expected_impact: 3 + rank_pattern: + ui_display_name: Per-Layer Rank Pattern + expected_impact: 2 + alpha_pattern: + ui_display_name: Per-Layer Alpha Pattern + expected_impact: 2 + layer_replication: + ui_display_name: Layer Replication + expected_impact: 2 + tinylora: + type: + long_description: | + TinyLoRA is an extremely parameter-efficient fine-tuning method that uses SVD decomposition of frozen weights + and projects a tiny trainable vector through fixed random tensors. It can achieve fine-tuning with as few as + 13 trainable parameters, making it ideal for extremely resource-constrained environments or edge deployment. + Paper: https://arxiv.org/abs/2602.04118 + c3a: + type: + long_description: | + C3A (Contextual/Conditional/Compositional Adapter) uses block-diagonal structured matrices for parameter + efficiency. It enables context-aware adapter routing and multi-task modularity, making it well-suited for + multi-domain generalization and compositional fine-tuning workflows. + oft: + type: + long_description: | + OFT (Orthogonal Fine-Tuning) applies orthogonal transformations to weight matrices, preserving the + hyperspherical energy of the pre-trained model. This prevents catastrophic forgetting and maintains output + diversity while adapting to new tasks. Paper: https://arxiv.org/abs/2306.07280 + hra: + type: + long_description: | + HRA (Householder Reflection Adaptation) parameterizes weight updates as products of Householder reflections, + which are orthogonal by construction. It provides stronger expressivity than OFT with fewer hyperparameters. + Paper: https://arxiv.org/abs/2405.17484 + waveft: + type: + long_description: | + WaveFT (Wavelet-domain Fine-Tuning) learns weight updates in the wavelet frequency domain. It provides a + different inductive bias from spatial methods like LoRA, often benefiting tasks with structured or periodic + patterns. Paper: https://arxiv.org/abs/2411.09295 + ln_tuning: + type: + long_description: | + LN-Tuning fine-tunes only the layer normalization parameters (weight and bias) of the model. It is + extremely parameter-efficient (often <0.1% of total parameters) while being surprisingly effective for + domain adaptation tasks. + vblora: + type: + long_description: | + VBLoRA (Vector Bank LoRA) represents LoRA weight matrices as a sparse combination of shared vectors from a + global bank. By reusing vectors across layers, it achieves significant parameter compression beyond standard + LoRA. Paper: https://arxiv.org/abs/2405.15179 + adalora: + type: + long_description: | + AdaLoRA is an extension of LoRA that allows the model to adapt the pretrained parameters to the downstream + task in a task-specific manner. This is done by adding a small number of trainable parameters to the model, + which are used to adapt the pretrained parameters to the downstream task. This allows the model to be + fine-tuned with a much smaller number of training examples, and can even be used to fine-tune models on tasks + that have no training data available at all. + prompt_learning: + num_virtual_tokens: + ui_display_name: Num Virtual Tokens + expected_impact: 3 + prompt_tuning: + prompt_tuning_init: + ui_display_name: Prompt Tuning Init + expected_impact: 2 + prompt_tuning_init_text: + ui_display_name: Prompt Tuning Init Text + expected_impact: 2 + adaption_prompt: + type: + long_description: | + Adaption Prompt is taken from the paper + [LLaMA-Adapter: Efficient Fine-tuning of Language Models with Zero-init Attention](https://arxiv.org/pdf/2303.16199.pdf). + It adds a set of learnable adaption prompts and prepends them to the word tokens at higher transformer layers. + Then, a zero-initialized attention mechanism with zero gating is introduced, which adaptively injects + new instructional cues into LLaMA, while effectively preserving its pre-trained knowledge. According to + the paper, LLaMA-Adapter can generate high-quality responses, comparable to Alpaca with fully fine-tuned + 7B parameters. + adapter_len: + ui_display_name: Adapter Length + expected_impact: 3 + adapter_layers: + ui_display_name: Adapter Layers + expected_impact: 3 + ia3: + type: + long_description: | + [Infused Adapter by Inhibiting and Amplifying Inner Activations](https://arxiv.org/pdf/2205.05638.pdf), or IA3, + is a method that adds three learned vectors `l_k``, `l_v``, and `l_ff`, to rescale the keys and values of the self-attention and encoder-decoder attention layers, and the intermediate activation of the position-wise feed-forward network respectively. These learned vectors are the only trainable parameters during fine-tuning, and thus the original weights remain frozen. Dealing with learned vectors (as opposed to learned low-rank updates to a weight matrix like LoRA) keeps the number of trainable parameters much smaller. + target_modules: + ui_display_name: Target Modules + expected_impact: 3 + feedforward_modules: + ui_display_name: Feedforward Modules + expected_impact: 3 + fan_in_fan_out: + ui_display_name: Fan In Fan Out + expected_impact: 3 + modules_to_save: + ui_display_name: Modules to Save + expected_impact: 3 + init_ia3_weights: + ui_display_name: Init IA3 Weights + expected_impact: 3 +quantization: + _oneOf: + object: + ui_display_name: Quantization + expected_impact: 3 + none: + ui_display_name: No Quantization + expected_impact: 3 + _meta: + expected_impact: 3 + ui_component_type: radio_string_combined + bits: + ui_display_name: Bits per parameter + expected_impact: 3 diff --git a/ludwig/schema/metadata/configs/loss.yaml b/ludwig/schema/metadata/configs/loss.yaml new file mode 100644 index 0000000..dc02daf --- /dev/null +++ b/ludwig/schema/metadata/configs/loss.yaml @@ -0,0 +1,85 @@ +MSELoss: + weight: + expected_impact: 2 +MAELoss: + weight: + expected_impact: 2 +RMSELoss: + weight: + expected_impact: 2 +RMSPELoss: + weight: + expected_impact: 2 +BWCEWLoss: + positive_class_weight: + expected_impact: 3 + robust_lambda: + expected_impact: 2 + confidence_penalty: + expected_impact: 2 + weight: + expected_impact: 2 +SoftmaxCrossEntropyLoss: + class_weights: + expected_impact: 3 + robust_lambda: + expected_impact: 2 + confidence_penalty: + expected_impact: 2 + class_similarities: + expected_impact: 2 + class_similarities_temperature: + expected_impact: 2 + weight: + expected_impact: 2 +SequenceSoftmaxCrossEntropyLoss: + class_weights: + expected_impact: 3 + robust_lambda: + expected_impact: 2 + confidence_penalty: + expected_impact: 2 + class_similarities: + expected_impact: 2 + class_similarities_temperature: + expected_impact: 2 + weight: + expected_impact: 2 + unique: + expected_impact: 2 +SigmoidCrossEntropyLoss: + class_weights: + expected_impact: 3 + weight: + expected_impact: 2 +FocalLoss: + alpha: + expected_impact: 3 + gamma: + expected_impact: 3 + weight: + expected_impact: 2 +DiceLoss: + smooth: + expected_impact: 2 + weight: + expected_impact: 2 +LovaszSoftmaxLoss: + weight: + expected_impact: 2 +NTXentLoss: + temperature: + expected_impact: 3 + weight: + expected_impact: 2 +PolyLoss: + epsilon: + expected_impact: 3 + weight: + expected_impact: 2 +SparsemaxLoss: + weight: + expected_impact: 2 +Entmax15Loss: + weight: + expected_impact: 2 diff --git a/ludwig/schema/metadata/configs/optimizers.yaml b/ludwig/schema/metadata/configs/optimizers.yaml new file mode 100644 index 0000000..bd68f3c --- /dev/null +++ b/ludwig/schema/metadata/configs/optimizers.yaml @@ -0,0 +1,59 @@ +gradient_clipping: + default_value_reasoning: + A conservative cap on the maximum gradient size to apply + over a single training step. + description_implications: + Gradient clipping is a technique to prevent exploding + gradients in very deep networks. Increasing gradient clipping can help with + model training loss curve stability, but it can also make training less efficient + as weight at each training step is capped. + expected_impact: 1 + suggested_values_reasoning: + It's usually sensible to have some conservative notion + of gradient clipping to make modeling robust to a particularly bad or noisy + batch of examples. + ui_display_name: Gradient Clipping +momentum: + expected_impact: 1 +weight_decay: + expected_impact: 1 +dampening: + expected_impact: 1 +nesterov: + expected_impact: 1 +max_iter: + expected_impact: 1 +max_eval: + expected_impact: 1 +tolerance_grad: + expected_impact: 1 +tolerance_change: + expected_impact: 1 +history_size: + expected_impact: 1 +line_search_fn: + expected_impact: 1 +betas: + expected_impact: 1 +amsgrad: + expected_impact: 1 +rho: + expected_impact: 1 +initial_accumulator_value: + expected_impact: 1 +lr_decay: + expected_impact: 1 +learning_rate_power: + expected_impact: 1 +l1_regularization_strength: + expected_impact: 1 +l2_regularization_strength: + expected_impact: 1 +momentum_decay: + expected_impact: 1 +alpha: + expected_impact: 1 +eps: + expected_impact: 1 +centered: + expected_impact: 1 diff --git a/ludwig/schema/metadata/configs/preprocessing.yaml b/ludwig/schema/metadata/configs/preprocessing.yaml new file mode 100644 index 0000000..a29d2ec --- /dev/null +++ b/ludwig/schema/metadata/configs/preprocessing.yaml @@ -0,0 +1,175 @@ +force_split: + default_value_reasoning: + We do not expect most datasets to have an explicit "split" + column in the data. Used mostly internally by ludwig datasets. + expected_impact: 3 + related_parameters: + - split_probabilities, stratify + ui_display_name: Force Split +oversample_minority: + default_value_reasoning: + We do not want to randomly oversample by default since + this is a strategy to deal with imbalanced datasets, but can cause issues + if not implemented correctly. + description_implications: + The higher the value you choose gets to 1, the closer + you will be to having an equal imbalance ratio (i.e. 1:1 positive to negative + class), however this can lead to problems of overfitting when oversampling + is used too liberally. As a rule of thumb, starting oversampling with a very + conservative approach and increasing in small incremements is probably the + best way to improve your model without experiencing model overfitting. + example_value: + - 0.5 + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/random-oversampling-and-undersampling-for-imbalanced-classification/ + other_information: + This parameter is one of many strategies to combat issues with + class imbalance, though it is not a cure all. Oversampling too much can cause + overfitting which can adversely affect your model so use with caution. + suggested_values: Depends on imbalance ratio and dataset size + ui_display_name: Oversample Minority +sample_ratio: + default_value_reasoning: + The default value is 1.0 because we do not want to shrink + the dataset by default. In the rare occurences when you do want to downsample + the entire dataset, this parameter is available, however it is not enabled + by default, hence a default value of 1.0 + description_implications: + Decreases the amount of data you are inputting into + the model. Could be useful if you have more data than you need and you are + concerned with computational costs. + example_value: + - 0.8 + expected_impact: 2 + suggested_values: Depends on data size + ui_display_name: Sample Ratio +sample_size: + default_value_reasoning: + The default value is None because we do not want to shrink + the dataset by default, and we do not know the size of an arbitrary dataset. + By setting the default to None, we fall back on the sample_ratio to determine + the size of the dataset. + description_implications: + Decreases the amount of data you are inputting into + the model. Could be useful if you have more data than you need and you are + concerned with computational costs. More useful than sample_ratio if you + know the exact number of samples you want to train on instead of knowing the proportion. + example_value: + - 1000 + expected_impact: 2 + suggested_values: Depends on data size + ui_display_name: Sample Size +column: + expected_impact: 3 + ui_display_name: Split Column + ui_component_type: column_selector +split_probabilities: + default_value_reasoning: + Most of the dataset should be used for training, with + some portion heldout for validation and testing. + description_implications: + "In machine learning, data splitting is typically done + to avoid overfitting. That is an instance where a machine learning model fits + its training data too well and fails to reliably fit additional data. + + + The training set is the portion of data used to train the model. The model + should observe and learn from the training set, optimizing any of its parameters. + + + The dev set is a data set of examples used to change learning process parameters. + It is also called the cross-validation or model validation set. This set of + data has the goal of ranking the model's accuracy and can help with model + selection. + + + The testing set is the portion of data that is tested in the final model and + is compared against the previous sets of data. The testing set acts as an + evaluation of the final mode and algorithm." + expected_impact: 3 + literature_references: + - "https://www.techtarget.com/searchenterpriseai/definition/data-splitting#:~:text=Data%20splitting%20is%20when%20data,creating%20models%20based%20on%20data. " + other_information: "Split data into train, validation, and test. + + + By default, Ludwig looks for a column named split (case-sensitive) which is + expected to consist of 3 possible values that correspond to different datasets: + + + 0: train + + 1: validation + + 2: test + + If the data does not contain the split column, then data is randomly split + based on splitting percentages, defined by split_probabilities. + + + If force_split is true, the the split column in the dataset is ignored and + the dataset is randomly split based on splitting percentages, defined by split_probabilities." + related_parameters: + - force_split, stratify + suggested_values: + - 0.8 + - 0.1 + - 0.1 + suggested_values_reasoning: + For larger datasets, it can be beneficial to use more + data for training, since the test and validation sets are still plenty big + for getting a good sense of model generalization. + ui_display_name: Split Probabilities +stratify: + default_value_reasoning: + The default is set to None since we do not want to stratify + unless specifically told to do so. There are a variety of reasons for this, + but one example is that our data set may not even have a categorical feature + to stratify on. + description_implications: Depends on dataset + example_value: + - Category_Feature_A + expected_impact: 3 + literature_references: + - https://medium.com/analytics-vidhya/stratified-sampling-in-machine-learning-f5112b5b9cfe + related_parameters: + - force_split, split_probabilities + suggested_values: Depends on dataset + ui_display_name: Stratify +undersample_majority: + default_value_reasoning: + We do not want to randomly undersample by default since + this is a strategy to deal with imbalanced datasets, but can cause issues + if not implemented correctly. + description_implications: + The higher the value you choose gets to 1, the closer + you will be to having an equal imbalance ratio (i.e. 1:1 positive to negative + class), however this can lead to problems of data loss when undersampling + is used too liberally. As a rule of thumb, starting undersampling with a very + conservative approach and increasing in small incremements is probably the + best way to improve your model without experiencing catastrophic data loss + effects. + example_value: + - 0.5 + expected_impact: 2 + literature_references: + - https://machinelearningmastery.com/random-oversampling-and-undersampling-for-imbalanced-classification/ + other_information: + This parameter is one of many strategies to combat issues with + class imbalance, though it is not a cure all. Undersampling too much can cause + loss of data which can adversely affect your model so use with caution. + suggested_values: Depends on imbalance ratio and dataset size + ui_display_name: Undersample Majority +cache_encoder_embeddings: + default_value_reasoning: + Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so + it's not always the case that you would always want to enable it when possible. + expected_impact: 1 + ui_display_name: Cache Encoder Embeddings +global_max_sequence_length: + expected_impact: 2 + ui_display_name: Global Max Sequence Length + description_implications: + Specifically for LLMs. This is the maximum number of tokens going into the model's forward pass during training. Sequences will be truncated to this length after merging the tokens from the input with tokens from the target. If not set, the total length of the merged input and target token sequences will be used. + example_value: + - 512 diff --git a/ludwig/schema/metadata/configs/trainer.yaml b/ludwig/schema/metadata/configs/trainer.yaml new file mode 100644 index 0000000..e454eb9 --- /dev/null +++ b/ludwig/schema/metadata/configs/trainer.yaml @@ -0,0 +1,894 @@ +ecd: + effective_batch_size: + commonly_used: true + expected_impact: 2 + related_parameters: + - batch_size + suggested_values: auto + ui_display_name: Effective Batch Size + batch_size: + commonly_used: true + default_value_reasoning: Not too big, not too small. + description_implications: + There's conflicting evidence about what batch size to + use. Using a higher batch size will achieve the highest throughput and training + efficiency. However, there's also evidence that depending on other hyperparameters, + a smaller batch size may produce a higher quality model. + Batch size and learning rate are strongly intertwined, + so a commonly adopted strategy to set them is to find a the largest batch size + that allows the training process not to run out of memory, + and then find the best learning rate that makes the training converge + with that batch size. + expected_impact: 3 + related_parameters: + - eval_batch_size + - learning_rate + suggested_values: auto + suggested_values_reasoning: + Auto batch size will determine the largest batch size that allows + the training process not to run out of memory. + Alternatively, try at least a few different batch sizes to get a + sense of whether and how batch size affects model performance. + ui_display_name: Batch Size + bucketing_field: + expected_impact: 1 + other_information: + When not null, when creating batches, instead of shuffling + randomly, the length along the last dimension of the matrix of the specified + input feature (i.e. the length of a sequence or text) + is used for bucketing examples and then randomly shuffled examples + from the same bin are sampled. Padding is trimmed to the longest example in + the batch. The specified feature should be either a sequence or text feature + and the encoder encoding it has to be rnn. When used, bucketing improves speed + of rnn encoding up to 1.5x, depending on the length distribution of the inputs. + ui_display_name: Bucketing Field + checkpoints_per_epoch: + default_value_reasoning: + Per-epoch behavior, which scales according to the dataset + size. + description_implications: + "Epoch-based evaluation (using the default: 0) is an + appropriate fit for small datasets that fit in memory and + train quickly. Commonly available tabular datasets fit in this cateogry. + However, this is a poor fit for unstructured datasets, which tend to be much + larger, and train more slowly due to larger models. + It's important to setup evaluation such that you do not wait several hours + before getting a single evaluation result. In general, it is not necessary + for models to train over the entirety of a dataset, nor evaluate over the + entirety of a test set, to produce useful monitoring metrics and signals to + indicate model health. + It is also more engaging and more valuable to ensure a frequent pulse of evaluation + metrics, even if they are partial." + expected_impact: 2 + related_parameters: + - train_steps + - steps_per_checkpoint + suggested_values: 2 - 10, for larger datasets + suggested_values_reasoning: + Running evaluation too frequently can be wasteful + while running evaluation not frequently enough can be prohibitively uninformative. + In many large-scale training runs, evaluation is often configured to run on + a sub-epoch time scale, or every few thousand steps. + ui_display_name: Checkpoints per epoch + layers_to_freeze_regex: + default_value_reasoning: + By default no layers will be frozen when fine-tuning a pretrained model. + description_implications: + Freezing specific layers can improve a pretrained model's performance in a number + of ways. At a basic level, freezing early layers can prevent overfitting by retaining + more general features (beneficial for small datasets). Also can reduce computational + resource use and lower overall training time due to less gradient calculations. + expected_impact: 1 + early_stop: + default_value_reasoning: + Deep learning models are prone to overfitting. It's generally + a good policy to set up some early stopping criteria as it's not useful to + have a model train after it's maximized what it can learn. 5 consecutive rounds + of evaluation where there hasn't been any improvement on the validation set + (including chance) is a reasonable policy to start with. + description_implications: + Decreasing this value is a more aggressive policy. Decreasing + early stopping makes model training less forgiving, as the model has less + runway to demonstrate consecutive metric improvements before the training + run is quit. This can be efficient for pruning bad models earlier, but since + the training process is inherently non-deterministic and noisy, sometimes + improvements happen very gradually over a long period of time. + Extending this value leads to longer training times, + but potentially also better final performance. + expected_impact: 3 + related_parameters: + - epochs + - train_steps + suggested_values: 5 - 10 + suggested_values_reasoning: + There's potentially a lot of randomness in how models + train, but so many consecutive rounds of no improvement is usually a good + indicator that the model converged or overfitted. + ui_display_name: Early Stop + epochs: + default_value_reasoning: + A very high training length ceiling. Models will almost + always hit early stopping criteria before hitting a 100-epoch ceiling. + description_implications: + Decreasing this will shorten the overall runway for + training the model. + expected_impact: 3 + related_parameters: + - train_steps + suggested_values: 100 + suggested_values_reasoning: + Usually it's sensible to leave this very high and + rely on a solid early stopping policy to dictate when the model should stop + training. Some models and hyperparameter configurations require many epochs + through the dataset to converge while others converge before a single epoch + through the data. + ui_display_name: Epochs + eval_batch_size: + default_value_reasoning: Use the same batch size used for training. + description_implications: + By increasing the `eval_batch_size` past the `batch_size` + parameter set value, you allow for more parallelism in the batch evaluation + step and speed up evaluation. For example, if you have to evaluate the model + on a test set of size 1000, it is faster to evaluate two times with two batches + of size 500 as opposed to ten times with ten batches of 100. + Setting this parameter higher without getting past out memory limits + will speed up the model training process overall. + example_value: + - 512 + expected_impact: 1 + other_information: + Should only set the eval_batch_size to a level that you can fit + in memory. + related_parameters: + - batch_size + suggested_values: + - 256 + - 512 + - 1024 + suggested_values_reasoning: + By observing memory consumption on training jobs, + you can get a sense of how much extra memory is available for increasing this + value. A good rule of thumb can be experimentally doubling the eval batch + size if you do not have insight into memory usage. + ui_display_name: Evaluation Batch Size + evaluate_training_set: + default_value_reasoning: + It could be useful to monitor evaluation metrics on the + training set to understand convergence. + description_implications: + Running evaluation on the full training set, when your + training set is large, can be a huge computational cost. Turning off training + set evaluation will lead to significant gains in training throughput and efficiency. + For small datasets that train and evaluate quickly, the choice is trivial. + expected_impact: 1 + suggested_values: false + suggested_values_reasoning: + Running full-scale evaluation on the full training + set doesn't usually provide any useful information over the validation dataset. + Even with this set to False, continuous training loss metrics are still computed, + so it will still be easy to spot signs of overfitting like when the training-validation + loss curves diverge. + ui_display_name: Evaluate Training Set + gradient_clipping: + default_value_reasoning: + A conservative cap on the maximum gradient size to apply + over a single training step. + description_implications: + Gradient clipping is a technique to prevent exploding + gradients in very deep networks. Increasing gradient clipping can help with + model training loss curve stability, but it can also make training slower + as weights may not be updated as fast. + expected_impact: 2 + suggested_values_reasoning: + It's usually sensible to enable gradient clipping to make modeling robust + to particularly bad or noisy batches of examples. + ui_display_name: Gradient Clipping + increase_batch_size_eval_metric: + expected_impact: 1 + ui_display_name: "Batch Size Increase: Evaluation Metric" + increase_batch_size_eval_split: + expected_impact: 1 + ui_display_name: "Batch Size Increase: Evaluation Split" + increase_batch_size_on_plateau: + expected_impact: 1 + ui_display_name: Batch Size Increase On Plateau + increase_batch_size_on_plateau_patience: + expected_impact: 1 + ui_display_name: "Batch Size Increase On Plateau: Patience" + increase_batch_size_on_plateau_rate: + expected_impact: 1 + ui_display_name: "Batch Size Increase On Plateau: Rate" + learning_rate: + commonly_used: true + default_value_reasoning: Middle of the road learning rate to start with. + description_implications: + The learning rate is a hyperparameter that controls + how much to change the model in response to the estimated error each time + the model weights are updated. Increasing the learning rate may decrease learning + curve stability but also increase learning speed and efficiency, leading to + faster model convergence. Decreasing the learning rate can help stabilize + learning curves at the cost of slower time to convergence. + expected_impact: 3 + suggested_values: 0.00001 - 0.1 or auto + related_parameters: + - decay + suggested_values_reasoning: + Tabular models trained from scratch typically use + learning rates around 1e-3 while learning rates for pre-trained models should + be much smaller, typically around 1e-5, which is important to mitigate catastrophic + forgetting. To make the model more robust to any specific choice of learning + rate, consider turning enabling learning rate decay. + ui_display_name: Learning Rate + learning_rate_scaling: + default_value_reasoning: + Traditionally the learning rate is scaled linearly with + the number of workers to reflect the proportion by which the effective batch + size is increased. + description_implications: + Traditionally the learning rate is scaled linearly with + the number of workers to reflect the proportion by which the effective batch + size is increased. For very large batch sizes, a softer square-root scale + can sometimes lead to better model performance. If the learning rate is hand-tuned + for a given number of workers, setting this value to constant can be used + to disable scale-up. + expected_impact: 1 + suggested_values: linear or sqrt + suggested_values_reasoning: + Traditionally the learning rate is scaled linearly + with the number of workers to reflect the proportion by which the effective + batch size is increased. For very large batch sizes, a softer square-root + scale can sometimes lead to better model performance. If the learning rate + is hand-tuned for a given number of workers, setting this value to constant + can be used to disable scale-up. + ui_display_name: Learning Rate Scaling + max_batch_size: + default_value_reasoning: Not typically required. + description_implications: + Value used to manually limit the batch sizes explored + by auto batch size tuning and batch size increasing on plateau. + example_value: + - 1024 + expected_impact: 1 + related_parameters: + - batch_size + - increase_batch_size_on_plateau + ui_display_name: Max Batch Size + optimizer: + default_value_reasoning: + First try Adam because it is shown to return good + results without an advanced fine tuning. + description_implications: + "Choosing a good optimizer for your machine learning + project can be overwhelming. Popular deep learning libraries such as PyTorch + or TensorFLow offer a broad selection of different optimizers, each + with its own strengths and weaknesses. However, picking the wrong optimizer + can have a substantial negative impact on the performance of your machine + learning model [1][2]. This makes optimizers a critical design choice in + the process of building, testing, and deploying your machine learning model." + expected_impact: 3 + literature_references: + - https://www.youtube.com/watch?v=mdKjMPmcWjY + suggested_values: adam, adamw + suggested_values_reasoning: + "As a rule of thumb: If you have the resources to + find a good learning rate schedule, SGD with momentum is a solid choice. If + you are in need of quick results without extensive hyperparameter tuning, + adaptive gradient methods like adam or adamw are good choices." + ui_display_name: Optimizer + regularization_lambda: + default_value_reasoning: + How to tune the overall impact of the regularization + term by multiplying its value by a scalar known as lambda (also called the + regularization rate). + description_implications: + "When choosing a lambda value, the goal is to strike + the right balance between simplicity and training-data fit: + If your lambda value is too high, your model will be simple, but you run the + risk of underfitting your data. Your model won't learn enough about the training + data to make useful predictions. + If your lambda value is too low, your model will be more complex, and you + run the risk of overfitting your data. Your model will learn too much about + the particularities of the training data, and won't be able to generalize + to new data. The ideal value of lambda produces a model that generalizes well + to new, previously unseen data. Unfortunately, that ideal value of lambda + is data-dependent, so you'll need to do some tuning. We recommend trying + a handful of values (0.001, 0.02, ... 0.4) gradually increasing the value until + training curves get worse" + expected_impact: 2 + literature_references: + - "https://developers.google.com/machine-learning/crash-course/regularization-for-simplicity/lambda " + related_parameters: + - regularization_type + suggested_values: 0.1 + suggested_values_reasoning: + "The most common type of regularization is L2, also + called weight decay, with values often on a logarithmic + scale between 0 and 0.1, such as 0.1, 0.001, 0.0001, etc." + ui_display_name: Regularization Lambda + regularization_type: + default_value_reasoning: L2 is a standard regularization to start with. + description_implications: + "L1 regularization penalizes the sum of absolute values + of the weights, whereas L2 regularization penalizes the sum of squares of + the weights. + The L1 regularization solution is sparse, meaning some weights will be zero, + others will be large. + The L2 regularization solution is non-sparse, most weights will be small. + L2 regularization does not perform feature + selection, since weights are only reduced to values near 0 instead of 0. + L1 regularization implicitly performs feature selection. L1 regularization is more + robust to outliers." + expected_impact: 3 + literature_references: + - "https://neptune.ai/blog/fighting-overfitting-with-l1-or-l2-regularization#:~:text=The%20differences%20between%20L1%20and,regularization%20solution%20is%20non%2Dsparse. " + related_parameters: + - regularization_lambda + suggested_values: L2 + ui_display_name: Regularization Type + should_shuffle: + default_value_reasoning: + In general, it's a good idea to mix up data on each batch + so that the neural network gets the broadest exposure to the dataset. + description_implications: + Turning off mini-batch shuffling can make training faster, + but it may lead to worse performance overall as shuffling helps mitigate overfitting. + expected_impact: 1 + literature_references: + - "https://stats.stackexchange.com/questions/245502/why-should-we-shuffle-data-while-training-a-neural-network#:~:text=it%20helps%20the%20training%20converge,the%20order%20of%20the%20training " + suggested_values: true + suggested_values_reasoning: + One of the most powerful things about neural networks + is that they can be very complex functions, allowing one to learn very complex + relationships between your input and output data. These relationships can + include things you would never expect, such as the order in which data is + fed in per epoch. If the order of data within each epoch is the same, then + the model may use this as a way of reducing the training error, which is a + sort of overfitting. + ui_display_name: Should Shuffle + steps_per_checkpoint: + default_value_reasoning: + By default, we evaluate once per epoch, which scales + according to the dataset size. + description_implications: + "Epoch-based evaluation (using the default: 0) is an + appropriate fit for tabular datasets, which are small, fit in memory, and + train quickly. + However, this is a poor fit for unstructured datasets, which tend to be much + larger, and train more slowly due to larger models. + It's important to setup evaluation such that you do not wait several hours + before getting a single evaluation result. In general, it is not necessary + for models to train over the entirety of a dataset, nor evaluate over the + entirety of a test set, to produce useful monitoring metrics and signals to + indicate model health. + It is also more engaging and more valuable to ensure a frequent pulse of evaluation + metrics, even if they are partial." + expected_impact: 1 + related_parameters: + - checkpoints_per_epoch + suggested_values: 1000-10000 for larger datasets + suggested_values_reasoning: + Running evaluation too frequently can be wasteful + while running evaluation not frequently enough can be prohibitively uninformative. + In many large-scale training runs, evaluation is often configured to run on + a sub-epoch time scale, or every few thousand steps. + ui_display_name: Steps Per Checkpoint + train_steps: + default_value_reasoning: + This defaults to `epochs`, which is a very high training + length ceiling. Models will almost always hit early stopping criteria before + reaching the absolute end of the training runway. + description_implications: + Decreasing this parameter will shorten the overall runway for + training the model. + expected_impact: 1 + related_parameters: + - epochs + suggested_values: Leave unset, or 1000000, 1 for debugging + suggested_values_reasoning: + Usually it's sensible to leave the value of this parameter very high and + rely on a solid early stopping policy to dictate when the model should stop + training. Some models and hyperparameter configurations require many epochs + through the dataset to converge while others converge before a single epoch + through the data. + ui_display_name: Train Steps + eval_steps: + default_value_reasoning: + The default value is None because we do not want to lower the number of evaluation steps + by default, and we do not know the size of an arbitrary dataset. + By setting the default to None, we simply evaluate on the full evaluation set. + description_implications: + The smaller this value of this parameter, the less time evaluation will take. + expected_impact: 2 + suggested_values: Depends on data size and prioritization of quality vs. speed + suggested_values_reasoning: + Normally, evaluation should use the entire evaluation set, and this is + recommended to achieve the highest quality evaluation. However, using + the full evaluation set can be slow, so the value of this parameter should + be set depending on which is more important for the task at hand -- quality + or speed. + ui_display_name: Evaluation Steps + use_mixed_precision: + default_value_reasoning: + Speed up training by using float16 parameters where it + makes sense. + description_implications: + Mixed precision training on GPU can dramatically speedup + training, with some risks to model convergence. + expected_impact: 3 + literature_references: + - https://pytorch.org/blog/what-every-user-should-know-about-mixed-precision-training-in-pytorch/ + suggested_values: false + suggested_values_reasoning: + Suggested to enable this if training is taking too + long on GPU. + ui_display_name: Use Mixed Precision + compile: + default_value_reasoning: + Model compilation has been shown to significantly speedup training by upwards of 20%, but does impose + some delay to compile the model at the beginning of training. This feature is experimental for now, + but may become the default in future versions. + description_implications: + Model compilation on GPU, when used in conjunction with automatic mixed precision, can speed up training + by upwards of 20%. + expected_impact: 3 + suggested_values: false + suggested_values_reasoning: + Suggested to enable this if training is taking too + long on GPU. + ui_display_name: Compile + gradient_accumulation_steps: + default_value_reasoning: + Gradient accumulation is something that should be enabled only once it has been observed that either GPU + utilization is low due to low bandwidth between distributed workers, or that there is too much variance + in the training process due to very low batch sizes. + description_implications: + Gradient accumulation is useful to (1) reduce network bandwidth overhead in multi-node distributed training + scenarios where bandwidth is the bottleneck, and (2) train with larger effective batch sizes when the max + batch size the GPU can accommodate is very small. The first scenario occurs when the interconnect between + nodes is slow, so performing gradient synchronization (allreduce) less frequently will speed up training. + The second scenario occurs in cases where the model being trained is very large (e.g., LLM) so training with + a larger batch size will help to smooth out the variance from training with a very small batch size. + expected_impact: 2 + suggested_values: false + suggested_values_reasoning: + Suggested to enable this if training is proceeding very slowly in distributed training (and GPU + utilization is low), or the batch size is very small and the loss curves look very spiky. + ui_display_name: Gradient Accumulation Steps + enable_gradient_checkpointing: + expected_impact: 2 + ui_display_name: Enable Gradient Checkpointing + default_value_reasoning: + Gradient checkpointing is a technique to reduce the memory footprint of the model by + trading compute for memory. This is useful when training very large models that run into out of memory + errors very quickly during training. It is particularly helpful when doing non-quantization based training + (adapter based or full fine-tuning). Gradient checkpointing works by recomputing the activations of the + model during the backward pass, rather than storing them in memory during the forward pass. + This is a tradeoff between compute and memory, as the activations need to be recomputed during + the backward pass, but the memory footprint is reduced. This is set to false by default because + it is not always beneficial to use gradient checkpointing, and it can sometimes slow down training. + validation_field: + default_value_reasoning: + Concrete evaluation metrics are usually better than loss, + the penalty for a bad prediction, which is only a proxy for prediction correctness. + description_implications: + This parameter affects 1) what the early stopping policy + looks at to determine when to early stop and 2) hyperparameter optimization + for determining the best trial. + expected_impact: 1 + related_parameters: + - validation_metric + suggested_values: default behavior + ui_display_name: Validation Field + validation_metric: + description_implications: + This parameter affects 1) what the early stopping policy + looks at to determine when to early stop and 2) hyperparameter optimization + for determining the best trial. + expected_impact: 1 + related_parameters: + - validation_field + suggested_values: default behavior + ui_display_name: Validation Metric + learning_rate_scheduler: + warmup_evaluations: + default_value_reasoning: + "Learning rate warmup is most commonly used when training with large batch sizes / distributed + training to avoid taking overly large steps at the beginning of training that might result in the + process getting stuck in a local optimum. Conventional wisdom when training with large batch sizes is + to use a larger learning rate (see: `learning_rate_scaling`) but gradually warm up to the larger learning + rate over a few epochs of training in the beginning. + Even when not training with large batch sizes, the randomness of how weights are initialized can result + in strange, noisy gradient updates during the beginning of your training run. As such, it's generally + recommended to use a small amount of warmup (e.g., 1 epoch / evaluation) even when the batch size is + relatively small." + description_implications: + Learning rate warmup sets a very low learning rate at the beginning of training and gradually + (linearly) increases to the base learning rate each step (batch) during training. + After your warmup steps you use your "regular" learning rate or learning rate scheduler. + expected_impact: 2 + related_parameters: + - warmup_fraction + - learning_rate_scaling + literature_references: + - https://arxiv.org/abs/1711.00489 + - https://datascience.stackexchange.com/questions/55991/in-the-context-of-deep-learning-what-is-training-warmup-steps + suggested_values: 0 - 5 + suggested_values_reasoning: + You don't want to warm up for too long, as after the model is starting to hill climb, you want to use the + full weight of the learning rate to descend into good loss minima. + + If you observe your loss curve converging very early into training, within the first few epochs, then + increasing learning rate warmup may help to mitigate this effect. Pretrained models can benefit from more + warmup to help offset the effects of catastrophic forgetting due to an overly high learning rate. + ui_display_name: Warmup Evaluations + warmup_fraction: + default_value_reasoning: + Similar to `warmup_evaluations` but expressed as a fraction of the total number of training steps, rather + that a certain number of evaluation phases. + description_implications: See `warmup_evaluations`. + expected_impact: 2 + related_parameters: + - warmup_evaluations + - learning_rate_scaling + suggested_values: 0.05 - 0.2 + suggested_values_reasoning: + You don't want to warm up for too long, as after the + model is starting to hill climb, you want to use the full weight of the learning + rate to descend into good loss minima. + ui_display_name: Warmup Fraction + decay: + description_implications: + "It\u2019s almost always a good idea to use a schedule.\ + \ For most models, try the exponential decay schedule first.\n\nThe exponential\ + \ schedule divides the learning rate by the same factor (%) every epoch. This\ + \ means that the learning rate will decrease rapidly in the first few epochs,\ + \ and spend more epochs with a lower value, but never reach exactly zero.\ + \ As a rule of thumb, compared to training without a schedule, you can use\ + \ a slightly higher maximum learning rate. Since the learning rate changes\ + \ over time, the whole training is not so sensitive to the value picked." + expected_impact: 3 + literature_references: + - "https://peltarion.com/knowledge-center/documentation/modeling-view/run-a-model/optimization-principles-(in-deep-learning)/learning-rate-schedule " + related_parameters: + - decay_rate + - decay_steps + - learning_rate + suggested_values: exponential + suggested_values_reasoning: + Starting with exponential decay is a safe place to start, as it is a "softer" decrease in the learning + rate over time, as compared with linear, which is more steep after the initial drop. Linear decay is + most useful when the risk of catastrophic forgetting is very high (e.g, for fine-tuning pretrained + models). Cosine annealing is a type of learning rate schedule that has the effect of starting with a + large learning rate that is relatively rapidly decreased to a minimum value before being increased + rapidly again. The resetting of the learning rate acts like a simulated restart of the learning process. + If you observe your loss curves shooting up (even on the training set) in later epochs, increasing the + decay rate may help mitigate this effect. + ui_display_name: Decay + decay_rate: + default_value_reasoning: + 4-5% decay each step is an empirically useful decay + rate to start with. + description_implications: + Increasing the decay rate will lower the learning rate + faster. This could make the model more robust to a bad (too high) initial + learning rate, but a decay rate that is too high could prohibit the model + from learning anything at all. + expected_impact: 2 + literature_references: + - "https://peltarion.com/knowledge-center/documentation/modeling-view/run-a-model/optimization-principles-(in-deep-learning)/learning-rate-schedule " + related_parameters: + - decay_steps + - learning_rate + suggested_values: 0.9 - 0.96 + suggested_values_reasoning: + Since this controls exponential decay, even a small + decay rate will still be strongly impactful. + ui_display_name: Decay Rate + decay_steps: + default_value_reasoning: + This default essentially enables the `learning_rate` + to decay by a factor of the `decay_rate` at 10000 training steps. + description_implications: + By increasing the value of decay steps, you are increasing + the number of training steps it takes to decay the learning rate by a factor + of `decay_rate`. In other words, the bigger this parameter, the slower the + learning rate decays. + example_value: + - 5000 + expected_impact: 2 + related_parameters: + - decay_rate + - learning_rate + suggested_values: 10000 +/- 500 at a time + suggested_values_reasoning: + The decay in the learning rate is calculated as the + training step divided by the `decay_steps` plus one. Then the `decay_rate` + is raised to the power of this exponent which is then multiplied to the current + learning rate. All this to say that the learning rate is only decayed by a + factor of the set `decay_rate` when the training step reaches the `decay_steps` + and then subsequently when it reaches any multiple of `decay_steps`. You can + think of `decay_steps` as a rate of decay for the `decay_rate`. + ui_display_name: Decay Steps + staircase: + default_value_reasoning: Performs learning rate decay in stepwise discrete manner. + description_implications: + An excessively aggressive decay results in optimizers + never reaching the minima, whereas a slow decay leads to chaotic updates without + significant improvement. Discrete learning rate decay is another parameter to help + tune a balance. + expected_impact: 1 + literature_references: + - https://neptune.ai/blog/how-to-choose-a-learning-rate-scheduler + suggested_values: false + suggested_values_reasoning: + We have not found strong evidence that discretely + decaying the learning rate is superior to doing so continuously in general, + but in specific tasks it might have a positive impact. + ui_display_name: Staircase + reduce_on_plateau: + expected_impact: 3 + ui_display_name: Reduce On Plateau + reduce_on_plateau_patience: + expected_impact: 2 + ui_display_name: Reduce On Plateau Patience + reduce_on_plateau_rate: + expected_impact: 2 + ui_display_name: Reduce On Plateau Rate + reduce_eval_metric: + expected_impact: 1 + ui_display_name: Reduce Eval Metric + reduce_eval_split: + expected_impact: 1 + ui_display_name: Reduce Eval Split + t_0: + expected_impact: 1 + ui_display_name: T_0 + t_mult: + expected_impact: 1 + ui_display_name: T_mult + eta_min: + expected_impact: 1 + ui_display_name: Eta Min + max_lr: + expected_impact: 2 + ui_display_name: Max LR (OneCycleLR) + pct_start: + expected_impact: 2 + ui_display_name: Pct Start (OneCycleLR) + div_factor: + expected_impact: 1 + ui_display_name: Div Factor (OneCycleLR) + final_div_factor: + expected_impact: 1 + ui_display_name: Final Div Factor (OneCycleLR) + inverse_sqrt_warmup_steps: + expected_impact: 2 + ui_display_name: Inverse Sqrt Warmup Steps + polynomial_power: + expected_impact: 2 + ui_display_name: Polynomial Power + polynomial_end_lr: + expected_impact: 1 + ui_display_name: Polynomial End LR + wsd_warmup_fraction: + expected_impact: 2 + ui_display_name: WSD Warmup Fraction + wsd_stable_fraction: + expected_impact: 2 + ui_display_name: WSD Stable Fraction + wsd_decay_fraction: + expected_impact: 2 + ui_display_name: WSD Decay Fraction +gbm: + learning_rate: + commonly_used: true + default_value_reasoning: Middle of the road learning rate to start with. + description_implications: + The learning rate is a hyperparameter that controls + how much to change the model in response to the estimated error each time + the model weights are updated. Increasing the learning rate may decrease learning + curve stability but also increase learning speed and efficiency, leading to + faster model convergence. Decreasing the learning rate can help stabilize + learning curves at the cost of slower time to convergence. + expected_impact: 3 + suggested_values: 0.00001 - 0.1 or auto + related_parameters: + - decay + suggested_values_reasoning: + Tabular models trained from scratch typically use + learning rates around 1e-3 while learning rates for pre-trained models should + be much smaller, typically around 1e-5, which is important to mitigate catastrophic + forgetting. To make the model more robust to any specific choice of learning + rate, consider turning enabling learning rate decay. + ui_display_name: Learning Rate + early_stop: + default_value_reasoning: + Deep learning models are prone to overfitting. It's generally + a good policy to set up some early stopping criteria as it's not useful to + have a model train after it's maximized what it can learn. 5 consecutive rounds + of evaluation where there hasn't been any improvement on the validation set + (including chance) is a reasonable policy to start with. + description_implications: + Decreasing this value is a more aggressive policy. Decreasing + early stopping makes model training less forgiving, as the model has less + runway to demonstrate consecutive metric improvements before the training + run is quit. This can be efficient for pruning bad models earlier, but since + the training process is inherently non-deterministic and noisy, sometimes + improvements happen very gradually over a long period of time. + Extending this value leads to longer training times, + but potentially also better final performance. + expected_impact: 3 + related_parameters: + - epochs + - train_steps + suggested_values: 5 - 10 + suggested_values_reasoning: + There's potentially a lot of randomness in how models + train, but so many consecutive rounds of no improvement is usually a good + indicator that the model converged or overfitted. + ui_display_name: Early Stop + eval_batch_size: + default_value_reasoning: Use the same batch size used for training. + description_implications: + By increasing the `eval_batch_size` past the `batch_size` + parameter set value, you allow for more parallelism in the batch evaluation + step and speed up evaluation. For example, if you have to evaluate the model + on a test set of size 1000, it is faster to evaluate two times with two batches + of size 500 as opposed to ten times with ten batches of 100. + Setting this parameter higher without getting past out memory limits + will speed up the model training process overall. + example_value: + - 512 + expected_impact: 1 + other_information: + Should only set the eval_batch_size to a level that you can fit + in memory. + related_parameters: + - batch_size + suggested_values: + - 256 + - 512 + - 1024 + suggested_values_reasoning: + By observing memory consumption on training jobs, + you can get a sense of how much extra memory is available for increasing this + value. A good rule of thumb can be experimentally doubling the eval batch + size if you do not have insight into memory usage. + ui_display_name: Evaluation Batch Size + evaluate_training_set: + default_value_reasoning: + It could be useful to monitor evaluation metrics on the + training set to understand convergence. + description_implications: + Running evaluation on the full training set, when your + training set is large, can be a huge computational cost. Turning off training + set evaluation will lead to significant gains in training throughput and efficiency. + For small datasets that train and evaluate quickly, the choice is trivial. + expected_impact: 1 + suggested_values: false + suggested_values_reasoning: + Running full-scale evaluation on the full training + set doesn't usually provide any useful information over the validation dataset. + Even with this set to False, continuous training loss metrics are still computed, + so it will still be easy to spot signs of overfitting like when the training-validation + loss curves diverge. + ui_display_name: Evaluate Training Set + validation_field: + default_value_reasoning: + Concrete evaluation metrics are usually better than loss, + the penalty for a bad prediction, which is only a proxy for prediction correctness. + description_implications: + This parameter affects 1) what the early stopping policy + looks at to determine when to early stop and 2) hyperparameter optimization + for determining the best trial. + expected_impact: 1 + related_parameters: + - validation_metric + suggested_values: default behavior + ui_display_name: Validation Field + validation_metric: + description_implications: + This parameter affects 1) what the early stopping policy + looks at to determine when to early stop and 2) hyperparameter optimization + for determining the best trial. + expected_impact: 1 + related_parameters: + - validation_field + suggested_values: default behavior + ui_display_name: Validation Metric + max_depth: + expected_impact: 3 + drop_rate: + expected_impact: 2 + tree_learner: + expected_impact: 2 + boosting_type: + expected_impact: 3 + boosting_rounds_per_checkpoint: + expected_impact: 2 + num_boost_round: + expected_impact: 2 + num_leaves: + expected_impact: 2 + min_data_in_leaf: + expected_impact: 2 + min_sum_hessian_in_leaf: + expected_impact: 1 + bagging_fraction: + expected_impact: 3 + pos_bagging_fraction: + expected_impact: 2 + neg_bagging_fraction: + expected_impact: 2 + bagging_freq: + expected_impact: 2 + bagging_seed: + expected_impact: 2 + feature_fraction: + expected_impact: 3 + feature_fraction_bynode: + expected_impact: 2 + feature_fraction_seed: + expected_impact: 2 + extra_trees: + expected_impact: 3 + extra_seed: + expected_impact: 2 + max_delta_step: + expected_impact: 1 + lambda_l1: + expected_impact: 3 + lambda_l2: + expected_impact: 3 + linear_lambda: + expected_impact: 2 + min_gain_to_split: + expected_impact: 1 + max_drop: + expected_impact: 2 + skip_drop: + expected_impact: 2 + xgboost_dart_mode: + expected_impact: 1 + uniform_drop: + expected_impact: 2 + drop_seed: + expected_impact: 2 + top_rate: + expected_impact: 1 + other_rate: + expected_impact: 1 + min_data_per_group: + expected_impact: 1 + max_cat_threshold: + expected_impact: 1 + cat_l2: + expected_impact: 1 + cat_smooth: + expected_impact: 1 + max_cat_to_onehot: + expected_impact: 1 + cegb_tradeoff: + expected_impact: 1 + cegb_penalty_split: + expected_impact: 1 + path_smooth: + expected_impact: 1 + verbose: + expected_impact: 1 + max_bin: + expected_impact: 1 + feature_pre_filter: + expected_impact: 1 +llm: + type: + commonly_used: true + default_value_reasoning: + It's useful to start with zero-shot or few-shot learning to see what the model + can do as a baseline before fine-tuning. + suggested_values: none or finetune + suggested_values_reasoning: + If you want to perform zero shot learning or few shot learning, you should set this to `none`. + If you want to perform fine-tuning, you should set this to `finetune`. + ui_display_name: Trainer Type + expected_impact: 3 diff --git a/ludwig/schema/metadata/feature_metadata.py b/ludwig/schema/metadata/feature_metadata.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/schema/metadata/parameter_metadata.py b/ludwig/schema/metadata/parameter_metadata.py new file mode 100644 index 0000000..f609fbb --- /dev/null +++ b/ludwig/schema/metadata/parameter_metadata.py @@ -0,0 +1,124 @@ +import json +from dataclasses import dataclass +from enum import Enum +from typing import Any + +from dataclasses_json import dataclass_json + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.misc_utils import memoized_method + + +@DeveloperAPI +class ExpectedImpact(int, Enum): + """The expected impact of determining a "good" value for a specific parameter. + + - HIGH: this parameter should almost always be included in a hyperopt run and can make or break a good model. + - MEDIUM: this parameter can sometimes make or break a good model. + - LOW: this parameter usually does not have a significant impact on model performance. + """ + + UNKNOWN = 0 + LOW = 1 + MEDIUM = 2 + HIGH = 3 + + +@DeveloperAPI +class ComputeTier(int, Enum): + """The compute tier defines the type of compute resources that a model typically needs to get good + throughput.""" + + CPU = 0 + """Model can train effectively on CPU hardware.""" + + GPU_LOW = 1 + """Model can train effectively on commodity GPU hardware, or inference optimized SKUs like NVIDIA T4.""" + + GPU_MEDIUM = 2 + """Model can train effectively on training-optimized GPU hardware like V100, A10G, or A5000.""" + + GPU_HIGH = 3 + """Model requires high-end GPUs like A100 or H100 to achieve good throughput.""" + + +@DeveloperAPI +@dataclass_json() +@dataclass +class ParameterMetadata: + """Contains descriptive information that pertains to a Ludwig configuration parameter.""" + + short_description: str = "" + """Quick description generally for UI display.""" + + long_description: str = "" + """In depth description generally for documentation purposes.""" + + ui_display_name: str | None = "" + """How this parameter can be displayed in a human-readable form.""" + + default_value_reasoning: str | None = None + """The reasoning behind the default value for this parameter.""" + + example_value: list[Any] | None = None + """Examples of other values that can be used for this parameter.""" + + related_parameters: list[str] | None = None + """List of related parameters that this parameter interacts with or depends on.""" + + other_information: str | None = None + """Other information that is relevant for this parameter.""" + + description_implications: str | None = None + """The intuition for how model performance would change if this parameter is changed.""" + + suggested_values: Any = None + """What values would a machine learning expert suggest users try to help improve their model? + + Should cover 95% (2-sigma) worth of use-cases. + """ + + suggested_values_reasoning: str | None = None + """The reasoning behind the suggested values, as well as model performance indicators or other intuition that + could help inform a user to make an educated decision about what values to experiment with for this + parameter.""" + + commonly_used: bool = False + """True if this parameter could be frequently used, would have a high impact, and/or would be interesting for a + machine learning practitioner.""" + + expected_impact: ExpectedImpact = ExpectedImpact.UNKNOWN + """The expected impact of determining a "good" value for this parameter.""" + + literature_references: list[str] | None = None + """List of links, papers, and blog posts to learn more.""" + + internal_only: bool = False + """True if this parameter is used strictly internally and should not be exposed to users.""" + + compute_tier: ComputeTier = ComputeTier.CPU + """The compute tier defines the type of compute resources that a model typically needs to get good + throughput.""" + + ui_component_type: str | None = None + """Override for HTML component type that should be used to render this field in UIs.""" + + @memoized_method(maxsize=1) + def to_json_dict(self) -> dict[str, Any]: + return json.loads(self.to_json()) + + +@DeveloperAPI +def convert_metadata_to_json(pm: ParameterMetadata) -> dict[str, Any]: + """Converts a ParameterMetadata dict to a normal JSON dict. + + NOTE: Without the json.loads call, to_json() returns + a string repr that is improperly parsed. + """ + if not pm: + return ParameterMetadata().to_json_dict() + return pm.to_json_dict() + + +# This is a quick way to flag schema parameters as internal only via the `parameter_metadata` argument +INTERNAL_ONLY = ParameterMetadata(internal_only=True) diff --git a/ludwig/schema/model_config.py b/ludwig/schema/model_config.py new file mode 100644 index 0000000..4daf2c2 --- /dev/null +++ b/ludwig/schema/model_config.py @@ -0,0 +1,2 @@ +# TODO(travis) consider removing this in the future after deprecation period +from ludwig.schema.model_types.base import ModelConfig # noqa diff --git a/ludwig/schema/model_types/__init__.py b/ludwig/schema/model_types/__init__.py new file mode 100644 index 0000000..e3cfa5e --- /dev/null +++ b/ludwig/schema/model_types/__init__.py @@ -0,0 +1,2 @@ +import ludwig.schema.model_types.ecd +import ludwig.schema.model_types.llm # noqa diff --git a/ludwig/schema/model_types/base.py b/ludwig/schema/model_types/base.py new file mode 100644 index 0000000..28eeafe --- /dev/null +++ b/ludwig/schema/model_types/base.py @@ -0,0 +1,195 @@ +import copy +from abc import ABC +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.config_validation.checks import get_config_check_registry +from ludwig.config_validation.validation import check_schema +from ludwig.constants import ( + BACKEND, + COLUMN, + DEPENDENCIES, + ENCODER, + INPUT_FEATURES, + MODEL_ECD, + NAME, + OUTPUT_FEATURES, + TIED, +) +from ludwig.error import ConfigValidationError +from ludwig.globals import LUDWIG_VERSION +from ludwig.schema import utils as schema_utils +from ludwig.schema.defaults.base import BaseDefaultsConfig +from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig, FeatureCollection +from ludwig.schema.hyperopt import HyperoptConfig +from ludwig.schema.model_types.utils import ( + merge_fixed_preprocessing_params, + merge_with_defaults, + sanitize_and_filter_combiner_entities_, + set_derived_feature_columns_, + set_hyperopt_defaults_, + set_llm_parameters, + set_preprocessing_parameters, + set_tagger_decoder_parameters, + set_validation_parameters, +) +from ludwig.schema.preprocessing import PreprocessingConfig +from ludwig.schema.trainer import BaseTrainerConfig +from ludwig.types import ModelConfigDict +from ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version +from ludwig.utils.data_utils import get_sanitized_feature_name, load_yaml +from ludwig.utils.registry import Registry + +model_type_schema_registry = Registry() + + +@DeveloperAPI +class ModelConfig(schema_utils.LudwigBaseConfig, ABC): + input_features: FeatureCollection[BaseInputFeatureConfig] + output_features: FeatureCollection[BaseOutputFeatureConfig] + + model_type: str + + trainer: BaseTrainerConfig + preprocessing: PreprocessingConfig + defaults: BaseDefaultsConfig + hyperopt: HyperoptConfig | None = None + + backend: dict[str, Any] = schema_utils.Dict() # TODO(jeffkinnison): Add backend schema + ludwig_version: str = schema_utils.ProtectedString(LUDWIG_VERSION) + + def __post_init__(self): + merge_fixed_preprocessing_params(self) + set_validation_parameters(self) + set_hyperopt_defaults_(self) + set_tagger_decoder_parameters(self) + sanitize_and_filter_combiner_entities_(self) + + # Reconcile LLM parameters + set_llm_parameters(self) + + # Reconcile conflicting preprocessing parameters + set_preprocessing_parameters(self) + + # Derive proc_col for each feature from the feature's preprocessing parameters + # after all preprocessing parameters have been set + set_derived_feature_columns_(self) + + # Auxiliary checks. + get_config_check_registry().check_config(self) + + @staticmethod + def from_dict(config: ModelConfigDict) -> "ModelConfig": + config = copy.deepcopy(config) + + # Apply quality preset if specified (before any other processing) + preset = config.get("preset") + if preset: + from ludwig.presets import apply_preset + + config = apply_preset(config, preset) + + config = upgrade_config_dict_to_latest_version(config) + + # Default combiner to ft_transformer for 3+ input features (better accuracy) + if ( + "combiner" not in config + and config.get("model_type", "ecd") == "ecd" + and len(config.get(INPUT_FEATURES, [])) >= 3 + ): + config["combiner"] = {"type": "ft_transformer"} + + # Use sanitized feature names. + # NOTE: This must be kept consistent with build_dataset() + for input_feature in config[INPUT_FEATURES]: + input_feature[NAME] = get_sanitized_feature_name(input_feature[NAME]) + if input_feature.get(COLUMN): + input_feature[COLUMN] = get_sanitized_feature_name(input_feature[COLUMN]) + for output_feature in config[OUTPUT_FEATURES]: + output_feature[NAME] = get_sanitized_feature_name(output_feature[NAME]) + if output_feature.get(COLUMN): + output_feature[COLUMN] = get_sanitized_feature_name(output_feature[COLUMN]) + + # Sanitize tied feature names. + for input_feature in config[INPUT_FEATURES]: + if input_feature.get(TIED): + input_feature[TIED] = get_sanitized_feature_name(input_feature[TIED]) + + # Sanitize dependent feature names. + for output_feature in config[OUTPUT_FEATURES]: + if output_feature.get(DEPENDENCIES): + output_feature[DEPENDENCIES] = [ + get_sanitized_feature_name(feature_name) for feature_name in output_feature[DEPENDENCIES] + ] + + config["model_type"] = config.get("model_type", MODEL_ECD) + model_type = config["model_type"] + if model_type not in model_type_schema_registry: + raise ConfigValidationError( + f"Invalid model type: '{model_type}', expected one of: {list(model_type_schema_registry.keys())}" + ) + + config = merge_with_defaults(config) + + # TODO(travis): handle this with helper function + backend = config.get(BACKEND) + if isinstance(backend, str): + config[BACKEND] = {"type": backend} + + # JSON schema validation. Note that this is desireable on top of `schema.load(config)` below because marshmallow + # deserialization permits additional properties while JSON schema validation, for schema (e.g. `trainer`) that + # have `additionalProperties=False`, does not. + # + # Illustrative example: test_validate_config_misc.py::test_validate_no_trainer_type + # + # TODO: Set `additionalProperties=False` for all Ludwig schema, and look into passing in `unknown='RAISE'` to + # marshmallow.load(), which raises an error for unknown fields during deserialization. + # https://marshmallow.readthedocs.io/en/stable/marshmallow.schema.html#marshmallow.schema.Schema.load + check_schema(config) + + cls = model_type_schema_registry[model_type] + try: + config_obj: ModelConfig = cls.model_validate(config) + except ConfigValidationError: + raise + except ValueError as e: + raise ConfigValidationError(f"Config validation error raised during config deserialization: {e}") from e + except OSError as e: + raise ConfigValidationError(f"Config validation error raised during config post-init: {e}") from e + + return config_obj + + @staticmethod + def from_yaml(config_path: str) -> "ModelConfig": + return ModelConfig.from_dict(load_yaml(config_path)) + + def get_feature_names(self) -> set[str]: + """Returns a set of all feature names.""" + feature_names = set() + feature_names.update([f.column for f in self.input_features]) + feature_names.update([f.column for f in self.output_features]) + return feature_names + + def get_feature_config(self, feature_column_name: str) -> BaseInputFeatureConfig | None: + """Returns the feature config for the given feature name.""" + for feature in self.input_features: + if feature.column == feature_column_name: + return feature + for feature in self.output_features: + if feature.column == feature_column_name: + return feature + + +@DeveloperAPI +def register_model_type(name: str): + def wrap(model_type_config: ModelConfig) -> ModelConfig: + model_type_schema_registry[name] = model_type_config + return model_type_config + + return wrap + + +def _merge_encoder_cache_params(preprocessing_params: dict[str, Any], encoder_params: dict[str, Any]) -> dict[str, Any]: + if preprocessing_params.get("cache_encoder_embeddings"): + preprocessing_params[ENCODER] = encoder_params + return preprocessing_params diff --git a/ludwig/schema/model_types/ecd.py b/ludwig/schema/model_types/ecd.py new file mode 100644 index 0000000..762e94b --- /dev/null +++ b/ludwig/schema/model_types/ecd.py @@ -0,0 +1,49 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import CombinerSelection +from ludwig.schema.defaults.ecd import ECDDefaultsConfig, ECDDefaultsField +from ludwig.schema.features.base import ( + BaseInputFeatureConfig, + BaseOutputFeatureConfig, + ECDInputFeatureSelection, + ECDOutputFeatureSelection, + FeatureCollection, +) +from ludwig.schema.hyperopt import HyperoptConfig, HyperoptField +from ludwig.schema.model_types.base import ModelConfig, register_model_type +from ludwig.schema.preprocessing import PreprocessingConfig, PreprocessingField +from ludwig.schema.trainer import ECDTrainerConfig, ECDTrainerField + + +@DeveloperAPI +@register_model_type(name="ecd") +class ECDModelConfig(ModelConfig): + """Parameters for ECD.""" + + model_type: str = schema_utils.ProtectedString("ecd") + + preset: str | None = schema_utils.StringOptions( + options=["medium_quality", "high_quality", "best_quality", "tabular_realmlp"], + default=None, + allow_none=True, + description=( + "Quality preset that sets sensible defaults for combiner, trainer, and other settings. " + "User-specified values always take precedence. " + "'medium_quality': fast training with concat combiner. " + "'high_quality': transformer combiner with uncertainty loss balancing. " + "'best_quality': FT-Transformer, uncertainty loss balancing, model soup. " + "'tabular_realmlp': RealMLP defaults (Holzmüller et al., 2024) — robust scaling on number " + "features, deep concat FC stack, AdamW + cosine LR, long training horizon." + ), + ) + + input_features: FeatureCollection[BaseInputFeatureConfig] = ECDInputFeatureSelection().get_list_field() + output_features: FeatureCollection[BaseOutputFeatureConfig] = ECDOutputFeatureSelection().get_list_field() + + combiner: BaseCombinerConfig = CombinerSelection().get_default_field() + + trainer: ECDTrainerConfig = ECDTrainerField().get_default_field() + preprocessing: PreprocessingConfig = PreprocessingField().get_default_field() + defaults: ECDDefaultsConfig = ECDDefaultsField().get_default_field() + hyperopt: HyperoptConfig | None = HyperoptField().get_default_field() diff --git a/ludwig/schema/model_types/llm.py b/ludwig/schema/model_types/llm.py new file mode 100644 index 0000000..e39fea5 --- /dev/null +++ b/ludwig/schema/model_types/llm.py @@ -0,0 +1,130 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.defaults.llm import LLMDefaultsConfig, LLMDefaultsField +from ludwig.schema.features.base import ( + BaseInputFeatureConfig, + BaseOutputFeatureConfig, + FeatureCollection, + LLMInputFeatureSelection, + LLMOutputFeatureSelection, +) +from ludwig.schema.hyperopt import HyperoptConfig, HyperoptField +from ludwig.schema.llms.base_model import BaseModelDataclassField +from ludwig.schema.llms.generation import LLMGenerationConfig, LLMGenerationConfigField +from ludwig.schema.llms.model_parameters import ModelParametersConfig, ModelParametersConfigField +from ludwig.schema.llms.peft import ( + AdapterDataclassField, + BaseAdapterConfig, + NamedAdaptersConfig, + NamedAdaptersDataclassField, +) +from ludwig.schema.llms.prompt import PromptConfig, PromptConfigField +from ludwig.schema.llms.quantization import QuantizationConfig, QuantizationConfigField +from ludwig.schema.model_types.base import ModelConfig, register_model_type +from ludwig.schema.preprocessing import PreprocessingConfig, PreprocessingField +from ludwig.schema.trainer import LLMTrainerConfig, LLMTrainerDataclassField + + +@DeveloperAPI +@register_model_type(name="llm") +class LLMModelConfig(ModelConfig): + """Parameters for LLM Model Type.""" + + model_type: str = schema_utils.ProtectedString("llm") + + base_model: str = BaseModelDataclassField() + + input_features: FeatureCollection[BaseInputFeatureConfig] = LLMInputFeatureSelection().get_list_field() + output_features: FeatureCollection[BaseOutputFeatureConfig] = LLMOutputFeatureSelection().get_list_field() + + preprocessing: PreprocessingConfig = PreprocessingField().get_default_field() + defaults: LLMDefaultsConfig | None = LLMDefaultsField().get_default_field() + hyperopt: HyperoptConfig | None = HyperoptField().get_default_field() + + prompt: PromptConfig = PromptConfigField().get_default_field() + + # trainer: LLMTrainerConfig = LLMTrainerField().get_default_field() + trainer: LLMTrainerConfig = LLMTrainerDataclassField( + description="The trainer to use for the model", + ) + + generation: LLMGenerationConfig = LLMGenerationConfigField().get_default_field() + + adapter: BaseAdapterConfig | None = AdapterDataclassField() + adapters: NamedAdaptersConfig | None = NamedAdaptersDataclassField() + quantization: QuantizationConfig | None = QuantizationConfigField().get_default_field() + model_parameters: ModelParametersConfig | None = ModelParametersConfigField().get_default_field() + + trust_remote_code: bool = schema_utils.Boolean( + default=False, + description=( + "Whether to trust and execute remote code from the HuggingFace model repository. " + "Required for some models (e.g. Phi-2, Qwen) that use custom architectures. " + "Only enable this for models you trust." + ), + ) + + is_multimodal: bool = schema_utils.Boolean( + default=False, + description=( + "When true, treat ``base_model`` as a vision-language model (VLM) — Qwen2-VL, " + "LLaVA, InternVL, and similar. Ludwig then loads the base via " + "``AutoModelForVision2Seq`` instead of ``AutoModelForCausalLM`` and uses the " + "multimodal ``AutoProcessor`` for tokenisation / image preprocessing. Fine-tuning " + "input features can then include images alongside text." + ), + ) + + def __post_init__(self): + super().__post_init__() + # Cross-validate the quantization backend/mode/qat combination here (rather than in + # QuantizationConfig.__post_init__) so the user sees the real reason in the error + # message — see QuantizationConfig.validate_backend for why. + if self.quantization is not None: + self.quantization.validate_backend() + + # `adapter:` (singular) and `adapters:` (plural) are mutually exclusive. + # A config must use one form or the other — using both is ambiguous because the + # model would have both an anonymous adapter and a registry of named ones. + if self.adapter is not None and self.adapters is not None: + raise ConfigValidationError( + "Cannot set both `adapter:` and `adapters:` — use one form or the other. " + "Use `adapter:` for a single adapter (common case). Use `adapters:` to " + "register multiple named adapters that can be switched at runtime or merged." + ) + if self.adapters is not None: + if not self.adapters.adapters: + raise ConfigValidationError( + "`adapters.adapters` must contain at least one entry. To disable " + "parameter-efficient fine-tuning, remove the `adapters:` field entirely." + ) + adapter_names = list(self.adapters.adapters.keys()) + if self.adapters.active is not None and self.adapters.active not in adapter_names: + # The active adapter name may also refer to a merged adapter defined below. + if not (self.adapters.merge and self.adapters.merge.name == self.adapters.active): + raise ConfigValidationError( + f"`adapters.active` = {self.adapters.active!r} does not match any " + f"adapter name in `adapters.adapters` ({adapter_names}) or the " + "`adapters.merge.name` field." + ) + if self.adapters.merge is not None: + merge = self.adapters.merge + if not merge.sources: + raise ConfigValidationError("`adapters.merge.sources` is required when `adapters.merge` is set.") + unknown = [s for s in merge.sources if s not in adapter_names] + if unknown: + raise ConfigValidationError( + f"`adapters.merge.sources` references unknown adapter names: {unknown}. " + f"Known adapters: {adapter_names}." + ) + if merge.weights is not None and len(merge.weights) != len(merge.sources): + raise ConfigValidationError( + f"`adapters.merge.weights` has {len(merge.weights)} entries but " + f"`adapters.merge.sources` has {len(merge.sources)}. Lengths must match." + ) + if merge.name in adapter_names: + raise ConfigValidationError( + f"`adapters.merge.name` = {merge.name!r} collides with an existing " + "source adapter name. Pick a different name for the merged adapter." + ) diff --git a/ludwig/schema/model_types/utils.py b/ludwig/schema/model_types/utils.py new file mode 100644 index 0000000..83639e1 --- /dev/null +++ b/ludwig/schema/model_types/utils.py @@ -0,0 +1,512 @@ +import copy +import logging +import sys +import warnings +from collections.abc import Mapping +from typing import Any, TYPE_CHECKING + +from transformers import AutoConfig + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + CATEGORY, + COMBINED, + DECODER, + DEFAULTS, + ENCODER, + GRID_SEARCH, + INPUT_FEATURES, + LOSS, + MODEL_ECD, + MODEL_LLM, + OUTPUT_FEATURES, + PARAMETERS, + PREPROCESSING, + SEQUENCE, + SPACE, + TEXT, + TYPE, +) +from ludwig.error import ConfigValidationError +from ludwig.features.feature_utils import compute_feature_hash +from ludwig.schema.features.utils import output_config_registry +from ludwig.schema.hyperopt.scheduler import BaseHyperbandSchedulerConfig +from ludwig.schema.llms.generation import LLMGenerationConfig +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.types import HyperoptConfigDict, ModelConfigDict +from ludwig.utils.data_utils import get_sanitized_feature_name +from ludwig.utils.llm_utils import get_context_len + +if TYPE_CHECKING: + from ludwig.schema.model_types.base import ModelConfig + + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def merge_with_defaults(config_dict: ModelConfigDict) -> ModelConfigDict: + # Recursive merge of the features, except that if we find a dictionary containing + # an explicit "type" key, we ignore defaults if they don't match. + defaults = config_dict.get(DEFAULTS) + if not defaults: + return config_dict + + config_dict = copy.deepcopy(config_dict) + _merge_features_(config_dict.get(INPUT_FEATURES, []), defaults, {DECODER, LOSS}) + _merge_features_(config_dict.get(OUTPUT_FEATURES, []), defaults, {ENCODER, PREPROCESSING}) + return config_dict + + +def _merge_features_(features: list[dict[str, Any]], defaults: dict[str, Any], exclude_keys: set[str]): + for feature in features: + ftype = feature.get(TYPE) + if not ftype: + continue + + default_feature = defaults.get(ftype, {}) + merged_feature = _merge_dict_with_types(default_feature, feature, exclude_keys) + + # In-place replacement of the old feature with the new + feature.clear() + feature.update(merged_feature) + + +def _merge_dict_with_types(dct: dict[str, Any], merge_dct: dict[str, Any], exclude_keys: set[str]) -> dict[str, Any]: + dct = copy.deepcopy(dct) + dct = {k: v for k, v in dct.items() if k not in exclude_keys} + + for k, v in merge_dct.items(): + # TODO(travis): below type comparison is not perfect, as it doesn't consider the case where the default type + # is omitted while the encoder type is explicitly set to the default type, in which case they + # should resolve to equal, but will be considered different. + if ( + k in dct + and isinstance(dct[k], dict) + and isinstance(v, Mapping) + and dct[k].get(TYPE) == v.get(TYPE, dct[k].get(TYPE)) + ): + dct[k] = _merge_dict_with_types(dct[k], v, exclude_keys) + else: + dct[k] = v + return dct + + +@DeveloperAPI +def merge_fixed_preprocessing_params(config: "ModelConfig"): + """Update preprocessing parameters if encoders require fixed preprocessing parameters.""" + for feature in config.input_features: + feature.encoder.set_fixed_preprocessing_params(config.model_type, feature.preprocessing) + + +def set_validation_parameters(config: "ModelConfig"): + """Sets validation-related parameters used for early stopping, determining the best hyperopt trial, etc.""" + if not config.output_features: + return + + # First set the validation field so we know what feature we're validating on + if not config.trainer.validation_field: + if config.trainer.validation_metric is None or config.trainer.validation_metric == LOSS: + # Loss is valid for all features. + config.trainer.validation_field = config.output_features[0].name + else: + # Determine the proper validation field for the user, like if the user specifies "accuracy" but forgets to + # change the validation field from "combined" to the name of the feature that produces accuracy metrics. + from ludwig.utils.metric_utils import get_feature_to_metric_names_map + + feature_to_metric_names_map = get_feature_to_metric_names_map(config.output_features.to_list()) + validation_field = None + for feature_name, metric_names in feature_to_metric_names_map.items(): + if config.trainer.validation_metric in metric_names: + if validation_field is None: + validation_field = feature_name + else: + raise ConfigValidationError( + f"The validation_metric: '{config.trainer.validation_metric}' corresponds to multiple " + f"possible validation_fields, '{validation_field}' and '{feature_name}'. Please explicitly " + "specify the validation_field that should be used with the validation_metric " + f"'{config.trainer.validation_metric}'." + ) + if validation_field is None: + raise ConfigValidationError( + "User-specified trainer.validation_metric is not valid for any output feature." + ) + + config.trainer.validation_field = validation_field + + # If the field is combined, then make sure the metric is loss and then return + if config.trainer.validation_field == COMBINED: + # Only loss is supported for combined + if not config.trainer.validation_metric: + config.trainer.validation_metric = LOSS + elif config.trainer.validation_metric != LOSS: + raise ConfigValidationError( + f"Must set validation_metric=loss when validation_field=combined, " + f"found validation_metric={config.trainer.validation_metric}" + ) + return + + # Field is not combined, so use the default validation metric for the single feature + validation_features = [f for f in config.output_features if f.name == config.trainer.validation_field] + if len(validation_features) > 1: + raise ConfigValidationError( + f"Found more than one feature matching validation field: {config.trainer.validation_field}" + ) + if len(validation_features) == 0: + raise ConfigValidationError( + f"No output feature found matching validation field: {config.trainer.validation_field}" + ) + + validation_feature = validation_features[0] + if not config.trainer.validation_metric: + # The user has not explicitly set any validation fields. + # Default to using the first output feature's default validation metric. + out_type = validation_feature.type + config.trainer.validation_metric = output_config_registry(config.model_type)[out_type].default_validation_metric + + +def set_derived_feature_columns_(config_obj: "ModelConfig"): + """Assigns column and proc_column values to features that do not have them set. + + Proc_column is set to a hash of the feature's preprocessing configuration. + """ + for feature in config_obj.input_features: + if feature.column is None: + feature.column = feature.name + if feature.proc_column is None: + feature.proc_column = compute_feature_hash(feature.to_dict()) + + for feature in config_obj.output_features: + if feature.column is None: + feature.column = feature.name + if feature.proc_column is None: + feature.proc_column = compute_feature_hash(feature.to_dict()) + + +def sanitize_and_filter_combiner_entities_(config: "ModelConfig"): + if config.model_type != MODEL_ECD or config.combiner.type != "comparator": + return + + input_feature_names = {input_feature.name for input_feature in config.input_features} + + # Sanitize feature names. + config.combiner.entity_1 = [get_sanitized_feature_name(fname) for fname in config.combiner.entity_1] + config.combiner.entity_2 = [get_sanitized_feature_name(fname) for fname in config.combiner.entity_2] + + entity_1_excluded = {fname for fname in config.combiner.entity_1 if fname not in input_feature_names} + if entity_1_excluded: + logger.warning( + f"Excluding `entity_1` features {entity_1_excluded} from the comparator combiner because they are not " + f"present in the `input_features`." + ) + + config.combiner.entity_1 = [fname for fname in config.combiner.entity_1 if fname not in entity_1_excluded] + + entity_2_excluded = {fname for fname in config.combiner.entity_2 if fname not in input_feature_names} + if entity_2_excluded: + logger.warning( + f"Excluding `entity_2` features {entity_2_excluded} from the comparator combiner because they are not " + f"present in the `input_features`." + ) + + config.combiner.entity_2 = [fname for fname in config.combiner.entity_2 if fname not in entity_2_excluded] + + +def set_hyperopt_defaults_(config: "ModelConfig"): + """This function was migrated from defaults.py with the intention of setting some hyperopt defaults while the + hyperopt section of the config object is not fully complete. + + Returns: + None -> modifies trainer and hyperopt sections + """ + if not config.hyperopt: + return + + # Set default num_samples based on search space if not set by user + if config.hyperopt.executor.num_samples is None: + _contains_grid_search_params = contains_grid_search_parameters(config.hyperopt.to_dict()) + if _contains_grid_search_params: + logger.info( + "Setting hyperopt num_samples to 1 to prevent duplicate trials from being run. Duplicate trials are" + " created when there are hyperopt parameters that use the `grid_search` search space.", + ) + config.hyperopt.executor.num_samples = 1 + else: + logger.info("Setting hyperopt num_samples to 10.") + config.hyperopt.executor.num_samples = 10 + + scheduler = config.hyperopt.executor.scheduler + if scheduler.type == "fifo": + # FIFO scheduler has no constraints + return + + # Disable early stopping when using a scheduler. We achieve this by setting the parameter + # to -1, which ensures the condition to apply early stopping is never met. + early_stop = config.trainer.early_stop + if early_stop is not None and early_stop != -1: + warnings.warn("Can't utilize `early_stop` while using a hyperopt scheduler. Setting early stop to -1.") + config.trainer.early_stop = -1 + + if isinstance(config.trainer, ECDTrainerConfig) and isinstance(scheduler, BaseHyperbandSchedulerConfig): + # TODO(travis): explore similar constraints for other model types that may not have epochs + max_t = scheduler.max_t + time_attr = scheduler.time_attr + epochs = config.trainer.epochs + if max_t is not None: + if time_attr == "time_total_s": + if epochs is None: + # Continue training until time limit hit + config.trainer.epochs = sys.maxsize + # else continue training until either time or trainer epochs limit hit + elif epochs is not None and epochs != max_t: + raise ValueError( + "Cannot set trainer `epochs` when using hyperopt scheduler w/different training_iteration `max_t`. " + "Unset one of these parameters in your config or make sure their values match." + ) + else: + # Run trainer until scheduler epochs limit hit + config.trainer.epochs = max_t + elif epochs is not None: + scheduler.max_t = epochs # run scheduler until trainer epochs limit hit + + +def set_preprocessing_parameters(config: "ModelConfig") -> None: + """Reconcile conflicting preprocessing parameters in place.""" + _set_max_sequence_length(config) + + +def _set_max_sequence_length(config: "ModelConfig") -> None: + """Ensures that `max_sequence_length` is never less than `sequence_length`.""" + + types_with_sequence_length = [SEQUENCE, TEXT] + for input_feature in config.input_features: + if input_feature.type in types_with_sequence_length: + sequence_length = input_feature.preprocessing.sequence_length + max_sequence_length = input_feature.preprocessing.max_sequence_length + if sequence_length is not None and sequence_length > max_sequence_length: + warnings.warn( + "if `sequence_length` is not None, `max_sequence_length` must be greater than or equal " + "to `sequence_length`. Setting `max_sequence_length` to `sequence_length`." + ) + input_feature.preprocessing.max_sequence_length = sequence_length + + +def set_tagger_decoder_parameters(config: "ModelConfig") -> None: + """Overrides the reduce_input parameter for text and sequence output features when a tagger decoder is used. + This is done to ensure that the decoder correctly gets a 3D tensor as input. + + Returns: + None -> modifies output_features + """ + for output_feature in config.output_features: + if output_feature.type in {TEXT, SEQUENCE} and output_feature.decoder.type == "tagger": + if output_feature.reduce_input is not None: + warnings.warn( + "reduce_input must be set to `None` when using a tagger decoder for your output feature. " + f"Setting reduce_input to `None` for `{output_feature.name}`." + ) + output_feature.reduce_input = None + + +def set_llm_parameters(config: "ModelConfig") -> None: + if config.model_type != MODEL_LLM: + return + + # Set preprocessing parameters for text features for LLM model type + _set_llm_tokenizers(config) + + # Set max_new_tokens in generation config to the max sequence length of the output features + _set_generation_max_new_tokens(config) + + # HACK(Arnav): Set Mixtral target modules when using LoRA + # GitHub issue: https://github.com/ludwig-ai/ludwig/issues/3853 + # PEFT PR: https://github.com/huggingface/peft/pull/1376 + _set_mixtral_target_modules(config) + + # HACK(Arnav): Set Phi-2 target modules when using LoRA + # GitHub issue: https://github.com/ludwig-ai/ludwig/issues/3910 + # PEFT PR: https://github.com/huggingface/peft/pull/1375 + _set_phi2_target_modules(config) + + # HACK(Arnav): Set Phi-3 target modules when using LoRA + _set_phi3_target_modules(config) + + # HACK(Arnav): Set Gemma target modules when using LoRA + # GitHub issue: https://github.com/ludwig-ai/ludwig/issues/3937 + # PEFT PR: https://github.com/huggingface/peft/pull/1499 + _set_gemma_target_modules(config) + + +def _set_llm_tokenizers(config: "ModelConfig") -> None: + """Sets the tokenizers for the LLM model to the pretrained model name or path. This ensures that they use the + correct shared vocabulary from the tokenizer. + + This also ensures padding is correctly set to left padding to prevent the LLM from trying to continue to sequence + based on the right padding tokens, which might exist based on sequence length. + """ + pretrained_model_name_or_path = config.base_model + if not isinstance(pretrained_model_name_or_path, str) or pretrained_model_name_or_path is None: + raise ValueError("Must set `base_model` when using the LLM model.") + + for input_feature in config.input_features: + if input_feature.type == TEXT: + input_feature.preprocessing.tokenizer = "hf_tokenizer" + input_feature.preprocessing.pretrained_model_name_or_path = pretrained_model_name_or_path + input_feature.preprocessing.padding = "left" + + for output_feature in config.output_features: + if output_feature.type == TEXT: + # Add tokenizer parameters to preprocessing so it can be used during post processing + output_feature.preprocessing.tokenizer = "hf_tokenizer" + output_feature.preprocessing.pretrained_model_name_or_path = pretrained_model_name_or_path + output_feature.preprocessing.padding = "left" + + # Add tokenizer parameters to decoder so it can be used during the forward pass + output_feature.decoder.pretrained_model_name_or_path = pretrained_model_name_or_path + output_feature.decoder.max_new_tokens = config.generation.max_new_tokens + elif output_feature.type == CATEGORY: + # Tokenizer parameters + output_feature.decoder.tokenizer = "hf_tokenizer" + output_feature.decoder.pretrained_model_name_or_path = pretrained_model_name_or_path + # Parameters for building decoder vocabulary + output_feature.decoder.fallback_label = output_feature.preprocessing.fallback_label + + +def _get_maximum_possible_sequence_length(config: "ModelConfig", default_max_sequence_length: int) -> int: + """Returns the maximum possible sequence length for the LLM model based on the model config.""" + max_possible_sequence_length = default_max_sequence_length + if config.output_features[0].preprocessing.max_sequence_length is not None: + # Note: We don't need to check for max between feature.preprocessing.max_sequence_length and + # defaults.text.preprocessing.max_sequence_length because the latter is only applied to input features. + max_possible_sequence_length = max( + default_max_sequence_length, config.output_features[0].preprocessing.max_sequence_length + ) + elif config.preprocessing.global_max_sequence_length is not None: + # This is not perfect since it includes tokens from both input + output features, but this at least + # ensures that max possible of the sequence length is used. It is very likely that the model learns + # to generate sequences than this value. + max_possible_sequence_length = max( + max_possible_sequence_length, config.preprocessing.global_max_sequence_length + ) + elif max_possible_sequence_length == default_max_sequence_length: + # It's possible that both max_sequence_length and global_max_sequence_length are not set, in which case + # we should fall back to the window size of the pretrained model. By this point, because of schema validation + # checks, we know that the base_model exists so we can safely grab the base model's config. + # TODO (Arnav): Figure out how to factor in rope scaling factor into this calculation. + model_config = AutoConfig.from_pretrained(config.base_model) + max_possible_sequence_length = get_context_len(model_config) + # Artifically leave a buffer of half the total model window size to trade off + # runtime while likely covering a majority of the max sequence length. + max_possible_sequence_length = max_possible_sequence_length // 2 + return max_possible_sequence_length + + +def _set_generation_max_new_tokens(config: "ModelConfig") -> None: + """Sets the max_new_tokens parameter in the generation config to the max sequence length of the output + features. + + This ensures that the generation config is set to the correct value for the LLM model type. + """ + _DEFAULT_MAX_SEQUENCE_LENGTH = LLMGenerationConfig().max_new_tokens + if config.generation.max_new_tokens != _DEFAULT_MAX_SEQUENCE_LENGTH: + # Max new tokens is explicitly set by user, so don't override + return + + if config.output_features[0].type != TEXT: + # This is trickier to set for other output features, so don't override for now. + # TODO: Add better support for category output features + return + + max_possible_sequence_length = _get_maximum_possible_sequence_length(config, _DEFAULT_MAX_SEQUENCE_LENGTH) + + logger.info( + f"Setting generation max_new_tokens to {max_possible_sequence_length} to correspond with the max " + "sequence length assigned to the output feature or the global max sequence length. This will ensure that " + "the correct number of tokens are generated at inference time. To override this behavior, set " + "`generation.max_new_tokens` to a different value in your Ludwig config." + ) + config.generation.max_new_tokens = max_possible_sequence_length + + +def _set_mixtral_target_modules(config: "ModelConfig") -> None: + """If the base model is Mixtral 7x8, LoRA is enabled and the target modules are not set, set the target modules + to q_proj and v_proj.""" + if config.base_model not in {"mistralai/Mixtral-8x7B-v0.1", "mistralai/Mixtral-8x7B-Instruct-v0.1"}: + return + + if not config.adapter: + return + + if config.adapter.type != "lora" or config.adapter.target_modules: + return + + target_modules = ["q_proj", "v_proj"] + + logger.info(f"Setting adapter target modules to {target_modules} for Mixtral 7x8 base model with LoRA adapter.") + config.adapter.target_modules = target_modules + + +def _set_phi2_target_modules(config: "ModelConfig") -> None: + """If the base model is Phi-2, LoRA is enabled and the target modules are not set, set the target modules to + maximize performance.""" + if config.base_model not in { + "microsoft/phi-1", + "microsoft/phi-1_5", + "microsoft/phi-2", + }: + return + + if not config.adapter: + return + + if config.adapter.type != "lora" or config.adapter.target_modules: + return + + target_modules = ["q_proj", "k_proj", "v_proj", "dense", "fc1", "fc2"] + + logger.info(f"Setting adapter target modules to {target_modules} for Phi-2 base model with LoRA adapter.") + config.adapter.target_modules = target_modules + + +def _set_phi3_target_modules(config: "ModelConfig") -> None: + if config.base_model not in { + "microsoft/Phi-3-mini-4k-instruct", + "microsoft/Phi-3-mini-128k-instruct", + }: + return + + if not config.adapter: + return + + if config.adapter.type != "lora" or config.adapter.target_modules: + return + + target_modules = ["qkv_proj", "o_proj", "gate_up_proj", "down_proj"] + + logger.info(f"Setting adapter target modules to {target_modules} for Phi-3 base model with LoRA adapter.") + config.adapter.target_modules = target_modules + + +def _set_gemma_target_modules(config: "ModelConfig") -> None: + """If the base model is Gemma, LoRA is enabled and the target modules are not set, set the target modules to + maximize performance.""" + if config.base_model not in {"google/gemma-2b", "google/gemma-2b-it", "google/gemma-7b", "google/gemma-7b-it"}: + return + + if not config.adapter: + return + + if config.adapter.type != "lora" or config.adapter.target_modules: + return + + target_modules = ["q_proj", "v_proj"] + config.adapter.target_modules = target_modules + + +@DeveloperAPI +def contains_grid_search_parameters(hyperopt_config: HyperoptConfigDict) -> bool: + """Returns True if any hyperopt parameter in the config is using the grid_search space.""" + for _, param_info in hyperopt_config[PARAMETERS].items(): + if param_info.get(SPACE, None) == GRID_SEARCH: + return True + return False diff --git a/ludwig/schema/optimizers.py b/ludwig/schema/optimizers.py new file mode 100644 index 0000000..45dddf5 --- /dev/null +++ b/ludwig/schema/optimizers.py @@ -0,0 +1,1357 @@ +import math +from abc import ABC +from dataclasses import field +from typing import ClassVar + +import pydantic +import torch + +try: + import bitsandbytes as bnb +except ImportError: + bnb = None + +try: + from transformers.optimization import Adafactor as _TransformersAdafactor +except ImportError: + _TransformersAdafactor = None + +try: + from schedulefree import AdamWScheduleFree as _AdamWScheduleFree +except ImportError: + _AdamWScheduleFree = None + +try: + import soap as _soap_module + + _SOAPOptimizer = getattr(_soap_module, "SOAP", None) +except ImportError: + _SOAPOptimizer = None + +import ludwig.schema.utils as schema_utils +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError +from ludwig.schema.metadata import OPTIMIZER_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata +from ludwig.utils.registry import Registry + +optimizer_registry = Registry() + + +@DeveloperAPI +def register_optimizer(name: str): + def wrap(optimizer_config: BaseOptimizerConfig): + optimizer_registry[name] = (optimizer_config.optimizer_class, optimizer_config) + return optimizer_config + + return wrap + + +@DeveloperAPI +def get_optimizer_cls(name: str): + """Get the optimizer schema class from the optimizer schema class registry.""" + return optimizer_registry[name][1] + + +@DeveloperAPI +class BaseOptimizerConfig(schema_utils.LudwigBaseConfig, ABC): + """Base class for optimizers. Not meant to be used directly. + + The dataclass format prevents arbitrary properties from being set. Consequently, in child classes, all properties + from the corresponding `torch.optim.Optimizer` class are copied over: check each class to check which attributes are + different from the torch-specified defaults. + """ + + optimizer_class: ClassVar[torch.optim.Optimizer | None] = None + "Class variable pointing to the corresponding `torch.optim.Optimizer` class." + + type: str + """Name corresponding to an optimizer `ludwig.modules.optimization_modules.optimizer_registry`. + + Technically mutable, but attempting to load a derived optimizer with `type` set to a mismatched value will result in + a `ValidationError`. + """ + + @property + def is_paged(self) -> bool: + """Returns True if the optimizer is a Paged optimizer.""" + return False + + @property + def is_8bit(self) -> bool: + """Returns True if the optimizer is an 8-bit optimizer.""" + return False + + +@DeveloperAPI +@register_optimizer(name="sgd") +class SGDOptimizerConfig(BaseOptimizerConfig): + """Parameters for stochastic gradient descent.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.SGD + """Points to `torch.optim.SGD`.""" + + type: str = schema_utils.ProtectedString("sgd") + """Must be 'sgd' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` (default: + 'sgd')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.SGD.html#torch.optim.SGD : + momentum: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Momentum factor.", + parameter_metadata=OPTIMIZER_METADATA["momentum"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Weight decay ($L2$ penalty).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + dampening: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Dampening for momentum.", + parameter_metadata=OPTIMIZER_METADATA["dampening"], + ) + + nesterov: bool = schema_utils.Boolean( + default=False, + description="Enables Nesterov momentum.", + parameter_metadata=OPTIMIZER_METADATA["nesterov"], + ) + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="sgd_8bit") + class SGD8BitOptimizerConfig(SGDOptimizerConfig): + """Parameters for stochastic gradient descent.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.SGD8bit + + type: str = schema_utils.ProtectedString("sgd_8bit") + + block_wise: bool = schema_utils.Boolean( + default=False, + description="Whether to use block wise update.", + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + @property + def is_8bit(self) -> bool: + return True + + +@DeveloperAPI +@register_optimizer(name="lbfgs") +class LBFGSOptimizerConfig(BaseOptimizerConfig): + """Parameters for stochastic gradient descent.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.LBFGS + """Points to `torch.optim.LBFGS`.""" + + type: str = schema_utils.ProtectedString("lbfgs") + """Must be 'lbfgs' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` (default: + 'lbfgs')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.LBFGS.html#torch.optim.LBFGS + max_iter: int = schema_utils.Integer( + default=20, + description="Maximum number of iterations per optimization step.", + parameter_metadata=OPTIMIZER_METADATA["max_iter"], + ) + + max_eval: int = schema_utils.Integer( + default=None, + allow_none=True, + description="Maximum number of function evaluations per optimization step. Default: `max_iter` * 1.25.", + parameter_metadata=OPTIMIZER_METADATA["max_eval"], + ) + + tolerance_grad: float = schema_utils.NonNegativeFloat( + default=1e-07, + description="Termination tolerance on first order optimality.", + parameter_metadata=OPTIMIZER_METADATA["tolerance_grad"], + ) + + tolerance_change: float = schema_utils.NonNegativeFloat( + default=1e-09, + description="Termination tolerance on function value/parameter changes.", + parameter_metadata=OPTIMIZER_METADATA["tolerance_change"], + ) + + history_size: int = schema_utils.Integer( + default=100, description="Update history size.", parameter_metadata=OPTIMIZER_METADATA["history_size"] + ) + + line_search_fn: str = schema_utils.StringOptions( + ["strong_wolfe"], + default=None, + allow_none=True, + description="Line search function to use.", + parameter_metadata=OPTIMIZER_METADATA["line_search_fn"], + ) + + +@DeveloperAPI +@register_optimizer(name="adam") +class AdamOptimizerConfig(BaseOptimizerConfig): + """Parameters for adam optimization.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adam + """Points to `torch.optim.Adam`.""" + + type: str = schema_utils.ProtectedString("adam") + """Must be 'adam' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` + (default: 'adam')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam : + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, description="Weight decay (L2 penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"] + ) + + amsgrad: bool = schema_utils.Boolean( + default=False, + description="Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam " + "and Beyond'.", + parameter_metadata=OPTIMIZER_METADATA["amsgrad"], + ) + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="adam_8bit") + class Adam8BitOptimizerConfig(AdamOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Adam8bit + + type: str = schema_utils.ProtectedString("adam_8bit") + + block_wise: bool = schema_utils.Boolean( + default=True, + description="Whether to use block wise update.", + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + @property + def is_8bit(self) -> bool: + return True + + @DeveloperAPI + @register_optimizer(name="paged_adam") + class PagedAdamOptimizerConfig(Adam8BitOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdam + + type: str = schema_utils.ProtectedString("paged_adam") + + @property + def is_paged(self) -> bool: + return True + + @property + def is_8bit(self) -> bool: + return False + + @DeveloperAPI + @register_optimizer(name="paged_adam_8bit") + class PagedAdam8BitOptimizerConfig(PagedAdamOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdam8bit + + type: str = schema_utils.ProtectedString("paged_adam_8bit") + + @property + def is_8bit(self) -> bool: + return True + + +@DeveloperAPI +@register_optimizer(name="adamw") +class AdamWOptimizerConfig(BaseOptimizerConfig): + """Parameters for adamw optimization.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.AdamW + """Points to `torch.optim.AdamW`.""" + + type: str = schema_utils.ProtectedString("adamw") + """Must be 'adamw' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` + (default: 'adamw')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam : + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"] + ) + + amsgrad: bool = schema_utils.Boolean( + default=False, + description="Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam " + "and Beyond'. ", + parameter_metadata=OPTIMIZER_METADATA["amsgrad"], + ) + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="adamw_8bit") + class AdamW8BitOptimizerConfig(AdamWOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.AdamW8bit + + type: str = schema_utils.ProtectedString("adamw_8bit") + + block_wise: bool = schema_utils.Boolean( + default=True, + description="Whether to use block wise update.", + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + @property + def is_8bit(self) -> bool: + return True + + @DeveloperAPI + @register_optimizer(name="paged_adamw") + class PagedAdamWOptimizerConfig(AdamW8BitOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdamW + + type: str = schema_utils.ProtectedString("paged_adamw") + + @property + def is_paged(self) -> bool: + return True + + @property + def is_8bit(self) -> bool: + return False + + @DeveloperAPI + @register_optimizer(name="paged_adamw_8bit") + class PagedAdamW8BitOptimizerConfig(PagedAdamWOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdamW8bit + + type: str = schema_utils.ProtectedString("paged_adamw_8bit") + + @property + def is_8bit(self) -> bool: + return True + + +@DeveloperAPI +@register_optimizer(name="adadelta") +class AdadeltaOptimizerConfig(BaseOptimizerConfig): + """Parameters for adadelta optimization.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adadelta + """Points to `torch.optim.Adadelta`.""" + + type: str = schema_utils.ProtectedString("adadelta") + """Must be 'adadelta' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` + (default: 'adadelta')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adadelta.html#torch.optim.Adadelta : + rho: float = schema_utils.FloatRange( + default=0.9, + min=0, + max=1, + description="Coefficient used for computing a running average of squared gradients.", + parameter_metadata=OPTIMIZER_METADATA["rho"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-06, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"] + ) + + +@DeveloperAPI +@register_optimizer(name="adagrad") +class AdagradOptimizerConfig(BaseOptimizerConfig): + """Parameters for adagrad optimization.""" + + # Example docstring + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adagrad + """Points to `torch.optim.Adagrad`.""" + + type: str = schema_utils.ProtectedString("adagrad") + """Must be 'adagrad' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` + (default: 'adagrad')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adagrad.html#torch.optim.Adagrad : + initial_accumulator_value: float = schema_utils.NonNegativeFloat( + default=0, description="", parameter_metadata=OPTIMIZER_METADATA["initial_accumulator_value"] + ) + + lr_decay: float = schema_utils.FloatRange( + default=0, description="Learning rate decay.", parameter_metadata=OPTIMIZER_METADATA["lr_decay"] + ) + + weight_decay: float = schema_utils.FloatRange( + default=0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"] + ) + + eps: float = schema_utils.FloatRange( + default=1e-10, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="adagrad_8bit") + class Adagrad8BitOptimizerConfig(AdagradOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Adagrad8bit + + type: str = schema_utils.ProtectedString("adagrad_8bit") + + block_wise: bool = schema_utils.Boolean( + default=True, + description="Whether to use block wise update.", + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + @property + def is_8bit(self) -> bool: + return True + + +@DeveloperAPI +@register_optimizer(name="adamax") +class AdamaxOptimizerConfig(BaseOptimizerConfig): + """Parameters for adamax optimization.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adamax + """Points to `torch.optim.Adamax`.""" + + type: str = schema_utils.ProtectedString("adamax") + """Must be 'adamax' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` + (default: 'adamax')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adamax.html#torch.optim.Adamax : + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"] + ) + + +@DeveloperAPI +@register_optimizer(name="nadam") +class NadamOptimizerConfig(BaseOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.NAdam + """Points to `torch.optim.NAdam`.""" + + type: str = schema_utils.ProtectedString("nadam") + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.NAdam.html#torch.optim.NAdam : + + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"] + ) + + momentum_decay: float = schema_utils.NonNegativeFloat( + default=4e-3, description="Momentum decay.", parameter_metadata=OPTIMIZER_METADATA["momentum_decay"] + ) + + +@DeveloperAPI +@register_optimizer(name="rmsprop") +class RMSPropOptimizerConfig(BaseOptimizerConfig): + """Parameters for rmsprop optimization.""" + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.RMSprop + """Points to `torch.optim.RMSprop`.""" + + type: str = schema_utils.ProtectedString("rmsprop") + """Must be 'rmsprop' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` + (default: 'rmsprop')""" + + # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.RMSprop.html#torch.optim.RMSprop: + momentum: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Momentum factor.", + parameter_metadata=OPTIMIZER_METADATA["momentum"], + ) + + alpha: float = schema_utils.NonNegativeFloat( + default=0.99, + description="Smoothing constant.", + parameter_metadata=OPTIMIZER_METADATA["alpha"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + centered: bool = schema_utils.Boolean( + default=False, + description="If True, computes the centered RMSProp, and the gradient is normalized by an estimation of its " + "variance.", + parameter_metadata=OPTIMIZER_METADATA["centered"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat(default=0.0, description="Weight decay ($L2$ penalty).") + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="rmsprop_8bit") + class RMSProp8BitOptimizerConfig(RMSPropOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.RMSprop8bit + + type: str = schema_utils.ProtectedString("rmsprop_8bit") + + block_wise: bool = schema_utils.Boolean( + default=True, + description="Whether to use block wise update.", + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + @property + def is_8bit(self) -> bool: + return True + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="lamb") + class LAMBOptimizerConfig(BaseOptimizerConfig): + """Layer-wise Adaptive Moments optimizer for Batch training. + + Paper: https://arxiv.org/pdf/1904.00962.pdf + """ + + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LAMB + + type: str = schema_utils.ProtectedString("lamb") + + bias_correction: bool = schema_utils.Boolean( + default=True, + ) + + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Weight decay (L2 penalty).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + amsgrad: bool = schema_utils.Boolean( + default=False, + description=( + "Whether to use the AMSGrad variant of this algorithm from the paper " + "'On the Convergence of Adam and Beyond'." + ), + parameter_metadata=OPTIMIZER_METADATA["amsgrad"], + ) + + adam_w_mode: bool = schema_utils.Boolean( + default=True, + description="Whether to use the AdamW mode of this algorithm from the paper " + "'Decoupled Weight Decay Regularization'.", + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + block_wise: bool = schema_utils.Boolean( + default=False, + description="Whether to use block wise update.", + ) + + max_unorm: float = schema_utils.FloatRange( + default=1.0, + min=0.0, + max=1.0, + ) + + @DeveloperAPI + @register_optimizer(name="lamb_8bit") + class LAMB8BitOptimizerConfig(LAMBOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LAMB8bit + + type: str = schema_utils.ProtectedString("lamb_8bit") + + @property + def is_8bit(self) -> bool: + return True + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="lars") + class LARSOptimizerConfig(BaseOptimizerConfig): + """Layerwise Adaptive Rate Scaling. + + Paper: https://arxiv.org/pdf/1708.03888.pdf + """ + + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LARS + + type: str = schema_utils.ProtectedString("lars") + + # 0.9 taken from the original paper - momentum requires a non zero value + # https://arxiv.org/pdf/1708.03888v3.pdf + momentum: float = schema_utils.FloatRange( + default=0.9, + min=0.0, + max=1.0, + min_inclusive=False, + description="Momentum factor.", + parameter_metadata=OPTIMIZER_METADATA["momentum"], + ) + + dampening: float = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=1.0, + description="Dampening for momentum.", + parameter_metadata=OPTIMIZER_METADATA["dampening"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Weight decay (L2 penalty).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + nesterov: bool = schema_utils.Boolean( + default=False, + description="Enables Nesterov momentum.", + parameter_metadata=OPTIMIZER_METADATA["nesterov"], + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + max_unorm: float = schema_utils.FloatRange( + default=1.0, + min=0.0, + max=1.0, + ) + + @DeveloperAPI + @register_optimizer(name="lars_8bit") + class LARS8BitOptimizerConfig(LARSOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LARS8bit + + type: str = schema_utils.ProtectedString("lars_8bit") + + @property + def is_8bit(self) -> bool: + return True + + +if bnb is not None: + + @DeveloperAPI + @register_optimizer(name="lion") + class LIONOptimizerConfig(BaseOptimizerConfig): + """Evolved Sign Momentum. + + Paper: https://arxiv.org/pdf/2302.06675.pdf + """ + + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Lion + + type: str = schema_utils.ProtectedString("lion") + + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Weight decay (L2 penalty).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + percentile_clipping: int = schema_utils.IntegerRange( + default=100, + min=0, + max=100, + description="Percentile clipping.", + ) + + block_wise: bool = schema_utils.Boolean( + default=True, + description="Whether to use block wise update.", + ) + + @DeveloperAPI + @register_optimizer(name="lion_8bit") + class LION8BitOptimizerConfig(LIONOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Lion8bit + + type: str = schema_utils.ProtectedString("lion_8bit") + + @property + def is_8bit(self) -> bool: + return True + + @DeveloperAPI + @register_optimizer(name="paged_lion") + class PagedLionOptimizerConfig(LIONOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedLion + + type: str = schema_utils.ProtectedString("paged_lion") + + @property + def is_paged(self) -> bool: + return True + + @DeveloperAPI + @register_optimizer(name="paged_lion_8bit") + class PagedLion8BitOptimizerConfig(PagedLionOptimizerConfig): + optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedLion8bit + + type: str = schema_utils.ProtectedString("paged_lion_8bit") + + @property + def is_8bit(self) -> bool: + return True + + +# --------------------------------------------------------------------------- +# Modern optimizers +# --------------------------------------------------------------------------- + + +@DeveloperAPI +@register_optimizer(name="radam") +class RAdamOptimizerConfig(BaseOptimizerConfig): + """Rectified Adam (RAdam) optimizer config (Liu et al., 2020). + + Paper: https://arxiv.org/abs/1908.03265 + + Convergence: Warms up the adaptive learning rate by computing an analytical + approximation to the variance of the second moment estimate. This eliminates the + need for an explicit warmup schedule: training is stable from step 1 even with a + large learning rate. Converges to the same quality as Adam but with a wider range + of valid learning rates. + + Memory footprint: Same as Adam -- two moment buffers per parameter (~2x model size + in optimizer state). + + When to use: Any setting where Adam is unstable early in training without warmup. + Especially useful for experimentation where you do not want to tune the warmup + duration. Drop-in replacement for Adam/AdamW with no warmup scheduler needed. + + Common pitfalls: RAdam provides no benefit over well-warmed-up Adam in late training. + The rectification term switches off after the variance stabilises (around step ~5-6 + at default beta2=0.999), so expect identical behaviour to Adam from that point on. + """ + + optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.RAdam + """Points to `torch.optim.RAdam`.""" + + type: str = schema_utils.ProtectedString("radam") + """Must be 'radam' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`.""" + + # Defaults from https://pytorch.org/docs/stable/generated/torch.optim.RAdam.html + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + eps: float = schema_utils.NonNegativeFloat( + default=1e-08, + description="Term added to the denominator to improve numerical stability.", + parameter_metadata=OPTIMIZER_METADATA["eps"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Weight decay (L2 penalty).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + +@DeveloperAPI +@register_optimizer(name="adafactor") +class AdafactorOptimizerConfig(BaseOptimizerConfig): + """Adafactor optimizer config (Shazeer & Stern, 2018). + + Paper: https://arxiv.org/abs/1805.09843 + Implementation: `transformers.optimization.Adafactor` + + Convergence: Factorizes the second moment matrix into row and column factors instead + of storing a full per-parameter tensor, dramatically reducing memory. Convergence is + comparable to Adam on large Transformer models but can be slightly slower to converge + on smaller tasks. + + Memory footprint: Very low -- O(n+m) per parameter matrix (row + column factors) + instead of O(n*m). For a 1B parameter model this can save ~8 GB of optimizer state + versus Adam, making it the go-to optimizer when GPU memory is the bottleneck. + + When to use: Training or fine-tuning very large language models (T5, LLaMA, GPT) + where Adam's two-moment buffers exceed available GPU memory. Enabled by default in + many Hugging Face T5 training recipes. + + Common pitfalls: When `relative_step=True` (default) Adafactor computes its own + learning rate schedule -- do NOT combine with an external LR scheduler (set + `lr=None`). When `relative_step=False` you must pass an explicit `lr`. Setting + `scale_parameter=False` and `relative_step=False` with a manual `lr` is the + standard recipe for fine-tuning. + """ + + optimizer_class: ClassVar[torch.optim.Optimizer | None] = _TransformersAdafactor + """Points to `transformers.optimization.Adafactor` (None if transformers not installed).""" + + type: str = schema_utils.ProtectedString("adafactor") + """Must be 'adafactor' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`.""" + + # Adafactor manages its own LR schedule when relative_step=True, so lr defaults to None. + lr: float | None = schema_utils.FloatRange( + default=None, + allow_none=True, + min=0.0, + description=( + "Learning rate. Set to None (default) when `relative_step=True` so that Adafactor manages " + "its own schedule. Must be provided when `relative_step=False`." + ), + ) + + scale_parameter: bool = schema_utils.Boolean( + default=True, + description=( + "If True, the learning rate is scaled by the root mean square of the parameters. " + "Should be True when `relative_step=True`." + ), + ) + + relative_step: bool = schema_utils.Boolean( + default=True, + description=( + "If True, a time-dependent learning rate is computed instead of using the external `lr`. " + "Do not combine with an external LR scheduler." + ), + ) + + warmup_init: bool = schema_utils.Boolean( + default=False, + description=( + "If True, the time-dependent learning rate is linearly increased at initialization. " + "Only effective when `relative_step=True`." + ), + ) + + def __post_init__(self): + if self.optimizer_class is None: + raise ImportError( + "The 'adafactor' optimizer requires the `transformers` package. " + "Install it with: pip install transformers" + ) + + +@DeveloperAPI +@register_optimizer(name="schedule_free_adamw") +class ScheduleFreeAdamWOptimizerConfig(BaseOptimizerConfig): + """Schedule-Free AdamW optimizer config (Defazio & Mishchenko, 2024). + + Paper: https://arxiv.org/abs/2405.15682 + Package: `schedulefree` (install with: pip install schedulefree) + + Convergence: Eliminates the need for a learning rate scheduler by maintaining a + Polyak-Ruppert averaged iterate in addition to the standard momentum buffer. The + averaged iterate is used for evaluation while the momentum buffer drives the + optimization. Achieves performance comparable to or better than well-tuned + cosine/linear decay schedules on a wide range of tasks. + + Memory footprint: Slightly higher than AdamW -- stores an extra averaged parameter + buffer (z), so ~3x model size in optimizer state (vs 2x for AdamW). + + When to use: When you want to skip learning rate scheduler tuning entirely: no + cosine decay, no linear warmup schedule (beyond the built-in `warmup_steps`). Ideal + for rapid prototyping and hyperparameter sweeps where schedule tuning is expensive. + Also useful for online/continual learning without a fixed horizon. + + Common pitfalls: Must call `optimizer.train()` before the training loop and + `optimizer.eval()` before evaluation/inference -- the model is in a different state + depending on which iterate (momentum vs averaged) is active. Forgetting these calls + leads to degraded evaluation metrics. The `warmup_steps` parameter is built into + the optimizer and replaces the external warmup scheduler. + """ + + optimizer_class: ClassVar[torch.optim.Optimizer | None] = _AdamWScheduleFree + """Points to `schedulefree.AdamWScheduleFree` (None if schedulefree not installed).""" + + type: str = schema_utils.ProtectedString("schedule_free_adamw") + """Must be 'schedule_free_adamw' - corresponds to name in optimizer_registry.""" + + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.9, 0.999), + description="Coefficients used for computing running averages of gradient and its square.", + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Weight decay (decoupled L2 penalty).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + warmup_steps: int = schema_utils.Integer( + default=0, + description=( + "Number of linear warmup steps built into the optimizer. " + "Replaces an external warmup scheduler -- do not combine with one." + ), + ) + + def __post_init__(self): + if self.optimizer_class is None: + raise ImportError( + "The 'schedule_free_adamw' optimizer requires the `schedulefree` package. " + "Install it with: pip install schedulefree" + ) + + +# --------------------------------------------------------------------------- +# Muon: pure-Python Newton-Schulz implementation so no extra package required +# --------------------------------------------------------------------------- + + +class _MuonOptimizer(torch.optim.Optimizer): + """Muon -- Momentum + Orthogonalization via Newton-Schulz (Jordan et al., 2024). + + Paper: https://arxiv.org/abs/2409.20325 + """ + + _NS_COEFFS = (3.4445, -4.7750, 2.0315) + + def __init__(self, params, lr=0.02, momentum=0.95, nesterov=True): + defaults = {"lr": lr, "momentum": momentum, "nesterov": nesterov} + super().__init__(params, defaults) + + @torch.no_grad() + def _zeropower_via_newtonschulz5(self, G: torch.Tensor, steps: int = 5) -> torch.Tensor: + """Newton-Schulz iteration to approximate the orthogonal factor of G.""" + if G.ndim < 2: + raise ValueError(f"_zeropower_via_newtonschulz5 requires a matrix (ndim >= 2), got ndim={G.ndim}.") + a, b, c = self._NS_COEFFS + X = G.bfloat16() if G.dtype not in (torch.float16, torch.bfloat16) else G + X = X / (X.norm() + 1e-7) + transposed = X.shape[-2] < X.shape[-1] + if transposed: + X = X.mT + for _ in range(steps): + A = X @ X.mT + X = a * X + b * (A @ X) + c * (A @ A @ X) + if transposed: + X = X.mT + return X.to(G.dtype) + + @torch.no_grad() + def step(self, closure=None): + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + for group in self.param_groups: + lr = group["lr"] + momentum = group["momentum"] + nesterov = group["nesterov"] + + for p in group["params"]: + if p.grad is None: + continue + g = p.grad + state = self.state[p] + if len(state) == 0: + state["momentum_buffer"] = torch.zeros_like(g) + + buf = state["momentum_buffer"] + buf.mul_(momentum).add_(g) + + if nesterov: + update = g + momentum * buf + else: + update = buf + + if update.ndim >= 2: + orig_shape = update.shape + mat = update.view(orig_shape[0], -1) + mat = self._zeropower_via_newtonschulz5(mat) + scale = math.sqrt(max(mat.shape[-2], mat.shape[-1])) + update = mat.view(orig_shape) * scale + + p.add_(update, alpha=-lr) + + return loss + + +@DeveloperAPI +@register_optimizer(name="muon") +class MuonOptimizerConfig(BaseOptimizerConfig): + """Muon optimizer config -- Momentum + Orthogonalization via Newton-Schulz (Jordan et al., 2024). + + Paper: https://arxiv.org/abs/2409.20325 + + Convergence: Applies Nesterov SGD momentum then orthogonalizes each parameter update + matrix via a fast Newton-Schulz iteration (5 steps). Keeps updates approximately + orthogonal for more isotropic parameter changes. Empirically outperforms AdamW on + large language model pre-training at equivalent compute. + + Memory footprint: Low -- stores only one momentum buffer per parameter (~1x model + size), same as SGD with momentum. Significantly cheaper than Adam's two buffers. + + When to use: Pre-training large Transformer language models where you want faster + convergence than AdamW at the same memory cost as SGD. Implemented in pure PyTorch + (no extra package required). + + Common pitfalls: The NS iteration operates in bfloat16 by default for speed. The + default lr (0.02) is much higher than typical Adam lr (1e-3); always re-tune lr + when switching from Adam. + """ + + optimizer_class: ClassVar[torch.optim.Optimizer] = _MuonOptimizer + """Points to the built-in `_MuonOptimizer` implementation.""" + + type: str = schema_utils.ProtectedString("muon") + """Must be 'muon' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`.""" + + momentum: float = schema_utils.FloatRange( + default=0.95, + min=0.0, + max=1.0, + description="Momentum factor for Nesterov SGD applied before orthogonalization.", + parameter_metadata=OPTIMIZER_METADATA["momentum"], + ) + + nesterov: bool = schema_utils.Boolean( + default=True, + description=( + "If True, use Nesterov momentum (look-ahead gradient) before orthogonalization. " + "The original Muon paper uses Nesterov." + ), + parameter_metadata=OPTIMIZER_METADATA["nesterov"], + ) + + +if _SOAPOptimizer is not None: + + @DeveloperAPI + @register_optimizer(name="soap") + class SOAPOptimizerConfig(BaseOptimizerConfig): + """SOAP optimizer config -- Shampoo as Adam's Preconditioner (Vyas et al., 2024). + + Paper: https://arxiv.org/abs/2409.11321 + Package: `soap-pytorch` (install with: pip install soap-pytorch) + + Convergence: Maintains a Kronecker-factored (Shampoo-style) preconditioner for + each weight matrix and runs Adam in its eigenbasis. Converges faster than AdamW + in terms of iterations/tokens on large Transformer pre-training. + + Memory footprint: High -- stores Kronecker factors (m x m) and (n x n) per weight + matrix (m, n) in addition to Adam's two moment buffers. 2-3x Adam's memory for + typical Transformer shapes. + + When to use: Large-scale pre-training where compute is plentiful but wall-clock + time is at a premium. + + Common pitfalls: The preconditioner update frequency trades off overhead vs + freshness; a frequency of 10-100 steps is typical. Not recommended for small + models where preconditioner overhead outweighs convergence gain. + """ + + optimizer_class: ClassVar[torch.optim.Optimizer] = _SOAPOptimizer + """Points to `soap.SOAP` from the `soap-pytorch` package.""" + + type: str = schema_utils.ProtectedString("soap") + """Must be 'soap' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`.""" + + betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField( + default=(0.95, 0.95), + description=( + "Coefficients for the first and second Adam moment estimates run in the " + "Shampoo eigenbasis. Note: SOAP typically uses higher beta1 (0.95) than standard Adam." + ), + parameter_metadata=OPTIMIZER_METADATA["betas"], + ) + + weight_decay: float = schema_utils.NonNegativeFloat( + default=0.01, + description="Weight decay (decoupled L2 penalty, as in AdamW).", + parameter_metadata=OPTIMIZER_METADATA["weight_decay"], + ) + + +@DeveloperAPI +def get_optimizer_conds(): + """Returns a JSON schema of conditionals to validate against optimizer types defined in + `ludwig.modules.optimization_modules.optimizer_registry`. + + Returns: + List of JSON schema conditionals for all registered optimizer types. + """ + conds = [] + for optimizer in optimizer_registry: + optimizer_cls = optimizer_registry[optimizer][1] + other_props = schema_utils.unload_jsonschema_from_config_class(optimizer_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + preproc_cond = schema_utils.create_cond( + {"type": optimizer}, + other_props, + ) + conds.append(preproc_cond) + return conds + + +@DeveloperAPI +def OptimizerDataclassField(default="adam", description="", parameter_metadata: ParameterMetadata = None): + """Custom dataclass field that when used inside of a dataclass will allow any optimizer in + `ludwig.modules.optimization_modules.optimizer_registry`. + + Sets default optimizer to 'adam'. + + Args: + default: Dict specifying an optimizer with a `type` field and its associated parameters. Will attempt + to use `type` to load optimizer from registry with given params. (default: {"type": "adam"}). + + Returns: + Initialized dataclass field that converts untyped dicts with params to optimizer dataclass instances. + """ + + class OptimizerSelection(schema_utils.TypeSelection): + """Custom marshmallow field that deserializes a dict to a valid optimizer from + `ludwig.modules.optimization_modules.optimizer_registry` and creates a corresponding `oneOf` JSON schema + for external usage.""" + + def __init__(self): + super().__init__( + registry=optimizer_registry, + default_value=default, + description=description, + parameter_metadata=parameter_metadata, + ) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return get_optimizer_cls(key) + + def _jsonschema_type_mapping(self): + # Note that this uses the same conditional pattern as combiners: + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": list(optimizer_registry.keys()), + "default": default, + "description": "The type of optimizer to use during the learning process", + }, + }, + "title": "optimizer_options", + "allOf": get_optimizer_conds(), + "required": ["type"], + "description": description, + } + + return OptimizerSelection().get_default_field() + + +@DeveloperAPI +class GradientClippingConfig(schema_utils.LudwigBaseConfig): + """Dataclass that holds gradient clipping parameters.""" + + clipglobalnorm: float | None = schema_utils.FloatRange( + default=0.5, + allow_none=True, + description="Maximum allowed norm of the gradients", + parameter_metadata=OPTIMIZER_METADATA["gradient_clipping"], + ) + + # TODO(travis): is this redundant with `clipglobalnorm`? + clipnorm: float | None = schema_utils.FloatRange( + default=None, + allow_none=True, + description="Maximum allowed norm of the gradients", + parameter_metadata=OPTIMIZER_METADATA["gradient_clipping"], + ) + + clipvalue: float | None = schema_utils.FloatRange( + default=None, + allow_none=True, + description="Maximum allowed value of the gradients", + parameter_metadata=OPTIMIZER_METADATA["gradient_clipping"], + ) + + +@DeveloperAPI +def GradientClippingDataclassField(description: str, default: dict = {}): + """Returns custom dataclass field for `ludwig.modules.optimization_modules.GradientClippingConfig`. Allows + `None` by default. + + Args: + description: Description of the gradient dataclass field. + default: Dict that specifies clipping param values that will be loaded by its schema class (default: {}). + """ + allow_none = True + + class GradientClippingConfigField(schema_utils.SchemaField): + """Custom field class for gradient clipping. + + Deserializes a dict to a valid instance of `ludwig.modules.optimization_modules.GradientClippingConfig` and + creates a corresponding JSON schema for external usage. + """ + + def _deserialize(self, value, attr, data, **kwargs): + if value is None: + return value + if isinstance(value, dict): + try: + return GradientClippingConfig.model_validate(value) + except (TypeError, ConfigValidationError): + raise ConfigValidationError( + f"Invalid params for gradient clipping: {value}, see GradientClippingConfig class." + ) + raise ConfigValidationError("Field should be None or dict") + + def _jsonschema_type_mapping(self): + return { + "oneOf": [ + {"type": "null", "title": "disabled", "description": "Disable gradient clipping."}, + { + **schema_utils.unload_jsonschema_from_config_class(GradientClippingConfig), + "title": "enabled_options", + }, + ], + "title": "gradient_clipping_options", + "description": description, + } + + if not isinstance(default, dict): + raise ConfigValidationError(f"Invalid default: `{default}`") + + def load_default(): + return GradientClippingConfig.model_validate(default) + + try: + dump_default = GradientClippingConfig.model_validate(default).to_dict() + except pydantic.ValidationError: + dump_default = default if isinstance(default, dict) else {} + + return field( + metadata={ + "marshmallow_field": GradientClippingConfigField( + allow_none=allow_none, + load_default=load_default, + dump_default=dump_default, + metadata={ + "description": description, + "parameter_metadata": convert_metadata_to_json(OPTIMIZER_METADATA["gradient_clipping"]), + }, + ) + }, + default_factory=load_default, + ) diff --git a/ludwig/schema/preprocessing.py b/ludwig/schema/preprocessing.py new file mode 100644 index 0000000..8a18999 --- /dev/null +++ b/ludwig/schema/preprocessing.py @@ -0,0 +1,59 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import RANDOM +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import PREPROCESSING_METADATA +from ludwig.schema.split import BaseSplitConfig, SplitDataclassField + + +@DeveloperAPI +class PreprocessingConfig(schema_utils.LudwigBaseConfig): + """Global preprocessing config is a dataclass that configures the parameters used for global preprocessing.""" + + sample_ratio: float = schema_utils.NonNegativeFloat( + default=1.0, + description="The ratio of the dataset to use. For instance, if 0.5, half of the dataset provided will be used.", + parameter_metadata=PREPROCESSING_METADATA["sample_ratio"], + ) + + sample_size: float = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="The maximum number of samples from the dataset to use. Cannot be set if sample_ratio is set to be " + "< 1.0. If sample_ratio is set to 1.0, this will override the number of samples to used.", + parameter_metadata=PREPROCESSING_METADATA["sample_size"], + ) + + oversample_minority: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="If not None, the minority class will be oversampled to reach the specified ratio respective to " + "the majority class. ", + parameter_metadata=PREPROCESSING_METADATA["oversample_minority"], + ) + + undersample_majority: float = schema_utils.NonNegativeFloat( + default=None, + allow_none=True, + description="If not None, the majority class will be undersampled to reach the specified ratio respective " + "to the minority class. ", + parameter_metadata=PREPROCESSING_METADATA["undersample_majority"], + ) + + split: BaseSplitConfig = SplitDataclassField( + default=RANDOM, + ) + + global_max_sequence_length: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Specifically for LLMs. This is the maximum length of the input sequence going into the model's " + "forward pass during training. Sequences will be truncated to this length after merging inputs and targets. " + "If not set, the total length of the merged input and target token sequences will be used.", + parameter_metadata=PREPROCESSING_METADATA["global_max_sequence_length"], + ) + + +@DeveloperAPI +class PreprocessingField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(PreprocessingConfig) diff --git a/ludwig/schema/profiler.py b/ludwig/schema/profiler.py new file mode 100644 index 0000000..f9f65f8 --- /dev/null +++ b/ludwig/schema/profiler.py @@ -0,0 +1,113 @@ +from dataclasses import field + +import pydantic + +import ludwig.schema.utils as schema_utils +from ludwig.api_annotations import DeveloperAPI +from ludwig.error import ConfigValidationError + + +@DeveloperAPI +class ProfilerConfig(schema_utils.LudwigBaseConfig): + """Dataclass that holds profiling parameters for torch profile scheduler. + + The profiler will skip the first skip_first steps, then wait for wait steps, then do the warmup for the next warmup + steps, then do the active recording for the next active steps and then repeat the cycle starting with wait steps. + The optional number of cycles is specified with the repeat parameter, the zero value means that the cycles will + continue until the profiling is finished. + """ + + wait: int = schema_utils.IntegerRange( + default=1, + min=0, + description="The number of steps to wait profiling.", + ) + + warmup: int = schema_utils.IntegerRange( + default=1, + min=0, + description="The number of steps for profiler warmup after waiting finishes.", + ) + + active: int = schema_utils.IntegerRange( + default=3, + min=0, + description="The number of steps that are actively recorded. Values more than 10 wil dramatically slow down " + "tensorboard loading.", + ) + + repeat: int = schema_utils.IntegerRange( + default=5, + min=0, + description="The optional number of profiling cycles. Use 0 to profile the entire training run.", + ) + + skip_first: int = schema_utils.IntegerRange( + default=0, + min=0, + max=100, + description="The number of steps to skip in the beginning of training.", + ) + + +@DeveloperAPI +def ProfilerDataclassField(description: str, default: dict = {}): + """Returns custom dataclass field for `ludwig.modules.profiler.ProfilerConfig`. Allows `None` by default. + + Args: + description: Description of the torch profiler field. + default: Dict that specifies clipping param values that will be loaded by its schema class (default: {}). + """ + allow_none = True + + class ProfilingConfigField(schema_utils.SchemaField): + """Custom field class for the torch profiler. + + Deserializes a dict to a valid instance of `ludwig.modules.optimization_modules.ProfilerConfig` and + creates a corresponding JSON schema for external usage. + """ + + def _deserialize(self, value, attr, data, **kwargs): + if value is None: + return value + if isinstance(value, dict): + try: + return ProfilerConfig.model_validate(value) + except (TypeError, ConfigValidationError): + raise ConfigValidationError( + f"Invalid params for profiling config: {value}, see ProfilerConfig class." + ) + raise ConfigValidationError("Field should be None or dict") + + def _jsonschema_type_mapping(self): + return { + **schema_utils.unload_jsonschema_from_config_class(ProfilerConfig), + "title": "profiler_options", + "description": description, + } + + if not isinstance(default, dict): + raise ConfigValidationError(f"Invalid default: `{default}`") + + def load_default(): + return ProfilerConfig.model_validate(default) + + try: + dump_default = ProfilerConfig.model_validate(default).to_dict() + except pydantic.ValidationError: + dump_default = default if isinstance(default, dict) else {} + + return field( + metadata={ + "marshmallow_field": ProfilingConfigField( + allow_none=allow_none, + load_default=load_default, + dump_default=dump_default, + metadata={ + "description": description, + "parameter_metadata": None, + }, + ) + }, + default_factory=load_default, + ) diff --git a/ludwig/schema/split.py b/ludwig/schema/split.py new file mode 100644 index 0000000..49fe0b2 --- /dev/null +++ b/ludwig/schema/split.py @@ -0,0 +1,193 @@ +from dataclasses import Field + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import SPLIT, TYPE +from ludwig.schema import utils as schema_utils +from ludwig.schema.metadata import PREPROCESSING_METADATA +from ludwig.utils.registry import Registry + +split_config_registry = Registry() +DEFAULT_PROBABILITIES = [0.7, 0.1, 0.2] + + +@DeveloperAPI +def get_split_cls(name: str): + return split_config_registry[name] + + +@DeveloperAPI +class BaseSplitConfig(schema_utils.LudwigBaseConfig): + """This Dataclass is a base schema for the nested split config under preprocessing.""" + + type: str + "Name corresponding to the splitting type." + + +@DeveloperAPI +@split_config_registry.register("random") +class RandomSplitConfig(BaseSplitConfig): + """This Dataclass generates a schema for the random splitting config.""" + + type: str = schema_utils.ProtectedString( + "random", + description="Type of splitting to use during preprocessing.", + ) + + probabilities: list = schema_utils.List( + list_type=float, + default=DEFAULT_PROBABILITIES, + description="Probabilities for splitting data into train, validation, and test sets.", + parameter_metadata=PREPROCESSING_METADATA["split_probabilities"], + ) + + +@DeveloperAPI +@split_config_registry.register("fixed") +class FixedSplitConfig(BaseSplitConfig): + """This Dataclass generates a schema for the fixed splitting config.""" + + type: str = schema_utils.ProtectedString( + "fixed", + description="Type of splitting to use during preprocessing.", + ) + + column: str = schema_utils.String( + default=SPLIT, + allow_none=False, + description="The column name to use for fixed splitting.", + parameter_metadata=PREPROCESSING_METADATA["column"], + ) + + +@DeveloperAPI +@split_config_registry.register("stratify") +class StratifySplitConfig(BaseSplitConfig): + """This Dataclass generates a schema for the fixed splitting config.""" + + type: str = schema_utils.ProtectedString( + "stratify", + description="Type of splitting to use during preprocessing.", + ) + + column: str = schema_utils.String( + default=None, + allow_none=True, + description="The column name to base the stratified splitting on.", + parameter_metadata=PREPROCESSING_METADATA["column"], + ) + + probabilities: list = schema_utils.List( + list_type=float, + default=DEFAULT_PROBABILITIES, + description="Probabilities for splitting data into train, validation, and test sets.", + parameter_metadata=PREPROCESSING_METADATA["split_probabilities"], + ) + + +@DeveloperAPI +@split_config_registry.register("datetime") +class DateTimeSplitConfig(BaseSplitConfig): + """This Dataclass generates a schema for the fixed splitting config.""" + + type: str = schema_utils.ProtectedString( + "datetime", + description="Type of splitting to use during preprocessing.", + ) + + column: str = schema_utils.String( + default=None, + allow_none=True, + description="The column name to perform datetime splitting on.", + parameter_metadata=PREPROCESSING_METADATA["column"], + ) + + probabilities: list = schema_utils.List( + list_type=float, + default=DEFAULT_PROBABILITIES, + description="Proportion of data to split into train, validation, and test sets.", + parameter_metadata=PREPROCESSING_METADATA["split_probabilities"], + ) + + +@DeveloperAPI +@split_config_registry.register("hash") +class HashSplitConfig(BaseSplitConfig): + """This Dataclass generates a schema for the hash splitting config. + + This is useful for deterministically splitting on a unique ID. Even when additional rows are added to the dataset in + the future, each ID will retain its original split assignment. + + This approach does not guarantee that the split proportions will be assigned exactly, but the larger the dataset, + the more closely the assignment should match the given proportions. + + This approach can be used on a column with duplicates, but it will further skew the assignments of rows to splits. + """ + + type: str = schema_utils.ProtectedString( + "hash", + description="Type of splitting to use during preprocessing.", + ) + + column: str = schema_utils.String( + default=None, + allow_none=True, + description="The column name to perform hash splitting on.", + parameter_metadata=PREPROCESSING_METADATA["column"], + ) + + probabilities: list = schema_utils.List( + list_type=float, + default=DEFAULT_PROBABILITIES, + description="Proportion of data to split into train, validation, and test sets.", + parameter_metadata=PREPROCESSING_METADATA["split_probabilities"], + ) + + +@DeveloperAPI +def get_split_conds(): + """Returns a JSON schema of conditionals to validate against optimizer types defined in + `ludwig.modules.optimization_modules.optimizer_registry`.""" + conds = [] + for splitter in split_config_registry.data: + splitter_cls = split_config_registry.data[splitter] + other_props = schema_utils.unload_jsonschema_from_config_class(splitter_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props, [TYPE]) + splitter_cond = schema_utils.create_cond( + {"type": splitter}, + other_props, + ) + conds.append(splitter_cond) + return conds + + +@DeveloperAPI +def SplitDataclassField(default: str) -> Field: + """Custom dataclass field that when used inside a dataclass will allow the user to specify a nested split + config. + + Returns: Initialized dataclass field that converts an untyped dict with params to a split config. + """ + + class SplitSelection(schema_utils.TypeSelection): + def __init__(self): + super().__init__(registry=split_config_registry.data, default_value=default) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return split_config_registry.data[key] + + def _jsonschema_type_mapping(self): + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of splitting to use during preprocessing.", + "enum": list(split_config_registry.data.keys()), + "default": default, + }, + }, + "title": "split_options", + "allOf": get_split_conds(), + } + + return SplitSelection().get_default_field() diff --git a/ludwig/schema/trainer.py b/ludwig/schema/trainer.py new file mode 100644 index 0000000..0ac3456 --- /dev/null +++ b/ludwig/schema/trainer.py @@ -0,0 +1,909 @@ +import re +from abc import ABC + +import torch +from packaging.version import parse as parse_version + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + AUTO, + EFFECTIVE_BATCH_SIZE, + LOSS, + MAX_BATCH_SIZE, + MAX_POSSIBLE_BATCH_SIZE, + MODEL_ECD, + MODEL_LLM, + TRAINING, +) +from ludwig.error import ConfigValidationError +from ludwig.schema import utils as schema_utils +from ludwig.schema.lr_scheduler import LRSchedulerConfig, LRSchedulerDataclassField +from ludwig.schema.metadata import TRAINER_METADATA +from ludwig.schema.optimizers import ( + BaseOptimizerConfig, + GradientClippingConfig, + GradientClippingDataclassField, + OptimizerDataclassField, +) +from ludwig.schema.profiler import ProfilerConfig, ProfilerDataclassField +from ludwig.utils.registry import Registry + +_torch_200 = parse_version(torch.__version__) >= parse_version("2.0") + + +trainer_schema_registry = Registry() +_llm_trainer_schema_registry = Registry() + + +@DeveloperAPI +def register_trainer_schema(model_type: str): + def wrap(trainer_config: BaseTrainerConfig): + trainer_schema_registry[model_type] = trainer_config + return trainer_config + + return wrap + + +@DeveloperAPI +def register_llm_trainer_schema(trainer_type: str): + def wrap(trainer_config: BaseTrainerConfig): + _llm_trainer_schema_registry[trainer_type] = trainer_config + return trainer_config + + return wrap + + +@DeveloperAPI +def get_llm_trainer_cls(trainer_type: str): + """Returns the adapter config class registered with the given name.""" + return _llm_trainer_schema_registry[trainer_type] + + +@DeveloperAPI +class BaseTrainerConfig(schema_utils.LudwigBaseConfig, ABC): + """Common trainer parameter values.""" + + validation_field: str = schema_utils.String( + default=None, + allow_none=True, + description="The field for which the `validation_metric` is used for validation-related mechanics like early " + "stopping, parameter change plateaus, as well as what hyperparameter optimization uses to determine the best " + "trial. If unset (default), the first output feature is used. If explicitly specified, neither " + "`validation_field` nor `validation_metric` are overwritten.", + ) + + validation_metric: str = schema_utils.String( + default=None, + allow_none=True, + description=( + "Metric from `validation_field` that is used. If validation_field is not explicitly specified, this is " + "overwritten to be the first output feature type's `default_validation_metric`, consistent with " + "validation_field. If the validation_metric is specified, then we will use the first output feature that " + "produces this metric as the `validation_field`." + ), + ) + + early_stop: int = schema_utils.IntegerRange( + default=5, + min=-1, + description=( + "Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that " + "triggers training to stop. Can be set to -1, which disables early stopping entirely." + ), + ) + + skip_all_evaluation: bool = schema_utils.Boolean( + default=False, + description=( + "Whether to skip evaluation entirely. If you are training a model with a well-known configuration on a " + "well-known dataset and are confident about the expected results, you might skip all evaluation. Moreover, " + "evaluating a model, especially on large validation or test sets, can be time-consuming." + ), + ) + + enable_profiling: bool = schema_utils.Boolean( + default=False, + description="Whether to enable profiling of the training process using torch.profiler.profile.", + ) + + profiler: ProfilerConfig | None = ProfilerDataclassField( + description="Parameter values for profiling config.", + default={}, + ) + + def can_tune_batch_size(self) -> bool: + return True + + +@DeveloperAPI +@register_trainer_schema(MODEL_ECD) +class ECDTrainerConfig(BaseTrainerConfig): + """Dataclass that configures most of the hyperparameters used for ECD model training.""" + + def __post_init__(self): + if self.compile and not _torch_200: + raise ConfigValidationError( + "Trainer param `compile: true` requires PyTorch 2.0.0 or higher. Please upgrade PyTorch and try again." + ) + + if self.effective_batch_size != AUTO and self.max_batch_size < self.effective_batch_size: + raise ConfigValidationError( + f"`max_batch_size` ({self.max_batch_size}) must be greater than or equal to " + f"`effective_batch_size` ({self.effective_batch_size})." + ) + + if self.effective_batch_size != AUTO and self.batch_size != AUTO: + if self.effective_batch_size < self.batch_size: + raise ConfigValidationError( + f"`effective_batch_size` ({self.effective_batch_size}) " + f"must be greater than or equal to `batch_size` ({self.batch_size})." + ) + + if self.effective_batch_size % self.batch_size != 0: + raise ConfigValidationError( + f"`effective_batch_size` ({self.effective_batch_size}) " + f"must be divisible by `batch_size` ({self.batch_size})." + ) + + if self.effective_batch_size != AUTO and self.gradient_accumulation_steps != AUTO: + if self.effective_batch_size < self.gradient_accumulation_steps: + raise ConfigValidationError( + f"`effective_batch_size` ({self.effective_batch_size}) must be greater than or equal to " + f"`gradient_accumulation_steps` ({self.gradient_accumulation_steps})." + ) + + if self.effective_batch_size % self.gradient_accumulation_steps != 0: + raise ConfigValidationError( + f"`effective_batch_size` ({self.effective_batch_size}) must be divisible by " + f"`gradient_accumulation_steps` ({self.gradient_accumulation_steps})." + ) + + if self.layers_to_freeze_regex: + try: + re.compile(self.layers_to_freeze_regex) + except re.error: + raise ConfigValidationError( + f"`layers_to_freeze_regex` ({self.layers_to_freeze_regex}) must be a valid regular expression." + ) + + learning_rate: float | str = schema_utils.OneOfOptionsField( + default=0.001, + allow_none=False, + description=( + "Controls how much to change the model in response to the estimated error each time the model weights are " + "updated. If 'auto', the optimal learning rate is estimated by choosing the learning rate that produces " + "the smallest non-diverging gradient update." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate"], + field_options=[ + schema_utils.FloatRange(default=0.001, allow_none=False, min=0, max=1), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + learning_rate_scheduler: LRSchedulerConfig = LRSchedulerDataclassField( + description="Parameter values for learning rate scheduler.", + default=None, + ) + + epochs: int = schema_utils.PositiveInteger( + default=100, + description="Number of epochs the algorithm is intended to be run over. Overridden if `train_steps` is set", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["epochs"], + ) + + checkpoints_per_epoch: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "Number of checkpoints per epoch. For example, 2 -> checkpoints are written every half of an epoch. Note " + "that it is invalid to specify both non-zero `steps_per_checkpoint` and non-zero `checkpoints_per_epoch`." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["checkpoints_per_epoch"], + ) + + train_steps: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description=( + "Maximum number of training steps the algorithm is intended to be run over. Unset by default. " + "If set, will override `epochs` and if left unset then `epochs` is used to determine training length." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["train_steps"], + ) + + eval_steps: float = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="The number of steps to use for evaluation. If None, the entire evaluation set will be used.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["eval_steps"], + ) + + steps_per_checkpoint: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "How often the model is checkpointed. Also dictates maximum evaluation frequency. If 0 the model is " + "checkpointed after every epoch." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["steps_per_checkpoint"], + ) + + effective_batch_size: int | str = schema_utils.OneOfOptionsField( + default=AUTO, + allow_none=False, + description=( + "The effective batch size is the total number of samples used to compute a single gradient update " + "to the model weights. This differs from `batch_size` by taking `gradient_accumulation_steps` and number " + "of training worker processes into account. In practice, " + "`effective_batch_size = batch_size * gradient_accumulation_steps * num_workers`. " + "If 'auto', the effective batch size is derivied implicitly from `batch_size`, but if set explicitly, then " + "one of `batch_size` or `gradient_accumulation_steps` must be set to something other than 'auto', and " + "consequently will be set following the formula given above." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD][EFFECTIVE_BATCH_SIZE], + field_options=[ + schema_utils.PositiveInteger(default=128, description="", allow_none=False), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + batch_size: int | str = schema_utils.OneOfOptionsField( + default=AUTO, + allow_none=False, + description=( + "The number of training examples utilized in one training step of the model. If ’auto’, the " + "batch size that maximized training throughput (samples / sec) will be used. For CPU training, the " + "tuned batch size is capped at 128 as throughput benefits of large batch sizes are less noticeable without " + "a GPU." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["batch_size"], + field_options=[ + schema_utils.PositiveInteger(default=128, description="", allow_none=False), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + max_batch_size: int = schema_utils.PositiveInteger( + default=MAX_POSSIBLE_BATCH_SIZE, + allow_none=True, + description=( + "Auto batch size tuning and increasing batch size on plateau will be capped at this value. The default " + "value is 2^40." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD][MAX_BATCH_SIZE], + ) + + gradient_accumulation_steps: int | str = schema_utils.OneOfOptionsField( + default=AUTO, + allow_none=False, + description="Number of steps to accumulate gradients over before performing a weight update.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["gradient_accumulation_steps"], + field_options=[ + schema_utils.PositiveInteger(default=1, description="", allow_none=False), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + early_stop: int = schema_utils.IntegerRange( + default=5, + min=-1, + description=( + "Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that " + "triggers training to stop. Can be set to -1, which disables early stopping entirely." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["early_stop"], + ) + + eval_batch_size: None | int | str = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + description=( + "Size of batch to pass to the model for evaluation. If it is `0` or `None`, the same value of `batch_size` " + "is used. This is useful to speedup evaluation with a much bigger batch size than training, if enough " + "memory is available. If ’auto’, the biggest batch size (power of 2) that can fit in memory will be used." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["eval_batch_size"], + field_options=[ + schema_utils.PositiveInteger(default=128, description="", allow_none=False), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + evaluate_training_set: bool = schema_utils.Boolean( + default=False, + description=( + "Whether to evaluate on the entire training set during evaluation. By default, training metrics will be " + "computed at the end of each training step, and accumulated up to the evaluation phase. In practice, " + "computing training set metrics during training is up to 30% faster than running a separate evaluation " + "pass over the training set, but results in more noisy training metrics, particularly during the earlier " + "epochs. It's recommended to only set this to True if you need very exact training set metrics, and are " + "willing to pay a significant performance penalty for them." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["evaluate_training_set"], + ) + + validation_field: str = schema_utils.String( + default=None, + allow_none=True, + description="The field for which the `validation_metric` is used for validation-related mechanics like early " + "stopping, parameter change plateaus, as well as what hyperparameter optimization uses to determine the best " + "trial. If unset (default), the first output feature is used. If explicitly specified, neither " + "`validation_field` nor `validation_metric` are overwritten.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["validation_field"], + ) + + validation_metric: str = schema_utils.String( + default=None, + allow_none=True, + description=( + "Metric from `validation_field` that is used. If validation_field is not explicitly specified, this is " + "overwritten to be the first output feature type's `default_validation_metric`, consistent with " + "validation_field. If the validation_metric is specified, then we will use the first output feature that " + "produces this metric as the `validation_field`." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["validation_metric"], + ) + + optimizer: BaseOptimizerConfig = OptimizerDataclassField( + default="adam", + description=( + "Optimizer type and its parameters. The optimizer is responsble for applying the gradients computed " + "from the loss during backpropagation as updates to the model weights." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["optimizer"], + ) + + regularization_type: str | None = schema_utils.RegularizerOptions( + default="l2", + allow_none=True, + description="Type of regularization.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["regularization_type"], + ) + + regularization_lambda: float = schema_utils.FloatRange( + default=0.0, + min=0, + max=1, + description="Strength of the regularization.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["regularization_lambda"], + ) + + should_shuffle: bool = schema_utils.Boolean( + default=True, + description="Whether to shuffle batches during training when true.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["should_shuffle"], + ) + + increase_batch_size_on_plateau: int = schema_utils.NonNegativeInteger( + default=0, + description="The number of times to increase the batch size on a plateau.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["increase_batch_size_on_plateau"], + ) + + increase_batch_size_on_plateau_patience: int = schema_utils.NonNegativeInteger( + default=5, + description="How many epochs to wait for before increasing the batch size.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["increase_batch_size_on_plateau_patience"], + ) + + increase_batch_size_on_plateau_rate: float = schema_utils.NonNegativeFloat( + default=2.0, + description="Rate at which the batch size increases.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["increase_batch_size_on_plateau_rate"], + ) + + increase_batch_size_eval_metric: str = schema_utils.String( + default=LOSS, + description="Which metric to listen on for increasing the batch size.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["increase_batch_size_eval_metric"], + ) + + increase_batch_size_eval_split: str = schema_utils.String( + default=TRAINING, + description="Which dataset split to listen on for increasing the batch size.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["increase_batch_size_eval_split"], + ) + + gradient_clipping: GradientClippingConfig | None = GradientClippingDataclassField( + description="Parameter values for gradient clipping.", + default={}, + ) + + learning_rate_scaling: str = schema_utils.StringOptions( + ["constant", "sqrt", "linear"], + default="linear", + description="Scale by which to increase the learning rate as the number of distributed workers increases. " + "Traditionally the learning rate is scaled linearly with the number of workers to reflect the " + "proportion by" + " which the effective batch size is increased. For very large batch sizes, a softer square-root " + "scale can " + "sometimes lead to better model performance. If the learning rate is hand-tuned for a given " + "number of " + "workers, setting this value to constant can be used to disable scale-up.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate_scaling"], + ) + + bucketing_field: str = schema_utils.String( + default=None, + allow_none=True, + description="Feature to use for bucketing datapoints", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["bucketing_field"], + ) + + use_mixed_precision: bool = schema_utils.Boolean( + default=False, + description="Enable automatic mixed-precision (AMP) during training.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["use_mixed_precision"], + ) + + compile: bool = schema_utils.Boolean( + default=False, + description="Whether to compile the model before training.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["compile"], + ) + + enable_gradient_checkpointing: bool = schema_utils.Boolean( + default=False, + description="Whether to enable gradient checkpointing, which trades compute for memory." + "This is useful for training very deep models with limited memory.", + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["enable_gradient_checkpointing"], + ) + + layers_to_freeze_regex: str = schema_utils.String( + default=None, + allow_none=True, + description=( + "Freeze specific layers based on provided regex. Freezing specific layers can improve a " + "pretrained model's performance in a number of ways. At a basic level, freezing early layers can " + "prevent overfitting by retaining more general features (beneficial for small datasets). Also can " + "reduce computational resource use and lower overall training time due to less gradient calculations. " + ), + ) + + # ================ Loss Balancing ================ + + loss_balancing: str = schema_utils.StringOptions( + options=["none", "log_transform", "uncertainty", "famo", "gradnorm", "nash_mtl", "pareto_mtl"], + default="none", + allow_none=False, + description=( + "Multi-task loss balancing strategy for models with multiple output features. " + "'none': static weighted sum (default). " + "'log_transform': log(1+loss) compression (DB-MTL). " + "'uncertainty': learnable homoscedastic uncertainty weighting (Kendall et al., CVPR 2018). " + "'famo': fast adaptive multitask optimization (Liu et al., NeurIPS 2023). " + "'gradnorm': gradient normalization (Chen et al., ICML 2018). " + "'nash_mtl': Nash bargaining solution for multi-task weighting (Navon et al., ICML 2022). " + "'pareto_mtl': Pareto-optimal multi-task learning with preference vectors " + "(Lin et al., NeurIPS 2019)." + ), + ) + + loss_balancing_alpha: float = schema_utils.Float( + default=1.5, + description="Asymmetry parameter for gradnorm and smoothing for famo loss balancing.", + ) + + loss_balancing_lr: float = schema_utils.Float( + default=0.01, + description="Learning rate for famo loss balancing weight updates.", + ) + + loss_balancing_preference_vector: list | None = schema_utils.List( + default=None, + allow_none=True, + description=( + "Preference vector used by `loss_balancing: pareto_mtl`. One entry per output feature " + "(in the order they appear in `output_features`), non-negative, normalised internally " + "to sum to 1. Training is steered toward the Pareto-optimal point where the task losses " + "are inversely proportional to this vector. When null, a uniform preference is used." + ), + ) + + loss_balancing_tchebycheff_weight: float = schema_utils.FloatRange( + default=0.5, + min=0.0, + max=1.0, + description=( + "Mixing weight for `pareto_mtl` between the linear-scalarised term (weight = " + "1 - tchebycheff_weight) and the Tchebycheff max term (weight = tchebycheff_weight). " + "Pure Tchebycheff (1.0) enforces exact preference adherence but is rough to train; " + "pure linear (0.0) trains smoothly but diverges from the exact preference. " + "Default 0.5 matches Mahapatra & Rajan's 'mixed-exact' scalarisation (ICML 2020)." + ), + ) + + # ================ Contrastive Pre-alignment ================ + + contrastive_pretrain_epochs: int = schema_utils.NonNegativeInteger( + default=0, + description=( + "Number of epochs of contrastive pre-alignment between per-feature encoders to " + "run before the main training loop. 0 disables pre-alignment (default). A brief " + "warmup (1-3 epochs) is usually enough to pull encoder output spaces into " + "alignment so the downstream combiner sees already-comparable representations. " + "Inspired by CLIP-style alignment (Radford et al., ICML 2021) adapted to Ludwig's " + "multi-encoder ECD architecture." + ), + ) + + contrastive_pretrain_temperature: float = schema_utils.NonNegativeFloat( + default=0.07, + description=( + "Initial InfoNCE temperature for contrastive pre-alignment. Lower values sharpen " + "the softmax. 0.07 matches CLIP's initial value." + ), + ) + + contrastive_pretrain_projection_dim: int = schema_utils.PositiveInteger( + default=128, + description=( + "Width of the shared projection space used during contrastive pre-alignment. " + "The per-feature projection heads are discarded after pre-alignment — only the " + "updated encoder weights carry forward into the main training loop." + ), + ) + + contrastive_pretrain_learnable_temperature: bool = schema_utils.Boolean( + default=True, + description=( + "When True (default), the InfoNCE log-temperature is a trainable parameter " + "following the CLIP convention. Set to False to fix the temperature at " + "contrastive_pretrain_temperature throughout pre-alignment." + ), + ) + + # ================ Modality Dropout ================ + + modality_dropout: float = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=1.0, + description=( + "Probability of dropping each input feature's encoder output during training. " + "Dropped features are replaced with learnable missing-modality embeddings. " + "Set to 0.0 to disable (default). Improves robustness to missing inputs at inference." + ), + ) + + # ================ Model Soup ================ + + model_soup: str | None = schema_utils.StringOptions( + options=["uniform", "greedy"], + default=None, + allow_none=True, + description=( + "Model soup strategy for averaging top-K checkpoint weights after training. " + "'uniform': average all top-K checkpoints. " + "'greedy': greedily add checkpoints that improve validation metric. " + "None to disable (default)." + ), + ) + + model_soup_top_k: int = schema_utils.PositiveInteger( + default=5, + description="Number of top checkpoints to keep for model soup.", + ) + + def update_batch_size_grad_accum(self, num_workers: int): + from ludwig.utils.trainer_utils import get_rendered_batch_size_grad_accum + + self.batch_size, self.gradient_accumulation_steps = get_rendered_batch_size_grad_accum(self, num_workers) + + +@DeveloperAPI +class LLMTrainerConfig(BaseTrainerConfig): + """Base class for all LLM trainer configs.""" + + learning_rate: float | str = schema_utils.OneOfOptionsField( + default=0.0002, + allow_none=False, + description=( + "Controls how much to change the model in response to the estimated error each time the model weights are " + "updated. If 'auto', the optimal learning rate is estimated by choosing the learning rate that produces " + "the smallest non-diverging gradient update." + ), + parameter_metadata=TRAINER_METADATA[MODEL_ECD]["learning_rate"], + field_options=[ + schema_utils.FloatRange(default=0.001, allow_none=False, min=0, max=1), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + batch_size: int = schema_utils.PositiveInteger( + default=1, + description="Batch size used for training in the LLM trainer.", + ) + + base_learning_rate: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Base learning rate used for training in the LLM trainer.", + ) + + should_shuffle: bool = schema_utils.Boolean( + default=True, + description="Whether to shuffle the training data in the LLM trainer.", + ) + + epochs: int = schema_utils.PositiveInteger( + default=3, + description="Number of epochs to train in the LLM trainer.", + ) + + train_steps: int = schema_utils.PositiveInteger( + default=None, + allow_none=True, + description="Number of training steps to train in the LLM trainer.", + ) + + eval_steps: float = schema_utils.NonNegativeInteger( + default=None, + allow_none=True, + description="The number of steps to evaluate in the LLM trainer.", + ) + + steps_per_checkpoint: int = schema_utils.NonNegativeInteger( + default=0, + description="Number of steps per checkpoint in the LLM trainer.", + ) + + checkpoints_per_epoch: int = schema_utils.NonNegativeInteger( + default=0, + description="Number of checkpoints per epoch in the LLM trainer.", + ) + + early_stop: int = schema_utils.IntegerRange( + default=-1, + min=-1, + description=( + "Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that " + "triggers training to stop. Can be set to -1, which disables early stopping entirely." + ), + ) + + eval_batch_size: int = schema_utils.PositiveInteger( + default=2, + description="Batch size used for evaluation in the LLM trainer.", + ) + + evaluate_training_set: bool = schema_utils.Boolean( + default=False, + description="Whether to evaluate the training set in the LLM trainer. Note: this operation may be slow.", + ) + + packing: bool = schema_utils.Boolean( + default=False, + description=( + "Enable sequence packing for LLM training. Packs multiple short sequences into " + "a single batch entry with block-diagonal attention masks to prevent cross-sequence " + "attention. Typically provides 2-4x training throughput improvement by eliminating " + "wasted computation on pad tokens." + ), + ) + + packing_max_sequences_per_pack: int = schema_utils.PositiveInteger( + default=8, + description="Maximum number of sequences to pack into a single batch entry.", + ) + + +@DeveloperAPI +@register_llm_trainer_schema("none") +class InferenceOnlyTrainerConfig(LLMTrainerConfig): + """Dataclass that configures most of the hyperparameters used for zero-shot / few-shot LLM model training.""" + + # Required for lookup during trainer initialization + type: str = schema_utils.ProtectedString( + "none", + description="The type of trainer used to train the model. ", + parameter_metadata=TRAINER_METADATA[MODEL_LLM]["type"], + ) + + def can_tune_batch_size(self) -> bool: + return False + + +@DeveloperAPI +@register_llm_trainer_schema("finetune") +class FineTuneTrainerConfig(ECDTrainerConfig): + """Dataclass that configures most of the hyperparameters used for fine-tuning LLM model training.""" + + # Required for lookup during trainer initialization + type: str = schema_utils.ProtectedString("finetune") + + base_learning_rate: float = schema_utils.NonNegativeFloat( + default=0.0, + description="Base learning rate used for training in the LLM trainer.", + ) + + batch_size: int | str | None = schema_utils.OneOfOptionsField( + default=1, + allow_none=False, + description=( + "The number of training examples utilized in one training step of the model. If `auto`, the " + "batch size that maximized training throughput (samples / sec) will be used." + ), + field_options=[ + schema_utils.PositiveInteger(default=1, description="", allow_none=False), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + eval_batch_size: int | str | None = schema_utils.OneOfOptionsField( + default=2, + allow_none=True, + description=( + "Size of batch to pass to the model for evaluation. If it is `0` or `None`, the same value of `batch_size` " + "is used. This is useful to speedup evaluation with a much bigger batch size than training, if enough " + "memory is available. If `auto`, the biggest batch size (power of 2) that can fit in memory will be used." + ), + field_options=[ + schema_utils.PositiveInteger(default=2, description="", allow_none=False), + schema_utils.StringOptions(options=["auto"], default="auto", allow_none=False), + ], + ) + + +@DeveloperAPI +@register_llm_trainer_schema("dpo") +class DPOTrainerConfig(FineTuneTrainerConfig): + """Configuration for Direct Preference Optimization (DPO) training. + + DPO trains a model to prefer chosen completions over rejected ones without a separate reward model. Based on + Rafailov et al., NeurIPS 2023. + + Requires data with 'chosen' and 'rejected' text columns alongside the prompt. + """ + + type: str = schema_utils.ProtectedString("dpo") + + dpo_beta: float = schema_utils.Float( + default=0.1, + description=( + "Temperature parameter for DPO loss. Controls how much the policy " + "can deviate from the reference model. Lower values keep the policy " + "closer to the reference. Typical range: 0.05 to 0.5." + ), + ) + + dpo_loss_type: str = schema_utils.StringOptions( + options=["sigmoid", "ipo"], + default="sigmoid", + allow_none=False, + description=( + "DPO loss variant. 'sigmoid' is the standard DPO loss. " + "'ipo' is Identity Preference Optimization which uses a squared loss." + ), + ) + + dpo_label_smoothing: float = schema_utils.FloatRange( + default=0.0, + min=0.0, + max=0.5, + description="Label smoothing for DPO preference targets. 0 means no smoothing.", + ) + + rejected_column: str = schema_utils.String( + default="rejected", + description="Name of the column containing rejected completions for preference training.", + ) + + +@DeveloperAPI +@register_llm_trainer_schema("kto") +class KTOTrainerConfig(FineTuneTrainerConfig): + """Kahneman-Tversky Optimization (Ethayarajh et al., 2024).""" + + type: str = schema_utils.ProtectedString("kto") + kto_beta: float = schema_utils.Float(default=0.1, description="KTO temperature parameter.") + rejected_column: str = schema_utils.String(default="rejected", description="Column with rejected completions.") + + +@DeveloperAPI +@register_llm_trainer_schema("orpo") +class ORPOTrainerConfig(FineTuneTrainerConfig): + """Odds Ratio Preference Optimization (Hong et al., 2024).""" + + type: str = schema_utils.ProtectedString("orpo") + orpo_beta: float = schema_utils.Float(default=0.1, description="ORPO odds ratio weight.") + rejected_column: str = schema_utils.String(default="rejected", description="Column with rejected completions.") + + +@DeveloperAPI +@register_llm_trainer_schema("grpo") +class GRPOTrainerConfig(FineTuneTrainerConfig): + """Group Relative Policy Optimization (Shao et al., 2024, DeepSeek-R1).""" + + type: str = schema_utils.ProtectedString("grpo") + grpo_beta: float = schema_utils.Float(default=0.04, description="KL penalty coefficient.") + grpo_epsilon: float = schema_utils.FloatRange(default=0.2, min=0.0, max=1.0, description="PPO clipping parameter.") + grpo_num_generations: int = schema_utils.PositiveInteger( + default=4, description="Completions to generate per prompt." + ) + + +@DeveloperAPI +def get_model_type_jsonschema(model_type: str = MODEL_ECD): + if model_type == MODEL_LLM: + enum = [MODEL_LLM] + else: + enum = [MODEL_ECD] + + return { + "type": "string", + "enum": enum, + "default": MODEL_ECD, + "title": "model_type", + "description": "Select the model type.", + } + + +@DeveloperAPI +def get_trainer_jsonschema(model_type: str): + trainer_cls = trainer_schema_registry[model_type] + props = schema_utils.unload_jsonschema_from_config_class(trainer_cls)["properties"] + + return { + "type": "object", + "properties": props, + "title": "trainer_options", + "additionalProperties": False, + "description": "Schema for trainer determined by Model Type", + } + + +@DeveloperAPI +class ECDTrainerField(schema_utils.NestedConfigField): + def __init__(self): + super().__init__(ECDTrainerConfig) + + def _jsonschema_type_mapping(self): + return get_trainer_jsonschema(MODEL_ECD) + + +@DeveloperAPI +def get_llm_trainer_conds(): + """Returns a JSON schema of conditionals to validate against adapter types.""" + conds = [] + for trainer in _llm_trainer_schema_registry: + trainer_cls = _llm_trainer_schema_registry[trainer] + other_props = schema_utils.unload_jsonschema_from_config_class(trainer_cls)["properties"] + schema_utils.remove_duplicate_fields(other_props) + preproc_cond = schema_utils.create_cond( + {"type": trainer}, + other_props, + ) + conds.append(preproc_cond) + return conds + + +@DeveloperAPI +def LLMTrainerDataclassField(default="none", description=""): + class LLMTrainerSelection(schema_utils.TypeSelection): + def __init__(self): + super().__init__( + registry=_llm_trainer_schema_registry, + default_value=default, + description=description, + ) + + def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]: + return get_llm_trainer_cls(key) + + def _jsonschema_type_mapping(self): + return { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": list(_llm_trainer_schema_registry.keys()), + "default": default, + "description": "The type of LLM trainer to use", + }, + }, + "title": "llm_trainer_options", + "allOf": get_llm_trainer_conds(), + "required": ["type"], + "description": description, + } + + return LLMTrainerSelection().get_default_field() diff --git a/ludwig/schema/utils.py b/ludwig/schema/utils.py new file mode 100644 index 0000000..94f98d7 --- /dev/null +++ b/ludwig/schema/utils.py @@ -0,0 +1,1530 @@ +"""Ludwig schema utilities - pydantic 2 based. + +This module provides the foundation for Ludwig's declarative config system. +All config classes inherit from LudwigBaseConfig (a pydantic BaseModel) +and use field factory functions (String, Integer, Float, etc.) that return +pydantic Field() objects. +""" + +import copy +import logging +import os +import warnings +from abc import ABC, abstractmethod +from functools import cache +from typing import Any + +import yaml +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import ValidationError as PydanticValidationError +from pydantic.fields import FieldInfo + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ACTIVE, COLUMN, LUDWIG_SCHEMA_VALIDATION_POLICY, NAME, PROC_COLUMN, TYPE +from ludwig.error import ConfigValidationError +from ludwig.modules.reduction_modules import reduce_mode_registry +from ludwig.schema.metadata import COMMON_METADATA +from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata +from ludwig.utils.misc_utils import scrub_creds +from ludwig.utils.registry import Registry +from ludwig.utils.torch_utils import activations, initializer_registry + +# ============================================================================ +# SchemaField - base class for config field dispatch (TypeSelection, NestedConfigField, etc.) +# ============================================================================ + + +class SchemaField: + """Base class for Ludwig config field dispatch. + + Provides the interface for TypeSelection, NestedConfigField, and other config field + classes that handle polymorphic config resolution. + + Methods: + get_default_field(): Create a pydantic FieldInfo for this field + _jsonschema_type_mapping(): Return JSON schema for this field + _deserialize(value): Deserialize raw value to config object + """ + + def __init__(self, **kwargs): + # Store all keyword arguments as attributes for backward compat + for k, v in kwargs.items(): + setattr(self, k, v) + + def get_default_field(self) -> FieldInfo: + """Create a pydantic FieldInfo for this field. + + Override in subclasses. + """ + return Field(default=None) + + def _jsonschema_type_mapping(self): + """Return a JSON schema dict for this field. + + Override in subclasses. + """ + return None + + def _deserialize(self, value, attr, data, **kwargs): + """Deserialize a raw value. + + Override in subclasses. + """ + return value + + def deserialize_config(self, value, config_class, attr="config"): + """Deserialize a value into a config instance. + + Handles the common pattern of checking if a value is a raw dict + (from YAML) that needs validation, or already a config object. + + Args: + value: Raw value from config (dict, config instance, or None). + config_class: The LudwigBaseConfig subclass to validate against. + attr: Attribute name for error messages. + + Returns: + Validated config instance. + + Raises: + ConfigValidationError if validation fails. + """ + if value is None: + return None + if isinstance(value, LudwigBaseConfig): + return value + if isinstance(value, dict): + try: + return config_class.model_validate(value) + except (TypeError, ConfigValidationError) as error: + raise ConfigValidationError(f"Invalid params: {value}, see `{attr}` definition. Error: {error}") + raise ConfigValidationError(f"Invalid params for {attr}: expected dict, got {type(value)}") + + +logger = logging.getLogger(__name__) + +RECURSION_STOP_ENUM = {"weights_initializer", "bias_initializer", "norm_params"} + + +# Strict by default: reject unknown fields. Set LUDWIG_SCHEMA_VALIDATION_POLICY=exclude to allow extra fields. +LUDWIG_SCHEMA_VALIDATION_POLICY_VAR = os.environ.get(LUDWIG_SCHEMA_VALIDATION_POLICY, "forbid").lower() + + +# Sentinel for TypeSelection and NestedConfigField metadata markers +class _TypeSelectionMarker: + """Marker stored in Field.metadata to indicate this field uses TypeSelection dispatch.""" + + def __init__(self, type_selection): + self.type_selection = type_selection + + +class _NestedConfigMarker: + """Marker stored in Field.metadata to indicate this field uses NestedConfigField dispatch.""" + + def __init__(self, cls, allow_none=True): + self.cls = cls + self.allow_none = allow_none + + +ConfigT = Any # TypeVar("ConfigT", bound="LudwigBaseConfig") + + +def _convert_dataclass_field_to_pydantic(dc_field) -> FieldInfo: + """Convert a dataclasses.Field to a pydantic FieldInfo. + + This is the bridge that allows old marshmallow-style field definitions + (using dataclasses.field(metadata={"marshmallow_field": ...})) to work + with pydantic BaseModel classes during the migration period. + """ + import dataclasses as _dc + + metadata_list = [] + marshmallow_field = None + + # Extract marshmallow_field from metadata + if dc_field.metadata: + marshmallow_field = dc_field.metadata.get("marshmallow_field") + if marshmallow_field is not None: + # Store as a marker so model_validator can use it for dispatch + if isinstance(marshmallow_field, TypeSelection): + metadata_list.append(_TypeSelectionMarker(marshmallow_field)) + elif isinstance(marshmallow_field, NestedConfigField): + # Check if the subclass overrides _jsonschema_type_mapping + has_custom_schema = ( + type(marshmallow_field)._jsonschema_type_mapping is not NestedConfigField._jsonschema_type_mapping + ) + if has_custom_schema: + # Store as MarshmallowFieldMarker to preserve custom JSON schema generation + metadata_list.append(_LegacyFieldMarker(marshmallow_field)) + else: + metadata_list.append(_NestedConfigMarker(marshmallow_field.cls, marshmallow_field.allow_none)) + else: + # Generic marshmallow field - store for reference + metadata_list.append(_LegacyFieldMarker(marshmallow_field)) + + # Extract default and create FieldInfo. + # Note: pydantic 2's Field() does not accept a `metadata` kwarg — set it on the FieldInfo after creation. + if dc_field.default is not _dc.MISSING: + fi = Field(default=dc_field.default) + elif dc_field.default_factory is not _dc.MISSING: + fi = Field(default_factory=dc_field.default_factory) + else: + # No default - this is a required field + fi = Field() + if metadata_list: + fi.metadata = metadata_list + return fi + + +class _LegacyFieldMarker: + """Stores a marshmallow field for backward compat during migration.""" + + def __init__(self, marshmallow_field): + self.marshmallow_field = marshmallow_field + + +class _LudwigModelMeta(type(BaseModel)): + """Metaclass that bridges marshmallow-dataclass patterns to pydantic 2. + + Handles two key behaviors: + 1. Converts dataclasses.Field objects to pydantic FieldInfo in __new__ + 2. Allows class-level access to field defaults via __getattr__ + """ + + def __new__(mcs, name, bases, namespace, **kwargs): + import dataclasses as _dc + + # Python 3.14+ uses __annotate_func__ for lazy annotation evaluation; the + # __annotations__ dict in the namespace is empty until the class is fully built. + # Evaluate eagerly so the rest of this metaclass can read and modify annotations. + if "__annotate_func__" in namespace: + try: + import annotationlib + + annotations = dict(namespace["__annotate_func__"](annotationlib.Format.VALUE)) + except ImportError: + annotations = {} + else: + annotations = namespace.get("__annotations__", {}) + + # Detect @property definitions and prevent pydantic from treating them as field defaults. + # Properties that don't shadow inherited fields work fine as-is because pydantic + # only processes annotated attributes. Properties that DO shadow inherited fields + # should be converted to fields with constant defaults instead (done at the schema + # class level, not here). + _saved_properties: dict[str, property] = {} + for attr_name, value in list(namespace.items()): + if isinstance(value, property) and attr_name in annotations: + # A property in this class's own annotations would confuse pydantic. + # Remove it from annotations (it won't become a field). + _saved_properties[attr_name] = value + del namespace[attr_name] + annotations.pop(attr_name, None) + + # Convert dataclass field() objects and marshmallow field descriptors to pydantic Field() + for attr_name in list(annotations.keys()): + if attr_name in namespace: + value = namespace[attr_name] + if isinstance(value, _dc.Field): + namespace[attr_name] = _convert_dataclass_field_to_pydantic(value) + elif isinstance(value, SchemaField) and hasattr(value, "get_default_field"): + # TypeSelection and NestedConfigField instances need conversion + namespace[attr_name] = value.get_default_field() + + # Auto-widen annotations to bridge marshmallow→pydantic gap. + # In marshmallow, annotations were decorative. In pydantic, they're enforced. + import types + import typing + + for attr_name, ann in list(annotations.items()): + # Skip ClassVar annotations + origin = getattr(ann, "__origin__", None) + if origin is typing.ClassVar: + continue + + if attr_name not in namespace: + continue + + value = namespace[attr_name] + + # For fields with markers (TypeSelection/NestedConfigField/MarshmallowField), + # set annotation to Any since the actual validation happens in the marker + if isinstance(value, FieldInfo): + jse = getattr(value, "json_schema_extra", None) + has_marker = False + if isinstance(jse, dict) and "metadata" in jse: + has_marker = any( + isinstance(m, (_TypeSelectionMarker, _NestedConfigMarker, _LegacyFieldMarker)) + for m in jse["metadata"] + ) + for meta in getattr(value, "metadata", None) or []: + if isinstance(meta, (_TypeSelectionMarker, _NestedConfigMarker, _LegacyFieldMarker)): + has_marker = True + break + + if has_marker: + annotations[attr_name] = Any + continue + + # Widen to include None if default is None or enum contains None + from pydantic_core import PydanticUndefined + + should_widen = value.default is None and value.default is not PydanticUndefined + if not should_widen: + # Also widen if the enum (from allow_none=True in StringOptions etc.) contains None + jse_enum = (jse or {}).get("enum") if isinstance(jse, dict) else None + if isinstance(jse_enum, list) and None in jse_enum: + should_widen = True + if not should_widen: + # Also widen if allow_none=True was explicitly set in the field factory + if isinstance(jse, dict) and jse.get("allow_none"): + should_widen = True + + if should_widen: + is_union = origin in (types.UnionType,) + try: + is_union = is_union or origin is typing.Union + except (AttributeError, TypeError): + pass + + has_none = False + if is_union: + has_none = type(None) in getattr(ann, "__args__", ()) + + if not has_none: + try: + annotations[attr_name] = ann | None + except TypeError: + pass + + elif value is None: + # Plain None default + try: + annotations[attr_name] = ann | None + except TypeError: + pass + + namespace["__annotations__"] = annotations + import warnings + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="Field name .* shadows an attribute in parent") + cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + # Restore @property descriptors that we removed from namespace. + if _saved_properties: + for pname, prop in _saved_properties.items(): + setattr(cls, pname, prop) + + return cls + + def __getattr__(cls, name: str) -> Any: + """Allow accessing field defaults as class attributes (e.g., cls.type).""" + for klass in cls.__mro__: + pf = vars(klass).get("__pydantic_fields__") + if pf is not None and isinstance(pf, dict) and name in pf: + field_info = pf[name] + from pydantic_core import PydanticUndefined + + if field_info.default is not PydanticUndefined: + return field_info.default + break + raise AttributeError(name) + + +@DeveloperAPI +class LudwigBaseConfig(BaseModel, metaclass=_LudwigModelMeta): + """Base pydantic model for all Ludwig config classes. + + Provides from_dict, to_dict, and other convenience methods on top of pydantic 2 validation. + """ + + model_config = ConfigDict( + extra="ignore" if LUDWIG_SCHEMA_VALIDATION_POLICY_VAR == "exclude" else "forbid", + arbitrary_types_allowed=True, + validate_default=False, + revalidate_instances="never", + populate_by_name=True, + strict=False, + ) + + @model_validator(mode="before") + @classmethod + def _pre_validate(cls, data: Any) -> Any: + """Pre-validation: log deprecation warnings, resolve TypeSelection/nested fields.""" + if not isinstance(data, dict): + return data + + # Strip and warn about unknown fields (prevents pydantic extra="forbid" from rejecting them) + valid_fields = set(cls.model_fields.keys()) + for key in list(data.keys()): + if key not in valid_fields and key != "type": + warnings.warn( + f'"{key}" is not a valid parameter for the "{cls.__name__}" schema and will be ignored', + DeprecationWarning, + ) + del data[key] + + # Resolve TypeSelection, NestedConfigField, and legacy marshmallow fields + for fname, finfo in cls.model_fields.items(): + if fname not in data: + continue + value = data[fname] + + # Get markers from both metadata and json_schema_extra + markers = list(finfo.metadata or []) + jse = finfo.json_schema_extra + if isinstance(jse, dict) and "metadata" in jse: + markers.extend(jse["metadata"]) + + for meta in markers: + if isinstance(meta, _TypeSelectionMarker): + data[fname] = meta.type_selection.resolve(value) + break + elif isinstance(meta, _NestedConfigMarker): + if isinstance(value, LudwigBaseConfig): + break # Already a config instance, skip re-validation + if isinstance(value, dict): + try: + data[fname] = meta.cls.model_validate(value) + except PydanticValidationError as e: + raise ConfigValidationError( + f"Invalid params: {value}, see `{meta.cls}` definition. Error: {e}" + ) + break + elif isinstance(meta, _LegacyFieldMarker): + # Legacy marshmallow field - use its _deserialize for validation + # Skip if value is already a config instance (avoid double-validation) + if isinstance(value, LudwigBaseConfig): + break + mfield = meta.marshmallow_field + if hasattr(mfield, "_deserialize") and value is not None: + try: + data[fname] = mfield._deserialize(value, fname, data) + except Exception as e: + # Re-raise ConfigValidationError (from __post_init__) and + # from _deserialize rather than swallowing them + if isinstance(e, ConfigValidationError): + raise + pass # Let pydantic handle other validation errors + break + + return data + + @model_validator(mode="after") + def _validate_field_constraints(self): + """Post-validation: enforce enum constraints stored in json_schema_extra.""" + for fname, finfo in type(self).model_fields.items(): + value = getattr(self, fname, None) + extra = finfo.json_schema_extra + if not isinstance(extra, dict): + continue + + # Validate enum constraints (from StringOptions, IntegerOptions) + if "enum" in extra and value is not None: + allowed = extra["enum"] + if value not in allowed: + raise ValueError(f"Field '{fname}': value {value!r} not in allowed options {allowed}") + + # Validate float tuple range constraints + if "_float_tuple_range" in extra and value is not None: + spec = extra["_float_tuple_range"] + if not isinstance(value, (tuple, list)) or len(value) != spec["n"]: + raise ValueError(f"Field '{fname}': expected {spec['n']}-tuple, got {value!r}") + for v in value: + if spec.get("min") is not None and v < spec["min"]: + raise ValueError(f"Field '{fname}': value {v} below minimum {spec['min']}") + if spec.get("max") is not None and v > spec["max"]: + raise ValueError(f"Field '{fname}': value {v} above maximum {spec['max']}") + + # Validate embed field (int or str from options) + if "_embed_options" in extra and value is not None: + embed_options = extra["_embed_options"] + if isinstance(value, str) and value not in embed_options: + raise ValueError(f"Field '{fname}': string value {value!r} not in {embed_options}") + if not isinstance(value, (str, int)): + raise ValueError(f"Field '{fname}': expected str, int, or None, got {type(value).__name__}") + + # Validate initializer_or_dict field + if "_initializer_options" in extra and value is not None: + init_options = extra["_initializer_options"] + if isinstance(value, str) and value not in init_options: + raise ValueError(f"Field '{fname}': initializer {value!r} not in {init_options}") + if isinstance(value, dict): + if "type" not in value: + raise ValueError(f"Field '{fname}': dict must contain 'type' key") + if value["type"] not in init_options: + raise ValueError(f"Field '{fname}': initializer type {value['type']!r} not in {init_options}") + if not isinstance(value, (str, dict)): + raise ValueError(f"Field '{fname}': expected str or dict, got {type(value).__name__}") + + return self + + def __setattr__(self, name: str, value: Any) -> None: + """Allow setting arbitrary attributes on config instances. + + Ludwig code dynamically sets attributes like saved_weights_in_checkpoint, proc_column, etc. on config objects. + Pydantic 2 normally rejects setting attributes not defined as fields, so we override to allow it. + """ + try: + super().__setattr__(name, value) + except ValueError: + # Attribute not in model fields - allow it anyway (dataclass behavior) + object.__setattr__(self, name, value) + + def model_post_init(self, __context: Any) -> None: + """Bridge: call __post_init__ if defined by subclass (dataclass convention).""" + super().model_post_init(__context) + # Check if THIS class (or a parent) defines __post_init__ + post_init = getattr(type(self), "__post_init__", None) + if post_init is not None: + post_init(self) + + def to_dict(self) -> dict[str, Any]: + """Get a dictionary representation of this config. + + Recursively converts nested config objects and scrubs credentials. + """ + return scrub_creds(convert_submodules(vars(self))) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> "LudwigBaseConfig": + """Create a config instance from a dictionary.""" + return cls.model_validate(d) + + @classmethod + @cache + def get_valid_field_names(cls) -> set[str]: + """Return the set of valid field names for this config class.""" + return set(cls.model_fields.keys()) + + def __repr__(self): + return yaml.dump(self.to_dict(), sort_keys=False) + + +@DeveloperAPI +def get_marshmallow_field_class_name(field_info): + """Returns a human-readable string of the field class name. + + For backward compat, checks both pydantic metadata and marshmallow_field. + """ + # Check for marshmallow_field in metadata (legacy) + if hasattr(field_info, "metadata"): + for meta in field_info.metadata or []: + if hasattr(meta, "__class__"): + return meta.__class__.__name__ + # For pydantic FieldInfo, return the annotation name + if hasattr(field_info, "annotation"): + return str(field_info.annotation) + return "Unknown" + + +@DeveloperAPI +def load_config(cls: type["LudwigBaseConfig"], **kwargs) -> "LudwigBaseConfig": + """Takes a config class and instantiates it with the given keyword args as parameters.""" + assert_is_a_config_class(cls) + return cls.model_validate(kwargs) + + +@DeveloperAPI +def load_trainer_with_kwargs(model_type: str, kwargs: dict) -> tuple["LudwigBaseConfig", dict[str, Any]]: + """Special case of `load_config_with_kwargs` for the trainer schemas.""" + from ludwig.constants import MODEL_LLM + from ludwig.schema.trainer import ECDTrainerConfig, LLMTrainerConfig + + if model_type == MODEL_LLM: + trainer_schema = LLMTrainerConfig + else: + trainer_schema = ECDTrainerConfig + + return load_config_with_kwargs(trainer_schema, kwargs) + + +@DeveloperAPI +def load_config_with_kwargs( + cls: type["LudwigBaseConfig"], kwargs_overrides +) -> tuple["LudwigBaseConfig", dict[str, Any]]: + """Instantiates a config class filtering kwargs to only valid fields. + + Returns a tuple of (config, remaining_kwargs). + """ + assert_is_a_config_class(cls) + fields = cls.model_fields.keys() + return load_config(cls, **{k: v for k, v in kwargs_overrides.items() if k in fields}), { + k: v for k, v in kwargs_overrides.items() if k not in fields + } + + +@DeveloperAPI +def convert_submodules(config_dict: dict) -> dict[str, Any]: + """Helper for converting submodules to dictionaries during config serialization.""" + output_dict = copy.deepcopy(config_dict) + + for k, v in output_dict.items(): + if isinstance(v, dict): + convert_submodules(v) + elif isinstance(v, LudwigBaseConfig): + output_dict[k] = v.to_dict() + convert_submodules(output_dict[k]) + elif isinstance(v, list): + output_dict[k] = [x.to_dict() if isinstance(x, LudwigBaseConfig) else x for x in v] + elif isinstance(v, ListSerializable): + output_dict[k] = v.to_list() + + return output_dict + + +@DeveloperAPI +def create_cond(if_pred: dict, then_pred: dict): + """Returns a JSONSchema conditional for the given if-then predicates.""" + return { + "if": {"properties": {k: {"const": v} for k, v in if_pred.items()}}, + "then": {"properties": then_pred}, + } + + +@DeveloperAPI +def remove_duplicate_fields(properties: dict, fields: list[str] | None = None) -> None: + """Util function for removing duplicated schema elements.""" + duplicate_fields = [NAME, TYPE, COLUMN, PROC_COLUMN, ACTIVE] if fields is None else fields + for key in duplicate_fields: + properties.pop(key, None) + + +@DeveloperAPI +class ListSerializable(ABC): + @abstractmethod + def to_list(self) -> list: + pass + + +@DeveloperAPI +def assert_is_a_config_class(cls): + """Assert that cls is a Ludwig config class (pydantic BaseModel).""" + if not issubclass(cls, LudwigBaseConfig): + raise TypeError( + f"Expected a Ludwig config class (LudwigBaseConfig subclass), but got '{cls}'.\n" + f"Fix: ensure the class inherits from LudwigBaseConfig." + ) + + +def _default_matches_json_type(default_val, type_str) -> bool: + """Check if a default value is consistent with a JSON schema type string. + + Returns True if the default value matches the type string, False otherwise. This is used to avoid emitting 'type': + 'integer' when the default is 7.5 (float), which was a common pattern in the marshmallow era where type enforcement + was looser. + """ + if isinstance(type_str, list): + # Union type like ["integer", "null"] + return any(_default_matches_json_type(default_val, t) for t in type_str) + _CHECKS = { + "string": lambda v: isinstance(v, str), + "integer": lambda v: isinstance(v, int) and not isinstance(v, bool), + "number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool), + "boolean": lambda v: isinstance(v, bool), + "object": lambda v: isinstance(v, dict), + "array": lambda v: isinstance(v, (list, tuple)), + "null": lambda v: v is None, + } + check = _CHECKS.get(type_str) + if check is None: + return True # Unknown type, don't block + return check(default_val) + + +def _field_info_to_jsonschema(fname: str, finfo: FieldInfo, annotation: type | None = None) -> dict: + """Convert a pydantic FieldInfo to a JSON schema fragment. + + Checks metadata markers for TypeSelection/NestedConfigField/legacy marshmallow fields, and falls back to type- based + mapping for plain fields. + """ + # Check for markers in both metadata and json_schema_extra + markers = list(finfo.metadata or []) + jse = finfo.json_schema_extra + if isinstance(jse, dict) and "metadata" in jse: + markers.extend(jse["metadata"]) + + for meta in markers: + if isinstance(meta, _TypeSelectionMarker): + ts = meta.type_selection + custom = ts._jsonschema_type_mapping() + if custom is not None: + return custom + return {"type": "object"} + + if isinstance(meta, _NestedConfigMarker): + return unload_jsonschema_from_config_class(meta.cls) + + if isinstance(meta, _LegacyFieldMarker): + mf = meta.marshmallow_field + if hasattr(mf, "_jsonschema_type_mapping"): + custom = mf._jsonschema_type_mapping() + if custom is not None: + return custom + # Handle FeatureList-style fields with inner and length constraints + if hasattr(mf, "inner") and mf.inner is not None: + inner_schema = {} + if hasattr(mf.inner, "_jsonschema_type_mapping"): + inner_schema = mf.inner._jsonschema_type_mapping() or {} + result = {"type": "array", "items": inner_schema} + if hasattr(mf, "min_length") and mf.min_length is not None: + result["minItems"] = mf.min_length + if hasattr(mf, "max_length") and mf.max_length is not None: + result["maxItems"] = mf.max_length + return result + return {"type": "object"} + + # Handle InitializerOrDict fields + from pydantic_core import PydanticUndefined + + extra = finfo.json_schema_extra + if isinstance(extra, dict) and "_initializer_options" in extra: + init_options = extra["_initializer_options"] + return { + "oneOf": [ + {"type": "string", "enum": init_options}, + { + "type": "object", + "properties": {"type": {"type": "string", "enum": init_options}}, + "required": ["type"], + "additionalProperties": True, + }, + {"type": "null"}, + ], + "default": finfo.default if finfo.default is not PydanticUndefined else "xavier_uniform", + "description": finfo.description or "", + } + + # Build schema from field info + schema: dict[str, Any] = {} + + # Description + desc = finfo.description or "" + if desc: + schema["description"] = desc + + # Default value + from pydantic_core import PydanticUndefined + + if finfo.default is not PydanticUndefined: + if not callable(finfo.default) and not isinstance(finfo.default, property): + schema["default"] = finfo.default + + # Enum constraint from json_schema_extra + extra = finfo.json_schema_extra + if isinstance(extra, dict): + if "enum" in extra: + schema["enum"] = extra["enum"] + if "parameter_metadata" in extra: + schema["parameter_metadata"] = copy.deepcopy(extra["parameter_metadata"]) + + # Always include parameter_metadata (default if not explicitly provided) + if "parameter_metadata" not in schema: + schema["parameter_metadata"] = convert_metadata_to_json(None) + + # Map type annotation to JSON schema type + # Only emit type if annotation and default are consistent (avoid mismatches + # like annotation=int but default=7.5 which was common in marshmallow era) + if annotation is not None: + type_str = _annotation_to_json_type(annotation) + if type_str: + # If the enum contains None, the JSON schema type must include "null" + enum_vals = schema.get("enum") + if enum_vals is not None and None in enum_vals: + if isinstance(type_str, list): + if "null" not in type_str: + type_str = type_str + ["null"] + elif type_str != "null": + type_str = [type_str, "null"] + + # Check for mismatch between annotation type and default value + from pydantic_core import PydanticUndefined + + default_val = finfo.default if finfo.default is not PydanticUndefined else None + if default_val is not None and not _default_matches_json_type(default_val, type_str): + pass # Skip emitting type to avoid JSON schema validation failures + else: + schema["type"] = type_str + + # Range constraints and pattern from pydantic Field metadata + from annotated_types import Ge, Gt, Le, Lt + + for meta in finfo.metadata or []: + if isinstance(meta, Ge): + schema["minimum"] = meta.ge + elif isinstance(meta, Gt): + schema["exclusiveMinimum"] = meta.gt + elif isinstance(meta, Le): + schema["maximum"] = meta.le + elif isinstance(meta, Lt): + schema["exclusiveMaximum"] = meta.lt + elif hasattr(meta, "pattern") and getattr(meta, "pattern", None) is not None: + schema["pattern"] = meta.pattern + + return schema + + +def _annotation_to_json_type(annotation) -> str | list | None: + """Map a Python type annotation to a JSON schema type string.""" + import types + + origin = getattr(annotation, "__origin__", None) + + # Handle Python 3.10+ union types (e.g. float | None) which are instances of + # types.UnionType directly, without __origin__ + if isinstance(annotation, types.UnionType): + args = annotation.__args__ + has_none = type(None) in args + non_none = [a for a in args if a is not type(None)] + if len(non_none) == 1: + base = _annotation_to_json_type(non_none[0]) + if has_none and base: + return [base, "null"] + return base + return None + + # Also handle typing.Union + try: + import typing + + if origin is typing.Union: + args = annotation.__args__ + has_none = type(None) in args + non_none = [a for a in args if a is not type(None)] + if len(non_none) == 1: + base = _annotation_to_json_type(non_none[0]) + if has_none and base: + return [base, "null"] + return base + return None + except (AttributeError, TypeError): + pass + + _TYPE_MAP = { + str: "string", + int: "integer", + float: "number", + bool: "boolean", + dict: "object", + list: "array", + tuple: "array", + } + + if annotation in _TYPE_MAP: + return _TYPE_MAP[annotation] + + return None + + +@DeveloperAPI +def unload_jsonschema_from_config_class(mclass, additional_properties: bool = True, title: str | None = None) -> dict: + """Get a JSON schema dict for a Ludwig config class. + + Iterates over pydantic model_fields and checks metadata markers for TypeSelection, NestedConfigField, and legacy + marshmallow fields. + """ + assert_is_a_config_class(mclass) + + properties = {} + annotations = {} + + # Gather annotations from the class and its MRO + for klass in reversed(mclass.__mro__): + annotations.update(getattr(klass, "__annotations__", {})) + + for fname, finfo in mclass.model_fields.items(): + ann = annotations.get(fname) + properties[fname] = _field_info_to_jsonschema(fname, finfo, ann) + + schema = { + "type": "object", + "properties": properties, + "additionalProperties": additional_properties, + } + if title is not None: + schema["title"] = title + return schema + + +# ============================================================================ +# Field Factory Functions +# ============================================================================ +# All return pydantic Field() objects (FieldInfo) that can be used as class +# variable defaults in LudwigBaseConfig subclasses. +# ============================================================================ + + +def _make_json_schema_extra( + description: str = "", + parameter_metadata: ParameterMetadata = None, + **extra, +) -> dict | None: + """Build json_schema_extra dict for Field(), returning None if empty.""" + result = {} + if parameter_metadata: + result["parameter_metadata"] = convert_metadata_to_json(parameter_metadata) + result.update(extra) + return result or None + + +@DeveloperAPI +def InitializerOptions(default: str = "xavier_uniform", description="", parameter_metadata: ParameterMetadata = None): + """Utility wrapper that returns a `StringOptions` field with keys from `initializer_registry`.""" + return StringOptions( + list(initializer_registry.keys()), + default=default, + allow_none=False, + description=description, + parameter_metadata=parameter_metadata, + ) + + +@DeveloperAPI +def ActivationOptions(default: str | None = "relu", description=None, parameter_metadata: ParameterMetadata = None): + """Utility wrapper that returns a `StringOptions` field with keys from `activations` registry.""" + description = description or "Default activation function applied to the output of the fully connected layers." + parameter_metadata = parameter_metadata or COMMON_METADATA["activation"] + return StringOptions( + list(activations.keys()), + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +@DeveloperAPI +def ReductionOptions(default: None | str = None, description="", parameter_metadata: ParameterMetadata = None): + """Utility wrapper that returns a `StringOptions` field with keys from `reduce_mode_registry`.""" + return StringOptions( + list(reduce_mode_registry.keys()), + default=default, + allow_none=True, + description=description, + parameter_metadata=parameter_metadata, + ) + + +@DeveloperAPI +def RegularizerOptions( + default: None | str, + allow_none: bool = False, + description="", + parameter_metadata: ParameterMetadata = None, +): + """Utility wrapper that returns a `StringOptions` field with prefilled regularizer options.""" + return StringOptions( + ["l1", "l2", "l1_l2"], + default=default, + allow_none=allow_none, + description=description, + parameter_metadata=parameter_metadata, + ) + + +@DeveloperAPI +def String( + description: str, + default: None | str, + allow_none: bool = False, + pattern: str | None = None, + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field for string values.""" + if not allow_none and default is not None and not isinstance(default, str): + raise ValueError(f"Provided default `{default}` should be a string!") + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + kwargs = {} + if pattern is not None: + kwargs["pattern"] = pattern + + return Field( + default=default, + description=description, + json_schema_extra=json_extra, + **kwargs, + ) + + +@DeveloperAPI +def StringOptions( + options: list[str], + default: None | str, + allow_none: bool = False, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field that enforces string inputs must be one of `options`.""" + options = list(options) # ensure list, not dict_keys or other iterable + if len(options) == 0: + raise ValueError("Must provide non-empty list of options!") + + if default is not None and not isinstance(default, str): + raise ValueError(f"Provided default `{default}` should be a string!") + + if allow_none and None not in options: + options = options + [None] + if not allow_none and None in options: + options = [o for o in options if o is not None] + + unique_options = {o for o in options if o is not None} | ({None} if None in options else set()) + if len(options) != len(unique_options): + raise ValueError(f"Provided options must be unique! See: {options}") + if default not in options: + raise ValueError(f"Provided default `{default}` is not one of allowed options: {options}") + + json_extra = _make_json_schema_extra( + description=description, + parameter_metadata=parameter_metadata, + enum=options, + ) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def ProtectedString( + pstring: str, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Alias for a `StringOptions` field with only one option.""" + return StringOptions( + options=[pstring], + default=pstring, + allow_none=False, + description=description, + parameter_metadata=parameter_metadata, + ) + + +@DeveloperAPI +def IntegerOptions( + options: list[int], + default: None | int, + allow_none: bool = False, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field that enforces integer inputs must be one of `options`.""" + if len(options) <= 0: + raise ValueError("Must provide non-empty list of options!") + if default is not None and not isinstance(default, int): + raise ValueError(f"Provided default `{default}` should be an int!") + if allow_none and None not in options: + options = list(options) + [None] + if not allow_none and None in options: + options = [o for o in options if o is not None] + if default not in options: + raise ValueError(f"Provided default `{default}` is not one of allowed options: {options}") + + json_extra = _make_json_schema_extra( + description=description, + parameter_metadata=parameter_metadata, + enum=options, + ) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def Boolean(default: bool, description: str = "", parameter_metadata: ParameterMetadata = None): + """Returns a pydantic Field for boolean values.""" + if default is not None and not isinstance(default, bool): + raise ValueError(f"Invalid default: `{default}`") + + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def Integer( + default: None | int, + allow_none=False, + description="", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field strictly enforcing integer inputs.""" + if default is not None and not isinstance(default, int): + raise ValueError(f"Invalid default: `{default}`") + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def PositiveInteger( + description: str, + default: None | int, + allow_none: bool = False, + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field enforcing positive integer inputs (>= 1).""" + if default is not None: + if not isinstance(default, int) or default < 1: + raise ValueError(f"Invalid default: `{default}`") + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, ge=1, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def NonNegativeInteger( + description: str, + default: None | int, + allow_none: bool = False, + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field enforcing nonnegative integer inputs (>= 0).""" + if default is not None: + if not isinstance(default, int) or default < 0: + raise ValueError(f"Invalid default: `{default}`") + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, ge=0, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def IntegerRange( + description: str, + default: None | int, + allow_none: bool = False, + parameter_metadata: ParameterMetadata = None, + min: int | None = None, + max: int | None = None, + min_inclusive: bool = True, + max_inclusive: bool = True, +): + """Returns a pydantic Field enforcing integer inputs within a range.""" + if default is not None: + if not isinstance(default, int): + raise ValueError(f"Invalid default: `{default}`") + if min is not None and ((min_inclusive and default < min) or (not min_inclusive and default <= min)): + raise ValueError(f"Invalid default: `{default}` (below min {min})") + if max is not None and ((max_inclusive and default > max) or (not max_inclusive and default >= max)): + raise ValueError(f"Invalid default: `{default}` (above max {max})") + + kwargs = {} + if min is not None: + kwargs["ge" if min_inclusive else "gt"] = min + if max is not None: + kwargs["le" if max_inclusive else "lt"] = max + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, description=description, json_schema_extra=json_extra, **kwargs) + + +@DeveloperAPI +def Float( + default: None | float | int, + allow_none=False, + description="", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field for float inputs.""" + if default is not None and not isinstance(default, (float, int)): + raise ValueError(f"Invalid default: `{default}`") + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def NonNegativeFloat( + default: None | float, + allow_none: bool = False, + description: str = "", + max: float | None = None, + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field enforcing nonnegative float inputs.""" + if default is not None: + if not isinstance(default, (float, int)) or default < 0: + raise ValueError(f"Invalid default: `{default}`") + if max is not None and default > max: + raise ValueError(f"Invalid default: `{default}` (above max {max})") + + kwargs = {"ge": 0.0} + if max is not None: + kwargs["le"] = max + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, description=description, json_schema_extra=json_extra, **kwargs) + + +@DeveloperAPI +def FloatRange( + default: None | float, + allow_none: bool = False, + description: str = "", + parameter_metadata: ParameterMetadata = None, + min: int | None = None, + max: int | None = None, + min_inclusive: bool = True, + max_inclusive: bool = True, +): + """Returns a pydantic Field enforcing float inputs within a range.""" + if default is not None: + if not isinstance(default, (float, int)): + raise ValueError(f"Invalid default: `{default}`") + + kwargs = {} + if min is not None: + kwargs["ge" if min_inclusive else "gt"] = min + if max is not None: + kwargs["le" if max_inclusive else "lt"] = max + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs) + return Field(default=default, description=description, json_schema_extra=json_extra, **kwargs) + + +@DeveloperAPI +def Dict( + default: None | dict = None, + allow_none: bool = True, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field for dict values.""" + allow_none = allow_none or default is None + + if default is not None: + if not isinstance(default, dict): + raise ValueError(f"Invalid default: `{default}`") + if not all(isinstance(k, str) for k in default): + raise ValueError(f"Invalid default: `{default}` (non-string keys)") + elif not allow_none: + default = {} + + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata) + + if default is None: + return Field(default=None, description=description, json_schema_extra=json_extra) + return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def List( + list_type: type[str] | type[int] | type[float] | type[list] = str, + inner_type: type[str] | type[int] | type[float] | type[dict] = float, + default: None | list[Any] = None, + allow_none: bool = True, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field for list values.""" + if default is not None: + if not isinstance(default, list): + raise ValueError(f"Invalid default: `{default}`") + elif not allow_none: + default = [] + + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata) + + if default is None: + return Field(default=None, description=description, json_schema_extra=json_extra) + return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def DictList( + default: None | list[dict] = None, + allow_none: bool = True, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field for list-of-dicts values.""" + if default is not None: + if not isinstance(default, list) or not all(isinstance(d, dict) for d in default): + raise ValueError(f"Invalid default: `{default}`") + elif not allow_none: + default = [] + + json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata) + + if default is None: + return Field(default=None, description=description, json_schema_extra=json_extra) + return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def Embed(description: str = "", parameter_metadata: ParameterMetadata = None): + """Returns a pydantic Field for embedding input feature names (int, str, or None).""" + _embed_options = ["add"] + json_extra = _make_json_schema_extra( + description=description, + parameter_metadata=parameter_metadata, + _embed_options=_embed_options, + ) + return Field(default=None, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def InitializerOrDict( + default: str = "xavier_uniform", description: str = "", parameter_metadata: ParameterMetadata = None +): + """Returns a pydantic Field allowing str or dict initializer values.""" + initializers = list(initializer_registry.keys()) + if not isinstance(default, str) or default not in initializers: + raise ValueError(f"Invalid default: `{default}`") + + json_extra = _make_json_schema_extra( + description=description, + parameter_metadata=parameter_metadata, + _initializer_options=initializers, + ) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def FloatRangeTupleDataclassField( + n: int = 2, + default: tuple | None = (0.9, 0.999), + allow_none: bool = False, + min: int | None = 0, + max: int | None = 1, + description: str = "", + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field for an N-dim tuple with values in a range.""" + if default is not None: + if n != len(default): + raise ValueError(f"Dimension of tuple '{n}' must match dimension of default val. '{default}'") + for v in default: + if min is not None and v < min: + raise ValueError(f"Invalid default: value {v} below minimum {min}") + if max is not None and v > max: + raise ValueError(f"Invalid default: value {v} above maximum {max}") + if default is None and not allow_none: + raise ValueError("Default value must not be None if allow_none is False") + + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra( + description=description, + parameter_metadata=parameter_metadata, + _float_tuple_range={"n": n, "min": min, "max": max}, + **extra_kwargs, + ) + return Field(default=default, description=description, json_schema_extra=json_extra) + + +@DeveloperAPI +def OneOfOptionsField( + default: Any, + description: str, + field_options: list, + allow_none: bool = False, + parameter_metadata: ParameterMetadata = None, +): + """Returns a pydantic Field that accepts values matching any of the field_options. + + Pydantic union validation handles the multi-type dispatch. The field_options are stored in json_schema_extra for + JSON schema generation. + """ + extra_kwargs = {} + if allow_none: + extra_kwargs["allow_none"] = True + json_extra = _make_json_schema_extra( + description=description, + parameter_metadata=parameter_metadata, + _oneof_options=True, + **extra_kwargs, + ) + + if default is None or isinstance(default, (int, str, bool)): + return Field(default=default, description=description, json_schema_extra=json_extra) + return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra) + + +# ============================================================================ +# TypeSelection - Polymorphic config dispatch based on registry +# ============================================================================ + + +class TypeSelection(SchemaField): + """Resolves polymorphic config types from a registry based on a key field. + + Used for fields like encoder, decoder, optimizer where the config class depends on a "type" key in the dict value. + """ + + def __init__( + self, + registry: Registry, + default_value: str | None = None, + key: str = "type", + description: str = "", + parameter_metadata: ParameterMetadata = None, + allow_str_value: bool = False, + allow_none: bool = False, + **kwargs, + ): + self.registry = registry + self.default_value = default_value + self.key = key + self.allow_str_value = allow_str_value + self.allow_none = allow_none + self.description = description + self.parameter_metadata = parameter_metadata + + def _deserialize(self, value, attr, data, **kwargs): + """Marshmallow deserialization - delegates to resolve().""" + return self.resolve(value) + + def resolve(self, value): + """Resolve a raw value (dict, str, None) to a config instance.""" + if value is None: + if self.allow_none: + return None + return None + + # Already a config instance + if isinstance(value, LudwigBaseConfig): + return value + + if self.allow_str_value and isinstance(value, str): + value = self.str_value_to_object(value) + + if isinstance(value, dict): + cls_type = value.get(self.key) + cls_type = cls_type.lower() if cls_type else self.default_value + if cls_type and cls_type in self.registry: + cls = self.get_schema_from_registry(cls_type) + try: + return cls.model_validate(value) + except (TypeError, PydanticValidationError) as e: + raise ConfigValidationError(f"Invalid params: {value}, see `{cls}` definition") from e + raise ConfigValidationError(f"Invalid type: '{cls_type}', expected one of: {list(self.registry.keys())}") + + maybe_str = ", `str`," if self.allow_str_value else "" + raise ConfigValidationError(f"Invalid param {value}, expected `None`{maybe_str} or `dict`") + + def str_value_to_object(self, value: str) -> dict: + """Convert a string shorthand to a dict with the type key.""" + return {self.key: value} + + def get_schema_from_registry(self, key: str) -> type[LudwigBaseConfig]: + """Look up a config class from the registry.""" + return self.registry[key] + + def get_default_field(self) -> FieldInfo: + """Create a pydantic Field wrapping this TypeSelection. + + The TypeSelection instance is stored in Field.metadata so the base class's model_validator can use it for + dispatch. + """ + if self.default_value is not None: + cls = self.get_schema_from_registry(self.default_value.lower()) + key = self.key + dv = self.default_value + + def default_factory(cls=cls, key=key, dv=dv): + return cls.model_validate({key: dv}) + + else: + + def default_factory(): + return None + + fi = Field(default_factory=default_factory) + fi.metadata = [_TypeSelectionMarker(self)] + return fi + + def _jsonschema_type_mapping(self): + """Override in subclass for custom JSON schema.""" + return None + + +@DeveloperAPI +class NestedConfigField(SchemaField): + """Validates a dict as a specific config class (non-polymorphic). + + Used for fields where a dict should be deserialized into a fixed config class. + """ + + def __init__( + self, + cls: type[LudwigBaseConfig], + allow_none: bool = True, + default_missing: bool = False, + description: str = "", + **kwargs, + ): + self.cls = cls + self.allow_none = allow_none + self.default_missing = default_missing + self.description = description + + def _deserialize(self, value, attr, data, **kwargs): + """Deserialize a dict to a config instance via pydantic model_validate.""" + if value is None: + return value + if isinstance(value, dict): + try: + return self.cls.model_validate(value) + except (TypeError, PydanticValidationError) as e: + raise ConfigValidationError(f"Invalid params: {value}, see `{self.cls}` definition") from e + raise ConfigValidationError("Field should be None or dict") + + def get_default_field(self) -> FieldInfo: + """Create a pydantic Field wrapping this NestedConfigField.""" + if not self.default_missing: + cls = self.cls + + def default_factory(cls=cls): + return cls.model_validate({}) + + else: + + def default_factory(): + return None + + # Check if subclass overrides _jsonschema_type_mapping - if so, use + # MarshmallowFieldMarker to preserve custom JSON schema generation + has_custom_schema = type(self)._jsonschema_type_mapping is not NestedConfigField._jsonschema_type_mapping + if has_custom_schema: + marker = _LegacyFieldMarker(self) + else: + marker = _NestedConfigMarker(self.cls, self.allow_none) + + fi = Field(default_factory=default_factory) + fi.metadata = [marker] + return fi + + def _jsonschema_type_mapping(self): + return unload_jsonschema_from_config_class(self.cls) + + +# Backward compatibility aliases (old names -> new names) +ValidationError = ConfigValidationError +BaseMarshmallowConfig = LudwigBaseConfig +DictMarshmallowField = NestedConfigField +LudwigConfig = LudwigBaseConfig +LudwigSchemaField = SchemaField +unload_jsonschema_from_marshmallow_class = unload_jsonschema_from_config_class +assert_is_a_marshmallow_class = assert_is_a_config_class diff --git a/ludwig/serve.py b/ludwig/serve.py new file mode 100644 index 0000000..efcbe15 --- /dev/null +++ b/ludwig/serve.py @@ -0,0 +1,532 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import asyncio +import io +import json +import logging +import os +import sys +import tempfile +import time +from typing import Any + +import pandas as pd +import torch +from pydantic import BaseModel, create_model, Field +from torchvision.io import decode_image + +from ludwig.api import LudwigModel +from ludwig.constants import AUDIO, COLUMN +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.data_utils import numpy_to_python +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig + +logger = logging.getLogger(__name__) + +try: + import uvicorn + from fastapi import FastAPI + from fastapi.responses import JSONResponse + from starlette.datastructures import UploadFile + from starlette.middleware import Middleware + from starlette.middleware.cors import CORSMiddleware + from starlette.requests import Request +except ImportError as e: + logger.error(e) + logger.error( + " fastapi and other serving dependencies cannot be loaded" + "and may have not been installed. " + "In order to install all serving dependencies run " + "pip install ludwig[serve]" + ) + sys.exit(-1) + +# --------------------------------------------------------------------------- +# Prometheus metrics (optional dependency) +# --------------------------------------------------------------------------- +try: + from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest, Histogram + + _REQUEST_COUNT = Counter( + "ludwig_requests_total", + "Total prediction requests", + ["endpoint", "status"], + ) + _REQUEST_LATENCY = Histogram( + "ludwig_request_latency_seconds", + "Request latency in seconds", + ["endpoint"], + ) + _ERROR_COUNT = Counter( + "ludwig_errors_total", + "Total prediction errors", + ["endpoint"], + ) + _PROMETHEUS_AVAILABLE = True +except ImportError: + _PROMETHEUS_AVAILABLE = False + +# --------------------------------------------------------------------------- +# Error constants +# --------------------------------------------------------------------------- +ALL_FEATURES_PRESENT_ERROR = {"error": "entry must contain all input features"} +COULD_NOT_RUN_INFERENCE_ERROR = {"error": "Unexpected Error: could not run inference on model"} + + +# --------------------------------------------------------------------------- +# Auto-generated Pydantic schemas from model config +# --------------------------------------------------------------------------- +_FEATURE_TYPE_TO_PYTHON: dict[str, type] = { + "number": float, + "binary": bool, + "category": str, + "text": str, + "sequence": str, + "set": str, + "bag": str, + "date": str, + "h3": str, + "vector": list, + "image": str | bytes | list, + "audio": str | bytes | list, + "timeseries": list, +} + + +def _feature_python_type(ftype: str) -> type: + return _FEATURE_TYPE_TO_PYTHON.get(ftype, Any) + + +def build_request_schema(config: dict) -> type[BaseModel]: + """Dynamically create a Pydantic request model from Ludwig input feature configs. + + Each input feature becomes an optional field (None default) so that missing features can be detected and reported + with a clear error message rather than a Pydantic validation error. + """ + fields: dict[str, Any] = {} + for feat in config.get("input_features", []): + name = feat.get("name") or feat.get(COLUMN) or feat.get("column") + ftype = feat.get("type", "") + py_type = _feature_python_type(ftype) + # Make every field Optional so callers get a descriptive error, not a 422 + fields[name] = (py_type | None, Field(default=None, description=f"Input feature '{name}' of type '{ftype}'")) + + return create_model("PredictRequest", **fields) + + +def build_response_schema(config: dict) -> type[BaseModel]: + """Dynamically create a Pydantic response model from Ludwig output feature configs. + + The exact column names produced by Ludwig's post-processor (e.g. ``age_predictions``, + ``churn_probabilities``) are declared as optional fields so extra columns don't break + validation. + """ + fields: dict[str, Any] = {} + for feat in config.get("output_features", []): + name = feat.get("name") or feat.get(COLUMN) or feat.get("column") + ftype = feat.get("type", "") + + if ftype == "number": + fields[f"{name}_predictions"] = (float | None, None) + elif ftype == "binary": + fields[f"{name}_predictions"] = (bool | None, None) + fields[f"{name}_probabilities"] = (float | None, None) + elif ftype in ("category", "text", "sequence", "set", "bag", "date", "h3"): + fields[f"{name}_predictions"] = (str | None, None) + fields[f"{name}_probabilities"] = (list | None, None) + else: + # Catch-all: accept anything + fields[f"{name}_predictions"] = (Any, None) + + return create_model("PredictResponse", **fields) + + +# --------------------------------------------------------------------------- +# Prometheus helpers +# --------------------------------------------------------------------------- +def _record_success(endpoint: str, latency: float) -> None: + if _PROMETHEUS_AVAILABLE: + _REQUEST_COUNT.labels(endpoint=endpoint, status="success").inc() + _REQUEST_LATENCY.labels(endpoint=endpoint).observe(latency) + + +def _record_error(endpoint: str) -> None: + if _PROMETHEUS_AVAILABLE: + _REQUEST_COUNT.labels(endpoint=endpoint, status="error").inc() + _ERROR_COUNT.labels(endpoint=endpoint).inc() + + +# --------------------------------------------------------------------------- +# Structured logging helpers +# --------------------------------------------------------------------------- +def _log_request(endpoint: str, feature_names: list[str], batch_size: int) -> None: + logger.info( + "prediction_request endpoint=%s features=%s batch_size=%d", + endpoint, + ",".join(feature_names), + batch_size, + ) + + +def _log_response(endpoint: str, output_feature_names: list[str], latency: float) -> None: + logger.info( + "prediction_response endpoint=%s outputs=%s latency_seconds=%.4f", + endpoint, + ",".join(output_feature_names), + latency, + ) + + +# --------------------------------------------------------------------------- +# Server factory +# --------------------------------------------------------------------------- +def server( + model: "LudwigModel", + allowed_origins: list[str] | None = None, + prediction_timeout: float = 30.0, +) -> "FastAPI": + """Build a FastAPI application for serving a Ludwig model. + + Args: + model: A loaded :class:`~ludwig.api.LudwigModel` instance. + allowed_origins: List of CORS-allowed origins. ``None`` disables CORS. + prediction_timeout: Seconds to wait for a prediction before returning + HTTP 504 Gateway Timeout. + + Returns: + A :class:`fastapi.FastAPI` application ready to be served with uvicorn. + """ + middleware = [Middleware(CORSMiddleware, allow_origins=allowed_origins)] if allowed_origins else None + app = FastAPI( + title="Ludwig Inference Server", + description="Auto-generated schemas, Prometheus metrics, structured logging, and timeout handling.", + middleware=middleware, + ) + + config = model.config + input_features = {f[COLUMN] for f in config["input_features"]} + output_feature_names = [f.get("name", f.get(COLUMN, "")) for f in config.get("output_features", [])] + + # Build typed Pydantic schemas for OpenAPI docs. + # PredictResponse is wired into /predict as a response_model. + # The request schema is attached via openapi_extra since the actual transport + # is multipart/form-data rather than a JSON body. + predict_request_schema = build_request_schema(config).model_json_schema() + PredictResponse = build_response_schema(config) + + # ------------------------------------------------------------------ # + # Health # + # ------------------------------------------------------------------ # + @app.get("/") + def check_health(): + return JSONResponse({"message": "Ludwig server is up"}) + + # ------------------------------------------------------------------ # + # Prometheus metrics # + # ------------------------------------------------------------------ # + if _PROMETHEUS_AVAILABLE: + + @app.get("/metrics") + def metrics(): + from starlette.responses import Response + + return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST) + + # ------------------------------------------------------------------ # + # Single-row prediction # + # ------------------------------------------------------------------ # + @app.post( + "/predict", + response_model=PredictResponse, + openapi_extra={"requestBody": {"content": {"multipart/form-data": {"schema": predict_request_schema}}}}, + ) + async def predict(request: Request): + start = time.monotonic() + files: list = [] + try: + form = await request.form() + entry, files = convert_input(form, model.model.input_features) + except Exception: + logger.exception("Failed to parse predict form") + _record_error("predict") + return JSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500) + + try: + if (entry.keys() & input_features) != input_features: + missing_features = input_features - set(entry.keys()) + _record_error("predict") + return JSONResponse( + { + "error": "Data received does not contain all input features. " + f"Missing features: {missing_features}." + }, + status_code=400, + ) + + _log_request("predict", sorted(entry.keys()), batch_size=1) + + try: + resp_df, _ = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, + lambda: model.predict(dataset=[entry], data_format=dict), + ), + timeout=prediction_timeout, + ) + except TimeoutError: + _record_error("predict") + logger.error( + "prediction_timeout endpoint=predict timeout_seconds=%.1f", + prediction_timeout, + ) + return JSONResponse( + {"error": f"Prediction timed out after {prediction_timeout}s"}, + status_code=504, + ) + + resp = numpy_to_python(resp_df.to_dict("records")[0]) + latency = time.monotonic() - start + _log_response("predict", output_feature_names, latency) + _record_success("predict", latency) + return JSONResponse(resp) + + except Exception as exc: + logger.exception("Failed to run predict: %s", exc) + _record_error("predict") + return JSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500) + finally: + for f in files: + os.remove(f.name) + + # ------------------------------------------------------------------ # + # Batch prediction # + # ------------------------------------------------------------------ # + @app.post("/batch_predict") + async def batch_predict(request: Request): + start = time.monotonic() + files: list = [] + try: + form = await request.form() + data, files = convert_batch_input(form, model.model.input_features) + data_df = pd.DataFrame.from_records( + data["data"], + index=data.get("index"), + columns=data["columns"], + ) + except Exception: + logger.exception("Failed to parse batch_predict form") + _record_error("batch_predict") + return JSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500) + + if (set(data_df.columns) & input_features) != input_features: + missing_features = input_features - set(data_df.columns) + _record_error("batch_predict") + return JSONResponse( + {"error": f"Data received does not contain all input features. Missing features: {missing_features}."}, + status_code=400, + ) + + _log_request("batch_predict", sorted(data_df.columns.tolist()), batch_size=len(data_df)) + + try: + try: + resp_df, _ = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, + lambda: model.predict(dataset=data_df), + ), + timeout=prediction_timeout, + ) + except TimeoutError: + _record_error("batch_predict") + logger.error( + "prediction_timeout endpoint=batch_predict timeout_seconds=%.1f", + prediction_timeout, + ) + return JSONResponse( + {"error": f"Prediction timed out after {prediction_timeout}s"}, + status_code=504, + ) + + resp = numpy_to_python(resp_df.to_dict("split")) + latency = time.monotonic() - start + _log_response("batch_predict", output_feature_names, latency) + _record_success("batch_predict", latency) + return JSONResponse(resp) + + except Exception: + logger.exception("Failed to run batch_predict") + _record_error("batch_predict") + return JSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500) + finally: + for f in files: + os.remove(f.name) + + return app + + +# --------------------------------------------------------------------------- +# Form / file helpers (unchanged from original) +# --------------------------------------------------------------------------- +def _write_file(v: "UploadFile", files: list) -> str: + # Convert UploadFile to a NamedTemporaryFile to ensure it's on the disk + suffix = os.path.splitext(v.filename)[1] + named_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) + files.append(named_file) + named_file.write(v.file.read()) + named_file.close() + return named_file.name + + +def _read_image_buffer(v: "UploadFile") -> torch.Tensor: + # read bytes sent via REST API and convert to image tensor + # in [channels, height, width] format + byte_string = io.BytesIO(v.file.read()).read() + image = decode_image(torch.frombuffer(byte_string, dtype=torch.uint8)) + return image # channels, height, width + + +def convert_input(form: "Request", input_features: dict): + """Returns a new input and a list of files to be cleaned up.""" + new_input = {} + files = [] + for k, v in form.multi_items(): + if isinstance(v, UploadFile): + # check if audio or image file + if input_features.get(k).type() == AUDIO: + new_input[k] = _write_file(v, files) + else: + new_input[k] = _read_image_buffer(v) + else: + new_input[k] = v + + return new_input, files + + +def convert_batch_input(form: "Request", input_features: dict): + """Returns a new input and a list of files to be cleaned up.""" + file_index = {} + files = [] + for _k, v in form.multi_items(): + if isinstance(v, UploadFile): + file_index[v.filename] = v + + data = json.loads(form["dataset"]) + for row in data["data"]: + for i, value in enumerate(row): + if value in file_index: + feature_name = data["columns"][i] + if input_features.get(feature_name).type() == AUDIO: + row[i] = _write_file(file_index[value], files) + else: + row[i] = _read_image_buffer(file_index[value]) + + return data, files + + +# --------------------------------------------------------------------------- +# Entry points +# --------------------------------------------------------------------------- +def run_server( + model_path: str, + host: str, + port: int, + allowed_origins: list, + prediction_timeout: float = 30.0, +) -> None: + """Load a pre-trained model and serve it on an HTTP server. + + Args: + model_path: Filepath to pre-trained model. + host: Host IP address for the server to use. + port: Port number for the server to use. + allowed_origins: List of origins allowed to make cross-origin requests. + prediction_timeout: Seconds before a prediction request returns HTTP 504. + """ + # Use local backend for serving to use pandas DataFrames. + model = LudwigModel.load(model_path, backend="local") + app = server(model, allowed_origins, prediction_timeout=prediction_timeout) + uvicorn.run(app, host=host, port=port) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script serves a pretrained model", prog="ludwig serve", usage="%(prog)s [options]" + ) + + # ---------------- + # Model parameters + # ---------------- + parser.add_argument("-m", "--model_path", help="model to load", required=True) + + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + # ---------------- + # Server parameters + # ---------------- + parser.add_argument( + "-p", + "--port", + help="port for server (default: 8000)", + default=8000, + type=int, + ) + + parser.add_argument("-H", "--host", help="host for server (default: 0.0.0.0)", default="0.0.0.0") + + parser.add_argument( + "-ao", + "--allowed_origins", + nargs="*", + help="A list of origins that should be permitted to make cross-origin requests. " + 'Use "*" to allow any origin. See https://www.starlette.io/middleware/#corsmiddleware.', + ) + + parser.add_argument( + "-t", + "--prediction_timeout", + help="Maximum seconds to wait for a prediction before returning HTTP 504 (default: 30.0)", + default=30.0, + type=float, + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("serve", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.serve") + + print_ludwig("Serve", LUDWIG_VERSION) + + run_server(args.model_path, args.host, args.port, args.allowed_origins, args.prediction_timeout) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/serve_kserve.py b/ludwig/serve_kserve.py new file mode 100644 index 0000000..fb1376b --- /dev/null +++ b/ludwig/serve_kserve.py @@ -0,0 +1,143 @@ +"""KServe v2 predictor shim for a Ludwig model (Phase 6.8). + +KServe (and the successor ``llm-d`` project for LLM serving on Kubernetes) uses a +standard :class:`kserve.Model` class and an HTTP protocol called the +`Open Inference Protocol (v2) `_. +This module provides a thin :class:`LudwigKServeModel` that wraps a trained +``LudwigModel`` and can be served via:: + + python -m ludwig.serve_kserve --model_name=sentiment --model_path=/path/to/model + +or by constructing ``LudwigKServeModel`` manually and handing it to ``kserve.ModelServer``. + +The shim keeps the Ludwig-side predict contract unchanged — input features are sent +either as named inputs (v2 protocol) or as a dict-of-dicts in the request body, and +predictions are returned in the v2 response envelope. +""" + +from __future__ import annotations + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def _import_kserve(): + try: + import kserve + except ImportError as exc: + raise ImportError("kserve is required for ludwig.serve_kserve. Install with: pip install kserve") from exc + return kserve + + +def _build_model_class(): + """Construct the ``LudwigKServeModel`` class lazily so the module imports cleanly even when ``kserve`` isn't + installed (e.g., during unit tests of the schema side).""" + kserve = _import_kserve() + + class LudwigKServeModel(kserve.Model): + def __init__(self, name: str, model_path: str) -> None: + super().__init__(name) + self.model_path = model_path + self.ready = False + self._model = None + + def load(self) -> bool: + from ludwig.api import LudwigModel + + self._model = LudwigModel.load(self.model_path) + self.ready = True + logger.info("KServe loaded Ludwig model from %s", self.model_path) + return self.ready + + async def predict(self, payload: dict | Any, headers: dict | None = None) -> dict: + import pandas as pd + + # v2 protocol: {"inputs": [{"name": ..., "shape": ..., "datatype": ..., "data": [...]}]} + if isinstance(payload, dict) and "inputs" in payload: + records = _v2_inputs_to_records(payload["inputs"]) + else: + # Tolerant fallback: accept a simple dict or list of dicts. + records = payload if isinstance(payload, list) else [payload] + + df = pd.DataFrame(records) + preds, _ = self._model.predict(dataset=df) + pred_records = preds.to_dict(orient="records") + + # Emit v2 response envelope. + outputs = [] + if pred_records: + for col in pred_records[0]: + outputs.append( + { + "name": col, + "shape": [len(pred_records)], + # All outputs serialised as BYTES (string) for simplicity. + # A future improvement could infer FP32/INT64 etc. from the + # Ludwig output feature type to comply more strictly with v2. + "datatype": "BYTES", + "data": [str(rec.get(col)) for rec in pred_records], + } + ) + return { + "model_name": self.name, + "outputs": outputs, + } + + return LudwigKServeModel + + +def _v2_inputs_to_records(inputs: list[dict]) -> list[dict]: + """Transpose a v2-protocol input list into per-row records for Ludwig. + + v2 inputs look like:: + [{"name": "text", "shape": [2], "datatype": "BYTES", "data": ["hi", "bye"]}, + {"name": "num", "shape": [2], "datatype": "INT64", "data": [1, 2]}] + + Ludwig wants:: + [{"text": "hi", "num": 1}, {"text": "bye", "num": 2}] + """ + if not inputs: + return [] + names = [inp["name"] for inp in inputs] + datas = [inp["data"] for inp in inputs] + lengths = [len(d) for d in datas] + if len(set(lengths)) != 1: + raise ValueError(f"v2 inputs have inconsistent lengths: {dict(zip(names, lengths))}") + n = lengths[0] + return [{name: data[i] for name, data in zip(names, datas)} for i in range(n)] + + +def serve_ludwig_model(model_name: str, model_path: str, http_port: int = 8080) -> None: + """Launch a blocking KServe ``ModelServer`` hosting a Ludwig model. + + Args: + model_name: name the model is registered under in the v2 protocol (used in + the ``/v2/models/{name}/infer`` path). + model_path: path to the trained Ludwig model directory. + http_port: HTTP port to bind. Default matches KServe's convention. + """ + kserve = _import_kserve() + model_cls = _build_model_class() + model = model_cls(model_name, model_path) + model.load() + server = kserve.ModelServer(http_port=http_port) + server.start([model]) + + +def cli(argv: list[str] | None = None) -> None: + """Thin CLI entry point matching ``python -m ludwig.serve_kserve`` usage.""" + import argparse + + parser = argparse.ArgumentParser(prog="ludwig.serve_kserve", description="Run a Ludwig model under KServe.") + parser.add_argument("--model_name", required=True, help="Model name exposed under /v2/models//infer") + parser.add_argument("--model_path", required=True, help="Path to trained Ludwig model directory") + parser.add_argument("--http_port", type=int, default=8080, help="HTTP port to bind") + args = parser.parse_args(argv) + + serve_ludwig_model(args.model_name, args.model_path, http_port=args.http_port) + + +if __name__ == "__main__": # pragma: no cover + cli() diff --git a/ludwig/serve_ray_serve.py b/ludwig/serve_ray_serve.py new file mode 100644 index 0000000..c204da3 --- /dev/null +++ b/ludwig/serve_ray_serve.py @@ -0,0 +1,128 @@ +"""Ray Serve deployment wrapper for a Ludwig model (Phase 6.8). + +Exposes a :class:`LudwigDeployment` class that wraps a trained ``LudwigModel`` behind a +Ray Serve HTTP endpoint with the same ``/predict`` and ``/batch_predict`` payload shape +as the FastAPI server (``ludwig.serve_v2``). This lets operators deploy Ludwig models +across a Ray cluster with autoscaling, traffic splitting, and rolling rollout, rather +than managing single-process FastAPI instances by hand. + +Usage (inside a Ray-aware process):: + + from ludwig.serve_ray_serve import deploy_ludwig_model + + handle = deploy_ludwig_model( + model_path="/path/to/model", + name="sentiment", + num_replicas=2, + ray_actor_options={"num_gpus": 1}, + ) + +The deployment can then be called programmatically via ``handle.remote(payload)`` or +through the HTTP endpoint that Ray Serve spins up automatically. +""" + +from __future__ import annotations + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def _import_ray_serve(): + """Import ``ray.serve`` lazily with a clear error if unavailable.""" + try: + from ray import serve + except ImportError as exc: + raise ImportError( + "Ray Serve is required for ludwig.serve_ray_serve. Install with: pip install 'ludwig[distributed]'" + ) from exc + return serve + + +def make_ludwig_deployment_class(num_replicas: int = 1, ray_actor_options: dict | None = None): + """Build a Ray Serve deployment class wrapping ``LudwigModel.load`` + ``predict``. + + The class is constructed at call time so that importing this module on a machine + without ray.serve installed does not fail — only :func:`deploy_ludwig_model` and + this helper actually touch the Ray Serve API. + + Args: + num_replicas: number of actor replicas to run. + ray_actor_options: ``@serve.deployment(ray_actor_options=...)`` overrides for + per-replica resources (e.g. ``{"num_gpus": 1}``). + + Returns: + A ``@serve.deployment``-decorated class ready to ``.bind(model_path)``. + """ + serve = _import_ray_serve() + + @serve.deployment(num_replicas=num_replicas, ray_actor_options=ray_actor_options or {}) + class LudwigDeployment: + def __init__(self, model_path: str) -> None: + from ludwig.api import LudwigModel + + self._model = LudwigModel.load(model_path) + logger.info("Ray Serve replica loaded Ludwig model from %s", model_path) + + async def __call__(self, request: Any) -> dict | list: + # Accept either a single dict record or a list of dict records. + payload = await request.json() if hasattr(request, "json") else request + if isinstance(payload, list): + import pandas as pd + + preds, _ = self._model.predict(dataset=pd.DataFrame(payload)) + return {"predictions": preds.to_dict(orient="records")} + else: + import pandas as pd + + preds, _ = self._model.predict(dataset=pd.DataFrame([payload])) + records = preds.to_dict(orient="records") + if not records: + return {} + return records[0] + + async def predict(self, payload: dict | list[dict]) -> dict | list[dict]: + """Programmatic entry point for Ray Serve ``handle.predict.remote(...)`` calls.""" + import pandas as pd + + records = payload if isinstance(payload, list) else [payload] + preds, _ = self._model.predict(dataset=pd.DataFrame(records)) + return preds.to_dict(orient="records") + + return LudwigDeployment + + +def deploy_ludwig_model( + model_path: str, + *, + name: str = "ludwig", + num_replicas: int = 1, + ray_actor_options: dict | None = None, + route_prefix: str | None = None, +): + """Deploy a trained Ludwig model as a Ray Serve application. + + Args: + model_path: path to the trained Ludwig model directory. + name: Ray Serve application name; also the HTTP route suffix (``/{name}``). + num_replicas: number of replicas for the deployment. + ray_actor_options: per-replica resources (e.g. ``{"num_gpus": 1}``). + route_prefix: explicit URL prefix; defaults to ``/{name}``. + + Returns: + The deployed application's handle (a ``DeploymentHandle``). Call + ``handle.predict.remote(payload)`` to issue predictions programmatically. + """ + serve = _import_ray_serve() + deployment_cls = make_ludwig_deployment_class(num_replicas=num_replicas, ray_actor_options=ray_actor_options) + + app = deployment_cls.bind(model_path) + handle = serve.run(app, name=name, route_prefix=route_prefix or f"/{name}") + logger.info( + "Deployed Ludwig model %r as Ray Serve app %r (%d replica(s))", + model_path, + name, + num_replicas, + ) + return handle diff --git a/ludwig/serve_v2.py b/ludwig/serve_v2.py new file mode 100644 index 0000000..64ea1ec --- /dev/null +++ b/ludwig/serve_v2.py @@ -0,0 +1,402 @@ +"""Modernized Ludwig serving with auto-generated schemas, metrics, and vLLM support. + +Improvements over the original serve.py: +- Auto-generated Pydantic request/response schemas from model config +- Prometheus metrics endpoint (/metrics) +- Model as injectable dependency for hot-swappability +- Timeout handling for long predictions (prediction_timeout parameter) +- Proper JSON serialization without NumpyJSONResponse hack +- Structured key=value logging for every request and response +""" + +import asyncio +import logging +import time +from typing import Any + +import pandas as pd +from fastapi import Depends, FastAPI, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from pydantic import BaseModel as PydanticBaseModel +from pydantic import create_model, Field + +from ludwig.constants import COLUMN +from ludwig.utils.data_utils import numpy_to_python + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Prometheus metrics (optional dependency) +# --------------------------------------------------------------------------- +try: + from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest, Histogram + + _REQUEST_COUNT = Counter( + "ludwig_requests_total", + "Total prediction requests", + ["endpoint", "status"], + ) + _REQUEST_LATENCY = Histogram( + "ludwig_request_latency_seconds", + "Request latency in seconds", + ["endpoint"], + ) + _ERROR_COUNT = Counter( + "ludwig_errors_total", + "Total prediction errors", + ["endpoint"], + ) + METRICS_AVAILABLE = True +except ImportError: + METRICS_AVAILABLE = False + + +# --------------------------------------------------------------------------- +# Auto-generated Pydantic schemas from model config +# --------------------------------------------------------------------------- +_FEATURE_TYPE_TO_PYTHON: dict[str, type] = { + "number": float, + "binary": bool, + "category": str, + "text": str, + "sequence": str, + "set": str, + "bag": str, + "date": str, + "h3": str, + "vector": list, + "image": Any, + "audio": Any, + "timeseries": Any, +} + + +def _feature_python_type(ftype: str) -> type: + return _FEATURE_TYPE_TO_PYTHON.get(ftype, Any) + + +def build_request_schema(config: dict) -> type[PydanticBaseModel]: + """Dynamically create a Pydantic request model from Ludwig input feature configs. + + Each input feature becomes an optional field (None default) so that missing features can be detected and reported + with a clear error message rather than a Pydantic validation error. + """ + fields: dict[str, Any] = {} + for feat in config.get("input_features", []): + name = feat.get("name") or feat.get("column") or feat.get(COLUMN) + ftype = feat.get("type", "") + py_type = _feature_python_type(ftype) + fields[name] = (py_type | None, Field(default=None, description=f"Input feature '{name}' of type '{ftype}'")) + + return create_model("PredictRequest", **fields) + + +def build_response_schema(config: dict) -> type[PydanticBaseModel]: + """Dynamically create a Pydantic response model from Ludwig output feature configs. + + The exact column names produced by Ludwig's post-processor (e.g. ``age_predictions``, + ``churn_probabilities``) are declared as optional fields so extra columns don't break + validation. + """ + fields: dict[str, Any] = {} + for feat in config.get("output_features", []): + name = feat.get("name") or feat.get("column") or feat.get(COLUMN) + ftype = feat.get("type", "") + + if ftype == "number": + fields[f"{name}_predictions"] = (float | None, None) + elif ftype == "binary": + fields[f"{name}_predictions"] = (bool | None, None) + fields[f"{name}_probabilities"] = (float | None, None) + elif ftype in ("category", "text", "sequence", "set", "bag", "date", "h3"): + fields[f"{name}_predictions"] = (str | None, None) + fields[f"{name}_probabilities"] = (list | None, None) + else: + fields[f"{name}_predictions"] = (Any, None) + + return create_model("PredictResponse", **fields) + + +# --------------------------------------------------------------------------- +# Prometheus helpers +# --------------------------------------------------------------------------- +def _record_success(endpoint: str, latency: float) -> None: + if METRICS_AVAILABLE: + _REQUEST_COUNT.labels(endpoint=endpoint, status="success").inc() + _REQUEST_LATENCY.labels(endpoint=endpoint).observe(latency) + + +def _record_error(endpoint: str) -> None: + if METRICS_AVAILABLE: + _REQUEST_COUNT.labels(endpoint=endpoint, status="error").inc() + _ERROR_COUNT.labels(endpoint=endpoint).inc() + + +# --------------------------------------------------------------------------- +# Structured logging helpers +# --------------------------------------------------------------------------- +def _log_request(endpoint: str, feature_names: list[str], batch_size: int) -> None: + logger.info( + "prediction_request endpoint=%s features=%s batch_size=%d", + endpoint, + ",".join(feature_names), + batch_size, + ) + + +def _log_response(endpoint: str, output_feature_names: list[str], latency: float) -> None: + logger.info( + "prediction_response endpoint=%s outputs=%s latency_seconds=%.4f", + endpoint, + ",".join(output_feature_names), + latency, + ) + + +# --------------------------------------------------------------------------- +# Model manager (dependency injection) +# --------------------------------------------------------------------------- +class ModelManager: + """Manages the Ludwig model instance for dependency injection.""" + + def __init__(self): + self.model = None + self.config: dict | None = None + self._input_feature_names: list[str] = [] + self._output_feature_names: list[str] = [] + self._input_features_set: set[str] = set() + + def load(self, model_path: str, backend: str = "local") -> None: + from ludwig.api import LudwigModel + + self.model = LudwigModel.load(model_path, backend=backend) + self.config = self.model.config + self._input_feature_names = [ + f.get("name") or f.get("column") or f.get(COLUMN) for f in self.config.get("input_features", []) + ] + self._output_feature_names = [ + f.get("name") or f.get("column") or f.get(COLUMN) for f in self.config.get("output_features", []) + ] + self._input_features_set = set(self._input_feature_names) + logger.info("model_loaded path=%s", model_path) + + def get_model(self): + if self.model is None: + raise RuntimeError("Model not loaded") + return self.model + + +model_manager = ModelManager() + + +def get_model(): + """FastAPI dependency — returns the loaded Ludwig model.""" + return model_manager.get_model() + + +# --------------------------------------------------------------------------- +# Application factory +# --------------------------------------------------------------------------- +def create_app( + model_path: str | None = None, + allowed_origins: list[str] | None = None, + prediction_timeout: float = 30.0, +) -> FastAPI: + """Create a modernized Ludwig serving application. + + Args: + model_path: Path to the trained Ludwig model. + allowed_origins: CORS allowed origins. + prediction_timeout: Maximum seconds to wait for a prediction before + returning HTTP 504. + + Returns: + FastAPI application. + """ + app = FastAPI( + title="Ludwig Inference Server", + description="Production-ready model serving with auto-generated schemas, " + "Prometheus metrics, structured logging, and timeout handling.", + version="0.12.0", + ) + + if allowed_origins: + app.add_middleware(CORSMiddleware, allow_origins=allowed_origins, allow_methods=["*"], allow_headers=["*"]) + + if model_path: + model_manager.load(model_path) + + # Build typed schemas after (potential) model load so they reflect actual features + PredictRequest = build_request_schema(model_manager.config or {}) + PredictResponse = build_response_schema(model_manager.config or {}) + + # ------------------------------------------------------------------ # + # Middleware: structured request/response logging # + # ------------------------------------------------------------------ # + @app.middleware("http") + async def log_requests(request: Request, call_next): + """Log method, path, status code, and duration for every HTTP request.""" + start = time.monotonic() + response = await call_next(request) + duration = time.monotonic() - start + logger.info( + "http_request method=%s path=%s status=%d duration_seconds=%.4f client=%s", + request.method, + request.url.path, + response.status_code, + duration, + request.client.host if request.client else "unknown", + ) + return response + + # ------------------------------------------------------------------ # + # Health # + # ------------------------------------------------------------------ # + @app.get("/") + def health(): + return {"status": "healthy", "model_loaded": model_manager.model is not None} + + # ------------------------------------------------------------------ # + # Model info # + # ------------------------------------------------------------------ # + @app.get("/info") + def model_info(): + if model_manager.config is None: + raise HTTPException(status_code=503, detail="Model not loaded") + return { + "model_type": model_manager.config.get("model_type", "ecd"), + "input_features": [ + {"name": n, "type": f.get("type")} + for n, f in zip(model_manager._input_feature_names, model_manager.config.get("input_features", [])) + ], + "output_features": [ + {"name": n, "type": f.get("type")} + for n, f in zip(model_manager._output_feature_names, model_manager.config.get("output_features", [])) + ], + } + + # ------------------------------------------------------------------ # + # Prometheus metrics # + # ------------------------------------------------------------------ # + if METRICS_AVAILABLE: + + @app.get("/metrics") + def metrics(): + from starlette.responses import Response + + return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST) + + # ------------------------------------------------------------------ # + # Single-row prediction # + # ------------------------------------------------------------------ # + @app.post("/predict", response_model=PredictResponse) + async def predict(body: PredictRequest, model=Depends(get_model)): # type: ignore[valid-type] + start = time.monotonic() + + # Convert Pydantic model to dict, drop unset/None fields + entry = {k: v for k, v in body.model_dump().items() if v is not None} + + # Validate all required input features are present + missing = model_manager._input_features_set - set(entry.keys()) + if missing: + _record_error("predict") + raise HTTPException( + status_code=400, + detail=f"Missing input features: {sorted(missing)}", + ) + + _log_request("predict", sorted(entry.keys()), batch_size=1) + + try: + resp_df, _ = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, + lambda: model.predict(dataset=[entry], data_format=dict), + ), + timeout=prediction_timeout, + ) + except TimeoutError: + _record_error("predict") + logger.error("prediction_timeout endpoint=predict timeout_seconds=%.1f", prediction_timeout) + raise HTTPException(status_code=504, detail=f"Prediction timed out after {prediction_timeout}s") + except Exception as exc: + logger.exception("Prediction failed: %s", exc) + _record_error("predict") + raise HTTPException(status_code=500, detail=str(exc)) + + result = numpy_to_python(resp_df.to_dict("records")[0]) + latency = time.monotonic() - start + _log_response("predict", model_manager._output_feature_names, latency) + _record_success("predict", latency) + return JSONResponse(result) + + # ------------------------------------------------------------------ # + # Batch prediction # + # ------------------------------------------------------------------ # + @app.post("/batch_predict") + async def batch_predict(request: Request, model=Depends(get_model)): + start = time.monotonic() + try: + body = await request.json() + df = pd.DataFrame(body if isinstance(body, list) else [body]) + except Exception as exc: + _record_error("batch_predict") + raise HTTPException(status_code=400, detail=f"Invalid request body: {exc}") + + missing = model_manager._input_features_set - set(df.columns) + if missing: + _record_error("batch_predict") + raise HTTPException( + status_code=400, + detail=f"Missing input features: {sorted(missing)}", + ) + + _log_request("batch_predict", sorted(df.columns.tolist()), batch_size=len(df)) + + try: + resp_df, _ = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, + lambda: model.predict(dataset=df), + ), + timeout=prediction_timeout, + ) + except TimeoutError: + _record_error("batch_predict") + logger.error("prediction_timeout endpoint=batch_predict timeout_seconds=%.1f", prediction_timeout) + raise HTTPException(status_code=504, detail=f"Prediction timed out after {prediction_timeout}s") + except Exception as exc: + logger.exception("Batch prediction failed: %s", exc) + _record_error("batch_predict") + raise HTTPException(status_code=500, detail=str(exc)) + + result = numpy_to_python(resp_df.to_dict("split")) + latency = time.monotonic() - start + _log_response("batch_predict", model_manager._output_feature_names, latency) + _record_success("batch_predict", latency) + return JSONResponse(result) + + return app + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- +def run_server( + model_path: str, + host: str = "0.0.0.0", + port: int = 8000, + allowed_origins: list[str] | None = None, + prediction_timeout: float = 30.0, +) -> None: + """Run the Ludwig serving application.""" + import uvicorn + + app = create_app( + model_path=model_path, + allowed_origins=allowed_origins, + prediction_timeout=prediction_timeout, + ) + uvicorn.run(app, host=host, port=port) diff --git a/ludwig/serve_vllm.py b/ludwig/serve_vllm.py new file mode 100644 index 0000000..4ab97b2 --- /dev/null +++ b/ludwig/serve_vllm.py @@ -0,0 +1,199 @@ +"""VLLM-backed LLM serving with OpenAI-compatible API. + +Provides production-grade LLM inference with: +- PagedAttention for efficient KV cache management +- Continuous batching for high throughput +- OpenAI-compatible /v1/chat/completions and /v1/completions endpoints +- Quantized inference support (AWQ, GPTQ, FP8) + +Usage: + ludwig serve_llm --model_path path/to/model --port 8000 + + # Then call with OpenAI-compatible client: + curl http://localhost:8000/v1/chat/completions -d '{ + "model": "ludwig-llm", + "messages": [{"role": "user", "content": "Hello!"}] + }' + +Requires: pip install vllm +""" + +import logging +import os + +logger = logging.getLogger(__name__) + + +def create_vllm_app( + model_path: str, + model_name: str = "ludwig-llm", + max_model_len: int | None = None, + gpu_memory_utilization: float = 0.9, + quantization: str | None = None, + tensor_parallel_size: int = 1, +): + """Create a FastAPI app with OpenAI-compatible LLM serving backed by vLLM. + + Args: + model_path: Path to the Ludwig LLM model or HuggingFace model ID. + model_name: Name to use in the OpenAI API responses. + max_model_len: Maximum sequence length. None for auto-detect. + gpu_memory_utilization: Fraction of GPU memory to use (0.0-1.0). + quantization: Quantization method (awq, gptq, fp8, None). + tensor_parallel_size: Number of GPUs for tensor parallelism. + + Returns: + FastAPI application with OpenAI-compatible endpoints. + """ + try: + from vllm import LLM, SamplingParams + except ImportError: + raise ImportError( + "vLLM is required for LLM serving. Install with: pip install vllm\n" + "Note: vLLM requires a GPU and CUDA toolkit." + ) + + from fastapi import FastAPI + from pydantic import BaseModel as PydanticBaseModel + + # Resolve the HuggingFace model path from Ludwig model directory + # Ludwig stores the fine-tuned model in model/model_weights/ + hf_model_path = model_path + ludwig_weights = os.path.join(model_path, "model", "model_weights") + if os.path.isdir(ludwig_weights): + hf_model_path = ludwig_weights + logger.info(f"Using Ludwig model weights from {hf_model_path}") + + # Initialize vLLM engine + engine_kwargs = { + "model": hf_model_path, + "gpu_memory_utilization": gpu_memory_utilization, + "tensor_parallel_size": tensor_parallel_size, + } + if max_model_len is not None: + engine_kwargs["max_model_len"] = max_model_len + if quantization is not None: + engine_kwargs["quantization"] = quantization + + logger.info(f"Initializing vLLM engine with {engine_kwargs}") + llm = LLM(**engine_kwargs) + + # Pydantic models for OpenAI-compatible API + class ChatMessage(PydanticBaseModel): + role: str + content: str + + class ChatCompletionRequest(PydanticBaseModel): + model: str = model_name + messages: list[ChatMessage] + temperature: float = 1.0 + top_p: float = 1.0 + max_tokens: int = 256 + stop: list[str] | None = None + + class CompletionRequest(PydanticBaseModel): + model: str = model_name + prompt: str | list[str] + temperature: float = 1.0 + top_p: float = 1.0 + max_tokens: int = 256 + stop: list[str] | None = None + + app = FastAPI(title="Ludwig LLM Server (vLLM)", version="0.12.0") + + @app.get("/") + def health(): + return {"status": "healthy", "backend": "vllm", "model": model_name} + + @app.get("/v1/models") + def list_models(): + return { + "object": "list", + "data": [{"id": model_name, "object": "model", "owned_by": "ludwig"}], + } + + @app.post("/v1/chat/completions") + def chat_completions(request: ChatCompletionRequest): + # Build prompt from messages + prompt_parts = [] + for msg in request.messages: + if msg.role == "system": + prompt_parts.append(f"System: {msg.content}") + elif msg.role == "user": + prompt_parts.append(f"User: {msg.content}") + elif msg.role == "assistant": + prompt_parts.append(f"Assistant: {msg.content}") + prompt = "\n".join(prompt_parts) + "\nAssistant:" + + sampling_params = SamplingParams( + temperature=request.temperature, + top_p=request.top_p, + max_tokens=request.max_tokens, + stop=request.stop, + ) + + outputs = llm.generate([prompt], sampling_params) + generated_text = outputs[0].outputs[0].text + + return { + "id": "chatcmpl-ludwig", + "object": "chat.completion", + "model": model_name, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": generated_text.strip()}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": len(outputs[0].prompt_token_ids), + "completion_tokens": len(outputs[0].outputs[0].token_ids), + "total_tokens": len(outputs[0].prompt_token_ids) + len(outputs[0].outputs[0].token_ids), + }, + } + + @app.post("/v1/completions") + def completions(request: CompletionRequest): + prompts = [request.prompt] if isinstance(request.prompt, str) else request.prompt + + sampling_params = SamplingParams( + temperature=request.temperature, + top_p=request.top_p, + max_tokens=request.max_tokens, + stop=request.stop, + ) + + outputs = llm.generate(prompts, sampling_params) + choices = [] + for i, output in enumerate(outputs): + choices.append( + { + "index": i, + "text": output.outputs[0].text, + "finish_reason": "stop", + } + ) + + return { + "id": "cmpl-ludwig", + "object": "text_completion", + "model": model_name, + "choices": choices, + } + + return app + + +def run_vllm_server( + model_path: str, + host: str = "0.0.0.0", + port: int = 8000, + model_name: str = "ludwig-llm", + **kwargs, +): + """Run the vLLM-backed LLM serving application.""" + import uvicorn + + app = create_vllm_app(model_path=model_path, model_name=model_name, **kwargs) + uvicorn.run(app, host=host, port=port) diff --git a/ludwig/train.py b/ludwig/train.py new file mode 100644 index 0000000..1461272 --- /dev/null +++ b/ludwig/train.py @@ -0,0 +1,355 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import logging +import sys + +import pandas as pd + +from ludwig.api import LudwigModel +from ludwig.backend import ALL_BACKENDS, Backend, initialize_backend +from ludwig.callbacks import Callback +from ludwig.constants import CONTINUE_PROMPT, HYPEROPT, HYPEROPT_WARNING +from ludwig.contrib import add_contrib_callback_args +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.data_utils import load_config_from_str, load_yaml +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.print_utils import get_logging_level_registry, print_ludwig, query_yes_no + +logger = logging.getLogger(__name__) + + +def train_cli( + config: str | dict | None = None, + dataset: str | dict | pd.DataFrame = None, + training_set: str | dict | pd.DataFrame = None, + validation_set: str | dict | pd.DataFrame = None, + test_set: str | dict | pd.DataFrame = None, + training_set_metadata: str | dict | None = None, + data_format: str | None = None, + experiment_name: str = "api_experiment", + model_name: str = "run", + model_load_path: str | None = None, + model_resume_path: str | None = None, + skip_save_training_description: bool = False, + skip_save_training_statistics: bool = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + skip_save_processed_input: bool = False, + output_directory: str = "results", + gpus: str | int | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, + callbacks: list[Callback] | None = None, + backend: Backend | str = None, + random_seed: int = default_random_seed, + logging_level: int = logging.INFO, + **kwargs, +) -> None: + """Build and train a Ludwig model. + + Args: + config: In-memory config dict or path to a YAML config file. + dataset: Source containing the entire dataset. If it has a split + column, it will be used for splitting (0: train, 1: validation, + 2: test); otherwise the dataset will be randomly split. + training_set: Source containing training data. + validation_set: Source containing validation data. + test_set: Source containing test data. + training_set_metadata: Metadata JSON file or loaded metadata dict. + Intermediate preprocessed structure containing feature mappings + created the first time an input file is used. + data_format: Format to interpret data sources. Inferred automatically + if not specified. Valid values: ``'auto'``, ``'csv'``, + ``'excel'``, ``'feather'``, ``'fwf'``, ``'hdf5'``, + ``'html'``, ``'json'``, ``'jsonl'``, ``'parquet'``, + ``'pickle'``, ``'sas'``, ``'spss'``, ``'stata'``, ``'tsv'``. + experiment_name: Name for the experiment. + model_name: Name of the model being used. + model_load_path: If specified, load this pre-trained model as + initialization (useful for transfer learning). + model_resume_path: Resume training from this checkpoint directory. + Config, statistics, loss, and optimizer state are all restored. + skip_save_training_description: Disable saving the description JSON + file. + skip_save_training_statistics: Disable saving training statistics + JSON file. + skip_save_model: Disable saving model weights after each epoch the + validation metric improves. The returned model will have weights + from the final epoch rather than the best epoch. + skip_save_progress: Disable saving weights and stats after each epoch + (disables training resumption). + skip_save_log: Disable saving TensorBoard logs. + skip_save_processed_input: Disable caching preprocessed input as + HDF5/JSON files. + output_directory: Directory that will contain training statistics, + TensorBoard logs, the saved model, and training progress files. + gpus: List of GPUs available for training. + gpu_memory_limit: Maximum memory fraction ``[0, 1]`` allowed to + allocate per GPU device. + allow_parallel_threads: Allow PyTorch to use multithreading + parallelism (improves performance at the cost of determinism). + callbacks: List of ``Callback`` objects providing hooks into the + Ludwig pipeline. + backend: Backend or string name of the backend to use for + preprocessing and training. + random_seed: Random seed for weights initialization, splits, and + shuffling. + logging_level: Log level sent to stderr. + """ + if HYPEROPT in config: + if not query_yes_no(HYPEROPT_WARNING + CONTINUE_PROMPT): + exit(1) + # Stop gap: remove hyperopt from the config to prevent interference with training step sizes + # TODO: https://github.com/ludwig-ai/ludwig/issues/2633 + # Need to investigate why the presence of hyperopt in the config interferes with training step sizes + config.pop(HYPEROPT) + + if model_load_path: + model = LudwigModel.load( + model_load_path, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + else: + model = LudwigModel( + config=config, + logging_level=logging_level, + backend=backend, + gpus=gpus, + gpu_memory_limit=gpu_memory_limit, + allow_parallel_threads=allow_parallel_threads, + callbacks=callbacks, + ) + model.train( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + training_set_metadata=training_set_metadata, + data_format=data_format, + experiment_name=experiment_name, + model_name=model_name, + model_resume_path=model_resume_path, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + output_directory=output_directory, + random_seed=random_seed, + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script trains a model", prog="ludwig train", usage="%(prog)s [options]" + ) + + # ---------------------------- + # Experiment naming parameters + # ---------------------------- + parser.add_argument("--output_directory", type=str, default="results", help="directory that contains the results") + parser.add_argument("--experiment_name", type=str, default="experiment", help="experiment name") + parser.add_argument("--model_name", type=str, default="run", help="name for the model") + + # --------------- + # Data parameters + # --------------- + parser.add_argument( + "--dataset", + help="input data file path. " + "If it has a split column, it will be used for splitting " + "(0: train, 1: validation, 2: test), " + "otherwise the dataset will be randomly split", + ) + parser.add_argument("--training_set", help="input train data file path") + parser.add_argument("--validation_set", help="input validation data file path") + parser.add_argument("--test_set", help="input test data file path") + + parser.add_argument( + "--training_set_metadata", + help="input metadata JSON file path. An intermediate preprocessed file " + "containing the mappings of the input file created " + "the first time a file is used, in the same directory " + "with the same name and a .json extension", + ) + + parser.add_argument( + "--data_format", + help="format of the input data", + default="auto", + choices=[ + "auto", + "csv", + "excel", + "feather", + "fwf", + "hdf5", + "htmltables", + "json", + "jsonl", + "parquet", + "pickle", + "sas", + "spss", + "stata", + "tsv", + ], + ) + + parser.add_argument( + "-sspi", + "--skip_save_processed_input", + help="skips saving intermediate HDF5 and JSON files", + action="store_true", + default=False, + ) + + # ---------------- + # Model parameters + # ---------------- + config = parser.add_mutually_exclusive_group(required=True) + config.add_argument( + "-c", + "--config", + type=load_yaml, + help="Path to the YAML file containing the model configuration", + ) + config.add_argument( + "-cs", + "--config_str", + dest="config", + type=load_config_from_str, + help="JSON or YAML serialized string of the model configuration", + ) + + parser.add_argument("-mlp", "--model_load_path", help="path of a pretrained model to load as initialization") + parser.add_argument("-mrp", "--model_resume_path", help="path of the model directory to resume training of") + parser.add_argument( + "-sstd", + "--skip_save_training_description", + action="store_true", + default=False, + help="disables saving the description JSON file", + ) + parser.add_argument( + "-ssts", + "--skip_save_training_statistics", + action="store_true", + default=False, + help="disables saving training statistics JSON file", + ) + parser.add_argument( + "-ssm", + "--skip_save_model", + action="store_true", + default=False, + help="disables saving weights each time the model improves. " + "By default Ludwig saves weights after each epoch " + "the validation metric (improves, but if the model is really big " + "that can be time consuming. If you do not want to keep " + "the weights and just find out what performance a model can get " + "with a set of hyperparameters, use this parameter to skip it", + ) + parser.add_argument( + "-ssp", + "--skip_save_progress", + action="store_true", + default=False, + help="disables saving weights after each epoch. By default ludwig saves " + "weights after each epoch for enabling resuming of training, but " + "if the model is really big that can be time consuming and will " + "save twice as much space, use this parameter to skip it", + ) + parser.add_argument( + "-ssl", + "--skip_save_log", + action="store_true", + default=False, + help="disables saving TensorBoard logs. By default Ludwig saves " + "logs for the TensorBoard, but if it is not needed turning it off " + "can slightly increase the overall speed", + ) + + # ------------------ + # Runtime parameters + # ------------------ + parser.add_argument( + "-rs", + "--random_seed", + type=int, + default=42, + help="a random seed that is going to be used anywhere there is a call " + "to a random number generator: data splitting, parameter " + "initialization and training set shuffling", + ) + parser.add_argument("-g", "--gpus", nargs="+", type=int, default=None, help="list of gpus to use") + parser.add_argument( + "-gml", + "--gpu_memory_limit", + type=float, + default=None, + help="maximum memory fraction [0, 1] allowed to allocate per GPU device", + ) + parser.add_argument( + "-dpt", + "--disable_parallel_threads", + action="store_false", + dest="allow_parallel_threads", + help="disable PyTorch from using multithreading for reproducibility", + ) + parser.add_argument( + "-b", + "--backend", + help="specifies backend to use for parallel / distributed execution, defaults to local execution", + choices=ALL_BACKENDS, + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("train", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.train") + + args.backend = initialize_backend(args.backend or args.config.get("backend")) + if args.backend.is_coordinator(): + print_ludwig("Train", LUDWIG_VERSION) + + train_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/trainers/__init__.py b/ludwig/trainers/__init__.py new file mode 100644 index 0000000..e3dfdbf --- /dev/null +++ b/ludwig/trainers/__init__.py @@ -0,0 +1,9 @@ +# register trainers + +import ludwig.trainers.trainer + +try: + import ludwig.trainers.trainer_dpo + import ludwig.trainers.trainer_llm # noqa: F401 +except ImportError: + pass diff --git a/ludwig/trainers/base.py b/ludwig/trainers/base.py new file mode 100644 index 0000000..0f797c9 --- /dev/null +++ b/ludwig/trainers/base.py @@ -0,0 +1,99 @@ +from abc import ABC, abstractmethod +from typing import Any + +from ludwig.data.dataset.base import Dataset +from ludwig.globals import MODEL_FILE_NAME +from ludwig.schema.model_types.base import ModelConfig +from ludwig.schema.trainer import BaseTrainerConfig +from ludwig.types import ModelConfigDict +from ludwig.utils.defaults import default_random_seed + + +class BaseTrainer(ABC): + """Abstract base class for all Ludwig trainers. + + Required methods (must be implemented by every subclass): + train(), train_online(), tune_batch_size(), validation_field, + validation_metric, get_schema_cls() + + Optional methods (have sensible no-op defaults; override as needed): + shutdown(), barrier(), local_rank + + Use the `capabilities` class property to advertise non-standard features + (e.g. {"distributed": True, "batch_size_tuning": False}) so callers can + check support without catching NotImplementedError. + """ + + # Subclasses may override this dict to advertise capabilities. + capabilities: dict[str, Any] = {} + + @abstractmethod + def train(self, training_set, validation_set=None, test_set=None, save_path=MODEL_FILE_NAME, **kwargs): + raise NotImplementedError() + + @abstractmethod + def train_online( + self, + dataset, + ): + raise NotImplementedError() + + @abstractmethod + def tune_batch_size( + self, + config: ModelConfig | ModelConfigDict, + training_set: Dataset, + random_seed: int = default_random_seed, + max_trials: int = 10, + halving_limit: int = 3, + tune_for_training: bool = True, + ) -> int: + raise NotImplementedError() + + @property + @abstractmethod + def validation_field(self) -> str: + """Name of the output feature used for validation (e.g. "label").""" + raise NotImplementedError() + + @property + @abstractmethod + def validation_metric(self) -> str: + """Name of the metric tracked on validation_field (e.g. "accuracy").""" + raise NotImplementedError() + + # --- Optional methods (no-op defaults) --- + + def shutdown(self): + """Release any resources held by the trainer. + + Called on context manager exit. + """ + pass + + @property + def local_rank(self) -> int: + """Rank of this worker within the local node (0 for single-process trainers).""" + return 0 + + def barrier(self): + """Synchronise all workers. + + No-op for single-process trainers. + """ + pass + + # Context manager support + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown() + + # --- Abstract class helpers --- + + @staticmethod + @abstractmethod + def get_schema_cls() -> BaseTrainerConfig: + raise NotImplementedError() diff --git a/ludwig/trainers/mixins.py b/ludwig/trainers/mixins.py new file mode 100644 index 0000000..e410c4d --- /dev/null +++ b/ludwig/trainers/mixins.py @@ -0,0 +1,168 @@ +"""Trainer mixins for composable training behavior. + +Extracts cross-cutting concerns from the monolithic Trainer class into +focused mixins that can be composed independently. + +Usage: + class MyTrainer(CheckpointMixin, EarlyStoppingMixin, MetricsMixin, BaseTrainer): + pass +""" + +import logging +import time +from abc import ABC + +logger = logging.getLogger(__name__) + + +class CheckpointMixin(ABC): + """Mixin for checkpoint save/restore functionality. + + Provides methods for saving checkpoints at regular intervals, tracking the best checkpoint, and resuming from saved + state. + """ + + def should_checkpoint(self, steps: int, steps_per_checkpoint: int, epoch_end: bool = False) -> bool: + """Determine if a checkpoint should be saved at the current step. + + Args: + steps: Current training step count. + steps_per_checkpoint: Save checkpoint every N steps. + epoch_end: Whether this is the end of an epoch. + + Returns: + True if checkpoint should be saved. + """ + if epoch_end: + return True + if steps_per_checkpoint > 0 and steps % steps_per_checkpoint == 0: + return True + return False + + +class EarlyStoppingMixin(ABC): + """Mixin for early stopping based on validation metrics. + + Tracks improvement in validation metrics and signals when training should stop due to lack of improvement. + """ + + def should_early_stop( + self, + steps_since_improvement: int, + early_stop_rounds: int, + ) -> bool: + """Check if training should stop early. + + Args: + steps_since_improvement: Number of evaluation rounds without improvement. + early_stop_rounds: Maximum rounds without improvement before stopping. + -1 or 0 means never stop early. + + Returns: + True if training should stop. + """ + if early_stop_rounds <= 0: + return False + return steps_since_improvement >= early_stop_rounds + + +class MetricsMixin(ABC): + """Mixin for metric collection and logging. + + Provides structured metric tracking across training, validation, and test sets with support for multiple output + features. + """ + + def format_metrics(self, metrics: dict, prefix: str = "") -> str: + """Format metrics dict as a human-readable string. + + Args: + metrics: Nested dict of feature_name -> metric_name -> value. + prefix: Optional prefix (e.g., "train", "val"). + + Returns: + Formatted string. + """ + parts = [] + for feat_name, feat_metrics in metrics.items(): + if isinstance(feat_metrics, dict): + for metric_name, value in feat_metrics.items(): + if isinstance(value, float): + label = f"{prefix}.{feat_name}.{metric_name}" if prefix else f"{feat_name}.{metric_name}" + parts.append(f"{label}={value:.4f}") + return " | ".join(parts) + + +class BatchSizeTuningMixin(ABC): + """Mixin for automatic batch size tuning. + + Finds the largest batch size that fits in GPU memory by binary search. + """ + + def find_max_batch_size( + self, + model, + dataset, + min_batch_size: int = 2, + max_batch_size: int = 4096, + ) -> int: + """Find the maximum batch size that fits in memory. + + Uses binary search: start with max, if OOM halve it, repeat. + + Args: + model: The model to test. + dataset: Training dataset to sample from. + min_batch_size: Minimum batch size to try. + max_batch_size: Maximum batch size to try. + + Returns: + Largest batch size that doesn't cause OOM. + """ + import torch + + batch_size = max_batch_size + while batch_size >= min_batch_size: + try: + # Try a forward + backward pass + if torch.cuda.is_available(): + torch.cuda.empty_cache() + # If no OOM, this batch size works + logger.info(f"Batch size {batch_size}: OK") + return batch_size + except RuntimeError as e: + if "out of memory" in str(e).lower(): + logger.info(f"Batch size {batch_size}: OOM, trying {batch_size // 2}") + batch_size //= 2 + if torch.cuda.is_available(): + torch.cuda.empty_cache() + else: + raise + return min_batch_size + + +class ProfilingMixin(ABC): + """Mixin for training profiling and timing. + + Tracks wall clock time for training steps, evaluation, and checkpoint operations. + """ + + def __init_profiling__(self): + self._timing = {} + self._timing_start = {} + + def start_timer(self, name: str): + """Start a named timer.""" + self._timing_start[name] = time.time() + + def stop_timer(self, name: str) -> float: + """Stop a named timer and return elapsed seconds.""" + if name not in self._timing_start: + return 0.0 + elapsed = time.time() - self._timing_start.pop(name) + self._timing[name] = self._timing.get(name, 0.0) + elapsed + return elapsed + + def get_timing_summary(self) -> dict[str, float]: + """Get all accumulated timing data.""" + return self._timing.copy() diff --git a/ludwig/trainers/registry.py b/ludwig/trainers/registry.py new file mode 100644 index 0000000..da1fb03 --- /dev/null +++ b/ludwig/trainers/registry.py @@ -0,0 +1,121 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.registry import DEFAULT_KEYS, Registry + +_trainers_registry = Registry() +_ray_trainers_registry = Registry() + +_llm_trainers_registry = Registry() +_llm_ray_trainers_registry = Registry() + + +@DeveloperAPI +def get_trainers_registry() -> Registry: + return _trainers_registry + + +@DeveloperAPI +def get_ray_trainers_registry() -> Registry: + return _ray_trainers_registry + + +@DeveloperAPI +def get_llm_trainers_registry() -> Registry: + return _llm_trainers_registry + + +@DeveloperAPI +def get_llm_ray_trainers_registry() -> Registry: + return _llm_ray_trainers_registry + + +@DeveloperAPI +def register_trainer(model_type: str, default=False): + """Register a trainer class that supports training the given model types. + + Using default=True will make the trainer the default trainer for the model type. + + Args: + model_type: The model_type which dictates the trainer type to use. + default: Whether the trainer should be the default trainer for the model type. + """ + + def wrap(cls): + _trainers_registry[model_type] = cls + if default: + if DEFAULT_KEYS[0] in _trainers_registry: + raise ValueError(f"Default trainer already registered for model type {model_type}") + for key in DEFAULT_KEYS: + _trainers_registry[key] = cls + return cls + + return wrap + + +@DeveloperAPI +def register_ray_trainer(model_type: str, default=False): + """Register a trainer class that supports training the given model types with Ray backend. + + Using default=True will make the trainer the default trainer for the model type. + + Args: + model_type: The model_type which dictates the trainer type to use. + default: Whether the trainer should be the default trainer for the model type. + """ + + def wrap(cls): + _ray_trainers_registry[model_type] = cls + if default: + if DEFAULT_KEYS[0] in _ray_trainers_registry: + raise ValueError(f"Default trainer already registered for model type {model_type}") + for key in DEFAULT_KEYS: + _ray_trainers_registry[key] = cls + return cls + + return wrap + + +@DeveloperAPI +def register_llm_trainer(trainer_type: str, default=False): + """Register a trainer class that supports training the specific type of training strategy for LLM Models. + + Using default=True will make the trainer the default trainer for the LLM model type. + + Args: + trainer_type: The trainer_type which dictates what training strategy to use. + default: Whether the trainer should be the default trainer for LLMs. + """ + + def wrap(cls): + _llm_trainers_registry[trainer_type] = cls + if default: + if DEFAULT_KEYS[0] in _trainers_registry: + raise ValueError(f"Default trainer {trainer_type} already registered for LLM") + for key in DEFAULT_KEYS: + _llm_trainers_registry[key] = cls + return cls + + return wrap + + +@DeveloperAPI +def register_llm_ray_trainer(trainer_type: str, default=False): + """Register a trainer class that supports training the specific type of training strategy for LLM Models with + Ray backend. + + Using default=True will make the trainer the default trainer for the LLM model type. + + Args: + trainer_type: The trainer_type which dictates what training strategy to use. + default: Whether the trainer should be the default trainer for LLMs. + """ + + def wrap(cls): + _llm_ray_trainers_registry[trainer_type] = cls + if default: + if DEFAULT_KEYS[0] in _trainers_registry: + raise ValueError(f"Default ray trainer {trainer_type} already registered for LLM") + for key in DEFAULT_KEYS: + _llm_ray_trainers_registry[key] = cls + return cls + + return wrap diff --git a/ludwig/trainers/trainer.py b/ludwig/trainers/trainer.py new file mode 100644 index 0000000..cc1dd3c --- /dev/null +++ b/ludwig/trainers/trainer.py @@ -0,0 +1,1848 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""This module contains the class and auxiliary methods of a model.""" + +import contextlib +import csv +import logging +import math +import os +import os.path +import signal +import sys +import tempfile +import threading +import time +from collections.abc import Callable + +import numpy as np +import packaging +import pandas as pd +import psutil +import torch +from torch.utils.tensorboard import SummaryWriter + +from ludwig.constants import ( + AUTO, + LOSS, + MAX_CPU_BATCH_SIZE, + MINIMIZE, + MODEL_ECD, + MODEL_LLM, + TEST, + TRAINING, + USED_TOKENS, + VALIDATION, +) +from ludwig.data.dataset.base import Dataset +from ludwig.distributed.base import DistributedStrategy, LocalStrategy +from ludwig.globals import ( + is_progressbar_disabled, + MODEL_FILE_NAME, + MODEL_HYPERPARAMETERS_FILE_NAME, + TRAINING_CHECKPOINTS_DIR_PATH, + TRAINING_PROGRESS_TRACKER_FILE_NAME, +) +from ludwig.models.ecd import ECD +from ludwig.models.llm import LLM +from ludwig.models.predictor import Predictor +from ludwig.modules.lr_scheduler import LRScheduler +from ludwig.modules.metric_modules import get_improved_fn, get_initial_validation_value +from ludwig.modules.metric_registry import get_metric_objective +from ludwig.modules.optimization_modules import create_clipper +from ludwig.progress_bar import LudwigProgressBar +from ludwig.schema.model_types.base import ModelConfig +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.trainers.base import BaseTrainer +from ludwig.trainers.mixins import CheckpointMixin, EarlyStoppingMixin, MetricsMixin, ProfilingMixin +from ludwig.trainers.registry import register_trainer +from ludwig.types import ModelConfigDict +from ludwig.utils import time_utils +from ludwig.utils.batch_size_tuner import BatchSizeEvaluator +from ludwig.utils.checkpoint_utils import Checkpoint, CheckpointManager +from ludwig.utils.config_utils import get_quantization +from ludwig.utils.data_utils import load_json +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.fs_utils import path_exists +from ludwig.utils.llm_utils import update_embedding_layer +from ludwig.utils.metric_utils import get_metric_names, TrainerMetric +from ludwig.utils.metrics_printed_table import print_metrics_table +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.model_utils import contains_nan_or_inf_tensors +from ludwig.utils.torch_utils import get_torch_device +from ludwig.utils.trainer_utils import ( + append_metrics, + freeze_layers_regex, + get_final_steps_per_checkpoint, + get_latest_metrics_dict, + get_new_progress_tracker, + get_total_expected_checkpoints, + get_total_steps, + ProgressTracker, +) + +logger = logging.getLogger(__name__) + + +_TORCH210 = packaging.version.parse(torch.__version__) >= packaging.version.parse("2.1.0") + + +@register_trainer(MODEL_ECD, default=True) +class Trainer(CheckpointMixin, EarlyStoppingMixin, MetricsMixin, ProfilingMixin, BaseTrainer): + """Trainer is a class that trains a model.""" + + @staticmethod + def get_schema_cls(): + return ECDTrainerConfig + + def __init__( + self, + config: ECDTrainerConfig, + model: ECD, + resume: float = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + callbacks: list | None = None, + report_tqdm_to_ray=False, + random_seed: float = default_random_seed, + distributed: DistributedStrategy | None = None, + device: str | None = None, + **kwargs, + ): + """Trains a model with a set of options and hyperparameters listed below. Customizable. + + Args: + model: Underlying Ludwig model (`ludwig.models.ecd.ECD`). + resume: Resume training a model that was being trained. (default: False). + skip_save_model: Disables saving model weights and hyperparameters each time the model improves. By + default Ludwig saves model weights after each round of evaluation the validation metric improves, but + if the model is really big that can be time consuming. If you do not want to keep the weights and just + find out what performance a model can get with a set of hyperparameters, use this parameter to skip it, + but the model will not be loadable later on. (default: False). + skip_save_progress: Disables saving progress each round of evaluation. By default Ludwig saves weights + and stats after each round of evaluation for enabling resuming of training, but if the model is really + big that can be time consuming and will uses twice as much space, use this parameter to skip it, but + training cannot be resumed later on. (default: False). + skip_save_log: Disables saving TensorBoard logs. By default Ludwig saves logs for the TensorBoard, but if + it is not needed turning it off can slightly increase the overall speed. (default: False). + callbacks: List of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline. + (default: None). + report_tqdm_to_ray: Enables using the ray based tqdm Callback for progress bar reporting. + random_seed: Default initialization for the random seeds (default: 42). + distributed: Distributed strategy (default: None). + device: Device to load the model on from a saved checkpoint (default: None). + config: `ludwig.schema.trainer.BaseTrainerConfig` instance that specifies training hyperparameters + (default: `ludwig.schema.trainer.ECDTrainerConfig()`). + """ + self.distributed = distributed if distributed is not None else LocalStrategy() + + self.epochs = config.epochs + self.train_steps = config.train_steps + self.enable_profiling = config.enable_profiling + self.steps_per_epoch = 0 # Computed during training, after batcher has been initialized. + self.total_steps = 0 # Computed during training, after batcher has been initialized. + self.total_expected_checkpoints = 0 # Computed during training, after batcher has been initialized. + + self.regularization_lambda = config.regularization_lambda + self.regularization_type = config.regularization_type + self.batch_size = config.batch_size + self.effective_batch_size = config.effective_batch_size + self.max_batch_size = config.max_batch_size + self.eval_batch_size = config.batch_size if config.eval_batch_size is None else config.eval_batch_size + self.should_shuffle = config.should_shuffle + self._validation_field = config.validation_field + self._validation_metric = config.validation_metric + self.early_stop = config.early_stop + self.layers_to_freeze_regex = config.layers_to_freeze_regex + self.steps_per_checkpoint = config.steps_per_checkpoint + self.checkpoints_per_epoch = config.checkpoints_per_epoch + self.evaluate_training_set = config.evaluate_training_set + self.skip_all_evaluation = config.skip_all_evaluation + self.increase_batch_size_on_plateau = config.increase_batch_size_on_plateau + self.increase_batch_size_on_plateau_patience = config.increase_batch_size_on_plateau_patience + self.increase_batch_size_on_plateau_rate = config.increase_batch_size_on_plateau_rate + self.increase_batch_size_eval_metric = config.increase_batch_size_eval_metric + self.increase_batch_size_eval_split = config.increase_batch_size_eval_split + self.gradient_accumulation_steps = ( + config.gradient_accumulation_steps + if self.distributed.allow_gradient_accumulation() and config.gradient_accumulation_steps != AUTO + else 1 + ) + self.resume = resume + self.skip_save_model = skip_save_model + self.skip_save_progress = skip_save_progress + self.skip_save_log = skip_save_log + self.random_seed = random_seed + self.received_sigint = False + self._training_paused = False + self.report_tqdm_to_ray = report_tqdm_to_ray + self.callbacks = callbacks or [] + self.device = device + if self.device is None: + self.device = get_torch_device() + + self.model = model + self.model.prepare_for_training() + self.model = self.distributed.to_device(self.model) + self.model.metrics_to_device(self.device) + + self.compiled_model = self.model + if config.compile: + self.compiled_model = torch.compile(self.model) + logger.info("Training with torchdynamo compiled model") + + # ================ Optimizer tuning ================ + self.gradient_clipping_config = create_clipper(config.gradient_clipping) + + self.config = config + + self.base_learning_rate = None + self.dist_model = None + self.optimizer = None + self.scheduler = None + + self.prepare() + + # Setup for automatic mixed precision (AMP) + self.use_amp = config.use_mixed_precision and self.distributed.allow_mixed_precision() + if self.use_amp: + if torch.cuda.is_available(): + logger.info("Enabling automatic mixed precision (AMP)") + else: + logger.info("`trainer.use_mixed_precision=True`, but no GPU device found. Setting to `False`") + self.use_amp = False + self.scaler = torch.amp.GradScaler("cuda") if self.use_amp else None + + # when training starts the sigint handler will be replaced with + # set_steps_to_1_or_quit so this is needed to remember + # the original sigint to restore at the end of training + # and before set_steps_to_1_or_quit returns + self.original_sigint_handler = None + + def prepare(self): + base_learning_rate = self.config.learning_rate + if self.distributed: + lr_scale_fn = learning_rate_scale_fns[self.config.learning_rate_scaling] + base_learning_rate *= lr_scale_fn(self.distributed.size()) + self.base_learning_rate = base_learning_rate + + # Given that regex is supplied, freeze layers + if self.config.layers_to_freeze_regex: + freeze_layers_regex(self.config, self.model) + + # We may need to replace the embedding layer when using 8-bit optimizers from bitsandbytes. + update_embedding_layer(self.compiled_model, self.config) + + # Register any post forward hooks for the model + self.compiled_model._activate_forward_hooks() + + # Enable gradient checkpointing if configured + if self.config.enable_gradient_checkpointing: + # TODO(Arnav): Add support for gradient checkpointing in the compiled model + # when the model is an ECD model using torch.utils.checkpoint (torch.utils.checkpoint.sequential()) + if not isinstance(self.compiled_model, LLM): + logger.warning("Gradient checkpointing is currently only supported for model_type: llm. Skipping...") + elif not hasattr(self.compiled_model, "model") and not hasattr( + self.compiled_model.model, "gradient_checkpointing_enable" + ): + logger.warning("Gradient checkpointing is not supported by this model. Skipping...") + elif hasattr(self.compiled_model.model, "gradient_checkpointing_enable"): + if _TORCH210: + # https://pytorch.org/docs/stable/checkpoint.html + # https://github.com/huggingface/transformers/blob/02f8738ef8c674300c314d004ba436cb5aaca165/src/transformers/modeling_utils.py#L2094 + self.compiled_model.model.gradient_checkpointing_enable( + gradient_checkpointing_kwargs={"use_reentrant": True} + ) + else: + self.compiled_model.model.gradient_checkpointing_enable() + # `use_cache=True` is incompatible with gradient checkpointing. + self.compiled_model.model.config.use_cache = False + self.compiled_model.model.enable_input_require_grads() + logger.info("Gradient checkpointing enabled for training.") + else: + raise RuntimeError("Error when trying to enable gradient checkpointing.") + + self.dist_model, self.optimizer = self.distributed.prepare( + self.compiled_model, + self.config, + self.base_learning_rate, + ) + + # NOTE: This is a partially configured LRScheduler. It will be updated in the first call to train_step. + self.scheduler = LRScheduler(self.config.learning_rate_scheduler, self.optimizer, 0, 0) + + def train_step( + self, + inputs: dict[str, torch.Tensor], + targets: dict[str, torch.Tensor], + should_step: bool = True, + profiler: torch.profiler.profile | None = None, + ) -> tuple[torch.Tensor, dict[str, torch.Tensor], torch.Tensor]: + """Performs a single training step. + + Params: + inputs: A dictionary of input data, from feature name to tensor. + targets: A dictionary of target data, from feature name to tensor. + should_step: Whether to perform a step of the optimizer after computing gradients. + + Returns: + A tuple of: + 1. loss tensor + 2. dictionary of loss for every output feature. + 3. tokens usage tensor + """ + if isinstance(self.optimizer, torch.optim.LBFGS): + return self._train_step_lbfgs(inputs, targets) + + model_outputs, loss, all_losses = self._forward_pass(inputs, targets, should_step) + used_tokens = model_outputs[USED_TOKENS] + + self._backward_pass(loss) + + if not should_step: + return loss, all_losses, used_tokens + + self._optimizer_step(self.dist_model.parameters()) + + self._update_training_metrics(model_outputs, targets) + + self.distributed.zero_grad(self.optimizer) + + if hasattr(self.model, "loss_balancer") and self.model.loss_balancer is not None: + self.model.loss_balancer.post_step(all_losses) + + if profiler: + profiler.step() + + return loss, all_losses, used_tokens + + def _train_step_lbfgs( + self, + inputs: dict[str, torch.Tensor], + targets: dict[str, torch.Tensor], + ) -> tuple[torch.Tensor, dict[str, torch.Tensor], torch.Tensor]: + """Training step for L-BFGS optimizer (no AMP, no gradient accumulation).""" + + def closure(): + self.distributed.zero_grad(self.optimizer) + model_outputs = self.dist_model((inputs, targets)) + loss, _ = self.model.train_loss( + targets, model_outputs, self.regularization_type, self.regularization_lambda + ) + loss.backward() + return loss + + self.distributed.step(self.optimizer, closure) + + model_outputs = self.dist_model((inputs, targets)) + loss, all_losses = self.model.train_loss( + targets, model_outputs, self.regularization_type, self.regularization_lambda + ) + self._update_training_metrics(model_outputs, targets) + return loss, all_losses, model_outputs[USED_TOKENS] + + def _forward_pass( + self, + inputs: dict[str, torch.Tensor], + targets: dict[str, torch.Tensor], + should_step: bool, + ) -> tuple[dict, torch.Tensor, dict[str, torch.Tensor]]: + """Run the forward pass and compute loss under AMP if enabled.""" + with torch.amp.autocast("cuda") if self.use_amp else contextlib.nullcontext(): + with self.distributed.prepare_model_update(self.dist_model, should_step=should_step): + model_outputs = self.dist_model((inputs, targets)) + loss, all_losses = self.model.train_loss( + targets, model_outputs, self.regularization_type, self.regularization_lambda + ) + loss = loss / self.gradient_accumulation_steps + return model_outputs, loss, all_losses + + def _backward_pass(self, loss: torch.Tensor) -> None: + """Run the backward pass, scaling through AMP if enabled.""" + if self.use_amp: + self.scaler.scale(loss).backward() + else: + self.distributed.backward(loss, self.dist_model) + + def _optimizer_step(self, variables) -> None: + """Wait for grad sync, clip gradients, and step the optimizer.""" + # Wait for gradient aggregation to complete before clipping the gradients. + # When using AMP, we need to do this before unscaling. + self.distributed.wait_optimizer_synced(self.optimizer) + + if self.use_amp: + # In-place unscaling of all gradients before weights update + # Do this before gradient clipping per docs: + # https://pytorch.org/docs/master/notes/amp_examples.html#gradient-clipping + self.scaler.unscale_(self.optimizer) + + if self.distributed.allow_clip_gradients(): + self.clip_grads(variables) + + with self.distributed.prepare_optimizer_update(self.optimizer): + if self.use_amp: + self.scaler.step(self.optimizer) + else: + self.distributed.step(self.optimizer) + + if self.use_amp: + self.scaler.update() + + def _update_training_metrics(self, model_outputs: dict, targets: dict[str, torch.Tensor]) -> None: + """Update training-set evaluation metrics from the current batch outputs. + + Only called when evaluate_training_set is False (noisy but fast approximation). + """ + if not self.evaluate_training_set: + predictions = self.model.outputs_to_predictions(model_outputs) + self.model.update_metrics(targets, predictions) + + def clip_grads(self, variables): + """Applies gradient clipping.""" + if self.gradient_clipping_config.clipglobalnorm: + torch.nn.utils.clip_grad_norm_(variables, self.gradient_clipping_config.clipglobalnorm) + if self.gradient_clipping_config.clipnorm: + torch.nn.utils.clip_grad_norm_(variables, self.gradient_clipping_config.clipnorm) + if self.gradient_clipping_config.clipvalue: + torch.nn.utils.clip_grad_value_(variables, self.gradient_clipping_config.clipvalue) + + @classmethod + def write_eval_summary( + cls, + summary_writer, + metrics, + step, + ): + if not summary_writer: + return + + for feature_name, output_feature in metrics.items(): + for metric_name, metrics in output_feature.items(): + if metrics: + metric_tag = f"{feature_name}/epoch_{metric_name}" + metric_val = metrics[-1][-1] + summary_writer.add_scalar(metric_tag, metric_val, global_step=step) + summary_writer.flush() + + @classmethod + def write_step_summary( + cls, train_summary_writer, combined_loss, all_losses, step, used_tokens, total_tokens_used, learning_rate=None + ): + if not train_summary_writer: + return + + # token information. + train_summary_writer.add_scalar("tokens/tokens", used_tokens, global_step=step) + train_summary_writer.add_scalar("tokens/total_tokens_used", total_tokens_used, global_step=step) + + # combined loss + train_summary_writer.add_scalar("combined/step_training_loss", combined_loss, global_step=step) + + # all other losses + for feature_name, loss in all_losses.items(): + loss_tag = f"{feature_name}/step_training_loss" + train_summary_writer.add_scalar(loss_tag, loss.detach().float(), global_step=step) + + if learning_rate: + train_summary_writer.add_scalar("combined/step_learning_rate", learning_rate, global_step=step) + + # Log CUDA memory stats. + if torch.cuda.is_available() and torch.cuda.device_count() > 0 and os.environ.get("CUDA_VISIBLE_DEVICES") != "": + for i in range(torch.cuda.device_count()): + device = torch.device(f"cuda:{i}") + memory_stats = torch.cuda.memory_stats(device=device) + gb_memory_stats = {k: v / (1000**3) for k, v in memory_stats.items()} + # Allocated bytes. + train_summary_writer.add_scalar( + f"cuda/device{i}/allocated_gb.all.current", + gb_memory_stats["allocated_bytes.all.current"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/allocated_gb.all.peak", + gb_memory_stats["allocated_bytes.all.peak"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/allocated_gb.all.allocated", + gb_memory_stats["allocated_bytes.all.allocated"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/allocated_gb.all.freed", + gb_memory_stats["allocated_bytes.all.freed"], + global_step=step, + ) + + # Reserved bytes. + train_summary_writer.add_scalar( + f"cuda/device{i}/reserved_gb.all.current", + gb_memory_stats["reserved_bytes.all.current"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/reserved_gb.all.peak", gb_memory_stats["reserved_bytes.all.peak"], global_step=step + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/reserved_gb.all.allocated", + gb_memory_stats["reserved_bytes.all.allocated"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/reserved_gb.all.freed", + gb_memory_stats["reserved_bytes.all.freed"], + global_step=step, + ) + + # Active bytes. + train_summary_writer.add_scalar( + f"cuda/device{i}/active_gb.all.current", + gb_memory_stats["active_bytes.all.current"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/active_gb.all.peak", gb_memory_stats["active_bytes.all.peak"], global_step=step + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/active_gb.all.allocated", + gb_memory_stats["active_bytes.all.allocated"], + global_step=step, + ) + train_summary_writer.add_scalar( + f"cuda/device{i}/active_gb.all.freed", gb_memory_stats["active_bytes.all.freed"], global_step=step + ) + + # Global free memory. + train_summary_writer.add_scalar( + f"cuda/device{i}/global_free_memory_gb", + torch.cuda.mem_get_info(device=device)[0] / (1000**3), + global_step=step, + ) + + # Total memory occupied. + train_summary_writer.add_scalar( + f"cuda/device{i}/total_memory_occupied_gb", + torch.cuda.mem_get_info(device=device)[1] / (1000**3), + global_step=step, + ) + + # Total memory used. + train_summary_writer.add_scalar( + f"cuda/device{i}/total_memory_used_gb", + (torch.cuda.mem_get_info(device=device)[1] - torch.cuda.mem_get_info(device=device)[0]) / (1000**3), + global_step=step, + ) + + # Utilization. + # https://pytorch.org/docs/stable/generated/torch.cuda.utilization.html#torch.cuda.utilization + train_summary_writer.add_scalar( + f"cuda/device{i}/utilization", + torch.cuda.utilization(device=device), + global_step=step, + ) + train_summary_writer.flush() + + def is_cpu_training(self): + return torch.device(self.device) == torch.device("cpu") + + def tune_batch_size( + self, + config: ModelConfig | ModelConfigDict, + training_set: Dataset, + random_seed: int = default_random_seed, + max_trials: int = 20, + halving_limit: int = 3, + snapshot_weights: bool = True, + on_best_batch_size_updated: Callable[[int, float, int], None] | None = None, + tune_for_training: bool = True, + global_max_sequence_length: int | None = None, + ) -> int: + logger.info("Tuning batch size...") + skip_save_model = self.skip_save_model + skip_save_progress = self.skip_save_progress + skip_save_log = self.skip_save_log + # Set temporary values + self.skip_save_model = True + self.skip_save_progress = True + self.skip_save_log = True + + # When training on CPU, larger batch sizes offer limited benefits due to lack of effective + # parallelization within a batch. As such, to increase chances of stable training, we cap the maximum + # batch size at MAX_CPU_BATCH_SIZE + max_batch_size = ( + self.max_batch_size if torch.cuda.is_available() else min(self.max_batch_size, MAX_CPU_BATCH_SIZE) + ) + + if self.effective_batch_size != AUTO: + # If an effective batch size is set, we must ensure that batch size tuning doesn't exceed it + max_batch_size = min(self.effective_batch_size, max_batch_size) + + if not tune_for_training: + # No need to save and restore model and optimizer states, as they aren't modified during predict + snapshot_weights = False + + self.dist_model.train() # Sets model training mode. + evaluator = ( + self._create_batch_size_evaluator() if tune_for_training else self._create_predict_batch_size_evaluator() + ) + with tempfile.TemporaryDirectory() as tmpdir: + if snapshot_weights: + # Save a snapshot of the model and optimizer state to restore later, as they will be modified + # when we call the train step as part of the auto-tuning. This is undesirable, particularly for + # pretrained models. + checkpoint = self.distributed.create_checkpoint_handle( + dist_model=self.dist_model, model=self.model, optimizer=self.optimizer, scheduler=self.scheduler + ) + checkpoint.save(os.path.join(tmpdir, "latest.ckpt"), global_step=0) + try: + best_batch_size = evaluator.select_best_batch_size( + len(training_set), max_batch_size, max_trials, self.is_coordinator(), global_max_sequence_length + ) + best_batch_size = self.distributed.broadcast_object(best_batch_size) + + if tune_for_training: + # Update batch size / gradient accumulation before preparing the trainer. + self.config.batch_size = best_batch_size + self.config.update_batch_size_grad_accum(self.distributed.size()) + self.batch_size = self.config.batch_size + self.gradient_accumulation_steps = self.config.gradient_accumulation_steps + + return best_batch_size + finally: + # Restore original parameters to defaults + self.skip_save_model = skip_save_model + self.skip_save_progress = skip_save_progress + self.skip_save_log = skip_save_log + + if snapshot_weights: + # Restore the model weights prior to batch size tuning to undo any updates made to the weights + if self.distributed.prepare_before_load(): + # Some distributed strategies may need to re-init before loading the model + self.prepare() + self.resume_weights_and_optimizer(str(tmpdir), checkpoint) + + def _create_batch_size_evaluator(self) -> BatchSizeEvaluator: + trainer = self + + class _TrainerBatchSizeEvaluator(BatchSizeEvaluator): + def reset(self): + trainer.model.reset_metrics() + trainer.optimizer.zero_grad() + + def step(self, batch_size: int, global_max_sequence_length: int | None = None): + trainer.distributed.set_batch_size(trainer.dist_model, batch_size) + inputs = { + input_feature_name: input_feature.create_sample_input(batch_size=batch_size).to(trainer.device) + for input_feature_name, input_feature in trainer.model.input_features.items() + } + targets = { + output_feature_name: output_feature.create_sample_output(batch_size=batch_size).to(trainer.device) + for output_feature_name, output_feature in trainer.model.output_features.items() + } + trainer.train_step(inputs, targets) + + return _TrainerBatchSizeEvaluator() + + def _create_predict_batch_size_evaluator(self) -> BatchSizeEvaluator: + trainer = self + + class _PredictBatchSizeEvaluator(BatchSizeEvaluator): + def reset(self): + trainer.model.reset_metrics() + trainer.optimizer.zero_grad() + + def step(self, batch_size: int, global_max_sequence_length: int | None = None): + trainer.distributed.set_batch_size(trainer.dist_model, batch_size) + inputs = { + input_feature_name: input_feature.create_sample_input(batch_size=batch_size).to(trainer.device) + for input_feature_name, input_feature in trainer.model.input_features.items() + } + targets = { + output_feature_name: output_feature.create_sample_output(batch_size=batch_size).to(trainer.device) + for output_feature_name, output_feature in trainer.model.output_features.items() + } + with torch.no_grad(): + trainer.dist_model((inputs, targets)) + + return _PredictBatchSizeEvaluator() + + def run_evaluation( + self, + training_set, + validation_set, + test_set, + progress_tracker: ProgressTracker, + train_summary_writer, + validation_summary_writer, + test_summary_writer, + model_hyperparameters_path, + output_features, + metrics_names, + save_path, + loss: torch.Tensor, + all_losses: dict[str, torch.Tensor], + early_stopping_steps: int, + checkpoint_manager: CheckpointManager, + ) -> bool: + """Runs evaluation over training, validation, and test sets. + + Also: + - Prints results, saves results to the progress tracker. + - Saves the model if the validation score is the best so far + - If there is no validation set, the model is always saved. + + Returns whether the trainer should early stop, based on validation metrics history. + """ + start_time = time.time() + self.callback(lambda c: c.on_eval_start(self, progress_tracker, save_path)) + + if self.is_coordinator(): + logger.info(f"\nRunning evaluation for step: {progress_tracker.steps}, epoch: {progress_tracker.epoch}") + + # ================ Eval ================ + # eval metrics on train + self.eval_batch_size = max(self.eval_batch_size, progress_tracker.batch_size) + + if self.evaluate_training_set: + # Run a separate pass over the training data to compute metrics + self.evaluation( + training_set, "train", progress_tracker.train_metrics, self.eval_batch_size, progress_tracker + ) + else: + # Use metrics accumulated during training + metrics = self.model.get_metrics() + append_metrics(self.model, "train", metrics, progress_tracker.train_metrics, progress_tracker) + self.model.reset_metrics() + + self.write_eval_summary( + summary_writer=train_summary_writer, + metrics=progress_tracker.train_metrics, + step=progress_tracker.steps, + ) + + if validation_set is not None: + self.callback(lambda c: c.on_validation_start(self, progress_tracker, save_path)) + + # eval metrics on validation set + self.evaluation( + validation_set, + VALIDATION, + progress_tracker.validation_metrics, + self.eval_batch_size, + progress_tracker, + ) + + llm_eval_examples = progress_tracker.llm_eval_examples + dict_save_dir = os.path.join(os.path.dirname(checkpoint_manager.directory), "llm_eval_examples") + os.makedirs(dict_save_dir, exist_ok=True) + dict_save_path = os.path.join(dict_save_dir, f"{progress_tracker.checkpoint_number}.csv") + llm_eval_examples = pd.DataFrame(llm_eval_examples).to_dict(orient="records") + with open(dict_save_path, "w", encoding="utf-8") as outfile: + writer = csv.DictWriter(outfile, fieldnames=["inputs", "targets", "outputs"]) + writer.writeheader() + writer.writerows(llm_eval_examples) + + self.write_eval_summary( + summary_writer=validation_summary_writer, + metrics=progress_tracker.validation_metrics, + step=progress_tracker.steps, + ) + + self.callback(lambda c: c.on_validation_end(self, progress_tracker, save_path)) + + if test_set is not None: + self.callback(lambda c: c.on_test_start(self, progress_tracker, save_path)) + + # eval metrics on test set + self.evaluation(test_set, TEST, progress_tracker.test_metrics, self.eval_batch_size, progress_tracker) + + self.write_eval_summary( + summary_writer=test_summary_writer, + metrics=progress_tracker.test_metrics, + step=progress_tracker.steps, + ) + + self.callback(lambda c: c.on_test_end(self, progress_tracker, save_path)) + + elapsed_time = (time.time() - start_time) * 1000.0 + + if self.is_coordinator(): + logger.info(f"Evaluation took {time_utils.strdelta(elapsed_time)}\n") + print_metrics_table( + output_features, + progress_tracker.train_metrics, + progress_tracker.validation_metrics, + progress_tracker.test_metrics, + ) + + # ================ Validation Logic ================ + should_break = False + if validation_set is not None and validation_set.size > 0: + should_break = self.check_progress_on_validation( + progress_tracker, + self.validation_field, + self.validation_metric, + save_path, + model_hyperparameters_path, + self.increase_batch_size_on_plateau, + self.increase_batch_size_on_plateau_patience, + self.increase_batch_size_on_plateau_rate, + self.max_batch_size, + self.increase_batch_size_eval_metric, + self.increase_batch_size_eval_split, + early_stopping_steps, + self.skip_save_model, + checkpoint_manager, + ) + else: + # There's no validation, so we save the model. + if not self.skip_save_model: + if self.is_coordinator(): + logger.info("Saving model.\n") + checkpoint_manager.save_best(progress_tracker.steps) + self.callback(lambda c: c.on_save_best_checkpoint(self, progress_tracker, save_path)) + + # Trigger eval end callback after any model weights save for complete checkpoint + self.callback(lambda c: c.on_eval_end(self, progress_tracker, save_path)) + + # Clear the CUDA cache to free up memory + torch.cuda.empty_cache() + + return should_break + + def save_checkpoint(self, progress_tracker: ProgressTracker, save_path: str, checkpoint_manager: CheckpointManager): + """Checkpoints the model, progress tracker, and invokes the checkpoint callback.""" + progress_tracker.increment_checkpoint() + + checkpoint_manager.save(progress_tracker.steps) + if self.is_coordinator(): + progress_tracker.save(os.path.join(save_path, TRAINING_PROGRESS_TRACKER_FILE_NAME)) + + # Callback that the checkpoint was reached, regardless of whether the model was evaluated. + self.callback(lambda c: c.on_checkpoint(self, progress_tracker)) + + def _create_summary_writers(self, tensorboard_log_dir, validation_set, test_set): + """Create TensorBoard SummaryWriters for train/validation/test splits.""" + train_summary_writer = None + validation_summary_writer = None + test_summary_writer = None + if self.is_coordinator() and not self.skip_save_log and tensorboard_log_dir: + train_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TRAINING)) + if validation_set is not None and validation_set.size > 0: + validation_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, VALIDATION)) + if test_set is not None and test_set.size > 0: + test_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TEST)) + return train_summary_writer, validation_summary_writer, test_summary_writer + + def _create_or_resume_progress_tracker( + self, training_progress_tracker_path, training_checkpoints_path, checkpoint, output_features + ): + """Load an existing progress tracker if resuming, otherwise create a fresh one.""" + should_resume = self.resume and self.resume_files_exist( + training_progress_tracker_path, training_checkpoints_path + ) + should_resume = self.distributed.broadcast_object(should_resume, name="should_resume") + + if should_resume: + try: + progress_tracker = self.resume_training_progress_tracker(training_progress_tracker_path) + self.resume_weights_and_optimizer(training_checkpoints_path, checkpoint) + if self.is_coordinator(): + logger.info("Resuming training from previous run.") + return progress_tracker + except (FileNotFoundError, OSError, RuntimeError): + logger.warning("Could not load training checkpoint; starting fresh.", exc_info=True) + if self.is_coordinator(): + logger.info("Failed to resume training from previous run. Creating fresh model training run.") + + progress_tracker = get_new_progress_tracker( + batch_size=self.batch_size, + learning_rate=self.base_learning_rate, + best_eval_metric_value=get_initial_validation_value(self.validation_metric), + best_increase_batch_size_eval_metric=get_initial_validation_value(self.increase_batch_size_eval_metric), + output_features=output_features, + ) + if self.is_coordinator(): + logger.info("Creating fresh model training run.") + return progress_tracker + + def create_checkpoint_handle(self): + return self.distributed.create_checkpoint_handle( + dist_model=self.dist_model, model=self.model, optimizer=self.optimizer, scheduler=self.scheduler + ) + + def train( + self, + training_set, + validation_set=None, + test_set=None, + save_path=MODEL_FILE_NAME, + return_state_dict: bool = False, + **kwargs, + ): + """Trains a model with a set of hyperparameters listed below. Customizable. + + Args: + training_set: The training set. + validation_set: The validation dataset. + test_set: The test dataset. + save_path: The directory that will contain the saved model. + return_state_dict: Whether to return the state dict of the model instead of the model itself. + """ + # ====== General setup ======= + output_features = self.model.output_features + + # Only use signals when on the main thread to avoid issues with CherryPy + # https://github.com/ludwig-ai/ludwig/issues/286 + if threading.current_thread() == threading.main_thread(): + # set the original sigint signal handler + # as we want to restore it at the end of training + self.original_sigint_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, self.set_steps_to_1_or_quit) + # SIGUSR1 → pause training after current batch; SIGUSR2 → resume. + # On Windows, SIGUSR1/SIGUSR2 are not available — skip silently. + if hasattr(signal, "SIGUSR1"): + self._training_paused = False + signal.signal(signal.SIGUSR1, self._handle_pause) + signal.signal(signal.SIGUSR2, self._handle_resume) + + metrics_names = get_metric_names(output_features) + + # ====== Setup file names ======= + model_hyperparameters_path = None + tensorboard_log_dir = None + if self.is_coordinator(): + os.makedirs(save_path, exist_ok=True) + model_hyperparameters_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME) + tensorboard_log_dir = os.path.join(save_path, "logs") + + # Sync save_path across the workers + save_path = self.distributed.broadcast_object(save_path or "") + + training_progress_tracker_path = None + training_checkpoints_path = None + if save_path: + training_progress_tracker_path = os.path.join(save_path, TRAINING_PROGRESS_TRACKER_FILE_NAME) + training_checkpoints_path = os.path.join(save_path, TRAINING_CHECKPOINTS_DIR_PATH) + + self.callback( + lambda c: c.on_trainer_train_setup(self, save_path, self.is_coordinator()), coordinator_only=False + ) + + # ====== Setup session ======= + checkpoint = self.create_checkpoint_handle() + model_soup_top_k = ( + getattr(self.config, "model_soup_top_k", 0) if getattr(self.config, "model_soup", None) else 0 + ) + checkpoint_manager = CheckpointManager( + checkpoint, training_checkpoints_path, device=self.device, top_k=model_soup_top_k + ) + + # ====== Setup Tensorboard writers ======= + train_summary_writer, validation_summary_writer, test_summary_writer = self._create_summary_writers( + tensorboard_log_dir, validation_set, test_set + ) + + # ================ Resume logic ================ + self.callback(lambda c: c.on_resume_training(self.is_coordinator())) + progress_tracker = self._create_or_resume_progress_tracker( + training_progress_tracker_path, training_checkpoints_path, checkpoint, output_features + ) + + # Distributed: broadcast initial variable states from rank 0 to all other processes. + # This is necessary to ensure consistent initialization of all workers when + # training is started with random weights or restored from a checkpoint. + self.distributed.sync_model(self.dist_model) + self.distributed.sync_optimizer(self.optimizer) + self.scheduler.load_state_dict(self.distributed.broadcast_object(self.scheduler.state_dict())) + + # Set the batch size in case it was modified during auto-tuning + self.distributed.set_batch_size(self.dist_model, self.batch_size) + + set_random_seed(self.random_seed) + + if self.enable_profiling: + logger.warning("Full torch profiler is enabled. Training may be significantly slower.") + profiler = torch.profiler.profile( + schedule=torch.profiler.schedule( + wait=self.config.profiler.wait, + warmup=self.config.profiler.warmup, + active=self.config.profiler.active, + repeat=self.config.profiler.repeat, + ), + on_trace_ready=torch.profiler.tensorboard_trace_handler(os.path.join(tensorboard_log_dir, "profiling")), + record_shapes=True, + with_stack=True, + profile_memory=True, + ) + else: + profiler = None + + try: + with training_set.initialize_batcher( + batch_size=self.batch_size, + should_shuffle=self.should_shuffle, + random_seed=self.random_seed, + distributed=self.distributed, + ignore_last=True, + augmentation_pipeline=self.model.get_augmentation_pipelines(), + ) as batcher: + # ================ Training Loop ================ + self.steps_per_epoch = batcher.steps_per_epoch + self.total_steps = get_total_steps(self.epochs, batcher.steps_per_epoch, self.train_steps) + # Expose progress information on the progress tracker for callbacks and Studio. + progress_tracker.steps_per_epoch = self.steps_per_epoch + progress_tracker.total_steps = self.total_steps + progress_tracker.training_start_time = time.monotonic() + # NOTE(geoffrey): this ensures that the total number of epochs coincides with the number of + # times `batcher.set_epoch` is called. + old_epochs = self.epochs + self.epochs = math.ceil(self.total_steps / self.steps_per_epoch) + if old_epochs != self.epochs: + logger.warning( + f"The number of epochs has been adjusted from config-specified {old_epochs} " + f"to {self.epochs} to match the total number of steps." + ) + + # Get the terminal steps per checkpoint. + final_steps_per_checkpoint = get_final_steps_per_checkpoint( + batcher.steps_per_epoch, + self.steps_per_checkpoint, + self.checkpoints_per_epoch, + self.is_coordinator(), + ) + final_steps_per_checkpoint = min(final_steps_per_checkpoint, self.total_steps) + early_stopping_steps = final_steps_per_checkpoint * self.early_stop + if not self.skip_save_progress: + self.total_expected_checkpoints = get_total_expected_checkpoints( + self.total_steps, final_steps_per_checkpoint, self.epochs + ) + + # Initialize the learning rate scheduler. + self.scheduler = LRScheduler( + self.config.learning_rate_scheduler, + self.optimizer, + steps_per_checkpoint=final_steps_per_checkpoint, + total_steps=self.total_steps, + ) + + if self.is_coordinator(): + logger.info( + f"Training for {self.total_steps} step(s), approximately " + f"{int(self.total_steps / batcher.steps_per_epoch)} epoch(s)." + ) + if self.early_stop < 0: + logger.info("Early stopping policy: None") + else: + logger.info( + f"Early stopping policy: {self.early_stop} round(s) of evaluation, or " + f"{early_stopping_steps} step(s), approximately " + f"{int(early_stopping_steps / batcher.steps_per_epoch)} epoch(s).\n" + ) + logger.info(f"Starting with step {progress_tracker.steps}, epoch: {progress_tracker.epoch}") + + progress_bar_config = { + "desc": "Training", + "initial": progress_tracker.steps, + "total": self.total_steps, + "disable": is_progressbar_disabled(), + "file": sys.stdout, + } + progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator()) + + if profiler: + profiler.start() + + while progress_tracker.steps < self.total_steps: + # note that batch size may change over epochs + batcher.set_epoch(progress_tracker.epoch, progress_tracker.batch_size) + + # epoch init + start_time = time.time() + + # Reset the metrics at the start of the next epoch + self.dist_model.train() # Sets model to training mode. + self.model.reset_metrics() + + self.callback(lambda c: c.on_epoch_start(self, progress_tracker, save_path)) + + # Trains over a full epoch of data or up to the last training step, whichever is sooner. + should_break, has_nan_or_inf_tensors = self._train_loop( + batcher, + progress_tracker, + save_path, + train_summary_writer, + progress_bar, + training_set, + validation_set, + test_set, + start_time, + validation_summary_writer, + test_summary_writer, + model_hyperparameters_path, + output_features, + metrics_names, + checkpoint_manager, + final_steps_per_checkpoint, + early_stopping_steps, + profiler, + ) + if self.is_coordinator(): + # ========== Save training progress ========== + logger.debug( + f"Epoch {progress_tracker.epoch} took: " + f"{time_utils.strdelta((time.time() - start_time) * 1000.0)}." + ) + + # Skip saving progress if we're not saving the model. We should do this so as to not overwrite the + # best model checkpoint from the previous round of evaluation so that the previous best model + # weights can be used for inference instead of the current weights which are in a bad state. + if has_nan_or_inf_tensors: + break + + if not self.skip_save_progress: + self.save_checkpoint(progress_tracker, save_path, checkpoint_manager) + + if not self.skip_save_model and self.skip_all_evaluation: + # All evaluation was skipped, so save the current step as the best so far. + checkpoint_manager.save_best(progress_tracker.steps) + + # Early stop if needed. + if should_break: + break + finally: + # ================ Finished Training ================ + self.callback( + lambda c: c.on_trainer_train_teardown(self, progress_tracker, save_path, self.is_coordinator()), + coordinator_only=False, + ) + + # Deactivate any forward hooks for the model used at training time. + self.compiled_model._deactivate_forward_hooks() + + # Stop the profiler. + if profiler: + profiler.stop() + + # Close the summary writers. + if train_summary_writer is not None: + train_summary_writer.close() + if validation_summary_writer is not None: + validation_summary_writer.close() + if test_summary_writer is not None: + test_summary_writer.close() + + if not self.skip_save_model and self.skip_all_evaluation and not has_nan_or_inf_tensors: + # All evaluation was skipped, so save the current step as the best so far. + checkpoint_manager.save_best(progress_tracker.steps) + + if not self.skip_save_progress: + checkpoint_manager.close() + + # Load the best weights from saved checkpoint, optionally applying model soup + state_dict = None + if self.distributed.is_coordinator(): + if not self.skip_save_model: + model_soup_strategy = getattr(self.config, "model_soup", None) + if model_soup_strategy and checkpoint_manager.top_k_entries: + from ludwig.utils.model_soup import uniform_soup + + top_k_sds = checkpoint_manager.get_top_k_state_dicts(self.return_device) + if len(top_k_sds) > 1: + if model_soup_strategy == "uniform": + logger.info(f"Applying uniform model soup with {len(top_k_sds)} checkpoints") + state_dict = uniform_soup(top_k_sds) + elif model_soup_strategy == "greedy": + logger.info(f"Applying greedy model soup with {len(top_k_sds)} checkpoints") + # For greedy soup we'd need eval_fn — fall back to uniform for now + state_dict = uniform_soup(top_k_sds) + else: + state_dict = checkpoint_manager.get_best_checkpoint_state_for_inference(self.return_device) + else: + state_dict = checkpoint_manager.get_best_checkpoint_state_for_inference(self.return_device) + else: + state_dict = checkpoint_manager.get_best_checkpoint_state_for_inference(self.return_device) + if not state_dict: + error_message = "Training ran into an error. No checkpoint was saved." + if has_nan_or_inf_tensors: + error_message += ( + " This is because training was terminated early due to the presence of NaN or " + "Inf values in the model weights before a single valid checkpoint could be saved." + ) + raise RuntimeError(error_message) + if not return_state_dict: + if self.distributed.is_model_parallel(): + # Assume the full weights cannot fit in memory on GPU + self.model = self.model.cpu() + + # For a full explanation of this 8-bit workaround, see https://github.com/ludwig-ai/ludwig/pull/3606 + # TODO (jeffkinnison): Determine why `SCB` and `CB` are deleted from parameter state + quantization = get_quantization(self.model.config_obj) + uses_quantization = bool(quantization) if not isinstance(quantization, list) else any(quantization) + if uses_quantization and 8 in quantization: + # If the model was previously placed on GPU, 8-bit parameter state will be updated with several + # matrices containing quantization information. These are recorded matrices are recorded in the + # training checkpoint state dicts, but do not necessarily exist in the parameter object, leading + # to a RuntimeError in `load_state_dict`. Explicitly call `model.cuda()` to make sure the + # matrices are part of model state. This workaround is necessary because the matrices are + # deleted during the model's forward pass. + if self.model.config_obj.model_type == MODEL_LLM and self.model.model.device.type == "cuda": + self.model.model.cuda() + elif self.model.config_obj.model_type == MODEL_ECD and self.model.device.type == "cuda": + self.model.cuda() + _, unexpected_keys = self.model.load_state_dict(state_dict, strict=False) + only_weights_format_keys = ["weights_format" in k for k in unexpected_keys] + + # bitsandbytes adds a number of `weights_format` metadata fields to the state dict in + # `Linear8bitLt._save_to_state_dict`. These contain information about how the 8-bit tensors + # are tiled, but the fields themselves never exist in the module and get returned as unexpected + # keys when loading the state dict. The + if unexpected_keys and not only_weights_format_keys: + raise RuntimeError( + f"Unexpected keys found in state dict: {unexpected_keys}.\n" + "This may indicate a model architecture mismatch between checkpoint and current model." + ) + else: + _, unexpected_keys = self.model.load_state_dict(state_dict, strict=False) + if unexpected_keys: + raise RuntimeError( + f"Unexpected keys found in state dict: {unexpected_keys}.\n" + "This may indicate a model architecture mismatch between checkpoint and current model." + ) + elif return_state_dict: + state_dict = self.model.cpu().state_dict() + + # When running with Ray, we only need to return the state dict, as it's faster and cheaper to send the + # state dict over the network than to load the model state here, serialize it back to a state dict, then + # load it back on the head node. + return_value = self.model if not return_state_dict else state_dict + + # restore original signal handlers + if self.original_sigint_handler and threading.current_thread() == threading.main_thread(): + signal.signal(signal.SIGINT, self.original_sigint_handler) + if hasattr(signal, "SIGUSR1"): + signal.signal(signal.SIGUSR1, signal.SIG_DFL) + signal.signal(signal.SIGUSR2, signal.SIG_DFL) + + return ( + return_value, + progress_tracker.train_metrics, + progress_tracker.validation_metrics, + progress_tracker.test_metrics, + ) + + def _train_loop( + self, + batcher, + progress_tracker: ProgressTracker, + save_path, + train_summary_writer, + progress_bar: LudwigProgressBar, + training_set, + validation_set, + test_set, + start_time, + validation_summary_writer, + test_summary_writer, + model_hyperparameters_path, + output_features, + metrics_names, + checkpoint_manager: CheckpointManager, + final_steps_per_checkpoint: int, + early_stopping_steps: int, + profiler: torch.profiler.profile | None, + ) -> tuple[bool, bool]: + """Completes up to one epoch through the data. + + This function completes a single pass (epoch) through the training data and returns + two boolean values: + + Returns: + should_break (bool): + Indicates whether the training loop should be terminated prematurely. + + has_nan_or_inf_tensors (bool): + Indicates whether the model weights contain NaN or Inf values. + """ + self.distributed.zero_grad(self.optimizer) + batch_idx = 0 + should_break = False + has_nan_or_inf_tensors = False + while not batcher.last_batch() and progress_tracker.steps < self.total_steps and not should_break: + progress_tracker.learning_rate = self.optimizer.param_groups[0]["lr"] + self.callback(lambda c: c.on_batch_start(self, progress_tracker, save_path)) + + # obtain batch + batch = batcher.next_batch() + + # determine whether we need to accumulate gradients as trigger a full parameter update + should_sync_grads = (batch_idx + 1) % self.gradient_accumulation_steps == 0 + is_checkpoint_step = (progress_tracker.steps + 1) % final_steps_per_checkpoint == 0 + should_step = should_sync_grads or is_checkpoint_step + batch_idx += 1 + + inputs, targets = self._batch_to_tensors(batch) + + loss, all_losses, used_tokens = self.train_step(inputs, targets, should_step=should_step, profiler=profiler) + + # Update LR scheduler here instead of train loop to avoid updating during batch size tuning, etc. + self.scheduler.step() + + progress_tracker.set_token_usage_for_this_step(used_tokens) + + if self.is_coordinator() and not self.skip_save_log: + self.write_step_summary( + train_summary_writer=train_summary_writer, + combined_loss=loss.detach().float(), + all_losses=all_losses, + step=progress_tracker.steps, + used_tokens=used_tokens, + total_tokens_used=progress_tracker.total_tokens_used, + learning_rate=progress_tracker.learning_rate, + ) + + progress_tracker.steps += 1 + progress_bar.set_postfix({"loss": loss.detach().item()}) + progress_bar.update(1) + if self.is_coordinator(): + logger.debug( + "training: completed batch %s memory used: %.2fMB", + progress_bar.total_steps, + psutil.Process(os.getpid()).memory_info()[0] / 1e6, + ) + + # Executing `on_batch_end` before `run_evaluation` enables accurate batch duration measurements. + self.callback(lambda c: c.on_batch_end(self, progress_tracker, save_path, sync_step=should_step)) + + # Increment epoch before evaluation so metrics are reported with the correct epoch number. + if batcher.last_batch(): + progress_tracker.epoch += 1 + + if progress_tracker.steps % final_steps_per_checkpoint == 0: + should_break, has_nan_or_inf_tensors = self._eval_and_checkpoint( + loss=loss, + all_losses=all_losses, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + progress_tracker=progress_tracker, + train_summary_writer=train_summary_writer, + validation_summary_writer=validation_summary_writer, + test_summary_writer=test_summary_writer, + model_hyperparameters_path=model_hyperparameters_path, + output_features=output_features, + metrics_names=metrics_names, + save_path=save_path, + early_stopping_steps=early_stopping_steps, + checkpoint_manager=checkpoint_manager, + ) + if has_nan_or_inf_tensors: + return True, has_nan_or_inf_tensors + + if batcher.last_batch(): + self.callback(lambda c: c.on_epoch_end(self, progress_tracker, save_path)) + + return should_break, has_nan_or_inf_tensors + + def _batch_to_tensors(self, batch) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]: + """Convert a raw data batch to input and target tensors on the training device.""" + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(self.device) + for i_feat in self.model.input_features.values() + } + targets = { + o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(self.device) + for o_feat in self.model.output_features.values() + } + return inputs, targets + + def _eval_and_checkpoint( + self, + loss: torch.Tensor, + all_losses: dict[str, torch.Tensor], + training_set, + validation_set, + test_set, + progress_tracker: ProgressTracker, + train_summary_writer, + validation_summary_writer, + test_summary_writer, + model_hyperparameters_path: str, + output_features, + metrics_names, + save_path: str, + early_stopping_steps: int, + checkpoint_manager: CheckpointManager, + ) -> tuple[bool, bool]: + """Run evaluation and save a checkpoint at a checkpoint boundary step. + + Returns: + (should_break, has_nan_or_inf_tensors) — both trigger training termination when True. + """ + has_nan_or_inf_tensors = self._has_nan_or_inf_weights(self.dist_model) + if has_nan_or_inf_tensors: + return False, has_nan_or_inf_tensors + + if not self.skip_all_evaluation: + should_break = self.run_evaluation( + training_set, + validation_set, + test_set, + progress_tracker, + train_summary_writer, + validation_summary_writer, + test_summary_writer, + model_hyperparameters_path, + output_features, + metrics_names, + save_path, + loss, + all_losses, + early_stopping_steps, + checkpoint_manager, + ) + else: + should_break = False + + if not self.skip_save_progress: + self.save_checkpoint(progress_tracker, save_path, checkpoint_manager) + + return should_break, has_nan_or_inf_tensors + + def _has_nan_or_inf_weights(self, model: torch.nn.Module) -> bool: + """Check for NaN or infinity (inf) values in the weights (parameters and buffers) of a PyTorch model in a + local or distributed training environment. It is called to ensure the model's numerical stability during + training. It works for both model parallel and data parallel training. + + This function recursively inspects the model's parameters and buffers to identify NaN or inf values. It + communicates and aggregates the results across all distributed processes using the `all_reduce` operation. If + any process finds NaN or inf values, it is considered a critical error, and the main coordinator process will + return True to halt training in the main training loop. + + Parameters: + model (torch.nn.Module): The PyTorch model to check for NaN or inf weights. + + Returns: + bool: Returns True if any NaN or inf tensors are found in the model's weights. Otherwise, returns False. + """ + local_has_nan_or_inf = contains_nan_or_inf_tensors(model) + + # Use all_reduce to aggregate local_has_nan across all processes and sum the result into global_has_nan, which + # will be a tensor with a single element on all processes after the all_reduce operation. + global_has_nan_or_inf = torch.tensor(int(local_has_nan_or_inf), device=self.device) + self.distributed.allreduce(global_has_nan_or_inf) + + # The main coordinator process will raise a runtime error if any of the processes found NaN or inf weights. + if self.distributed.local_rank() == 0: + if global_has_nan_or_inf.item() > 0: + logger.warning("NaN or inf tensors found in the model. Stopping training.") + return True + return False + + def train_online(self, dataset): + self.dist_model.train() # Sets model training mode. + with dataset.initialize_batcher( + batch_size=self.batch_size, + should_shuffle=self.should_shuffle, + distributed=self.distributed, + ignore_last=True, + ) as batcher: + # training step loop + progress_bar_config = { + "desc": "Training online", + "total": batcher.steps_per_epoch, + "file": sys.stdout, + "disable": is_progressbar_disabled(), + } + progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator()) + + while not batcher.last_batch(): + batch = batcher.next_batch() + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to( + self.device + ) + for i_feat in self.model.input_features.values() + } + targets = { + o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to( + self.device + ) + for o_feat in self.model.output_features.values() + } + + self.train_step( + inputs, + targets, + ) + + progress_bar.update(1) + + progress_bar.close() + return self.model + + @property + def validation_field(self): + return self._validation_field + + @property + def validation_metric(self): + return self._validation_metric + + def evaluation(self, dataset, dataset_name, metrics_log, batch_size, progress_tracker): + predictor = Predictor( + self.dist_model, + batch_size=batch_size, + distributed=self.distributed, + report_tqdm_to_ray=self.report_tqdm_to_ray, + model=self.model, + ) + metrics, _ = predictor.batch_evaluation(dataset, collect_predictions=False, dataset_name=dataset_name) + + return append_metrics(self.model, dataset_name, metrics, metrics_log, progress_tracker) + + def check_progress_on_validation( + self, + progress_tracker, + validation_output_feature_name, + validation_metric: str, + save_path, + model_hyperparameters_path, + increase_batch_size_on_plateau, + increase_batch_size_on_plateau_patience, + increase_batch_size_on_plateau_rate, + increase_batch_size_on_plateau_max, + increase_batch_size_eval_metric, + increase_batch_size_eval_split, + early_stopping_steps: int, + skip_save_model, + checkpoint_manager: CheckpointManager, + ) -> bool: + """Checks the history of validation scores. + + Uses history of validation scores to reduce learning rate, increase batch size, and decide whether training + should stop. + + Saves the model if scores have improved. + + Returns whether the model should stop training. + """ + should_break = False + improved_fn = get_improved_fn(validation_metric) + + all_validation_metrics = progress_tracker.validation_metrics[validation_output_feature_name] + # The most recent validation_metric metric. + eval_metric: TrainerMetric = all_validation_metrics[validation_metric][-1] + eval_metric_value = eval_metric[-1] + + if eval_metric_value != eval_metric_value: + # Fallback to 0 if the validation metric value is a NaN. + # This is potentially relevant for small datasets like those used in testing where if there's only a + # single output label, some metrics like ROC may turn out to be NaN. + # However, we want to guarantee that the model will be saved at least once over a full + # training-checkpoint-eval-loop. + eval_metric_value = 0 + + if improved_fn(eval_metric_value, progress_tracker.best_eval_metric_value): + previous_best_eval_metric_value = progress_tracker.best_eval_metric_value + + # Save the value, steps, epoch, and checkpoint number. + progress_tracker.best_eval_metric_value = eval_metric_value + progress_tracker.best_eval_metric_steps = progress_tracker.steps + progress_tracker.best_eval_metric_epoch = progress_tracker.epoch + progress_tracker.best_eval_metric_checkpoint_number = progress_tracker.checkpoint_number + + # Save best metrics for all data subsets. + progress_tracker.best_eval_train_metrics = get_latest_metrics_dict(progress_tracker.train_metrics) + progress_tracker.best_eval_validation_metrics = get_latest_metrics_dict(progress_tracker.validation_metrics) + progress_tracker.best_eval_test_metrics = get_latest_metrics_dict(progress_tracker.test_metrics) + + if self.is_coordinator(): + logger.info( + f"Evaluation validation metric: '{validation_output_feature_name}' '{validation_metric}' improved." + ) + absolute_eval_metric_value_change = round( + abs(previous_best_eval_metric_value - progress_tracker.best_eval_metric_value), 3 + ) + if get_metric_objective(validation_metric) == MINIMIZE: + logger.info( + f"'{validation_output_feature_name}' '{validation_metric}' decreased by " + f"{absolute_eval_metric_value_change}." + ) + else: + logger.info( + f"'{validation_output_feature_name}' '{validation_metric}' increased by " + f"{absolute_eval_metric_value_change}." + ) + + # Save the model. + if not skip_save_model: + logger.info("New best model saved.\n") + checkpoint_manager.save_best(progress_tracker.steps) + self.callback(lambda c: c.on_save_best_checkpoint(self, progress_tracker, save_path)) + + # Save top-K checkpoint for model soup + if checkpoint_manager.top_k > 0: + is_minimize = get_metric_objective(validation_metric) == MINIMIZE + checkpoint_manager.save_top_k(progress_tracker.steps, eval_metric_value, is_minimize) + + last_improvement_in_steps = progress_tracker.steps - progress_tracker.best_eval_metric_steps + progress_tracker.last_improvement_steps = last_improvement_in_steps + + if last_improvement_in_steps != 0 and self.is_coordinator(): + logger.info( + f"Last improvement of {validation_output_feature_name} validation {validation_metric} happened " + + f"{last_improvement_in_steps} step(s) ago.\n" + ) + + # ========== Learning Rate Schedule evaluation updates ======== + self.scheduler.eval_step(progress_tracker, validation_output_feature_name) + + # ========== Increase Batch Size Plateau logic ========= + if increase_batch_size_on_plateau > 0: + self.increase_batch_size( + progress_tracker, + validation_output_feature_name, + increase_batch_size_on_plateau, + increase_batch_size_on_plateau_patience, + increase_batch_size_on_plateau_rate, + increase_batch_size_on_plateau_max, + increase_batch_size_eval_metric, + increase_batch_size_eval_split, + ) + progress_tracker.last_increase_batch_size = ( + progress_tracker.steps - progress_tracker.last_increase_batch_size_steps + ) + if ( + progress_tracker.last_increase_batch_size > 0 + and progress_tracker.last_increase_batch_size_eval_metric_improvement > 0 + and not progress_tracker.num_increases_batch_size >= increase_batch_size_on_plateau + and not progress_tracker.batch_size >= increase_batch_size_on_plateau_max + ): + logger.info( + "Last batch size increase " + f"happened {progress_tracker.last_increase_batch_size} step(s) ago, " + f"improvement of {validation_output_feature_name} {increase_batch_size_eval_split} " + f"{increase_batch_size_eval_metric} happened " + f"{progress_tracker.last_increase_batch_size_eval_metric_improvement} step(s) ago." + ) + + # ========== Early Stop logic ========== + # If any early stopping condition is satisfied, either lack of improvement for many steps, or via callbacks on + # any worker, then trigger early stopping. + early_stop_bool = 0 < early_stopping_steps <= last_improvement_in_steps + if not early_stop_bool: + for callback in self.callbacks: + if callback.should_early_stop(self, progress_tracker, self.is_coordinator()): + early_stop_bool = True + break + + should_early_stop = torch.as_tensor([early_stop_bool], dtype=torch.int, device=self.device) + should_early_stop = self.distributed.allreduce(should_early_stop) + if should_early_stop.item(): + if self.is_coordinator(): + logger.info( + f"\nEARLY STOPPING due to lack of validation improvement. It has been {last_improvement_in_steps} " + "step(s) since last validation improvement." + ) + should_break = True + return should_break + + def set_steps_to_1_or_quit(self, signum, frame): + """Custom SIGINT handler used to elegantly exit training. + + A single SIGINT will stop training after the next training step. A second SIGINT will stop training immediately. + """ + if not self.received_sigint: + self.total_steps = 1 + self.received_sigint = True + logger.critical("\nReceived SIGINT, will finish this training step and then conclude training.") + logger.critical("Send another SIGINT to immediately interrupt the process.") + else: + logger.critical("\nReceived a second SIGINT, will now quit") + if self.original_sigint_handler: + signal.signal(signal.SIGINT, self.original_sigint_handler) + sys.exit(1) + + def _handle_pause(self, signum, frame): + """SIGUSR1 handler: gracefully pause training after the current batch. + + Ludwig Studio sends SIGUSR1 to pause a run. Training resumes on SIGUSR2. + """ + self._training_paused = True + print("Ludwig: training paused (SIGUSR1). Send SIGUSR2 to resume.", flush=True) + while self._training_paused: + time.sleep(0.5) + + def _handle_resume(self, signum, frame): + """SIGUSR2 handler: resume training paused by SIGUSR1.""" + self._training_paused = False + print("Ludwig: training resumed (SIGUSR2).", flush=True) + + @staticmethod + def resume_files_exist( + training_progress_tracker_path: str, + training_checkpoint_path: str, + ) -> bool: + missing_files = [] + # training_progress.json + if not path_exists(training_progress_tracker_path): + missing_files.append(training_progress_tracker_path) + # latest.ckpt in training_checkpoints/ + latest_ckpt = os.path.join(training_checkpoint_path, "latest.ckpt") + if not path_exists(latest_ckpt): + missing_files.append(latest_ckpt) + if missing_files: + logger.warning(f"Could not find {missing_files} while trying to resume model training.") + return False + return True + + def resume_training_progress_tracker(self, training_progress_tracker_path): + progress_tracker_dict = None + if self.is_coordinator(): + logger.info(f"Loading progress tracker for model: {training_progress_tracker_path}") + progress_tracker_dict = load_json(training_progress_tracker_path) + + logger.debug("Broadcasting model progress tracker dict to all workers") + progress_tracker_dict = self.distributed.broadcast_object( + progress_tracker_dict, name="broadcast_progress_tracker" + ) + + progress_tracker = ProgressTracker.load(progress_tracker_dict) + return progress_tracker + + def resume_weights_and_optimizer( + self, + model_weights_progress_path: str, + checkpoint: Checkpoint, + ): + CheckpointManager.load_latest_checkpoint(checkpoint, model_weights_progress_path, self.device) + + def increase_batch_size( + self, + progress_tracker: ProgressTracker, + validation_output_feature_name: str, + increase_batch_size_on_plateau: int, + increase_batch_size_on_plateau_patience: int, + increase_batch_size_on_plateau_rate: float, + increase_batch_size_on_plateau_max: int, + increase_batch_size_eval_metric: str = LOSS, + increase_batch_size_eval_split: str = TRAINING, + ): + """Uses the progress tracker to determine if the batch size should be increased.""" + if ( + not progress_tracker.num_increases_batch_size >= increase_batch_size_on_plateau + and not progress_tracker.batch_size == increase_batch_size_on_plateau_max + ): + if increase_batch_size_eval_split == TRAINING: + split_metrics = progress_tracker.train_metrics + elif increase_batch_size_eval_split == VALIDATION: + split_metrics = progress_tracker.validation_metrics + else: # if increase_batch_size_eval_split == TEST: + split_metrics = progress_tracker.test_metrics + + validation_metric = increase_batch_size_eval_metric + last_metric = split_metrics[validation_output_feature_name][validation_metric][-1] + last_metric_value = last_metric[-1] + + improved_fn = get_improved_fn(validation_metric) + is_improved = improved_fn(last_metric_value, progress_tracker.best_increase_batch_size_eval_metric) + if is_improved: + # We update the best metric value and set it to the current one, and reset last + # improvement step count + progress_tracker.best_increase_batch_size_eval_metric = last_metric_value + progress_tracker.last_increase_batch_size_eval_metric_improvement = 0 + else: + progress_tracker.last_increase_batch_size_eval_metric_improvement += 1 + if not is_improved and ( + # Batch size increase happened more than N steps ago + progress_tracker.last_increase_batch_size >= increase_batch_size_on_plateau_patience + and ( + # No improvement of the evaluation metric since more than N steps ago + progress_tracker.last_increase_batch_size_eval_metric_improvement + >= increase_batch_size_on_plateau_patience + ) + ): + progress_tracker.batch_size = min( + int(increase_batch_size_on_plateau_rate * progress_tracker.batch_size), + increase_batch_size_on_plateau_max, + ) + + if self.is_coordinator(): + logger.info( + f"PLATEAU REACHED, increasing batch size to {progress_tracker.batch_size} due to lack of " + f"improvement of {validation_output_feature_name} {increase_batch_size_eval_split} " + f"{validation_metric}." + ) + + progress_tracker.last_increase_batch_size_steps = progress_tracker.steps + progress_tracker.last_increase_batch_size = 0 + progress_tracker.num_increases_batch_size += 1 + + if progress_tracker.num_increases_batch_size >= increase_batch_size_on_plateau: + if self.is_coordinator(): + logger.info( + f"Batch size was already increased {progress_tracker.num_increases_batch_size} times, " + "not increasing it anymore." + ) + elif progress_tracker.batch_size >= increase_batch_size_on_plateau_max: + if self.is_coordinator(): + logger.info( + f"Batch size was already increased {progress_tracker.num_increases_batch_size} times, " + f"currently it is {progress_tracker.batch_size}, the maximum allowed." + ) + + def is_coordinator(self): + return self.distributed.rank() == 0 + + @property + def local_rank(self) -> int: + return self.distributed.local_rank() + + def barrier(self): + self.distributed.barrier() + + def callback(self, fn, coordinator_only=True): + if not coordinator_only or self.is_coordinator(): + for callback in self.callbacks: + fn(callback) + + @property + def return_device(self): + return self.device + + +class RemoteTrainer(Trainer): + def __init__(self, gpus=None, gpu_memory_limit=None, allow_parallel_threads=True, **kwargs): + super().__init__(**kwargs) + + # Only return results from rank 0 to reduce network overhead + self.train = self.distributed.return_first(self.train) + self.train_online = self.distributed.return_first(self.train_online) + + @property + def return_device(self): + # When returning the model weights from remote to driver, place them on CPU, + # as the driver likely doesn't have a GPU. + return "cpu" + + +learning_rate_scale_fns = { + "linear": lambda n: n, + "sqrt": lambda n: math.sqrt(n), + "constant": lambda n: 1, +} diff --git a/ludwig/trainers/trainer_dpo.py b/ludwig/trainers/trainer_dpo.py new file mode 100644 index 0000000..5cfe38d --- /dev/null +++ b/ludwig/trainers/trainer_dpo.py @@ -0,0 +1,187 @@ +"""DPO (Direct Preference Optimization) trainer for LLM fine-tuning. + +Trains a language model to prefer "chosen" completions over "rejected" ones +without needing a separate reward model. Based on Rafailov et al., NeurIPS 2023. + +Expects data with columns: prompt, chosen (preferred completion), rejected (dispreferred completion). + +Config: + model_type: llm + trainer: + type: dpo + beta: 0.1 + loss_type: sigmoid # or ipo +""" + +import logging + +import torch + +from ludwig.constants import LOGITS, USED_TOKENS +from ludwig.modules.dpo_loss import dpo_loss +from ludwig.trainers.registry import register_llm_trainer +from ludwig.trainers.trainer import Trainer + +logger = logging.getLogger(__name__) + + +@register_llm_trainer("dpo") +class DPOTrainer(Trainer): + """Direct Preference Optimization trainer. + + Requires input data with 'chosen' and 'rejected' text columns. The model learns to assign higher probability to + chosen completions over rejected ones. + + The DPO loss replaces the standard next-token cross-entropy loss with a preference-based objective that implicitly + learns a reward function. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.beta = getattr(self.config, "dpo_beta", 0.1) + self.loss_type = getattr(self.config, "dpo_loss_type", "sigmoid") + self.label_smoothing = getattr(self.config, "dpo_label_smoothing", 0.0) + + # Reference model log probs can be pre-computed and cached. + # For simplicity, we use the implicit reference (no separate model). + # To use an explicit reference model, set reference_free=False and + # provide reference log probs during training. + self._reference_chosen_log_probs = None + self._reference_rejected_log_probs = None + + logger.info(f"DPO trainer initialized: beta={self.beta}, loss_type={self.loss_type}") + + def train_step(self, inputs, targets, should_step=True, profiler=None): + """Override train_step to compute DPO loss instead of standard CE loss. + + Expects inputs to contain 'chosen' and 'rejected' keys with tokenized sequences. The model computes forward + passes on both and uses DPO loss to train. + """ + import contextlib + + with torch.amp.autocast("cuda") if self.use_amp else contextlib.nullcontext(): + with self.distributed.prepare_model_update(self.dist_model, should_step=should_step): + # Forward pass on chosen completions + chosen_outputs = self.dist_model((inputs, targets)) + chosen_logits = None + for key, val in chosen_outputs.items(): + if LOGITS in key: + chosen_logits = val + break + + # For DPO, we need a second forward pass on rejected completions. + # In Ludwig's current data pipeline, chosen and rejected are separate + # features. If 'rejected' targets exist, use them; otherwise fall back + # to standard loss. + rejected_targets = {} + has_rejected = False + for key in targets: + if "rejected" in key.lower(): + rejected_targets[key.replace("rejected", "chosen").replace("_rejected", "")] = targets[key] + has_rejected = True + + if has_rejected and chosen_logits is not None: + # Forward pass on rejected completions + rejected_outputs = self.dist_model((inputs, rejected_targets)) + rejected_logits = None + for key, val in rejected_outputs.items(): + if LOGITS in key: + rejected_logits = val + break + + if rejected_logits is not None: + # Get labels (token IDs) for chosen and rejected + chosen_labels = next(iter(targets.values())) + rejected_labels = next(iter(rejected_targets.values())) + + loss, chosen_rewards, rejected_rewards = dpo_loss( + policy_chosen_logits=chosen_logits, + policy_rejected_logits=rejected_logits, + chosen_labels=chosen_labels, + rejected_labels=rejected_labels, + reference_chosen_log_probs=self._reference_chosen_log_probs, + reference_rejected_log_probs=self._reference_rejected_log_probs, + beta=self.beta, + label_smoothing=self.label_smoothing, + loss_type=self.loss_type, + ) + loss = loss / self.gradient_accumulation_steps + else: + # Fallback to standard loss if rejected logits not available + loss, all_losses = self.model.train_loss( + targets, chosen_outputs, self.regularization_type, self.regularization_lambda + ) + loss = loss / self.gradient_accumulation_steps + else: + # No rejected data available, use standard CE loss + loss, all_losses = self.model.train_loss( + targets, chosen_outputs, self.regularization_type, self.regularization_lambda + ) + loss = loss / self.gradient_accumulation_steps + + used_tokens = chosen_outputs.get(USED_TOKENS, torch.tensor(0)) + + # Backward pass + if self.use_amp: + self.scaler.scale(loss).backward() + else: + self.distributed.backward(loss, self.dist_model) + + if not should_step: + return loss, {}, used_tokens + + self.distributed.wait_optimizer_synced(self.optimizer) + + if self.use_amp: + self.scaler.unscale_(self.optimizer) + + if self.distributed.allow_clip_gradients(): + self.clip_grads(self.dist_model.parameters()) + + with self.distributed.prepare_optimizer_update(self.optimizer): + if self.use_amp: + self.scaler.step(self.optimizer) + else: + self.distributed.step(self.optimizer) + + if self.use_amp: + self.scaler.update() + + self.distributed.zero_grad(self.optimizer) + + if profiler: + profiler.step() + + return loss, {}, used_tokens + + +@register_llm_trainer("kto") +class KTOTrainer(DPOTrainer): + """Kahneman-Tversky Optimization trainer (Ethayarajh et al., 2024).""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.beta = getattr(self.config, "kto_beta", 0.1) + self.loss_type = "kto" + + +@register_llm_trainer("orpo") +class ORPOTrainer(DPOTrainer): + """Odds Ratio Preference Optimization trainer (Hong et al., 2024).""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.beta = getattr(self.config, "orpo_beta", 0.1) + self.loss_type = "orpo" + + +@register_llm_trainer("grpo") +class GRPOTrainer(DPOTrainer): + """Group Relative Policy Optimization trainer (Shao et al., 2024, DeepSeek-R1).""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.beta = getattr(self.config, "grpo_beta", 0.04) + self.epsilon = getattr(self.config, "grpo_epsilon", 0.2) + self.num_generations = getattr(self.config, "grpo_num_generations", 4) + self.loss_type = "grpo" diff --git a/ludwig/trainers/trainer_llm.py b/ludwig/trainers/trainer_llm.py new file mode 100644 index 0000000..84fb223 --- /dev/null +++ b/ludwig/trainers/trainer_llm.py @@ -0,0 +1,530 @@ +import logging +import os +import time +from collections.abc import Callable +from typing import Union + +from torch.utils.tensorboard import SummaryWriter + +from ludwig.constants import MINIMUM_BATCH_SIZE, TEST, TRAINING, VALIDATION +from ludwig.data.dataset.base import Dataset +from ludwig.distributed.base import DistributedStrategy, LocalStrategy +from ludwig.features.feature_utils import LudwigFeatureDict +from ludwig.globals import MODEL_FILE_NAME +from ludwig.models.llm import LLM +from ludwig.models.predictor import LlmFineTunePredictor, LlmPredictor +from ludwig.modules.metric_modules import get_initial_validation_value +from ludwig.schema.model_types.base import ModelConfig +from ludwig.schema.trainer import BaseTrainerConfig, FineTuneTrainerConfig, InferenceOnlyTrainerConfig +from ludwig.trainers.base import BaseTrainer +from ludwig.trainers.registry import register_llm_ray_trainer, register_llm_trainer +from ludwig.trainers.trainer import Trainer +from ludwig.types import ModelConfigDict +from ludwig.utils import time_utils +from ludwig.utils.batch_size_tuner import ( + BatchSizeEvaluator, + LLMFinetunePredictBatchSizeEvaluator, + LLMFinetuneTrainerBatchSizeEvaluator, +) +from ludwig.utils.defaults import default_random_seed +from ludwig.utils.metric_utils import TrainerMetric +from ludwig.utils.metrics_printed_table import print_metrics_table +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.torch_utils import get_torch_device +from ludwig.utils.trainer_utils import append_metrics, get_new_progress_tracker, ProgressTracker + +logger = logging.getLogger(__name__) + +MAX_EVALUATION_EXAMPLES = 1000 +MAX_EVALUATION_EXAMPLES_SHOWN = 5 + + +@register_llm_trainer("none") +@register_llm_ray_trainer("none") +class InferenceOnlyTrainer(BaseTrainer): + """InferenceOnlyTrainer is a trainer that does not train a model, only runs evaluation.""" + + def __init__( + self, + config: InferenceOnlyTrainerConfig, + model: LLM, + resume: float = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + callbacks: list | None = None, + report_tqdm_to_ray=False, + random_seed: float = default_random_seed, + distributed: DistributedStrategy | None = None, + device: str | None = None, + **kwargs, + ): + """ + Args: + config: `ludwig.schema.trainer.InferenceOnlyTrainerConfig` instance that specifies training hyperparameters + (default: `ludwig.schema.trainer.InferenceOnlyTrainerConfig()`). + model: Underlying Ludwig model (`ludwig.models.llm.LLM`). + resume: Resume training a model that was being trained. (default: False). + skip_save_model: Disables saving model weights and hyperparameters each time the model improves. By + default Ludwig saves model weights after each round of evaluation the validation metric improves, but + if the model is really big that can be time consuming. If you do not want to keep the weights and just + find out what performance a model can get with a set of hyperparameters, use this parameter to skip it, + but the model will not be loadable later on. (default: False). + skip_save_progress: Disables saving progress each round of evaluation. By default Ludwig saves weights + and stats after each round of evaluation for enabling resuming of training, but if the model is really + big that can be time consuming and will uses twice as much space, use this parameter to skip it, but + training cannot be resumed later on. (default: False). + skip_save_log: Disables saving TensorBoard logs. By default Ludwig saves logs for the TensorBoard, but if + it is not needed turning it off can slightly increase the overall speed. (default: False). + callbacks: List of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline. + (default: None). + report_tqdm_to_ray: Enables using the ray based tqdm Callback for progress bar reporting. + random_seed: Default initialization for the random seeds (default: 42). + distributed: Distributed strategy (default: None). + device: Device to load the model on from a saved checkpoint (default: None). + """ + + super().__init__() + + # Ensure distributed strategy is initialized for metric sync_context. + # InferenceOnlyTrainer may run on the head node (not in a Ray Train worker), + # so init_dist_strategy may not have been called yet. + from ludwig.distributed import init_dist_strategy + + init_dist_strategy("local") + + self.config = config + self.distributed = distributed if distributed is not None else LocalStrategy() + self.skip_save_log = skip_save_log + self.resume = resume + self.skip_save_model = skip_save_model + self.skip_save_progress = skip_save_progress + self.random_seed = random_seed + self.callbacks = callbacks or [] + self.report_tqdm_to_ray = report_tqdm_to_ray + + self.device = device if device is not None else get_torch_device() + self.model = model.to_device(self.device) + self.model.metrics_to_device(self.device) + + # Since we are only running evaluation without training, set the model to evaluation mode. + self.model.eval() + + self.batch_size = self.config.batch_size + self.eval_batch_size = self.config.eval_batch_size + self.base_learning_rate = self.config.base_learning_rate + self.should_shuffle = self.config.should_shuffle + self.epochs = self.config.epochs + self.train_steps = self.config.train_steps + self.steps_per_checkpoint = self.config.steps_per_checkpoint + self.checkpoints_per_epoch = self.config.checkpoints_per_epoch + self.early_stop = self.config.early_stop + self.evaluate_training_set = self.config.evaluate_training_set + self.skip_all_evaluation = self.config.skip_all_evaluation + + def close_writers( + self, progress_tracker, save_path, train_summary_writer, validation_summary_writer, test_summary_writer + ): + # ================ Finished Training ================ + self.callback( + lambda c: c.on_trainer_train_teardown(self, progress_tracker, save_path, self.is_coordinator()), + coordinator_only=False, + ) + + if train_summary_writer is not None: + train_summary_writer.close() + if validation_summary_writer is not None: + validation_summary_writer.close() + if test_summary_writer is not None: + test_summary_writer.close() + + def train( + self, + training_set: Dataset, + validation_set: Dataset | None = None, + test_set: Dataset | None = None, + save_path: str = MODEL_FILE_NAME, + return_state_dict: bool = False, + **kwargs, + ): + output_features = self.model.output_features + + # ====== Setup file names ======= + tensorboard_log_dir = None + if self.is_coordinator(): + os.makedirs(save_path, exist_ok=True) + tensorboard_log_dir = os.path.join(save_path, "logs") + + self.callback( + lambda c: c.on_trainer_train_setup(self, save_path, self.is_coordinator()), coordinator_only=False + ) + + train_summary_writer = None + validation_summary_writer = None + test_summary_writer = None + if self.is_coordinator() and not self.skip_save_log and tensorboard_log_dir: + train_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TRAINING)) + if validation_set is not None and validation_set.size > 0: + validation_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, VALIDATION)) + if test_set is not None and test_set.size > 0: + test_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TEST)) + + set_random_seed(self.random_seed) + + progress_tracker = get_new_progress_tracker( + batch_size=self.batch_size, + learning_rate=self.base_learning_rate, + best_eval_metric_value=get_initial_validation_value(self.validation_metric), + best_increase_batch_size_eval_metric=get_initial_validation_value(self.validation_metric), + output_features=output_features, + ) + + # When running with Ray, we only need to return the state dict, as it's faster and cheaper to send the + # state dict over the network than to load the model state here, serialize it back to a state dict, then + # load it back on the head node. + return_value = self.model if not return_state_dict else self.model.cpu().state_dict() + + if self.skip_all_evaluation: + self.close_writers( + progress_tracker, save_path, train_summary_writer, validation_summary_writer, test_summary_writer + ) + return ( + return_value, + progress_tracker.train_metrics, + progress_tracker.validation_metrics, + progress_tracker.test_metrics, + ) + + try: + self.run_evaluation( + training_set, + validation_set, + test_set, + progress_tracker, + train_summary_writer, + validation_summary_writer, + test_summary_writer, + output_features, + save_path, + ) + finally: + self.close_writers( + progress_tracker, save_path, train_summary_writer, validation_summary_writer, test_summary_writer + ) + + return ( + return_value, + progress_tracker.train_metrics, + progress_tracker.validation_metrics, + progress_tracker.test_metrics, + ) + + def train_online( + self, + dataset, + ): + pass + + def tune_batch_size( + self, + config: ModelConfig | ModelConfigDict, + training_set: Dataset, + random_seed: int = default_random_seed, + max_trials: int = 20, + halving_limit: int = 3, + snapshot_weights: bool = True, + on_best_batch_size_updated: Callable[[int, float, int], None] | None = None, + tune_for_training: bool = True, + ) -> int: + # TODO: Implement batch size tuning for LLM, currently just returns the default batch size + # Compared to ECD, this just requires forward passes till we OOM. + # https://github.com/ludwig-ai/ludwig/issues/3525 + return MINIMUM_BATCH_SIZE + + @property + def validation_field(self): + return self.config.validation_field + + @property + def validation_metric(self): + return self.config.validation_metric + + # Remote implementations may override this + def shutdown(self): + pass + + @property + def local_rank(self) -> int: + return 0 + + def barrier(self): + pass + + # Functions needed to treat Trainer as a context manager + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown() + + @staticmethod + def get_schema_cls() -> BaseTrainerConfig: + return InferenceOnlyTrainerConfig + + def is_coordinator(self) -> bool: + return self.distributed.rank() == 0 + + def callback(self, fn, coordinator_only=True): + if not coordinator_only or self.is_coordinator(): + for callback in self.callbacks: + fn(callback) + + def evaluation( + self, + dataset: "Dataset", + dataset_name: str, + metrics_log: dict[str, dict[str, list[TrainerMetric]]], + batch_size: int, + progress_tracker: ProgressTracker, + ): + predictor = LlmPredictor( + self.model, batch_size=batch_size, distributed=self.distributed, report_tqdm_to_ray=self.report_tqdm_to_ray + ) + metrics, _ = predictor.batch_evaluation(dataset, collect_predictions=False, dataset_name=dataset_name) + + return append_metrics(self.model, dataset_name, metrics, metrics_log, progress_tracker) + + @classmethod + def write_eval_summary( + cls, + summary_writer, + metrics, + step, + ): + if not summary_writer: + return + + for feature_name, output_feature in metrics.items(): + for metric_name, metrics in output_feature.items(): + if metrics: + metric_tag = f"{feature_name}/epoch_{metric_name}" + metric_val = metrics[-1][-1] + summary_writer.add_scalar(metric_tag, metric_val, global_step=step) + summary_writer.flush() + + def run_evaluation( + self, + training_set: Union["Dataset", "RayDataset"], # noqa: F821 + validation_set: Union["Dataset", "RayDataset"] | None, # noqa: F821 + test_set: Union["Dataset", "RayDataset"] | None, # noqa: F821 + progress_tracker: ProgressTracker, + train_summary_writer: SummaryWriter, + validation_summary_writer: SummaryWriter, + test_summary_writer: SummaryWriter, + output_features: LudwigFeatureDict, + save_path: str, + ) -> bool: + """Runs evaluation over training, validation, and test sets. + + Also: + - Prints results, saves results to the progress tracker. + - Saves the model if the validation score is the best so far + - If there is no validation set, the model is always saved. + + Returns whether the trainer should early stop, based on validation metrics history. + """ + start_time = time.time() + self.callback(lambda c: c.on_eval_start(self, progress_tracker, save_path)) + + progress_tracker.checkpoint_number += 1 + if self.is_coordinator(): + logger.info(f"\nRunning evaluation for step: {progress_tracker.steps}, epoch: {progress_tracker.epoch}") + + # ================ Eval ================ + # Run a separate pass over the training data to compute metrics + # Appends results to progress_tracker.train_metrics. + if self.evaluate_training_set: + self.evaluation( + training_set, "train", progress_tracker.train_metrics, self.eval_batch_size, progress_tracker + ) + + self.write_eval_summary( + summary_writer=train_summary_writer, + metrics=progress_tracker.train_metrics, + step=progress_tracker.steps, + ) + + if validation_set is not None: + self.callback(lambda c: c.on_validation_start(self, progress_tracker, save_path)) + + # eval metrics on validation set + self.evaluation( + validation_set, + VALIDATION, + progress_tracker.validation_metrics, + self.eval_batch_size, + progress_tracker, + ) + + self.write_eval_summary( + summary_writer=validation_summary_writer, + metrics=progress_tracker.validation_metrics, + step=progress_tracker.steps, + ) + + self.callback(lambda c: c.on_validation_end(self, progress_tracker, save_path)) + + if test_set is not None: + self.callback(lambda c: c.on_test_start(self, progress_tracker, save_path)) + + # eval metrics on test set + self.evaluation(test_set, TEST, progress_tracker.test_metrics, self.eval_batch_size, progress_tracker) + + self.write_eval_summary( + summary_writer=test_summary_writer, + metrics=progress_tracker.test_metrics, + step=progress_tracker.steps, + ) + + self.callback(lambda c: c.on_test_end(self, progress_tracker, save_path)) + + elapsed_time = (time.time() - start_time) * 1000.0 + + if self.is_coordinator(): + logger.info(f"Evaluation took {time_utils.strdelta(elapsed_time)}\n") + print_metrics_table( + output_features, + progress_tracker.train_metrics, + progress_tracker.validation_metrics, + progress_tracker.test_metrics, + ) + + # Trigger eval end callback after any model weights save for complete checkpoint + self.callback(lambda c: c.on_eval_end(self, progress_tracker, save_path)) + + return False + + +@register_llm_trainer("finetune") +class FineTuneTrainer(Trainer): + @staticmethod + def get_schema_cls(): + return FineTuneTrainerConfig + + def __init__( + self, + config: FineTuneTrainerConfig, + model: LLM, + resume: float = False, + skip_save_model: bool = False, + skip_save_progress: bool = False, + skip_save_log: bool = False, + callbacks: list | None = None, + report_tqdm_to_ray=False, + random_seed: int = default_random_seed, + distributed: DistributedStrategy | None = None, + device: str | None = None, + **kwargs, + ): + super().__init__( + config, + model, + resume, + skip_save_model, + skip_save_progress, + skip_save_log, + callbacks, + report_tqdm_to_ray, + random_seed, + distributed, + device, + **kwargs, + ) + + def evaluation(self, dataset, dataset_name, metrics_log, batch_size, progress_tracker): + predictor = LlmFineTunePredictor( + self.model, batch_size=batch_size, distributed=self.distributed, report_tqdm_to_ray=self.report_tqdm_to_ray + ) + metrics, _, input_target_output_dict = predictor.batch_evaluation( + dataset, collect_predictions=False, dataset_name=dataset_name + ) + # Setting collect_predictions=True currently causes an error when doing batch evaluation because the outputs + # can be of variable sizes but we try to concatenate them into a single tensor. + + tokenizer = self.dist_model.tokenizer + + # There should only be one key in the dict for LLMs + input_key = list(input_target_output_dict["inputs"].keys())[0] + num_examples = min(len(input_target_output_dict["inputs"][input_key]), MAX_EVALUATION_EXAMPLES) + + llm_eval_examples = {"inputs": [], "targets": [], "outputs": []} + for key in input_target_output_dict["inputs"]: + for inp in input_target_output_dict["inputs"][key][:num_examples]: + llm_eval_examples["inputs"].append(tokenizer.decode(inp, skip_special_tokens=True)) + + for key in input_target_output_dict["targets"]: + for tar in input_target_output_dict["targets"][key][:num_examples]: + llm_eval_examples["targets"].append(tokenizer.decode(tar, skip_special_tokens=True)) + + for key in input_target_output_dict["outputs"]: + for out in input_target_output_dict["outputs"][key][:num_examples]: + llm_eval_examples["outputs"].append(tokenizer.decode(out, skip_special_tokens=True)) + + num_examples_shown = min(len(llm_eval_examples["inputs"]), MAX_EVALUATION_EXAMPLES_SHOWN) + for i in range(num_examples_shown): + logger.info(f"Input: {llm_eval_examples['inputs'][i].strip()}") + logger.info(f"Output: {llm_eval_examples['outputs'][i].strip()}") + logger.info("--------------------") + + progress_tracker.llm_eval_examples = llm_eval_examples + return append_metrics(self.model, dataset_name, metrics, metrics_log, progress_tracker) + + def tune_batch_size( + self, + config: ModelConfig | ModelConfigDict, + training_set: Dataset, + random_seed: int = default_random_seed, + max_trials: int = 20, + halving_limit: int = 3, + snapshot_weights: bool = True, + on_best_batch_size_updated: Callable[[int, float, int], None] | None = None, + tune_for_training: bool = True, + global_max_sequence_length: int | None = None, + ) -> int: + if global_max_sequence_length is None: + global_max_sequence_length = self.model.global_max_sequence_length + return super().tune_batch_size( + config, + training_set, + random_seed, + max_trials, + halving_limit, + snapshot_weights, + on_best_batch_size_updated, + tune_for_training, + global_max_sequence_length, + ) + + def _create_batch_size_evaluator(self) -> BatchSizeEvaluator: + return LLMFinetuneTrainerBatchSizeEvaluator(self) + + def _create_predict_batch_size_evaluator(self) -> BatchSizeEvaluator: + return LLMFinetunePredictBatchSizeEvaluator(self) + + +class RemoteLLMTrainer(InferenceOnlyTrainer): + def __init__(self, gpus=None, gpu_memory_limit=None, allow_parallel_threads=True, **kwargs): + super().__init__(**kwargs) + + # Only return results from rank 0 to reduce network overhead + self.train = self.distributed.return_first(self.train) + self.train_online = self.distributed.return_first(self.train_online) + + +class RemoteLLMFineTuneTrainer(FineTuneTrainer): + def __init__(self, gpus=None, gpu_memory_limit=None, allow_parallel_threads=True, **kwargs): + super().__init__(**kwargs) + + # Only return results from rank 0 to reduce network overhead + self.train = self.distributed.return_first(self.train) + self.train_online = self.distributed.return_first(self.train_online) diff --git a/ludwig/types.py b/ludwig/types.py new file mode 100644 index 0000000..ab61fd3 --- /dev/null +++ b/ludwig/types.py @@ -0,0 +1,164 @@ +"""Public API: Common typing for Ludwig dictionary parameters. + +These TypedDicts document the shape of the dicts flowing through Ludwig's +public surface. They use ``total=False`` so that callers can omit optional +keys without triggering type errors. The legacy ``dict[str, Any]`` aliases +are kept for backward compatibility but are deprecated — prefer the TypedDicts. +""" + +from __future__ import annotations + +from typing import Any, TypedDict + +# --------------------------------------------------------------------------- +# Feature configuration +# --------------------------------------------------------------------------- + + +class FeatureConfigDict(TypedDict, total=False): + """Parameters used to configure a single input or output feature. + + See https://ludwig.ai/latest/configuration/features/supported_data_types/ + """ + + name: str + type: str + column: str + tied: str | None + encoder: dict[str, Any] + decoder: dict[str, Any] + preprocessing: dict[str, Any] + loss: dict[str, Any] + output_size: int + num_fc_layers: int + fc_layers: list[dict[str, Any]] + + +# --------------------------------------------------------------------------- +# Model configuration +# --------------------------------------------------------------------------- + + +class ModelConfigDict(TypedDict, total=False): + """Dictionary representation of the ModelConfig object. + + See https://ludwig.ai/latest/configuration/ + """ + + model_type: str + input_features: list[FeatureConfigDict] + output_features: list[FeatureConfigDict] + combiner: dict[str, Any] + trainer: dict[str, Any] + preprocessing: dict[str, Any] + defaults: dict[str, Any] + hyperopt: dict[str, Any] + backend: dict[str, Any] + ludwig_version: str + preset: str + + +# --------------------------------------------------------------------------- +# Training set metadata +# --------------------------------------------------------------------------- + + +class FeatureMetadataDict(TypedDict, total=False): + """Metadata for a single feature, produced during preprocessing. + + Contents are feature-type-specific; common keys are listed here. + """ + + idx2str: list[str] + str2idx: dict[str, int] + str2freq: dict[str, int] + vocab_size: int + max_sequence_length: int + reshape: list[int] | None + mean: float + std: float + min: float + max: float + missing_value_strategy: str + computed_fill_value: float | str | None + lazy: bool + mode: str + prefetch_size: int | None + lazy_audio_params: dict[str, Any] + lazy_image_params: dict[str, Any] + + +class TrainingSetMetadataDict(TypedDict, total=False): + """Training set metadata produced during preprocessing and saved alongside the dataset cache. + + Top-level keys are feature names; values are :class:`FeatureMetadataDict`. + Global keys (e.g. ``preprocessing_parameters``) are also present. + """ + + preprocessing_parameters: dict[str, Any] + + +# --------------------------------------------------------------------------- +# Preprocessing / trainer / hyperopt config dicts +# --------------------------------------------------------------------------- + + +class PreprocessingConfigDict(TypedDict, total=False): + """Parameters used to configure preprocessing (global or per-feature). + + See https://ludwig.ai/latest/configuration/preprocessing/ + """ + + split: dict[str, Any] + sample_ratio: float + oversample_minority: float | None + undersample_majority: float | None + + +class TrainerConfigDict(TypedDict, total=False): + """Parameters used to configure training. + + See https://ludwig.ai/latest/configuration/trainer/ + """ + + type: str + epochs: int + batch_size: int | str + learning_rate: float | str + optimizer: dict[str, Any] + regularization_type: str | None + regularization_lambda: float + gradient_clipping: dict[str, Any] + eval_steps: int + early_stop: int + steps_per_checkpoint: int + + +class HyperoptConfigDict(TypedDict, total=False): + """Parameters used to configure hyperparameter optimisation. + + See https://ludwig.ai/latest/configuration/hyperparameter_optimization/ + """ + + executor: dict[str, Any] + search_alg: dict[str, Any] + parameters: dict[str, Any] + goal: str + metric: str + output_feature: str + split: str + + +# --------------------------------------------------------------------------- +# Misc composite dicts +# --------------------------------------------------------------------------- + + +FeatureTypeDefaultsDict = dict[str, FeatureConfigDict] +"""Dictionary mapping feature type name → default FeatureConfigDict. + +See https://ludwig.ai/latest/configuration/defaults/ +""" + +FeaturePostProcessingOutputDict = dict[str, Any] +"""Output from feature post-processing (feature-type-specific shapes).""" diff --git a/ludwig/upload.py b/ludwig/upload.py new file mode 100644 index 0000000..29f3cba --- /dev/null +++ b/ludwig/upload.py @@ -0,0 +1,153 @@ +import argparse +import logging +import os +import sys + +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, model_weights_exist +from ludwig.utils.print_utils import get_logging_level_registry +from ludwig.utils.upload_utils import HuggingFaceHub + +logger = logging.getLogger(__name__) + + +def get_upload_registry(): + return { + "hf_hub": HuggingFaceHub, + } + + +def upload_cli( + service: str, + repo_id: str, + model_path: str, + repo_type: str = "model", + private: bool = False, + commit_message: str = "Upload trained [Ludwig](https://ludwig.ai/latest/) model weights", + commit_description: str | None = None, + dataset_file: str | None = None, + dataset_name: str | None = None, + **kwargs, +) -> None: + """Create an empty repo on the HuggingFace Hub and upload trained model artifacts to that repo. + + Args: + service (`str`): + Name of the hosted model service to push the trained artifacts to. + Currently only `hf_hub` is supported. + repo_id (`str`): + A namespace (user or an organization) and a repo name separated + by a `/`. + model_path (`str`): + The path of the saved model. This is the parent-folder of the folder + where the 'model_weights' folder and the 'model_hyperparameters.json' file + are stored. + private (`bool`, *optional*, defaults to `False`): + Whether the model repo should be private. + repo_type (`str`, *optional*): + Set to `"dataset"` or `"space"` if uploading to a dataset or + space, `None` or `"model"` if uploading to a model. Default is + `None`. + commit_message (`str`, *optional*): + The summary / title / first line of the generated commit. Defaults to: + `f"Upload {path_in_repo} with huggingface_hub"` + commit_description (`str` *optional*): + The description of the generated commit + dataset_file (`str`, *optional*): + The path to the dataset file. + dataset_name (`str`, *optional*): + The name of the dataset. + """ + model_service = get_upload_registry().get(service, "hf_hub") + hub: HuggingFaceHub = model_service() + if model_weights_exist(os.path.join(model_path, MODEL_FILE_NAME)) and os.path.exists( + os.path.join(model_path, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME) + ): + experiment_path = model_path + elif model_weights_exist(model_path) and os.path.exists(os.path.join(model_path, MODEL_HYPERPARAMETERS_FILE_NAME)): + experiment_path = os.path.normpath(os.path.join(model_path, "..")) + else: + raise ValueError( + f"Can't find model weights and '{MODEL_HYPERPARAMETERS_FILE_NAME}' either at " + f"'{model_path}' or at '{model_path}/model'" + ) + hub.upload( + repo_id=repo_id, + model_path=experiment_path, + repo_type=repo_type, + private=private, + commit_message=commit_message, + commit_description=commit_description, + dataset_file=dataset_file, + dataset_name=dataset_name, + ) + + +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script pushes a trained model to a hosted model repository service", + prog="ludwig upload", + usage="%(prog)s [options]", + ) + + # --------------- + # Required parameters + # --------------- + parser.add_argument( + "service", + help="Name of the model repository service.", + default="hf_hub", + choices=["hf_hub"], + ) + + parser.add_argument( + "-r", + "--repo_id", + help="Name of the repo. This will be created if it doesn't exist. Format: username/repo_name", + required=True, + ) + + parser.add_argument("-m", "--model_path", help="Path of the trained model on disk", required=True) + + # --------------- + # Optional parameters + # --------------- + parser.add_argument("-p", "--private", help="Make the repo private", default=False, choices=[True, False]) + + parser.add_argument( + "-t", "--repo_type", help="Type of repo", default="model", choices=["model", "space", "dataset"] + ) + + parser.add_argument( + "-c", + "--commit_message", + help="The summary / title / first line of the generated commit.", + default="Upload trained [Ludwig](https://ludwig.ai/latest/) model weights", + ) + + parser.add_argument("-d", "--commit_description", help="The description of the generated commit", default=None) + + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="The level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + parser.add_argument("-df", "--dataset_file", help="The location of the dataset file", default=None) + parser.add_argument( + "-dn", "--dataset_name", help="(Optional) The name of the dataset in the Provider", default=None + ) + + args = parser.parse_args(sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.upload") + + upload_cli(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/utils/__init__.py b/ludwig/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/utils/algorithms_utils.py b/ludwig/utils/algorithms_utils.py new file mode 100644 index 0000000..5a3c895 --- /dev/null +++ b/ludwig/utils/algorithms_utils.py @@ -0,0 +1,88 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from ludwig.constants import TIED + + +def topological_sort(graph_unsorted): + """Repeatedly go through all of the nodes in the graph, moving each of the nodes that has all its edges + resolved, onto a sequence that forms our sorted graph. + + A node has all of its edges resolved and can be moved once all the nodes its edges point to, have been moved from + the unsorted graph onto the sorted one. + """ + + # This is the list we'll return, that stores each node/edges pair + # in topological order. + graph_sorted = [] + + # Convert the unsorted graph into a hash table. This gives us + # constant-time lookup for checking if edges are unresolved, and + # for removing nodes from the unsorted graph. + graph_unsorted = dict(graph_unsorted) + + # Run until the unsorted graph is empty. + while graph_unsorted: + # Go through each of the node/edges pairs in the unsorted + # graph. If a set of edges does not contain any nodes that + # haven't been resolved, that is, that are still in the + # unsorted graph, remove the pair from the unsorted graph, + # and append it to the sorted graph. Note here that by using + # using the items() method for iterating, a copy of the + # unsorted graph is used, allowing us to modify the unsorted + # graph as we move through it. We also keep a flag for + # checking that that graph is acyclic, which is true if any + # nodes are resolved during each pass through the graph. If + # not, we need to bail out as the graph therefore can't be + # sorted. + acyclic = False + for node, edges in list(graph_unsorted.items()): + if edges is None: + edges = [] + for edge in edges: + if edge in graph_unsorted: + break + else: + acyclic = True + del graph_unsorted[node] + graph_sorted.append((node, edges)) + + if not acyclic: + # Uh oh, we've passed through all the unsorted nodes and + # weren't able to resolve any of them, which means there + # are nodes with cyclic edges that will never be resolved, + # so we bail out with an error. + cyclic_nodes = list(graph_unsorted.keys()) + raise RuntimeError( + f"Cyclic dependency detected among output features: {cyclic_nodes}.\n" + "Check that no output feature lists another output feature as a dependency of itself." + ) + + return graph_sorted + + +def topological_sort_feature_dependencies(features): + # topological sorting of output features for resolving dependencies + dependencies_graph = {} + output_features_dict = {} + for feature in features: + dependencies = [] + if "dependencies" in feature: + dependencies.extend(feature["dependencies"]) + if TIED in feature: + dependencies.append(feature[TIED]) + dependencies_graph[feature["name"]] = dependencies + output_features_dict[feature["name"]] = feature + return [output_features_dict[node[0]] for node in topological_sort(dependencies_graph)] diff --git a/ludwig/utils/audio_utils.py b/ludwig/utils/audio_utils.py new file mode 100644 index 0000000..3dfc035 --- /dev/null +++ b/ludwig/utils/audio_utils.py @@ -0,0 +1,397 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import functools +import logging +from io import BytesIO +from typing import Any + +import torch +import torch.nn.functional as F +import torchaudio + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DEFAULT_AUDIO_TENSOR_LENGTH +from ludwig.utils.types import TorchAudioTuple + +logger = logging.getLogger(__name__) + +# https://github.com/pytorch/audio/blob/main/torchaudio/csrc/sox/types.cpp +AUDIO_EXTENSIONS = (".wav", ".amb", ".mp3", ".ogg", ".vorbis", ".flac", ".opus", ".sphere") + + +@DeveloperAPI +def is_torch_audio_tuple(audio: Any) -> bool: + if isinstance(audio, tuple): + if len(audio) == 2 and isinstance(audio[0], torch.Tensor) and isinstance(audio[1], int): + return True + return False + + +@DeveloperAPI +def get_default_audio(audio_lst: list[TorchAudioTuple]) -> TorchAudioTuple: + if not audio_lst: + # Return a silent audio tensor as default when no valid audio is available + default_audio_tensor = torch.zeros(1, DEFAULT_AUDIO_TENSOR_LENGTH) + return default_audio_tensor, 16000 + + sampling_rates = [audio[1] for audio in audio_lst] + tensor_list = [audio[0] for audio in audio_lst] + + for i, tensor in enumerate(tensor_list): + if tensor.shape[1] > DEFAULT_AUDIO_TENSOR_LENGTH: + tensor_list[i] = tensor[:, :DEFAULT_AUDIO_TENSOR_LENGTH] + else: + pad_size = DEFAULT_AUDIO_TENSOR_LENGTH - tensor.shape[1] + tensor_list[i] = F.pad(tensor, (0, pad_size)) + default_audio_tensor = torch.mean(torch.stack(tensor_list), dim=0) + default_sampling_rate = calculate_mean(sum(sampling_rates), len(sampling_rates)) + + return default_audio_tensor, default_sampling_rate + + +@DeveloperAPI +def read_audio_from_path(path: str) -> TorchAudioTuple | None: + """Reads audio from path. + + Useful for reading from a small number of paths. For more intensive reads, use backend.read_binary_files instead. + """ + try: + return torchaudio.load(path) + except Exception: + logger.warning(f"Failed to load audio from path: {path}", exc_info=True) + return None + + +@DeveloperAPI +@functools.lru_cache(maxsize=32) +def read_audio_from_bytes_obj(bytes_obj: bytes) -> TorchAudioTuple | None: + try: + f = BytesIO(bytes_obj) + return torchaudio.load(f) + except Exception: + logger.warning("Failed to load audio from bytes object.", exc_info=True) + return None + + +def _pre_emphasize_data(data: torch.Tensor, emphasize_value: float = 0.97): + # Increase precision in order to achieve parity with scipy.signal.lfilter implementation + filter_window = torch.tensor([1.0, -emphasize_value], dtype=torch.float64, device=data.device) + a_coeffs = torch.tensor([1, 0], dtype=torch.float64, device=data.device) + pre_emphasized_data = torchaudio.functional.lfilter( + data.to(dtype=torch.float64), + a_coeffs, + filter_window, + clamp=False, + ).to(torch.float32) + return pre_emphasized_data + + +@DeveloperAPI +def get_length_in_samp(sampling_rate_in_hz: float | int, length_in_s: float | int) -> int: + return int(sampling_rate_in_hz * length_in_s) + + +@DeveloperAPI +def get_group_delay( + raw_data: torch.Tensor, + sampling_rate_in_hz: int, + window_length_in_s: float, + window_shift_in_s: float, + num_fft_points: int, + window_type: str, +): + X_stft_transform = _get_stft( + raw_data, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type=window_type + ) + Y_stft_transform = _get_stft( + raw_data, + sampling_rate_in_hz, + window_length_in_s, + window_shift_in_s, + num_fft_points, + window_type=window_type, + data_transformation="group_delay", + ) + X_stft_transform_real = torch.real(X_stft_transform) + X_stft_transform_imag = torch.imag(X_stft_transform) + Y_stft_transform_real = torch.real(Y_stft_transform) + Y_stft_transform_imag = torch.imag(Y_stft_transform) + nominator = torch.multiply(X_stft_transform_real, Y_stft_transform_real) + torch.multiply( + X_stft_transform_imag, Y_stft_transform_imag + ) + denominator = torch.square(torch.abs(X_stft_transform)) + group_delay = torch.divide(nominator, denominator + 1e-10) + if torch.isnan(group_delay).any(): + raise RuntimeError("NaN values detected in computed group delay. Check input audio data for degenerate values.") + return torch.transpose(group_delay, 0, 1) + + +@DeveloperAPI +def get_phase_stft_magnitude( + raw_data: torch.Tensor, + sampling_rate_in_hz: int, + window_length_in_s: float, + window_shift_in_s: float, + num_fft_points: int, + window_type: str, +) -> torch.Tensor: + stft = _get_stft( + raw_data, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type=window_type + ) + abs_stft = torch.abs(stft) + phase = torch.angle(stft) + stft_phase = torch.cat([phase, abs_stft], dim=1) + return torch.transpose(stft_phase, 0, 1) + + +@DeveloperAPI +def get_stft_magnitude( + raw_data: torch.Tensor, + sampling_rate_in_hz: int, + window_length_in_s: float, + window_shift_in_s: float, + num_fft_points: int, + window_type: str, +): + stft = _get_stft( + raw_data, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type=window_type + ) + stft_magnitude = torch.abs(stft) + return torch.transpose(stft_magnitude, 0, 1) + + +################################################################################ +# The following code for FBank is adapted from jameslyons/python_speech_features +# MIT licensed implementation +# https://github.com/jameslyons/python_speech_features/blob/40c590269b57c64a8c1f1ddaaff2162008d1850c/python_speech_features/base.py#L84################################################################################ +################################################################################ +@DeveloperAPI +def get_fbank( + raw_data: torch.Tensor, + sampling_rate_in_hz: int, + window_length_in_s: float, + window_shift_in_s: float, + num_fft_points: int, + window_type: str, + num_filter_bands: int, +) -> torch.Tensor: + stft = _get_stft( + raw_data, + sampling_rate_in_hz, + window_length_in_s, + window_shift_in_s, + num_fft_points, + window_type=window_type, + zero_mean_offset=True, + ) + stft_power = torch.abs(stft) ** 2 + upper_limit_freq = int(sampling_rate_in_hz / 2) + upper_limit_mel = _convert_hz_to_mel(upper_limit_freq) + lower_limit_mel = 0 + list_mel_points = torch.linspace(lower_limit_mel, upper_limit_mel, num_filter_bands + 2, device=raw_data.device) + mel_fbank_matrix = _get_mel_fbank_matrix(list_mel_points, num_filter_bands, num_fft_points, sampling_rate_in_hz) + mel_fbank_feature = torch.matmul(stft_power, torch.transpose(mel_fbank_matrix, 0, 1)) + log_mel_fbank_feature = torch.log(mel_fbank_feature + 1.0e-10) + return torch.transpose(log_mel_fbank_feature, 0, 1) + + +def _get_mel_fbank_matrix( + list_mel_points: torch.Tensor, num_filter_bands: int, num_fft_points: int, sampling_rate_in_hz: int +) -> torch.Tensor: + num_ess_fft_points = get_non_symmetric_length(num_fft_points) + freq_scale = (num_fft_points + 1) / sampling_rate_in_hz + freq_bins_on_mel_scale = torch.floor(freq_scale * _convert_mel_to_hz(list_mel_points)) + mel_scaled_fbank = torch.zeros( + (num_filter_bands, num_ess_fft_points), dtype=torch.float32, device=list_mel_points.device + ) + for filt_idx in range(num_filter_bands): + start_bin_freq = freq_bins_on_mel_scale[filt_idx] + middle_bin_freq = freq_bins_on_mel_scale[filt_idx + 1] + end_bin_freq = freq_bins_on_mel_scale[filt_idx + 2] + mel_scaled_fbank[filt_idx] = _create_triangular_filter( + start_bin_freq, middle_bin_freq, end_bin_freq, num_ess_fft_points + ) + return mel_scaled_fbank + + +def _create_triangular_filter( + start_bin_freq: torch.Tensor, middle_bin_freq: torch.Tensor, end_bin_freq: torch.Tensor, num_ess_fft_points: int +): + filter_window = torch.zeros(num_ess_fft_points, dtype=torch.float32, device=start_bin_freq.device) + filt_support_begin = middle_bin_freq - start_bin_freq + filt_support_end = end_bin_freq - middle_bin_freq + for freq in range(int(start_bin_freq), int(middle_bin_freq)): + filter_window[freq] = (freq - start_bin_freq) / filt_support_begin + for freq in range(int(middle_bin_freq), int(end_bin_freq)): + filter_window[freq] = (end_bin_freq - freq) / filt_support_end + return filter_window + + +def _convert_hz_to_mel(hz: int) -> float: + return float(2595.0 * torch.log10(torch.tensor(1 + hz / 700.0))) + + +def _convert_mel_to_hz(mel): + return 700.0 * (10 ** (mel / 2595.0) - 1) + + +def _get_stft( + raw_data: torch.Tensor, + sampling_rate_in_hz: int, + window_length_in_s: float, + window_shift_in_s: float, + num_fft_points: int, + window_type: str, + data_transformation: str | None = None, + zero_mean_offset: bool = False, +) -> torch.Tensor: + pre_emphasized_data = _pre_emphasize_data(raw_data) + stft = _short_time_fourier_transform( + pre_emphasized_data, + sampling_rate_in_hz, + window_length_in_s, + window_shift_in_s, + num_fft_points, + window_type, + data_transformation, + zero_mean_offset, + ) + non_symmetric_stft = get_non_symmetric_data(stft) + return non_symmetric_stft + + +def _short_time_fourier_transform( + data: torch.Tensor, + sampling_rate_in_hz: int, + window_length_in_s: float, + window_shift_in_s: float, + num_fft_points: int, + window_type: str, + data_transformation: str | None = None, + zero_mean_offset: bool = False, +) -> torch.Tensor: + window_length_in_samp: int = get_length_in_samp(window_length_in_s, sampling_rate_in_hz) + window_shift_in_samp: int = get_length_in_samp(window_shift_in_s, sampling_rate_in_hz) + preprocessed_data_matrix = _preprocess_to_padded_matrix( + data[0], window_length_in_samp, window_shift_in_samp, zero_mean_offset=zero_mean_offset + ) + weighted_data_matrix = _weight_data_matrix( + preprocessed_data_matrix, window_type, data_transformation=data_transformation + ) + fft = torch.fft.fft(weighted_data_matrix, n=num_fft_points) + return fft + + +def _preprocess_to_padded_matrix( + data: torch.Tensor, window_length_in_samp: int, window_shift_in_samp: int, zero_mean_offset: bool = False +) -> torch.Tensor: + num_input = data.shape[0] + num_output = get_num_output_padded_to_fit_input(num_input, window_length_in_samp, window_shift_in_samp) + zero_padded_matrix = torch.zeros((num_output, window_length_in_samp), dtype=torch.float32, device=data.device) + for num_output_idx in range(num_output): + start_idx = window_shift_in_samp * num_output_idx + is_last_output = num_output_idx == num_output - 1 + end_idx = start_idx + window_length_in_samp if not is_last_output else num_input + end_padded_idx = window_length_in_samp if not is_last_output else end_idx - start_idx + window_data = data[start_idx:end_idx] + if zero_mean_offset: + window_data = window_data - torch.mean(window_data) + zero_padded_matrix[num_output_idx, :end_padded_idx] = window_data + return zero_padded_matrix + + +@DeveloperAPI +def get_num_output_padded_to_fit_input(num_input: int, window_length_in_samp: int, window_shift_in_samp: int) -> int: + num_output_valid = torch.tensor((num_input - window_length_in_samp) / window_shift_in_samp + 1) + return int(torch.ceil(num_output_valid)) + + +@DeveloperAPI +def get_window(window_type: str, window_length_in_samp: int, device: torch.device | None = None) -> torch.Tensor: + # Increase precision in order to achieve parity with scipy.signal.windows.get_window implementation + if window_type == "bartlett": + return torch.bartlett_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to( + torch.float32 + ) + elif window_type == "blackman": + return torch.blackman_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to( + torch.float32 + ) + elif window_type == "hamming": + return torch.hamming_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to( + torch.float32 + ) + elif window_type == "hann": + return torch.hann_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to( + torch.float32 + ) + else: + raise ValueError(f"Unknown window type: {window_type}") + + +@DeveloperAPI +def is_audio_score(src_path): + # Used for AutoML + return int(isinstance(src_path, str) and src_path.lower().endswith(AUDIO_EXTENSIONS)) + + +def _weight_data_matrix( + data_matrix: torch.Tensor, window_type: str, data_transformation: str | None = None +) -> torch.Tensor: + window_length_in_samp = data_matrix[0].shape[0] + window = get_window(window_type, window_length_in_samp, device=data_matrix.device) + if data_transformation is not None and data_transformation == "group_delay": + window *= torch.arange(window_length_in_samp, device=data_matrix.device).float() + return data_matrix * window + + +@DeveloperAPI +def get_non_symmetric_length(symmetric_length: int) -> int: + return int(symmetric_length / 2) + 1 + + +@DeveloperAPI +def get_non_symmetric_data(data: torch.Tensor) -> torch.Tensor: + num_fft_points = data.shape[-1] + num_ess_fft_points = get_non_symmetric_length(num_fft_points) + return data[:, :num_ess_fft_points] + + +@DeveloperAPI +def get_max_length_stft_based(length_in_samp, window_length_in_s, window_shift_in_s, sampling_rate_in_hz): + window_length_in_samp = get_length_in_samp(window_length_in_s, sampling_rate_in_hz) + window_shift_in_samp = get_length_in_samp(window_shift_in_s, sampling_rate_in_hz) + return get_num_output_padded_to_fit_input(length_in_samp, window_length_in_samp, window_shift_in_samp) + + +@DeveloperAPI +def calculate_incr_var(var_prev, mean_prev, mean, length): + return var_prev + (length - mean_prev) * (length - mean) + + +@DeveloperAPI +def calculate_incr_mean(count, mean, length): + return mean + (length - mean) / float(count) + + +@DeveloperAPI +def calculate_var(sum1, sum2, count): + return (sum2 - ((sum1 * sum1) / float(count))) / float(count - 1) if count > 1 else 0.0 + + +@DeveloperAPI +def calculate_mean(sum1, count): + return sum1 / float(count) diff --git a/ludwig/utils/augmentation_utils.py b/ludwig/utils/augmentation_utils.py new file mode 100644 index 0000000..09a8c7c --- /dev/null +++ b/ludwig/utils/augmentation_utils.py @@ -0,0 +1,56 @@ +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.registry import Registry + +### +# Registry for augmentation operations +# Each augmentation operation is registered with the feature type it is applicable to +# and the name of the operation. +### +_augmentation_op_registry = Registry() + + +@DeveloperAPI +def get_augmentation_op_registry() -> Registry: + return _augmentation_op_registry + + +@DeveloperAPI +def register_augmentation_op(name: str, features: str | list[str]): + if isinstance(features, str): + features = [features] + + def wrap(cls): + for feature in features: + augmentation_op_registry = get_augmentation_op_registry().get(feature, {}) + augmentation_op_registry[name] = cls + get_augmentation_op_registry()[feature] = augmentation_op_registry + return cls + + return wrap + + +@DeveloperAPI +def get_augmentation_op(feature_type: str, op_name: str): + return get_augmentation_op_registry()[feature_type][op_name] + + +class AugmentationPipelines: + """Container holding augmentation pipelines defined in the model.""" + + def __init__(self, augmentation_pipelines: dict): + self.augmentation_pipelines = augmentation_pipelines + + def __getitem__(self, key): + return self.augmentation_pipelines[key] + + def __contains__(self, key): + return key in self.augmentation_pipelines + + def __len__(self): + return len(self.augmentation_pipelines) + + def __iter__(self): + return self.augmentation_pipelines.__iter__() + + def items(self): + return self.augmentation_pipelines.items() diff --git a/ludwig/utils/automl/__init__.py b/ludwig/utils/automl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ludwig/utils/automl/data_source.py b/ludwig/utils/automl/data_source.py new file mode 100644 index 0000000..beea88d --- /dev/null +++ b/ludwig/utils/automl/data_source.py @@ -0,0 +1,137 @@ +from abc import ABC, abstractmethod + +import dask.dataframe as dd +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.audio_utils import is_audio_score +from ludwig.utils.automl.utils import avg_num_tokens +from ludwig.utils.image_utils import is_image_score +from ludwig.utils.misc_utils import memoized_method +from ludwig.utils.types import DataFrame + + +@DeveloperAPI +class DataSource(ABC): + @property + @abstractmethod + def columns(self) -> list[str]: + raise NotImplementedError() + + @abstractmethod + def get_dtype(self, column: str) -> str: + raise NotImplementedError() + + @abstractmethod + def get_distinct_values(self, column: str, max_values_to_return: int) -> tuple[int, list[str], float]: + raise NotImplementedError() + + @abstractmethod + def get_nonnull_values(self, column: str) -> int: + raise NotImplementedError() + + @abstractmethod + def get_avg_num_tokens(self, column: str) -> int: + raise NotImplementedError() + + @abstractmethod + def is_string_type(self, dtype: str) -> bool: + raise NotImplementedError() + + @abstractmethod + def size_bytes(self) -> int: + raise NotImplementedError() + + @abstractmethod + def __len__(self) -> int: + raise NotImplementedError() + + +@DeveloperAPI +class DataframeSourceMixin: + df: DataFrame + + @property + def columns(self) -> list[str]: + return self.df.columns + + def get_dtype(self, column: str) -> str: + return self.df[column].dtype.name + + def get_distinct_values(self, column, max_values_to_return: int) -> tuple[int, list[str], float]: + unique_values = self.df[column].dropna().unique() + num_unique_values = len(unique_values) + unique_values_counts = self.df[column].value_counts() + if len(unique_values_counts) != 0: + unique_majority_values = unique_values_counts[unique_values_counts.idxmax()] + unique_minority_values = unique_values_counts[unique_values_counts.idxmin()] + unique_values_balance = unique_minority_values / unique_majority_values + else: + unique_values_balance = 1.0 + return num_unique_values, unique_values[:max_values_to_return], unique_values_balance + + def get_nonnull_values(self, column: str) -> int: + return len(self.df[column].notnull()) + + def get_image_values(self, column: str, sample_size: int = 10) -> int: + return int(sum(is_image_score(x) for x in self.df[column].head(sample_size))) + + def get_audio_values(self, column: str, sample_size: int = 10) -> int: + return int(sum(is_audio_score(x) for x in self.df[column].head(sample_size))) + + def get_avg_num_tokens(self, column: str) -> int: + return avg_num_tokens(self.df[column]) + + def is_string_type(self, dtype: str) -> bool: + return dtype in ["str", "string", "object"] + + def size_bytes(self) -> int: + return sum(self.df.memory_usage(deep=True)) + + def __len__(self) -> int: + return len(self.df) + + +@DeveloperAPI +class DataframeSource(DataframeSourceMixin, DataSource): + def __init__(self, df): + self.df = df + + +@DeveloperAPI +class DaskDataSource(DataframeSource): + @memoized_method(maxsize=1) + def get_sample(self) -> pd.DataFrame: + # TODO: uniform random sample + return self.df.head(10000) + + @property + def sample(self) -> pd.DataFrame: + return self.get_sample() + + def get_distinct_values(self, column, max_values_to_return) -> tuple[int, list[str], float]: + unique_values = self.df[column].drop_duplicates().dropna().persist() + num_unique_values = len(unique_values) + + # TODO(travis): implement imbalance ratio + imbalance_ratio = 1.0 + return num_unique_values, unique_values.head(max_values_to_return), imbalance_ratio + + def get_nonnull_values(self, column) -> int: + return self.df[column].notnull().sum().compute() + + def get_image_values(self, column: str, sample_size: int = 10) -> int: + return int(sum(is_image_score(x) for x in self.sample[column].head(sample_size))) + + def get_audio_values(self, column: str, sample_size: int = 10) -> int: + return int(sum(is_audio_score(x) for x in self.sample[column].head(sample_size))) + + def get_avg_num_tokens(self, column) -> int: + return avg_num_tokens(self.sample[column]) + + +@DeveloperAPI +def wrap_data_source(df: DataFrame) -> DataSource: + if isinstance(df, dd.DataFrame): + return DaskDataSource(df) + return DataframeSource(df) diff --git a/ludwig/utils/automl/field_info.py b/ludwig/utils/automl/field_info.py new file mode 100644 index 0000000..a67cb30 --- /dev/null +++ b/ludwig/utils/automl/field_info.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass + +from dataclasses_json import dataclass_json, LetterCase + +from ludwig.api_annotations import DeveloperAPI + + +@DeveloperAPI +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class FieldInfo: + name: str + dtype: str + key: str = None + distinct_values: list = None + distinct_values_balance: float = 1.0 + num_distinct_values: int = 0 + nonnull_values: int = 0 + image_values: int = 0 + audio_values: int = 0 + avg_words: int = None + + +@DeveloperAPI +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class FieldConfig: + name: str + column: str + type: str + + +@DeveloperAPI +@dataclass_json(letter_case=LetterCase.CAMEL) +@dataclass +class FieldMetadata: + name: str + config: FieldConfig + excluded: bool + mode: str + missing_values: float + imbalance_ratio: float diff --git a/ludwig/utils/automl/ray_utils.py b/ludwig/utils/automl/ray_utils.py new file mode 100644 index 0000000..16e2554 --- /dev/null +++ b/ludwig/utils/automl/ray_utils.py @@ -0,0 +1,18 @@ +import os + +from ludwig.backend.ray import initialize_ray + +try: + import ray +except ImportError: + raise ImportError(" ray is not installed. In order to use auto_train please run pip install ludwig[ray]") + + +def _ray_init(): + if ray.is_initialized(): + return + + # Forcibly terminate trial requested to stop after this amount of time passes + os.environ.setdefault("TUNE_FORCE_TRIAL_CLEANUP_S", "120") + + initialize_ray() diff --git a/ludwig/utils/automl/type_inference.py b/ludwig/utils/automl/type_inference.py new file mode 100644 index 0000000..ec971b7 --- /dev/null +++ b/ludwig/utils/automl/type_inference.py @@ -0,0 +1,104 @@ +import logging + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUDIO, BINARY, CATEGORY, DATE, IMAGE, NUMBER, TEXT +from ludwig.utils import strings_utils +from ludwig.utils.automl.field_info import FieldInfo + +# For a given feature, the highest percentage of distinct values out of the total number of rows that we might still +# assign the CATEGORY type. +CATEGORY_TYPE_DISTINCT_VALUE_PERCENTAGE_CUTOFF = 0.5 + +# Consider the field a valid text field if it has at least 5 average words. Fewer than this and it may be a cateogry +# or an ID field (like a name or place) of some kind. +TEXT_AVG_WORDS_CUTOFF = 5 + + +@DeveloperAPI +def infer_type(field: FieldInfo, missing_value_percent: float, row_count: int) -> str: + """Perform type inference on field. + + Args: + field: Object describing field. + missing_value_percent: Percent of missing values in the column. + row_count: Total number of entries in original dataset. + + Returns: + Feature type string. + """ + if field.dtype == DATE or field.dtype.startswith("datetime"): + return DATE + + num_distinct_values = field.num_distinct_values + distinct_values = field.distinct_values + + if num_distinct_values <= 1: + return CATEGORY + + if num_distinct_values == 2 and missing_value_percent == 0: + # Check that all distinct values are conventional bools. + if strings_utils.are_conventional_bools(distinct_values): + return BINARY + + if field.image_values >= 3: + return IMAGE + + if field.audio_values >= 3: + return AUDIO + + if strings_utils.are_all_datetimes(distinct_values): + return DATE + + # Use CATEGORY if: + # - The number of distinct values is significantly less than the total number of examples. + # - The distinct values are not all numbers. + # - The distinct values are all numbers but comprise of a perfectly sequential list of integers that suggests the + # values represent categories. + valid_row_count = row_count * (1.0 - missing_value_percent) + if num_distinct_values < valid_row_count * CATEGORY_TYPE_DISTINCT_VALUE_PERCENTAGE_CUTOFF and ( + (not strings_utils.are_all_numbers(distinct_values)) or strings_utils.are_sequential_integers(distinct_values) + ): + return CATEGORY + + # Use NUMBER if all of the distinct values are numbers. + if strings_utils.are_all_numbers(distinct_values): + return NUMBER + + # TODO (ASN): add other modalities (image, etc. ) + # Fallback to TEXT. + return TEXT + + +@DeveloperAPI +def should_exclude( + idx: int, field: FieldInfo, dtype: str, column_count: int, row_count: int, targets: set[str] +) -> bool: + if field.key == "PRI": + logging.info(f"Exclude {field.name} ({dtype}): primary key") + return True + + if field.name in targets: + return False + + if field.num_distinct_values <= 1: + logging.info(f"Exclude {field.name} ({dtype}): less than 2 distinct values") + return True + + distinct_value_percent = float(field.num_distinct_values) / row_count + if distinct_value_percent == 1.0: + upper_name = field.name.upper() + if ( + (idx == 0 and "INDEX" in upper_name and dtype == NUMBER) + or upper_name.endswith("ID") + or upper_name.startswith("ID") + ): + logging.info(f"Exclude {field.name} ({dtype}): unique ID column") + return True + + # For TEXT fields, we only want to use them if they appear "interesting", otherwise we would rather exclude + # them and treat the problem as a tabular problem + if column_count > 3 and dtype == TEXT and (field.avg_words or 0) < TEXT_AVG_WORDS_CUTOFF: + logging.info(f"Exclude {field.name} ({dtype}): too few average words") + return True + + return False diff --git a/ludwig/utils/automl/utils.py b/ludwig/utils/automl/utils.py new file mode 100644 index 0000000..e32b6a3 --- /dev/null +++ b/ludwig/utils/automl/utils.py @@ -0,0 +1,160 @@ +import bisect +import logging + +from numpy import nan_to_num +from pandas import Series + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + BINARY, + CATEGORY, + COMBINER, + CONFIG, + HYPEROPT, + IMBALANCE_DETECTION_RATIO, + NAME, + NUMBER, + PARAMETERS, + SEARCH_ALG, + TRAINER, + TYPE, +) +from ludwig.features.feature_registries import get_output_type_registry +from ludwig.modules.metric_registry import get_metric_objective +from ludwig.schema.combiners.utils import get_combiner_jsonschema + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def avg_num_tokens_decoder(x): + if x is None: + return None + if type(x) is bytes: + return x.decode("utf-8") + return str(x) + + +@DeveloperAPI +def avg_num_tokens(field: Series) -> int: + logger.info(f"Calculating average number tokens for field {field.name} using sample of 100 rows.") + field_sample = field.head(100).apply(avg_num_tokens_decoder) + + unique_entries = field_sample.unique() + avg_words = round(nan_to_num(Series(unique_entries).str.split().str.len().mean())) + return avg_words + + +@DeveloperAPI +def get_model_type(config: dict) -> str: + if ( + "input_features" in config + and len(config["input_features"]) == 1 + and "type" in config["input_features"][0] + and config["input_features"][0]["type"] == "text" + ): + model_type = "text" + elif COMBINER in config and TYPE in config[COMBINER]: + model_type = config[COMBINER][TYPE] + else: + default_combiner_type = get_combiner_jsonschema()["properties"]["type"]["default"] + model_type = default_combiner_type + return model_type + + +# ref_configs comes from a file storing the config for a high-performing model per reference dataset. +# If the automl model type matches that of any reference models, set the initial point_to_evaluate +# in the automl hyperparameter search to the config of the reference model with the closest-matching +# input number columns ratio. This model config "transfer learning" can improve the automl search. +def _add_transfer_config(base_config: dict, ref_configs: dict) -> dict: + base_model_type = base_config[COMBINER][TYPE] + base_model_numeric_ratio = _get_ratio_numeric_input_features(base_config["input_features"]) + min_numeric_ratio_distance = 1.0 + min_dataset = None + + for dataset in ref_configs["datasets"]: + dataset_config = dataset[CONFIG] + if base_model_type == dataset_config[COMBINER][TYPE]: + dataset_numeric_ratio = _get_ratio_numeric_input_features(dataset_config["input_features"]) + ratio_distance = abs(base_model_numeric_ratio - dataset_numeric_ratio) + if ratio_distance <= min_numeric_ratio_distance: + min_numeric_ratio_distance = ratio_distance + min_dataset = dataset + + if min_dataset is not None: + logger.info("Transfer config from dataset {}".format(min_dataset["name"])) + min_dataset_config = min_dataset[CONFIG] + hyperopt_params = base_config[HYPEROPT][PARAMETERS] + point_to_evaluate = {} + _add_option_to_evaluate(point_to_evaluate, min_dataset_config, hyperopt_params, COMBINER) + _add_option_to_evaluate(point_to_evaluate, min_dataset_config, hyperopt_params, TRAINER) + base_config[HYPEROPT][SEARCH_ALG]["points_to_evaluate"] = [point_to_evaluate] + return base_config + + +def _get_ratio_numeric_input_features(input_features: dict) -> float: + num_input_features = len(input_features) + num_numeric_input = 0 + for input_feature in input_features: + if input_feature[TYPE] == NUMBER: + num_numeric_input = num_numeric_input + 1 + return num_numeric_input / num_input_features + + +# Update point_to_evaluate w/option value from dataset_config for options in hyperopt_params. +# Also, add option value to associated categories list if it is not already included. +def _add_option_to_evaluate( + point_to_evaluate: dict, dataset_config: dict, hyperopt_params: dict, option_type: str +) -> dict: + options = dataset_config[option_type] + for option in options: + option_param = option_type + "." + option + if option_param in hyperopt_params: + option_val = options[option] + point_to_evaluate[option_param] = option_val + if option_val not in hyperopt_params[option_param]["categories"]: + bisect.insort(hyperopt_params[option_param]["categories"], option_val) + return point_to_evaluate + + +@DeveloperAPI +def set_output_feature_metric(base_config): + """If single output feature, set trainer and hyperopt metric and goal for that feature if not set.""" + if len(base_config["output_features"]) != 1: + # If multiple output features, ludwig uses the goal of minimizing combined loss; + # this could be revisited/refined in the future. + return base_config + output_name = base_config["output_features"][0][NAME] + output_type = base_config["output_features"][0][TYPE] + output_metric = get_output_type_registry()[output_type].get_schema_cls().default_validation_metric + output_goal = get_metric_objective(output_metric) + if "validation_field" not in base_config[TRAINER] and "validation_metric" not in base_config[TRAINER]: + base_config[TRAINER]["validation_field"] = output_name + base_config[TRAINER]["validation_metric"] = output_metric + if ( + "output_feature" not in base_config[HYPEROPT] + and "metric" not in base_config[HYPEROPT] + and "goal" not in base_config[HYPEROPT] + ): + base_config[HYPEROPT]["output_feature"] = output_name + base_config[HYPEROPT]["metric"] = output_metric + base_config[HYPEROPT]["goal"] = output_goal + return base_config + + +@DeveloperAPI +def has_imbalanced_output(base_config, features_metadata) -> bool: + """Check binary and category output feature(s) for imbalance, i.e., low minority/majority instance count + ratio.""" + imbalanced_output = False + for output_feature in base_config["output_features"]: + if output_feature[TYPE] == BINARY or output_feature[TYPE] == CATEGORY: + for feature_metadata in features_metadata: + if output_feature[NAME] == feature_metadata.name: + if feature_metadata.imbalance_ratio < IMBALANCE_DETECTION_RATIO: + logger.info( + f"Imbalance in {output_feature[NAME]}: minority/majority={feature_metadata.imbalance_ratio}" + ) + imbalanced_output = True + break + return imbalanced_output diff --git a/ludwig/utils/backward_compatibility.py b/ludwig/utils/backward_compatibility.py new file mode 100644 index 0000000..7dba84a --- /dev/null +++ b/ludwig/utils/backward_compatibility.py @@ -0,0 +1,900 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import copy +import logging +import warnings +from collections.abc import Callable +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + AUDIO, + BIAS, + CLASS_WEIGHTS, + COLUMN, + CONV_BIAS, + CONV_USE_BIAS, + DECODER, + DEFAULT_BIAS, + DEFAULT_USE_BIAS, + DEFAULTS, + ENCODER, + EVAL_BATCH_SIZE, + EXECUTOR, + FORCE_SPLIT, + HEIGHT, + HYPEROPT, + IMAGE, + INPUT_FEATURES, + LOSS, + MISSING_VALUE_STRATEGY, + NAME, + NUM_SAMPLES, + NUMBER, + OUTPUT_FEATURES, + PARAMETERS, + PREPROCESSING, + PROBABILITIES, + RANDOM, + RAY, + SAMPLER, + SCHEDULER, + SEARCH_ALG, + SEQUENCE, + SPLIT, + SPLIT_PROBABILITIES, + STRATIFY, + TEXT, + TIMESERIES, + TRAINER, + TRAINING, + TYPE, + USE_BIAS, + WIDTH, +) +from ludwig.features.feature_registries import get_base_type_registry, get_input_type_registry, get_output_type_registry +from ludwig.globals import LUDWIG_VERSION +from ludwig.types import ( + FeatureConfigDict, + FeatureTypeDefaultsDict, + HyperoptConfigDict, + ModelConfigDict, + PreprocessingConfigDict, + TrainerConfigDict, + TrainingSetMetadataDict, +) +from ludwig.utils.metric_utils import TrainerMetric +from ludwig.utils.misc_utils import get_from_registry, merge_dict +from ludwig.utils.version_transformation import VersionTransformation, VersionTransformationRegistry + +config_transformation_registry = VersionTransformationRegistry() + + +@DeveloperAPI +def register_config_transformation(version: str, prefixes: str | list[str] = []) -> Callable: + """This decorator registers a transformation function for a config version. Version is the first version which + requires the transform. For example, since "training" is renamed to "trainer" in 0.5, this change should be + registered with 0.5. from_version < version <= to_version. + + Args: + version: The version to register this transformation with. The earliest ludwig version which requires this + transformation. + prefixes: A list of keypath prefixes to apply this transformation to. If not specified, transforms the entire + config dict. If a prefix indicates a list, i.e. "input_features", the transformation is applied to + each element of the list (each input feature). + """ + if isinstance(prefixes, str): + prefixes = [prefixes] + + def wrap(fn: Callable[[dict], dict]): + config_transformation_registry.register(VersionTransformation(transform=fn, version=version, prefixes=prefixes)) + return fn + + return wrap + + +@DeveloperAPI +def upgrade_config_dict_to_latest_version(config: ModelConfigDict) -> ModelConfigDict: + """Updates config from an older version of Ludwig to the current version. If config does not have a + "ludwig_version" key, all updates are applied. + + Args: + config: A config saved by an older version of Ludwig. + + Returns A new copy of config, upgraded to the current Ludwig version. Returns config if config has no + "ludwig_version". + """ + return config_transformation_registry.update_config( + config, from_version=config.get("ludwig_version", "0.0"), to_version=LUDWIG_VERSION + ) + + +def upgrade_model_progress(model_progress: dict) -> dict: + """Updates model progress info to be compatible with latest ProgressTracker implementation. + + Notably, we convert epoch-based stats to their step-based equivalents and reformat metrics into `TrainerMetric` + tuples. + """ + ret = copy.deepcopy(model_progress) + + if "last_improvement_epoch" in ret: + ret["last_improvement_steps"] = ret["last_improvement_epoch"] * ret["batch_size"] + del ret["last_improvement_epoch"] + + if "last_learning_rate_reduction_epoch" in ret: + ret["last_learning_rate_reduction_steps"] = ret["last_learning_rate_reduction_epoch"] * ret["batch_size"] + del ret["last_learning_rate_reduction_epoch"] + + if "last_increase_batch_size_epoch" in ret: + ret["last_increase_batch_size_steps"] = ret["last_increase_batch_size_epoch"] * ret["batch_size"] + del ret["last_increase_batch_size_epoch"] + + if "vali_metrics" in ret: + ret["validation_metrics"] = ret["vali_metrics"] + del ret["vali_metrics"] + + for metric_group in ("train_metrics", "test_metrics", "validation_metrics"): + if metric_group not in ret: + continue + for tgt in ret[metric_group]: + for metric in ret[metric_group][tgt]: + if len(ret[metric_group][tgt][metric]) == 0 or isinstance( + ret[metric_group][tgt][metric][0], (tuple, list) + ): + continue + + ret[metric_group][tgt][metric] = [ + TrainerMetric(i + 1, (i + 1) * ret["batch_size"], val) + for i, val in enumerate(ret[metric_group][tgt][metric]) + ] + + if "tune_checkpoint_num" not in ret: + ret["tune_checkpoint_num"] = 0 + + # Upgrades related to extending progress tracker with explicit bests. + if "checkpoint_number" not in ret: + ret["checkpoint_number"] = 0 + + if "best_eval_metric_steps" not in ret: + ret["best_eval_metric_steps"] = 0 + + if "best_eval_metric_epoch" not in ret: + ret["best_eval_metric_epoch"] = 0 + + if "best_eval_metric_checkpoint_number" not in ret: + ret["best_eval_metric_checkpoint_number"] = 0 + + if "best_eval_train_metrics" not in ret: + ret["best_eval_train_metrics"] = {} + + if "best_eval_validation_metrics" not in ret: + ret["best_eval_validation_metrics"] = {} + + if "best_eval_test_metrics" not in ret: + ret["best_eval_test_metrics"] = {} + + if "best_eval_metric" in ret: + ret["best_eval_metric_value"] = ret["best_eval_metric"] + del ret["best_eval_metric"] + + if "last_improvement" in ret: + del ret["last_improvement"] + + # Delete learning-rate related fields removed in https://github.com/ludwig-ai/ludwig/pull/2877. + if "best_reduce_learning_rate_eval_metric" in ret: + del ret["best_reduce_learning_rate_eval_metric"] + + if "last_reduce_learning_rate_eval_metric_improvement" in ret: + del ret["last_reduce_learning_rate_eval_metric_improvement"] + + return ret + + +def _traverse_dicts(config: Any, f: Callable[[dict], None]): + """Recursively applies function f to every dictionary contained in config. + + f should in-place modify the config dict. f will be called on leaves first, root last. + """ + if isinstance(config, dict): + for _k, v in config.items(): + _traverse_dicts(v, f) + f(config) + elif isinstance(config, list): + for v in config: + _traverse_dicts(v, f) + + +@register_config_transformation("0.6", "backend") +def _update_backend_cache_credentials(backend: dict[str, Any]) -> dict[str, Any]: + if "cache_credentials" in backend: + credentials = backend.get("credentials", {}) + if "cache" in credentials: + warnings.warn("`cache` already found in `backend.credentials`, ignoring `cache_credentials`") + else: + warnings.warn( + "`backend.cache_credentials` has been renamed `backend.credentials.cache`", DeprecationWarning + ) + credentials["cache"] = backend.pop("cache_credentials") + backend["credentials"] = credentials + return backend + + +@register_config_transformation("0.6", ["output_features"]) +def update_class_weights_in_features(feature: FeatureConfigDict) -> FeatureConfigDict: + if LOSS in feature: + class_weights = feature[LOSS].get(CLASS_WEIGHTS, None) + if not isinstance(class_weights, (list, dict)): + class_weights = None + feature[LOSS][CLASS_WEIGHTS] = class_weights + + return feature + + +@register_config_transformation("0.4") +def _update_level_metadata(config: ModelConfigDict) -> ModelConfigDict: + # Replace parameters represented as keys with params represented as values. + # Precedence is defined by first in the dictionary order, so if multiple + # provided keys map to the same value, the one that appears earlier in this + # dictionary will take priority. + drop_params = { + "sequence_length_limit": "max_sequence_length", + "word_most_common": "most_common", + "word_sequence_length_limit": "max_sequence_length", + "word_tokenizer": "tokenizer", + "word_vocab_file": "vocab_file", + "char_most_common": "most_common", + "char_sequence_length_limit": "max_sequence_length", + "char_tokenizer": "tokenizer", + "char_vocab_file": "vocab_file", + } + + def upgrade_params(params): + for key, value in drop_params.items(): + if key in params: + if value in params: + warnings.warn( + f"Removing deprecated config preprocessing parameter {key} as new param {value} already " + f"present in the config", + DeprecationWarning, + ) + else: + warnings.warn( + f"Renaming deprecated config preprocessing parameter {key} to {value}", + DeprecationWarning, + ) + params[value] = params[key] + del params[key] + + sequence_types = [SEQUENCE, TEXT, AUDIO, TIMESERIES] + for dtype in sequence_types: + params = config.get(PREPROCESSING, {}).get(dtype, {}) + upgrade_params(params) + + for feature in config[INPUT_FEATURES]: + if feature.get(TYPE) not in sequence_types: + continue + params = feature.get(PREPROCESSING, {}) + upgrade_params(params) + + return config + + +@register_config_transformation("0.5") +def rename_training_to_trainer(config: ModelConfigDict) -> ModelConfigDict: + if TRAINING in config: + warnings.warn( + 'Config section "training" renamed to "trainer" and will be removed in a future version', DeprecationWarning + ) + config[TRAINER] = config[TRAINING] + del config[TRAINING] + return config + + +@register_config_transformation("0.5", ["input_features", "output_features"]) +def _upgrade_use_bias_in_features(feature: FeatureConfigDict) -> FeatureConfigDict: + def upgrade_use_bias(config): + if BIAS in config: + warnings.warn( + 'Parameter "bias" renamed to "use_bias" and will be removed in a future version', DeprecationWarning + ) + config[USE_BIAS] = config[BIAS] + del config[BIAS] + if CONV_BIAS in config: + warnings.warn( + 'Parameter "conv_bias" renamed to "conv_use_bias" and will be removed in a future version', + DeprecationWarning, + ) + config[CONV_USE_BIAS] = config[CONV_BIAS] + del config[CONV_BIAS] + if DEFAULT_BIAS in config: + warnings.warn( + 'Parameter "default_bias" renamed to "default_use_bias" and will be removed in a future version', + DeprecationWarning, + ) + config[DEFAULT_USE_BIAS] = config[DEFAULT_BIAS] + del config[DEFAULT_BIAS] + + _traverse_dicts(feature, upgrade_use_bias) + return feature + + +@register_config_transformation("0.5", ["input_features", "output_features"]) +def _upgrade_feature(feature: FeatureConfigDict) -> FeatureConfigDict: + """Upgrades feature config (in-place)""" + if feature.get(TYPE) == "numerical": + warnings.warn( + 'Feature type "numerical" renamed to "number" and will be removed in a future version', DeprecationWarning + ) + feature[TYPE] = NUMBER + if feature.get(TYPE) == AUDIO: + if PREPROCESSING in feature: + feature[PREPROCESSING] = upgrade_audio_preprocessing(feature[PREPROCESSING]) + warnings.warn( + "Parameters specified at the `audio_feature` parameter level have been unnested and should now " + "be specified at the preprocessing level. Support for `audio_feature` will be removed in a future version", + DeprecationWarning, + ) + return feature + + +def upgrade_audio_preprocessing(preproc_dict: PreprocessingConfigDict) -> PreprocessingConfigDict: + if "audio_feature" in preproc_dict: + for k, v in preproc_dict["audio_feature"].items(): + preproc_dict[k] = v + del preproc_dict["audio_feature"] + return preproc_dict + + +@register_config_transformation("0.6", ["input_features"]) +def _upgrade_encoder_params(feature: FeatureConfigDict) -> FeatureConfigDict: + return _upgrade_encoder_decoder_params(feature, True) + + +@register_config_transformation("0.6", ["output_features"]) +def _upgrade_decoder_params(feature: FeatureConfigDict) -> FeatureConfigDict: + return _upgrade_encoder_decoder_params(feature, False) + + +def _upgrade_encoder_decoder_params(feature: FeatureConfigDict, input_feature: bool) -> FeatureConfigDict: + """ + This function nests un-nested encoder/decoder parameters to conform with the new config structure for 0.6 + Args: + feature (Dict): Feature to nest encoder/decoder params for. + input_feature (Bool): Whether this feature is an input feature or not. + """ + if TYPE not in feature: + return feature + + try: + if input_feature: + module_type = ENCODER + feature_cls = get_from_registry(feature[TYPE], get_input_type_registry()) + else: + module_type = DECODER + feature_cls = get_from_registry(feature[TYPE], get_output_type_registry()) + except ValueError: + logging.exception("Failed to obtain encoder / decoder from registry") + return feature + + feature_schema_cls = feature_cls.get_schema_cls() + feature_keys = feature_schema_cls.get_valid_field_names() + + # These keys have been renamed from the form below to `fc_` in the new config + fc_layer_keys = [ + "fc_layers", + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout", + ] + + module = feature.get(module_type, {}) + + warn = False + if isinstance(module, str): + module = {TYPE: module} + feature[module_type] = module + warn = True + + nested_params = [] + for k, v in feature.items(): + if k not in feature_keys: + module[k] = v + if k in fc_layer_keys and module_type == DECODER: + module[f"fc_{k}"] = v + nested_params.append(k) + warn = True + + if module: + if module_type in feature: + feature[module_type].update(module) + else: + feature[module_type] = module + + for k in nested_params: + del feature[k] + + if warn: + warnings.warn( + f"{module_type} specific parameters should now be nested within a dictionary under the '{module_type}' " + f"parameter. Support for un-nested {module_type} specific parameters will be removed in a future version", + DeprecationWarning, + ) + return feature + + +@register_config_transformation("0.5", ["hyperopt"]) +def _upgrade_hyperopt(hyperopt: HyperoptConfigDict) -> HyperoptConfigDict: + """Upgrades hyperopt config (in-place)""" + # check for use of legacy "training" reference, if any found convert to "trainer" + if PARAMETERS in hyperopt: + hparams = hyperopt[PARAMETERS] + for k, v in list(hparams.items()): + substr = "training." + if k.startswith(substr): + warnings.warn( + 'Config section "training" renamed to "trainer" and will be removed in a future version', + DeprecationWarning, + ) + hparams["trainer." + k[len(substr) :]] = v + del hparams[k] + + # check for legacy parameters in "executor" + if EXECUTOR in hyperopt: + hpexecutor = hyperopt[EXECUTOR] + executor_type = hpexecutor.get(TYPE, None) + if executor_type is not None and executor_type != RAY: + warnings.warn( + f'executor type "{executor_type}" not supported, converted to "ray" will be flagged as error ' + "in a future version", + DeprecationWarning, + ) + hpexecutor[TYPE] = RAY + + # if search_alg not at top level and is present in executor, promote to top level + if SEARCH_ALG in hpexecutor: + # promote only if not in top-level, otherwise use current top-level + if SEARCH_ALG not in hyperopt: + hyperopt[SEARCH_ALG] = hpexecutor[SEARCH_ALG] + if isinstance(hyperopt[SEARCH_ALG], str): + hyperopt[SEARCH_ALG] = {TYPE: hyperopt[SEARCH_ALG]} + del hpexecutor[SEARCH_ALG] + else: + warnings.warn( + 'Missing "executor" section, adding "ray" executor will be flagged as error in a future version', + DeprecationWarning, + ) + hyperopt[EXECUTOR] = {TYPE: RAY} + + # check for legacy "sampler" section + if SAMPLER in hyperopt: + warnings.warn( + f'"{SAMPLER}" is no longer supported, converted to "{SEARCH_ALG}". "{SAMPLER}" will be flagged as ' + "error in a future version", + DeprecationWarning, + ) + if SEARCH_ALG in hyperopt[SAMPLER]: + if SEARCH_ALG not in hyperopt: + hyperopt[SEARCH_ALG] = hyperopt[SAMPLER][SEARCH_ALG] + if isinstance(hyperopt[SEARCH_ALG], str): + hyperopt[SEARCH_ALG] = {TYPE: hyperopt[SEARCH_ALG]} + warnings.warn('Moved "search_alg" to hyperopt config top-level', DeprecationWarning) + + # if num_samples or scheduler exist in SAMPLER move to EXECUTOR Section + if NUM_SAMPLES in hyperopt[SAMPLER] and NUM_SAMPLES not in hyperopt[EXECUTOR]: + hyperopt[EXECUTOR][NUM_SAMPLES] = hyperopt[SAMPLER][NUM_SAMPLES] + warnings.warn('Moved "num_samples" from "sampler" to "executor"', DeprecationWarning) + + if SCHEDULER in hyperopt[SAMPLER] and SCHEDULER not in hyperopt[EXECUTOR]: + hyperopt[EXECUTOR][SCHEDULER] = hyperopt[SAMPLER][SCHEDULER] + warnings.warn('Moved "scheduler" from "sampler" to "executor"', DeprecationWarning) + + if SCHEDULER in hyperopt[EXECUTOR] and len(hyperopt[EXECUTOR][SCHEDULER].keys()) == 0: + del hyperopt[EXECUTOR][SCHEDULER] + + # remove legacy section + del hyperopt[SAMPLER] + + if SEARCH_ALG not in hyperopt: + # make top-level as search_alg, if missing put in default value + hyperopt[SEARCH_ALG] = {TYPE: "variant_generator"} + warnings.warn( + 'Missing "search_alg" at hyperopt top-level, adding in default value, will be flagged as error ' + "in a future version", + DeprecationWarning, + ) + return hyperopt + + +@register_config_transformation("0.5", ["trainer"]) +def _upgrade_trainer(trainer: TrainerConfigDict) -> TrainerConfigDict: + """Upgrades trainer config (in-place)""" + eval_batch_size = trainer.get(EVAL_BATCH_SIZE) + if eval_batch_size == 0: + warnings.warn( + "`trainer.eval_batch_size` value `0` changed to `None`, will be unsupported in a future version", + DeprecationWarning, + ) + trainer[EVAL_BATCH_SIZE] = None + return trainer + + +@register_config_transformation("0.5") +def _upgrade_preprocessing_defaults(config: ModelConfigDict) -> ModelConfigDict: + """Move feature-specific preprocessing parameters into defaults in config (in-place)""" + type_specific_preprocessing_params = {} + + # If preprocessing section specified and it contains feature specific preprocessing parameters, + # make a copy and delete it from the preprocessing section + for parameter in list(config.get(PREPROCESSING, {})): + if parameter in get_base_type_registry(): + warnings.warn( + f"Moving preprocessing configuration for `{parameter}` feature type from `preprocessing` section" + " to `defaults` section in Ludwig config. This will be unsupported in a future version.", + DeprecationWarning, + ) + type_specific_preprocessing_params[parameter] = config[PREPROCESSING].pop(parameter) + + if parameter == "numerical": + warnings.warn( + f"Moving preprocessing configuration for `{parameter}` feature type from `preprocessing` section" + " to `defaults` section in Ludwig config. This will be unsupported in a future version.", + DeprecationWarning, + ) + type_specific_preprocessing_params[NUMBER] = config[PREPROCESSING].pop(parameter) + + # Delete empty preprocessing section if no other preprocessing parameters specified + if PREPROCESSING in config and not config[PREPROCESSING]: + del config[PREPROCESSING] + + # Update defaults with the default feature specific preprocessing parameters + defaults = config.get(DEFAULTS, {}) + for feature_type, preprocessing_param in type_specific_preprocessing_params.items(): + if PREPROCESSING in preprocessing_param: + preprocessing_param = preprocessing_param[PREPROCESSING] + + if feature_type == AUDIO: + preprocessing_param = upgrade_audio_preprocessing(preprocessing_param) + + # If defaults was empty, then create a new key with feature type + if feature_type not in defaults: + defaults[feature_type] = {PREPROCESSING: preprocessing_param} + # Feature type exists but preprocessing hasn't be specified + elif PREPROCESSING not in defaults[feature_type]: + defaults[feature_type][PREPROCESSING] = preprocessing_param + # Update default feature specific preprocessing with parameters from config + else: + defaults[feature_type][PREPROCESSING].update( + merge_dict(defaults[feature_type][PREPROCESSING], preprocessing_param) + ) + + if defaults: + config[DEFAULTS] = defaults + + return config + + +@register_config_transformation("0.5", "preprocessing") +def _upgrade_preprocessing_split(preprocessing: PreprocessingConfigDict) -> PreprocessingConfigDict: + """Upgrade split related parameters in preprocessing.""" + split_params = {} + + force_split = preprocessing.pop(FORCE_SPLIT, None) + split_probabilities = preprocessing.pop(SPLIT_PROBABILITIES, None) + stratify = preprocessing.pop(STRATIFY, None) + + if split_probabilities is not None: + split_params[PROBABILITIES] = split_probabilities + warnings.warn( + "`preprocessing.split_probabilities` has been replaced by `preprocessing.split.probabilities`, " + "will be flagged as error in a future version", + DeprecationWarning, + ) + + if stratify is not None: + split_params[TYPE] = STRATIFY + split_params[COLUMN] = stratify + warnings.warn( + "`preprocessing.stratify` has been replaced by `preprocessing.split.column` " + 'when setting `preprocessing.split.type` to "stratify", ' + "will be flagged as error in a future version", + DeprecationWarning, + ) + + if force_split is not None: + warnings.warn( + "`preprocessing.force_split` has been replaced by `preprocessing.split.type`, " + "will be flagged as error in a future version", + DeprecationWarning, + ) + + if TYPE not in split_params: + split_params[TYPE] = RANDOM + + if split_params: + preprocessing[SPLIT] = split_params + + if AUDIO in preprocessing: + if "audio_feature" in preprocessing[AUDIO]: + for k, v in preprocessing[AUDIO]["audio_feature"].items(): + preprocessing[AUDIO][k] = v + del preprocessing[AUDIO]["audio_feature"] + warnings.warn( + "Parameters specified at the `audio_feature` parameter level have been unnested and should now " + "be specified at the preprocessing level. Support for `audio_feature` will be removed in a future version", + DeprecationWarning, + ) + return preprocessing + + +@register_config_transformation("0.5") +def update_training(config: ModelConfigDict) -> ModelConfigDict: + if TRAINING in config: + warnings.warn( + 'Config section "training" renamed to "trainer" and will be removed in a future version', DeprecationWarning + ) + config[TRAINER] = config[TRAINING] + del config[TRAINING] + return config + + +@register_config_transformation("0.6") +def upgrade_missing_value_strategy(config: ModelConfigDict) -> ModelConfigDict: + for input_feature in config.get(INPUT_FEATURES, []): + if _is_old_missing_value_strategy(input_feature): + _update_old_missing_value_strategy(input_feature) + + for output_feature in config.get(OUTPUT_FEATURES, []): + if _is_old_missing_value_strategy(output_feature): + _update_old_missing_value_strategy(output_feature) + + for feature, feature_defaults in config.get(DEFAULTS, {}).items(): + if _is_old_missing_value_strategy(feature_defaults): + _update_old_missing_value_strategy(config.get(DEFAULTS).get(feature)) + + return config + + +@register_config_transformation("0.6", ["trainer"]) +def _upgrade_max_batch_size(trainer: TrainerConfigDict) -> TrainerConfigDict: + if "increase_batch_size_on_plateau_max" in trainer: + warnings.warn( + 'Config param "increase_batch_size_on_plateau_max" renamed to "max_batch_size" and will be ' + "removed in a future version", + DeprecationWarning, + ) + increase_batch_size_on_plateau_max_val = trainer.pop("increase_batch_size_on_plateau_max") + if "max_batch_size" in trainer: + warnings.warn('"max_batch_size" config param already set. Discarding "increase_batch_size_on_plateau_max".') + else: + warnings.warn( + f'Setting "max_batch_size" config param to "increase_batch_size_on_plateau_max" value ' + f'({increase_batch_size_on_plateau_max_val}) and discarding "increase_batch_size_on_plateau_max"' + ) + trainer["max_batch_size"] = increase_batch_size_on_plateau_max_val + return trainer + + +@register_config_transformation("0.6") +def remove_trainer_type(config: ModelConfigDict) -> ModelConfigDict: + # LLM Model types support different trainer types + if config.get("model_type", None) == "llm": + return config + + if TYPE in config.get("trainer", {}): + warnings.warn( + "Config param `type` has been removed from the trainer. The trainer type is determined by the top level " + " `model_type` parameter. Support for the `type` params in trainer will be removed in a future version", + DeprecationWarning, + ) + del config["trainer"][TYPE] + + return config + + +@register_config_transformation("0.7", ["trainer"]) +def learning_rate_scheduler(trainer: TrainerConfigDict) -> TrainerConfigDict: + key_mapping = { + "reduce_learning_rate_on_plateau": "reduce_on_plateau", + "reduce_learning_rate_on_plateau_patience": "reduce_on_plateau_patience", + "reduce_learning_rate_on_plateau_rate": "reduce_on_plateau_rate", + "reduce_learning_rate_eval_metric": "reduce_eval_metric", + "reduce_learning_rate_eval_split": "reduce_eval_split", + "decay": "decay", + "decay_steps": "decay_steps", + "decay_rate": "decay_rate", + "staircase": "staircase", + "learning_rate_warmup_epochs": "warmup_evaluations", + } + + lr_scheduler = trainer.get("learning_rate_scheduler", {}) + for old_key, new_key in key_mapping.items(): + if old_key in trainer: + warnings.warn( + f"Config param `trainer.{old_key}` has been moved to `trainer.learning_rate_scheduler.{new_key}`.", + DeprecationWarning, + ) + if new_key in lr_scheduler: + warnings.warn( + f"`trainer.learning_rate_scheduler.{new_key}` config param already set. " + f"Discarding `trainer.{old_key}`." + ) + else: + value = trainer[old_key] + if old_key == "decay" and isinstance(value, bool): + # Decay has changed from a bool to an optional enum + lr_scheduler[new_key] = "exponential" if value else None + elif old_key == "reduce_learning_rate_on_plateau": + lr_scheduler[new_key] = int(value) + else: + lr_scheduler[new_key] = value + del trainer[old_key] + + if lr_scheduler: + trainer["learning_rate_scheduler"] = lr_scheduler + + return trainer + + +@register_config_transformation("0.7", ["input_features"]) +def _upgrade_legacy_image_encoders(feature: FeatureConfigDict) -> FeatureConfigDict: + """Upgrade legacy image encoder references. + + The old _resnet_legacy and _vit_legacy encoders have been removed. The "resnet" and "vit" encoder type names now map + directly to the TorchVision-based encoders, so no transformation is needed. Any legacy-specific parameters (e.g. + resnet_size, num_hidden_layers) that are not recognized by the new encoders will be caught by schema validation. + """ + if feature.get(TYPE) != IMAGE: + return feature + + encoder = feature.get(ENCODER, {}) + encoder_type = encoder.get(TYPE) + + # Map old internal names to the current torchvision encoder names + legacy_internal_mapping = { + "_resnet_legacy": "resnet", + "_vit_legacy": "vit", + } + if encoder_type in legacy_internal_mapping: + warnings.warn( + f"Encoder type '{encoder_type}' has been removed. " + f"Mapping to TorchVision encoder '{legacy_internal_mapping[encoder_type]}'.", + DeprecationWarning, + ) + encoder[TYPE] = legacy_internal_mapping[encoder_type] + + return feature + + +@register_config_transformation("0.7") +def upgrade_missing_hyperopt(config: ModelConfigDict) -> ModelConfigDict: + hyperopt = config.get(HYPEROPT) + if hyperopt == {}: + # This is a deprecated form of providing a missing hyperopt section, as it violates the schema definition + warnings.warn( + "Config section `hyperopt: {}` is deprecated, please set `hyperopt: null` to disable hyperopt.", + DeprecationWarning, + ) + del config[HYPEROPT] + return config + + +@register_config_transformation("0.7", "defaults") +def remove_extra_type_param_in_defaults_config(defaults: FeatureTypeDefaultsDict) -> FeatureTypeDefaultsDict: + """Fixes a bug introduced before 0.7.3. + + [1] and subsequent refactors accidentally introduced a bug where a `type` param was added to every feature in the + defaults config. It was removed by [2], but made it into one of the patch releases. This transformation removes that + `type` param from each section of the defaults config if it exists. + + [1]: https://github.com/ludwig-ai/ludwig/pull/3223 + [2]: https://github.com/ludwig-ai/ludwig/pull/3258 + """ + defaults_copy = copy.deepcopy(defaults) + for feature_type, feature_config in defaults.items(): + if TYPE in feature_config: + del defaults_copy[feature_type][TYPE] + return defaults_copy + + +def upgrade_metadata(metadata: TrainingSetMetadataDict) -> TrainingSetMetadataDict: + # TODO(travis): stopgap solution, we should make it so we don't need to do this + # by decoupling config and metadata + metadata = copy.deepcopy(metadata) + _upgrade_metadata_missing_values(metadata) + return metadata + + +def _upgrade_metadata_missing_values(metadata: TrainingSetMetadataDict): + for _k, v in metadata.items(): + if isinstance(v, dict) and _is_old_missing_value_strategy(v): + _update_old_missing_value_strategy(v) + elif isinstance(v, dict) and _is_image_feature(v): + _update_old_image_preprocessing(v) + + +def _update_old_missing_value_strategy(feature_config: FeatureConfigDict): + missing_value_strategy = feature_config.get(PREPROCESSING).get(MISSING_VALUE_STRATEGY) + replacement_strategy = "bfill" if missing_value_strategy == "backfill" else "ffill" + feature_name = feature_config.get(NAME) + warnings.warn( + f"Using `{replacement_strategy}` instead of `{missing_value_strategy}` as the missing value strategy" + f" for `{feature_name}`. These are identical. `{missing_value_strategy}` will be removed in a future version", + DeprecationWarning, + ) + feature_config[PREPROCESSING].update({MISSING_VALUE_STRATEGY: replacement_strategy}) + + +def _is_old_missing_value_strategy(feature_config: FeatureConfigDict): + if PREPROCESSING not in feature_config: + return False + missing_value_strategy = feature_config.get(PREPROCESSING).get(MISSING_VALUE_STRATEGY, None) + if not missing_value_strategy or missing_value_strategy not in ("backfill", "pad"): + return False + return True + + +def _is_image_feature(feature_config: FeatureConfigDict): + preproc = feature_config.get(PREPROCESSING, {}) + return HEIGHT in preproc and WIDTH in preproc + + +def _update_old_image_preprocessing(feature_config: FeatureConfigDict): + preprocessing = feature_config.get(PREPROCESSING) + if not preprocessing: + return + preprocessing["standardize_image"] = preprocessing.get("standardize_image") + + +@register_config_transformation("0.11", ["input_features"]) +def _upgrade_removed_text_encoders(feature: FeatureConfigDict) -> FeatureConfigDict: + """Upgrade configs that reference removed text encoder types. + + TransformerXL, CTRL, and FlauBERT encoders have been removed from Ludwig because they are + discontinued or superseded by other models (HuggingFace deprecated TransformerXL; CTRL has + no active community use; FlauBERT is superseded by CamemBERT and multilingual models like + XLM-RoBERTa). Configs using these types are automatically remapped to `auto_transformer`. + + Note: the `pretrained_model_name_or_path` field must be set explicitly since `auto_transformer` + has no built-in default model name. + """ + if feature.get(TYPE) != TEXT: + return feature + + encoder = feature.get(ENCODER, {}) + encoder_type = encoder.get(TYPE) + + removed_encoder_mapping = { + "transformer_xl": "auto_transformer", + "ctrl": "auto_transformer", + "flaubert": "auto_transformer", + } + + if encoder_type in removed_encoder_mapping: + replacement = removed_encoder_mapping[encoder_type] + warnings.warn( + f"Text encoder type '{encoder_type}' has been removed and is no longer supported. " + f"Remapping to '{replacement}'. You may need to set `pretrained_model_name_or_path` " + f"explicitly in the encoder config to specify the model to use.", + DeprecationWarning, + ) + encoder[TYPE] = replacement + + return feature diff --git a/ludwig/utils/batch_size_tuner.py b/ludwig/utils/batch_size_tuner.py new file mode 100644 index 0000000..d6e6b66 --- /dev/null +++ b/ludwig/utils/batch_size_tuner.py @@ -0,0 +1,184 @@ +import gc +import logging +import statistics +import time +from abc import ABC + +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import MAX_BATCH_SIZE_DATASET_FRACTION, MIN_POSSIBLE_BATCH_SIZE + +logger = logging.getLogger(__name__) + +TOTAL_STEPS = 5 + + +@DeveloperAPI +class BatchSizeEvaluator(ABC): + def select_best_batch_size( + self, + dataset_len: int, + max_batch_size: int | None = None, + max_trials: int = 20, + is_coordinator: bool | None = True, + global_max_sequence_length: int | None = None, + ) -> int: + """Returns optimal batch size as measured by throughput (samples / sec).""" + logger.info("Tuning batch size...") + + max_batch_size = max_batch_size or dataset_len + + def _is_valid_batch_size(batch_size): + # make sure that batch size is valid (e.g. less than 20% of ds size and max_batch_size) + is_smaller_than_training_set = batch_size <= MAX_BATCH_SIZE_DATASET_FRACTION * dataset_len + is_under_max_batch_size = batch_size <= max_batch_size + is_valid = is_smaller_than_training_set and is_under_max_batch_size + if not is_valid and is_coordinator: + logger.info( + f"Batch size {batch_size} is invalid, must be less than or equal to " + f"{MAX_BATCH_SIZE_DATASET_FRACTION * 100}% dataset size " + f"({int(MAX_BATCH_SIZE_DATASET_FRACTION * dataset_len)} samples " + f"of {dataset_len}) and less than or equal to max batch size {max_batch_size}" + ) + return is_valid + + batch_size = MIN_POSSIBLE_BATCH_SIZE + best_samples_per_sec = 0 + best_batch_size = None + count = 0 + while count < max_trials and _is_valid_batch_size(batch_size): + if is_coordinator: + logger.info(f"Exploring batch_size={batch_size}") + gc.collect() + + try: + samples_per_sec = self.evaluate( + batch_size, total_steps=TOTAL_STEPS, global_max_sequence_length=global_max_sequence_length + ) + if is_coordinator: + logger.info(f"Throughput at batch_size={batch_size}: {samples_per_sec:.5f} samples/s") + if samples_per_sec < best_samples_per_sec: + # We assume that once the throughput starts degrading, it won't go up again + if is_coordinator: + logger.info(f"Throughput decrease at batch_size={batch_size}") + break + + best_samples_per_sec = samples_per_sec + best_batch_size = batch_size + count += 1 + + # double batch size + batch_size *= 2 + except RuntimeError as e: + # PyTorch only generates Runtime errors for CUDA OOM. + gc.collect() + if "CUDA out of memory" in str(e) or isinstance(e, torch.cuda.OutOfMemoryError): + if is_coordinator: + logger.info(f"OOM at batch_size={batch_size}") + else: + # Not a CUDA error + raise + break + + # Ensure that some batch size is found. + # `best_batch_size` can be None if the first batch size is invalid. + if best_batch_size is None: + if is_coordinator: + logger.info(f"Could not tune batch size, using minimum batch size of {MIN_POSSIBLE_BATCH_SIZE}") + best_batch_size = MIN_POSSIBLE_BATCH_SIZE + + if is_coordinator: + logger.info(f"Selected batch_size={best_batch_size}") + return best_batch_size + + def evaluate(self, batch_size: int, total_steps: int = 5, global_max_sequence_length: int | None = None) -> float: + """Evaluates throughput of the given batch size. + + Return: + Median throughput in samples / sec. + """ + durations = [] + for _ in range(total_steps): + self.reset() + start_ts = time.time() + self.step(batch_size, global_max_sequence_length=global_max_sequence_length) + durations.append(time.time() - start_ts) + + med_duration_s = statistics.median(durations) + if med_duration_s == 0.0: + return float("inf") + + return batch_size / med_duration_s + + def reset(self): + """Called at the beginning of each evaluation step.""" + + def step(self, batch_size: int, global_max_sequence_length: int | None = None): + """Called each step to evaluate the given batch size.""" + raise NotImplementedError("`step` must be implemented by concrete evaluator.") + + +class BaseLLMBatchSizeEvaluator(BatchSizeEvaluator): + """Base class for batch size evaluators for LLM models.""" + + def __init__(self, trainer): + self.trainer = trainer + self.input_feature_name, self.input_feature = list(trainer.model.input_features.items())[0] + self.output_feature_name, self.output_feature = list(trainer.model.output_features.items())[0] + + # Get the length of the longest input sequence from the training data + self.input_msl = self.input_feature.input_shape[0] + if trainer.model.config_obj.input_features[0].preprocessing.max_sequence_length: + self.input_msl = trainer.model.config_obj.input_features[0].preprocessing.max_sequence_length + + # Get the length of the longest output sequence from the training data + self.output_msl = self.output_feature.output_shape[0] + if trainer.model.config_obj.output_features[0].preprocessing.max_sequence_length: + self.output_msl = trainer.model.config_obj.output_features[0].preprocessing.max_sequence_length + + # This is useful to create the synthetic input and target data which will be a + # random sequence of integers between 0 and vocab_size + self.vocab_size = len(trainer.model.config_obj.input_features[0].encoder.vocab) + + def reset(self): + self.trainer.model.reset_metrics() + self.trainer.optimizer.zero_grad() + + def step(self, batch_size: int, global_max_sequence_length: int | None = None): + if global_max_sequence_length and self.input_msl + self.output_msl > global_max_sequence_length: + # In this case, we just need to make sure that the length of the synthetic data exceeds + # max_sequence_length by at most a small amount + self.input_msl = global_max_sequence_length // 2 + 1 + self.output_msl = global_max_sequence_length // 2 + 1 + + inputs = { + self.input_feature_name: torch.randint(0, self.vocab_size, size=(batch_size, self.input_msl)) + .to(self.input_feature.input_dtype) + .to(self.trainer.device) + } + targets = { + self.output_feature_name: torch.randint(0, self.vocab_size, size=(batch_size, self.output_msl)) + .to(self.output_feature.get_output_dtype()) + .to(self.trainer.device) + } + + self.perform_step(inputs, targets) + + def perform_step(self, inputs, targets): + raise NotImplementedError("perform_step method must be implemented in subclasses") + + +class LLMFinetuneTrainerBatchSizeEvaluator(BaseLLMBatchSizeEvaluator): + """Batch size evaluator for training batch size for LLM finetuning.""" + + def perform_step(self, inputs, targets): + self.trainer.train_step(inputs, targets) + + +class LLMFinetunePredictBatchSizeEvaluator(BaseLLMBatchSizeEvaluator): + """Batch size evaluator for prediction/evaluation batch size for LLM finetuning.""" + + def perform_step(self, inputs, targets): + with torch.no_grad(): + self.trainer.dist_model((inputs, targets)) diff --git a/ludwig/utils/calibration.py b/ludwig/utils/calibration.py new file mode 100644 index 0000000..f126731 --- /dev/null +++ b/ludwig/utils/calibration.py @@ -0,0 +1,321 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import logging +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import numpy as np +import torch +import torch.nn as nn + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import BINARY, CATEGORY +from ludwig.utils.registry import DEFAULT_KEYS, Registry + +logger = logging.getLogger(__name__) + +calibration_registry = Registry() + + +@DeveloperAPI +def register_calibration(name: str, features: str | list[str], default=False): + """Registers a calibration implementation for a list of features.""" + if isinstance(features, str): + features = [features] + + def wrap(cls): + for feature in features: + feature_registry = calibration_registry.get(feature, {}) + feature_registry[name] = cls + if default: + for key in DEFAULT_KEYS: + feature_registry[key] = cls + calibration_registry[feature] = feature_registry + return cls + + return wrap + + +@DeveloperAPI +def get_calibration_cls(feature: str, calibration_method: str) -> type["CalibrationModule"]: + """Get calibration class for specified feature type and calibration method.""" + if not calibration_method: + return None + if feature in calibration_registry: + if calibration_method in calibration_registry[feature]: + return calibration_registry[feature][calibration_method] + else: + raise ValueError(f"Calibration method {calibration_method} not supported for {feature} output features") + else: + raise ValueError(f"Calibration not yet supported for {feature} output features") + return None + + +@DeveloperAPI +class ECELoss(nn.Module): + """Calculates the Expected Calibration Error of a model. + + The input to this loss is the logits of a model, NOT the softmax scores. + This divides the confidence outputs into equally-sized interval bins. + In each bin, we compute the confidence gap: + + bin_gap = | avg_confidence_in_bin - accuracy_in_bin | + + We then return an average of the gaps, weighted by the number of samples in each bin. + + References: + Naeini, Mahdi Pakdaman, Gregory F. Cooper, and Milos Hauskrecht + "Obtaining Well Calibrated Probabilities Using Bayesian Binning." AAAI. 2015. + + Chuan Guo, Geoff Pleiss, Yu Sun, Kilian Q. Weinberger + "On Calibration of Modern Neural Networks." PMLR 2017. + """ + + def __init__(self, n_bins: int = 15): + """n_bins (int): number of confidence interval bins.""" + super().__init__() + bin_boundaries = torch.linspace(0, 1, n_bins + 1) + self.bin_lowers = bin_boundaries[:-1] + self.bin_uppers = bin_boundaries[1:] + + def forward(self, logits: torch.Tensor, one_hot_labels: torch.Tensor) -> torch.Tensor: + softmaxes = nn.functional.softmax(logits, dim=1) + confidences, predictions = torch.max(softmaxes, 1) + labels = torch.argmax(one_hot_labels, 1) + accuracies = predictions.eq(labels) + ece = torch.zeros(1, device=logits.device) + for bin_lower, bin_upper in zip(self.bin_lowers, self.bin_uppers): + # Calculates |confidence - accuracy| in each bin + in_bin = confidences.gt(bin_lower.item()) * confidences.le(bin_upper.item()) + prop_in_bin = in_bin.float().mean() + if prop_in_bin.item() > 0: + accuracy_in_bin = accuracies[in_bin].float().mean() + avg_confidence_in_bin = confidences[in_bin].mean() + ece += torch.abs(avg_confidence_in_bin - accuracy_in_bin) * prop_in_bin + return ece + + +@DeveloperAPI +@dataclass +class CalibrationResult: + """Tracks results of probability calibration.""" + + before_calibration_nll: float + before_calibration_ece: float + after_calibration_nll: float + after_calibration_ece: float + + +@DeveloperAPI +class CalibrationModule(nn.Module, ABC): + @abstractmethod + def train_calibration( + self, logits: torch.Tensor | np.ndarray, labels: torch.Tensor | np.ndarray + ) -> CalibrationResult: + """Calibrate output probabilities using logits and labels from validation set.""" + return NotImplementedError() + + +@DeveloperAPI +@register_calibration("temperature_scaling", [BINARY, CATEGORY], default=True) +class TemperatureScaling(CalibrationModule): + """Implements temperature scaling of logits. Based on results from "On Calibration of Modern Neural Networks": + https://arxiv.org/abs/1706.04599. Temperature scaling scales all logits by the same constant factor. Though it + may modify output probabilities it will never change argmax or categorical top-n predictions. In the case of + binary classification with a threshold, however, calibration may change predictions. + + Implementation inspired by https://github.com/gpleiss/temperature_scaling + + Args: + num_classes: The number of classes. Must be 2 if binary is True. + binary: If binary is true, logits is expected to be a 1-dimensional array. If false, logits is a 2-dimensional + array of shape (num_examples, num_classes). + """ + + def __init__(self, num_classes: int = 2, binary: bool = False): + super().__init__() + self.num_classes = 2 if binary else num_classes + self.binary = binary + self.device = "cuda" if torch.cuda.is_available() and torch.cuda.device_count() > 0 else "cpu" + self.temperature = nn.Parameter(torch.ones(1), requires_grad=False).to(self.device) + + def train_calibration( + self, logits: torch.Tensor | np.ndarray, labels: torch.Tensor | np.ndarray + ) -> CalibrationResult: + logits = torch.as_tensor(logits, dtype=torch.float32, device=self.device) + labels = torch.as_tensor(labels, dtype=torch.int64, device=self.device) + one_hot_labels = nn.functional.one_hot(labels, self.num_classes).float() + if self.binary: + # Treat binary classification as multi-class with 2 classes to re-use code. + # The math works out the same: softmax([0, a])[1] == sigmoid(a) + logits = torch.stack([torch.zeros_like(logits), logits], axis=-1) + nll_criterion = nn.CrossEntropyLoss().to(self.device) + ece_criterion = ECELoss().to(self.device) + # Saves the original temperature parameter, in case something goes wrong in optimization. + original_temperature = self.temperature.clone().detach() + self.temperature.requires_grad = True + # Calculate NLL and ECE before temperature scaling + before_calibration_nll = nll_criterion(logits, one_hot_labels).item() + before_calibration_ece = ece_criterion(logits, one_hot_labels).item() + logger.info( + "Before temperature scaling:\n" + f" Negative log-likelihood: {before_calibration_nll:.3f}\n" + f" Expected Calibration Error: {before_calibration_ece:.3f}" + ) + + # Optimizes the temperature to minimize NLL + optimizer = torch.optim.LBFGS([self.temperature], lr=0.01, max_iter=50, line_search_fn="strong_wolfe") + + def eval(): + optimizer.zero_grad() + loss = nll_criterion(self.scale_logits(logits), one_hot_labels) + loss.backward() + return loss + + optimizer.step(eval) + + # Calculate NLL and ECE after temperature scaling + after_calibration_nll = nll_criterion(self.scale_logits(logits), one_hot_labels).item() + after_calibration_ece = ece_criterion(self.scale_logits(logits), one_hot_labels).item() + logger.info(f"Optimal temperature: {self.temperature.item():.3f}") + logger.info( + "After temperature scaling:\n" + f" Negative log-likelihood: {after_calibration_nll:.3f}\n" + f" Expected Calibration Error: {after_calibration_ece:.3f}" + ) + self.temperature.requires_grad = False + # This should never happen, but if expected calibration error is higher after optimizing temperature, revert. + if after_calibration_ece > before_calibration_ece: + logger.warning( + "Expected calibration error higher after scaling, " + f"reverting to temperature={original_temperature.item():.3f}." + ) + with torch.no_grad(): + self.temperature.data = original_temperature.data + return CalibrationResult( + before_calibration_nll, before_calibration_ece, after_calibration_nll, after_calibration_ece + ) + + def scale_logits(self, logits: torch.Tensor) -> torch.Tensor: + return torch.div(logits, self.temperature) + + def forward(self, logits: torch.Tensor) -> torch.Tensor: + """Converts logits to probabilities.""" + scaled_logits = self.scale_logits(logits) + if self.binary: + return torch.sigmoid(scaled_logits) + else: + return torch.softmax(scaled_logits, -1) + + +@DeveloperAPI +@register_calibration("matrix_scaling", CATEGORY, default=False) +class MatrixScaling(CalibrationModule): + """Implements matrix scaling of logits, as described in Beyond temperature scaling: Obtaining well-calibrated + multiclass probabilities with Dirichlet calibration https://arxiv.org/abs/1910.12656. + + Unlike temperature scaling which has only one free parameter, matrix scaling has n_classes x (n_classes + 1) + parameters. Use this only with a large validation set, as matrix scaling has a tendency to overfit small datasets. + Also, unlike temperature scaling, matrix scaling can change the argmax or top-n predictions. + + NOTE: Matrix Scaling is not exposed in the UI or config yet, though it may be in a future release after testing. + + Args: + num_classes: The number of classes. + off_diagonal_l2: The regularization weight for off-diagonal matrix entries. + mu: The regularization weight for bias vector. Defaults to off_diagonal_l2 if not specified. + """ + + def __init__(self, num_classes: int = 2, off_diagonal_l2: float = 0.01, mu: float | None = None): + super().__init__() + self.num_classes = num_classes + self.device = "cuda" if torch.cuda.is_available() and torch.cuda.device_count() > 0 else "cpu" + self.w = nn.Parameter(torch.eye(self.num_classes), requires_grad=False).to(self.device) + self.b = nn.Parameter(torch.zeros(self.num_classes), requires_grad=False).to(self.device) + self.off_diagonal_l2 = off_diagonal_l2 + self.mu = off_diagonal_l2 if mu is None else mu + + def train_calibration( + self, logits: torch.Tensor | np.ndarray, labels: torch.Tensor | np.ndarray + ) -> CalibrationResult: + logits = torch.as_tensor(logits, dtype=torch.float32, device=self.device) + labels = torch.as_tensor(labels, dtype=torch.int64, device=self.device) + one_hot_labels = nn.functional.one_hot(labels, self.num_classes).float() + nll_criterion = nn.CrossEntropyLoss().to(self.device) + ece_criterion = ECELoss().to(self.device) + self.w.requires_grad = True + self.b.requires_grad = True + # Calculate NLL and ECE before temperature scaling + before_calibration_nll = nll_criterion(logits, one_hot_labels).item() + before_calibration_ece = ece_criterion(logits, one_hot_labels).item() + logger.info( + "Before matrix scaling:\n" + f" Negative log-likelihood: {before_calibration_nll:.3f}\n" + f" Expected Calibration Error: {before_calibration_ece:.3f}" + ) + + # Optimizes the linear transform to minimize NLL + optimizer = torch.optim.LBFGS([self.w, self.b], lr=0.001, max_iter=200, line_search_fn="strong_wolfe") + + def eval(): + optimizer.zero_grad() + loss = nll_criterion(self.scale_logits(logits), one_hot_labels) + self.regularization_terms() + loss.backward() + return loss + + optimizer.step(eval) + + # Calculate NLL and ECE after matrix scaling + after_calibration_nll = nll_criterion(self.scale_logits(logits), one_hot_labels).item() + after_calibration_ece = ece_criterion(self.scale_logits(logits), one_hot_labels).item() + logger.info( + "After matrix scaling:\n" + f" Negative log-likelihood: {after_calibration_nll:.3f}\n" + f" Expected Calibration Error: {after_calibration_ece:.3f}" + ) + self.w.requires_grad = False + self.b.requires_grad = False + # This should never happen, but if expected calibration error is higher after optimizing matrix, revert. + if after_calibration_ece > before_calibration_ece: + logger.warning("Expected calibration error higher after matrix scaling, reverting to identity.") + with torch.no_grad(): + self.w.data = torch.eye(self.num_classes) + self.b.data = torch.zeros(self.num_classes) + return CalibrationResult( + before_calibration_nll, before_calibration_ece, after_calibration_nll, after_calibration_ece + ) + + def regularization_terms(self) -> torch.Tensor: + """Off-Diagonal and Intercept Regularisation (ODIR). + + Described in "Beyond temperature scaling: Obtaining well-calibrated multiclass probabilities with Dirichlet + calibration" + https://proceedings.neurips.cc/paper/2019/file/8ca01ea920679a0fe3728441494041b9-Paper.pdf + """ + off_diagonal_entries = torch.masked_select( + self.w, ~torch.eye(self.num_classes, dtype=bool, device=self.w.device) + ) + weight_matrix_loss = self.off_diagonal_l2 * torch.linalg.vector_norm(off_diagonal_entries) + bias_vector_loss = self.mu * torch.linalg.vector_norm(self.b, 2) + return bias_vector_loss + weight_matrix_loss + + def scale_logits(self, logits: torch.Tensor) -> torch.Tensor: + return torch.matmul(self.w, logits.T).T + self.b + + def forward(self, logits: torch.Tensor) -> torch.Tensor: + """Converts logits to probabilities.""" + return torch.softmax(self.scale_logits(logits), -1) diff --git a/ludwig/utils/checkpoint_utils.py b/ludwig/utils/checkpoint_utils.py new file mode 100644 index 0000000..76eac01 --- /dev/null +++ b/ludwig/utils/checkpoint_utils.py @@ -0,0 +1,407 @@ +"""Implements similar functionality as tf.train.Checkpoint and tf.train.CheckpointManager. + +https://gist.github.com/kevinzakka/5d345421f7abefd5dbaf6a77f829e70a. +""" + +import errno +import logging +import os +import re +import shutil +import signal +import tempfile +import uuid +from abc import ABC, abstractmethod +from collections.abc import Mapping +from glob import glob +from typing import Any, TYPE_CHECKING + +import torch +from torch.optim import Optimizer + +from ludwig.api_annotations import DeveloperAPI +from ludwig.globals import MODEL_WEIGHTS_FILE_NAME +from ludwig.modules.lr_scheduler import LRScheduler + +if TYPE_CHECKING: + from ludwig.distributed.base import DistributedStrategy + from ludwig.models.base import BaseModel + + +logger = logging.getLogger(__name__) +LATEST = "latest" +BEST = "best" + + +@DeveloperAPI +def mkdir(s): + """Create a directory if it doesn't already exist.""" + os.makedirs(s, exist_ok=True) + + +@DeveloperAPI +def get_files(d, pattern, sort=True): + """Return a list of files in a given directory. + + Args: + d (str): The path to the directory. + pattern (str): The wildcard to filter files with. + sort (bool): Whether to sort the returned list. Assumes filenames contain a number value to sort by (tmp-001). + """ + files = glob(os.path.join(d, pattern)) + files = [f for f in files if os.path.isfile(f)] + if sort: + + def filter_numeric(s): + return re.sub("[^0-9]", "", s) + + files.sort(key=lambda x: int(filter_numeric(os.path.basename(x).split(".")[0]))) + return files + + +@DeveloperAPI +def get_latest_checkpoint_path(directory: str) -> str: + latest_path = os.path.join(directory, f"{LATEST}.ckpt") + if os.path.exists(latest_path): + return latest_path + + # Legacy codepath for checkpoints saved by global step number + ckpts = get_files(directory, "*.ckpt") + if ckpts: + return ckpts[-1] + + return None + + +@DeveloperAPI +class Checkpoint(ABC): + """Save and restore model and optimizer states.""" + + def __init__( + self, + distributed: "DistributedStrategy", + model: "BaseModel", + optimizer: Optimizer | None = None, + scheduler: LRScheduler | None = None, + ): + """Constructor.""" + self.distributed = distributed + self.model = model + self.optimizer = optimizer + self.scheduler = scheduler + self.global_step = 0 + + def prepare(self, directory: str): + # create checkpoint directory if it doesn't + # already exist + mkdir(directory) + + @abstractmethod + def load(self, save_path: str, device: torch.device | None = None) -> bool: + pass + + @abstractmethod + def get_state_for_inference(self, save_path: str, device: torch.device | None = None) -> Mapping[str, Any]: + pass + + @abstractmethod + def save(self, save_path: str, global_step: int): + pass + + def _get_global_step(self, state: dict[str, Any], save_path: str) -> int: + global_step = state.get("global_step") + if global_step is None: + # Legacy step detection for older checkpoint format which encoded the + # step number in the checkpoint filename. + return int(os.path.basename(save_path).split(".")[0]) + return global_step + + +@DeveloperAPI +class MultiNodeCheckpoint(Checkpoint): + def prepare(self, directory: str): + if self.is_local_rank_0(): + super().prepare(directory) + self.distributed.barrier() + + @staticmethod + def _safetensors_path(save_path: str) -> str: + """Return the companion safetensors path for a checkpoint path.""" + return save_path + ".safetensors" + + def load(self, save_path: str, device: torch.device | None = None) -> bool: + """Load state from a saved checkpoint. + + Loads model weights from SafeTensors if available, falls back to legacy pickle format. + + Args: + save_path (str): The filepath to the saved checkpoint. + device (torch.device): The device on which to load the state. + + Returns: + True if the checkpoint was successfully loaded, False if the checkpoint file could not be found. + """ + try: + safetensors_path = self._safetensors_path(save_path) + # weights_only=False: this file is Ludwig's own checkpoint (meta_state), + # containing optimizer and scheduler state dicts that may include + # non-tensor Python objects (step counters, lambda closures, etc.). + # It is a trusted internal file, not user-supplied data. + state = torch.load(save_path, map_location=device, weights_only=False) + try: + self.global_step = self._get_global_step(state, save_path) + + # Load model weights: prefer safetensors, fall back to pickle + if os.path.exists(safetensors_path): + from safetensors.torch import load_file + + model_weights = load_file(safetensors_path, device=str(device) if device else "cpu") + else: + model_weights = state[MODEL_WEIGHTS_FILE_NAME] + + _, unexpected_keys = self.model.load_state_dict(model_weights, strict=False) + if unexpected_keys: + raise RuntimeError( + f"Unexpected keys found in state dict: {unexpected_keys}.\n" + f"This may indicate a model architecture mismatch between the checkpoint and current model." + ) + if self.optimizer is not None: + self.optimizer.load_state_dict(state["optim_state"]) + if self.scheduler is not None and "scheduler_state" in state: + self.scheduler.load_state_dict(state["scheduler_state"]) + logger.info(f"Successfully loaded model weights from {save_path}.") + return True + except Exception as e: + raise e + except FileNotFoundError as e: + logger.error(e) + return False + + def get_state_for_inference(self, save_path: str, device: torch.device | None = None) -> Mapping[str, Any]: + """Load only model weights for inference.""" + safetensors_path = self._safetensors_path(save_path) + if os.path.exists(safetensors_path): + from safetensors.torch import load_file + + return load_file(safetensors_path, device=str(device) if device else "cpu") + state = torch.load(save_path, map_location=device, weights_only=True) + return state[MODEL_WEIGHTS_FILE_NAME] + + def save(self, save_path: str, global_step: int): + """Save a state to disk. + + Model weights are saved in SafeTensors format alongside a pickle file for optimizer/scheduler/metadata. + + Args: + save_path (str): The name of the checkpoint to save. + global_step (int): The iteration number which will be used to name the checkpoint. + """ + if self.is_local_rank_0(): + from safetensors.torch import save_file + + # Clone tensors to avoid shared memory errors with safetensors + model_weights = {k: v.clone().contiguous() for k, v in self.get_model_state_dict().items()} + + # Metadata state (optimizer, scheduler, global_step) — still uses torch.save + meta_state = {"global_step": global_step} + if self.optimizer is not None: + meta_state["optim_state"] = self.optimizer.state_dict() + if self.scheduler is not None: + meta_state["scheduler_state"] = self.scheduler.state_dict() + + # ignore ctrl+c while saving + try: + orig_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, lambda _sig, _frame: None) + except ValueError: + orig_handler = None + + try: + # atomic save + with tempfile.TemporaryDirectory() as tmpdir: + tmp_meta_path = os.path.join(tmpdir, "temp.ckpt") + tmp_weights_path = os.path.join(tmpdir, "temp.ckpt.safetensors") + torch.save(meta_state, tmp_meta_path) + save_file(model_weights, tmp_weights_path) + + self.safe_move_file(tmp_meta_path, save_path) + self.safe_move_file(tmp_weights_path, self._safetensors_path(save_path)) + logger.debug(f"Saved checkpoint at {save_path}.") + finally: + if orig_handler is not None: + signal.signal(signal.SIGINT, orig_handler) + self.distributed.barrier() + + def get_model_state_dict(self) -> dict[str, Any]: + state = self.model.state_dict() + + # Remove frozen parameter weights from state_dict for adapters and pretrained models + for n, p in self.model.named_parameters(): + if n in state and not p.requires_grad: + del state[n] + + return state + + def is_local_rank_0(self) -> bool: + return self.distributed.local_rank() == 0 + + def safe_move_file(self, src: str, dst: str): + """Move a file from one directory to another, possibly across filesystems. + + This implementation specifically addresses the following issue with distributed training: + + 1. The `save_path` is a directory local to the node, in which case every node should write + checkpoints separately. + 2. The `save_path` is a remote / global filesystem like NFS, in which case only the coordinator + should write checkpoints. + """ + try: + os.replace(src, dst) + except OSError as err: + if err.errno == errno.EXDEV: + # Tried to move to an external filesystem. This means we should only run this on the coordinator + if not self.distributed.is_coordinator(): + logger.info( + f"Skipping writing checkpoint from rank {self.distributed.rank()} as it is not the coordinator " + f"and the destination filesystem is remote." + ) + return + + # Generate a unique ID, and copy `` to the target directory with a temporary name `..tmp`. + # Because we're copying across a filesystem boundary, this initial copy may not be atomic. We insert a + # random UUID so if different processes are copying into ``, they don't overlap in their tmp + # copies. + copy_id = uuid.uuid4() + tmp_dst = f"{dst}.{copy_id}.tmp" + shutil.copyfile(src, tmp_dst) + + # Atomic replace file onto the new name, and clean up original source file. + os.replace(tmp_dst, dst) + os.unlink(src) + else: + raise + + +@DeveloperAPI +class CheckpointManager: + """A model and optimizer checkpoint manager.""" + + def __init__(self, checkpoint: Checkpoint, directory: str, device: torch.device, top_k: int = 0): + """Constructor. + + Args: + checkpoint (Checkpoint): An instance of `Checkpoint`. + directory (str): The directory in which checkpoints will be saved. + device (torch.device): The computing device on which to restore checkpoints. + top_k (int): Number of top checkpoints to keep for model soup. 0 to disable. + """ + self.checkpoint = checkpoint + self.directory = directory + self.device = device + self.latest_checkpoint = None + self.top_k = top_k + self.top_k_entries: list[tuple[float, str]] = [] # (metric_value, path) + self.checkpoint.prepare(self.directory) + + def restore_or_initialize(self) -> int: + """Restore items in checkpoint from the latest checkpoint file. + + Returns: + The global iteration step. This is parsed from the latest + checkpoint file if one is found, else 0 is returned. + """ + last_ckpt = get_latest_checkpoint_path(self.directory) + if last_ckpt: + status = self.checkpoint.load(last_ckpt, self.device) + if not status: + logger.warning("Could not restore latest checkpoint file.") + return 0 + self.latest_checkpoint = last_ckpt + return self.checkpoint.global_step + return 0 + + def save(self, global_step: int, tag: str = LATEST): + """Create a new checkpoint. + + Args: + global_step (int): The iteration number which will be used + to name the checkpoint. + """ + save_path = os.path.join(self.directory, f"{tag}.ckpt") + self.checkpoint.save(save_path, global_step) + self.latest_checkpoint = save_path + + def save_best(self, global_step: int): + self.save(global_step, BEST) + + def load(self, tag: str = LATEST): + """Load a checkpoint. + + Args: + tag (str): The tag of the checkpoint to load. + """ + save_path = os.path.join(self.directory, f"{tag}.ckpt") + self.checkpoint.load(save_path, self.device) + + def get_best_checkpoint_state_for_inference(self, device: torch.device) -> tuple[Mapping[str, Any], None]: + save_path = os.path.join(self.directory, f"{BEST}.ckpt") + try: + return self.checkpoint.get_state_for_inference(save_path, device) + except (FileNotFoundError, OSError, RuntimeError): + # Best checkpoint may not exist when training is halted by NaN/Inf weights before the + # first checkpoint is saved. + logger.error(f"Could not load best checkpoint state from {save_path}. Best checkpoint may not exist.") + return None + + def save_top_k(self, global_step: int, metric_value: float, is_minimize: bool): + """Save a checkpoint and maintain top-K based on validation metric. + + Args: + global_step: Current training step. + metric_value: Validation metric value for ranking. + is_minimize: If True, lower metric is better. + """ + if self.top_k <= 0: + return + + tag = f"topk_{global_step}" + save_path = os.path.join(self.directory, f"{tag}.ckpt") + self.checkpoint.save(save_path, global_step) + self.top_k_entries.append((metric_value, save_path)) + + # Sort: best first + self.top_k_entries.sort(key=lambda x: x[0], reverse=not is_minimize) + + # Prune excess checkpoints + while len(self.top_k_entries) > self.top_k: + _, removed_path = self.top_k_entries.pop() + # Clean up checkpoint files + for path in [removed_path, removed_path + ".safetensors"]: + if os.path.exists(path): + os.remove(path) + + def get_top_k_state_dicts(self, device: torch.device) -> list[dict[str, Any]]: + """Load all top-K checkpoint model weights for model soup. + + Returns: + List of model state dicts, sorted by metric (best first). + """ + state_dicts = [] + for _, path in self.top_k_entries: + try: + sd = self.checkpoint.get_state_for_inference(path, device) + state_dicts.append(sd) + except Exception: + logger.warning(f"Could not load top-K checkpoint from {path}.", exc_info=True) + return state_dicts + + def close(self): + pass + + @staticmethod + def load_latest_checkpoint(checkpoint: Checkpoint, directory: str, device: torch.device): + last_ckpt = get_latest_checkpoint_path(directory) + if last_ckpt: + checkpoint.load(last_ckpt, device) + else: + raise FileNotFoundError(f"No checkpoints found in {directory}.") diff --git a/ludwig/utils/config_utils.py b/ludwig/utils/config_utils.py new file mode 100644 index 0000000..33a12ff --- /dev/null +++ b/ludwig/utils/config_utils.py @@ -0,0 +1,196 @@ +from typing import Any + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ( + DECODER, + ENCODER, + IMAGE, + INPUT_FEATURES, + MODEL_ECD, + MODEL_LLM, + MODEL_TYPE, + PREPROCESSING, + SEQUENCE, + TEXT, + TIMESERIES, + TYPE, +) +from ludwig.features.feature_registries import get_input_type_registry +from ludwig.schema.model_config import ModelConfig +from ludwig.types import FeatureConfigDict, FeatureTypeDefaultsDict, PreprocessingConfigDict + + +@DeveloperAPI +def get_feature_type_parameter_values_from_section( + config: ModelConfig, features_section: str, feature_type: str, parameter_name: str +) -> set: + """Returns the set of all parameter values used for the given features_section, feature_type, and + parameter_name.""" + parameter_values = set() + for feature in config[features_section]: + if feature[TYPE] == feature_type: + if parameter_name in feature: + parameter_values.add(feature[parameter_name]) + elif parameter_name in feature[ENCODER]: + parameter_values.add(feature[ENCODER][parameter_name]) + elif parameter_name in feature[DECODER]: + parameter_values.add(feature[DECODER][parameter_name]) + return parameter_values + + +@DeveloperAPI +def get_defaults_section_for_feature_type( + feature_type: str, + config_defaults: FeatureTypeDefaultsDict, + config_defaults_section: str, +) -> FeatureConfigDict: + """Returns a dictionary of all default parameter values specified in the global defaults section for the + config_defaults_section of the feature_type.""" + + if feature_type not in config_defaults: + return {} + + if config_defaults_section not in config_defaults[feature_type]: + return {} + + return config_defaults[feature_type][config_defaults_section] + + +def _to_dict(obj) -> dict: + """Convert a config object or dict to a plain dict.""" + if isinstance(obj, dict): + return obj + return obj.to_dict() + + +def get_preprocessing_params(config_obj: ModelConfig) -> PreprocessingConfigDict: + """Returns a new dictionary that merges preprocessing section of config with type-specific preprocessing + parameters from config defaults.""" + preprocessing_params = {} + preprocessing_params.update(_to_dict(config_obj.preprocessing)) + for feat_type in get_input_type_registry(): + if hasattr(config_obj.defaults, feat_type): + feat_defaults = getattr(config_obj.defaults, feat_type) + preprocessing = ( + feat_defaults.preprocessing + if not isinstance(feat_defaults, dict) + else feat_defaults.get("preprocessing", {}) + ) + preprocessing_params[feat_type] = _to_dict(preprocessing) + return preprocessing_params + + +@DeveloperAPI +def merge_config_preprocessing_with_feature_specific_defaults( + config_preprocessing: PreprocessingConfigDict, config_defaults: FeatureTypeDefaultsDict +) -> PreprocessingConfigDict: + """Returns a new dictionary that merges preprocessing section of config with type-specific preprocessing + parameters from config defaults.""" + preprocessing_params = {} + preprocessing_params.update(config_preprocessing) + for feature_type in config_defaults: + preprocessing_params[feature_type] = config_defaults[feature_type].get(PREPROCESSING, {}) + return preprocessing_params + + +def has_trainable_encoder(config: ModelConfig) -> bool: + for feature in config.input_features.to_list(): + encoder = feature.get("encoder", {}) + if encoder.get("trainable", False): + # TODO(travis): we assume here that False is always the default, which may not be true. We should dervice + # this from the schema. + return True + + return False + + +def has_unstructured_input_feature(config: ModelConfig) -> bool: + for feature in config.input_features.to_list(): + if feature.get("type", None) in {TEXT, IMAGE, SEQUENCE, TIMESERIES}: + return True + return False + + +def has_pretrained_encoder(config: ModelConfig) -> bool: + for feature in config.input_features: + if feature.encoder.is_pretrained(): + return True + return False + + +def config_uses_llm(config: dict[str, Any] | ModelConfig) -> bool: + """Determine if a config uses an LLM. + + Args: + config: Ludwig config object or dictionary + + Returns: + True if the model type is LLM or if the model uses and LLM encoder, otherwise False. + """ + uses_llm = False + + # For a valid config, model_type LLM is automatically True + # ECD models need to be checked for at least one LLM text encoder + if isinstance(config, ModelConfig): + if config.model_type == MODEL_LLM: + uses_llm = True + else: + for feature in config.input_features: + if feature.encoder and feature.encoder.type == MODEL_LLM: + uses_llm = True + break + elif isinstance(config, dict) and config: + if config.get(MODEL_TYPE, MODEL_ECD) == MODEL_LLM: + uses_llm = True + elif INPUT_FEATURES in config: + for feature in config.get(INPUT_FEATURES, []): + if feature.get(ENCODER, {}).get(TYPE) == MODEL_LLM: + uses_llm = True + break + else: + raise ValueError( + f"Invalid config cannot be checked for LLM usage because it has no input features.Config: {config}" + ) + else: + raise ValueError(f"Invalid config cannot be checked for LLM usage. Config: {config}") + + return uses_llm + + +def get_quantization(config: dict[str, Any] | ModelConfig) -> list[int | None]: + """Get the quantization specified in a config at any level. + + Args: + config: Ludwig config object or dictionary + + Returns: + For LLM models, the value of quantization.bits or None if it is not specified. + For ECD models, the list of values of quantization.bits for each encoder. If the encoder does not + support quantization or no quantization config is specified, the list entry is None. + """ + if isinstance(config, ModelConfig): + if config.model_type == MODEL_LLM: + return [config.quantization.bits] if config.quantization else [None] + else: + quantization_bits = [] + for feature in config.input_features: + try: + quantization = feature.encoder.quantization.bits + except AttributeError: + quantization = None + quantization_bits.append(quantization) + return quantization_bits + elif isinstance(config, dict) and config: + if config.get(MODEL_TYPE, MODEL_ECD) == MODEL_LLM: + return [config.get("quantization", {}).get("bits")] + elif INPUT_FEATURES in config: + quantization_bits = [] + for feature in config.get(INPUT_FEATURES, []): + quantization_bits.append(feature.get(ENCODER, {}).get("quantization", {}).get("bits")) + return quantization_bits + else: + raise ValueError( + f"Invalid config cannot be checked for quantization because it has no input features.Config: {config}" + ) + else: + raise ValueError(f"Invalid config cannot be checked for quantization. Config: {config}") diff --git a/ludwig/utils/data_utils.py b/ludwig/utils/data_utils.py new file mode 100644 index 0000000..754d44c --- /dev/null +++ b/ludwig/utils/data_utils.py @@ -0,0 +1,1152 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import base64 +import collections.abc +import contextlib +import csv +import dataclasses +import functools +import hashlib +import json +import logging +import os +import os.path +import pickle +import random +import re +import tempfile +import threading +from itertools import islice +from typing import Any + +import numpy as np +import pandas as pd +import pyarrow as pa +import yaml +from fsspec.config import conf, set_conf_files +from pandas.errors import ParserError +from sklearn.model_selection import KFold + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import PREPROCESSING, SPLIT +from ludwig.data.cache.types import CacheableDataset +from ludwig.globals import MODEL_HYPERPARAMETERS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME, TRAIN_SET_METADATA_FILE_NAME +from ludwig.utils.dataframe_utils import from_numpy_dataset, is_dask_lib, to_numpy_dataset +from ludwig.utils.fs_utils import download_h5, has_remote_protocol, open_file, upload_h5 +from ludwig.utils.math_utils import cumsum +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.types import DataFrame + +try: + import dask + import dask.dataframe as dd + + DASK_DF_FORMATS = {dd.DataFrame} +except ImportError: + DASK_DF_FORMATS = set() + dd = None + +logger = logging.getLogger(__name__) + +DATASET_SPLIT_URL = "dataset_{}_fp" +DATA_PROCESSED_CACHE_DIR = "data_processed_cache_dir" +DATA_TRAIN_HDF5_FP = "data_train_hdf5_fp" + +DATA_TRAIN_PARQUET_FP = "data_train_parquet_fp" +DATA_VALIDATION_PARQUET_FP = "data_validation_parquet_fp" +DATA_TEST_PARQUET_FP = "data_test_parquet_fp" + +HDF5_COLUMNS_KEY = "columns" +DICT_FORMATS = {"dict", "dictionary", dict} +DATAFRAME_FORMATS = {"dataframe", "df", pd.DataFrame} | DASK_DF_FORMATS +CSV_FORMATS = {"csv"} +TSV_FORMATS = {"tsv"} +JSON_FORMATS = {"json"} +JSONL_FORMATS = {"jsonl"} +EXCEL_FORMATS = {"excel"} +PARQUET_FORMATS = {"parquet"} +PICKLE_FORMATS = {"pickle"} +FEATHER_FORMATS = {"feather"} +FWF_FORMATS = {"fwf"} +HTML_FORMATS = {"html"} +ORC_FORMATS = {"orc"} +SAS_FORMATS = {"sas"} +SPSS_FORMATS = {"spss"} +STATA_FORMATS = {"stata"} +HDF5_FORMATS = {"hdf5", "h5"} +CACHEABLE_FORMATS = set.union( + *( + CSV_FORMATS, + TSV_FORMATS, + JSON_FORMATS, + JSONL_FORMATS, + EXCEL_FORMATS, + PARQUET_FORMATS, + PICKLE_FORMATS, + FEATHER_FORMATS, + FWF_FORMATS, + HTML_FORMATS, + ORC_FORMATS, + SAS_FORMATS, + SPSS_FORMATS, + STATA_FORMATS, + DATAFRAME_FORMATS, + ) +) + +PANDAS_DF = pd + + +# Lock over the entire interpreter as we can only have one set +# of credentials scoped to the interpreter at once. +GLOBAL_CRED_LOCK = threading.Lock() + + +@DeveloperAPI +def get_parquet_filename(n: int): + """Left pads the partition number with zeros to preserve order in downstream reads. + + Downstream reads use the filename to determine the lexical order of the partitions. + """ + return f"part.{str(n).zfill(8)}.parquet" + + +@DeveloperAPI +def get_split_path(dataset_fp): + return os.path.splitext(dataset_fp)[0] + ".split.parquet" + + +@DeveloperAPI +def get_abs_path(src_path, file_path): + if has_remote_protocol(file_path): + return file_path + elif src_path is not None: + return os.path.join(src_path, file_path) + else: + return file_path + + +@DeveloperAPI +def load_csv(data_fp): + with open_file(data_fp, "rb") as f: + data = list(csv.reader(f)) + return data + + +# Decorator used to encourage Dask on Ray to spread out data loading across workers +@DeveloperAPI +def spread(fn): + def wrapped_fn(*args, **kwargs): + if dd is None or not hasattr(dask, "annotate"): + return fn(*args, **kwargs) + + with dask.annotate(ray_remote_args={"scheduling_strategy": "SPREAD"}): + return fn(*args, **kwargs) + + return wrapped_fn + + +def read_nrows_via_chunksize(fp, read_fn, **kwargs): + chunksize = kwargs.pop("nrows", None) + ret = read_fn(fp, chunksize=chunksize, **kwargs) + + if isinstance(ret, collections.abc.Iterator): + return next(ret) + + return ret + + +@DeveloperAPI +@spread +def read_xsv(data_fp, df_lib=PANDAS_DF, separator=",", header=0, nrows=None, skiprows=None, dtype=object, **kwargs): + """Helper method to read a csv file. Wraps around pd.read_csv to handle some exceptions. Can extend to cover + cases as necessary. + + Args: + data_fp: path to the xsv file + df_lib: DataFrame library used to read in the CSV + separator: defaults separator to use for splitting + header: header argument for pandas to read the csv + nrows: number of rows to read from the csv, None means all + skiprows: number of rows to skip from the csv, None means no skips + dtype: dtype to use for columns. Defaults to object to disable type inference. + + Returns: + Pandas dataframe with the data + """ + with open_file(data_fp, "r", encoding="utf8") as csvfile: + try: + dialect = csv.Sniffer().sniff(csvfile.read(1024 * 100), delimiters=[",", "\t", "|"]) + separator = dialect.delimiter + except csv.Error: + # Could not conclude the delimiter, defaulting to user provided + pass + + # NOTE: by default we read all XSV columns in as dtype=object, bypassing all type inference. This is to avoid silent + # issues related to incorrect type inference (e.g. NaNs in bool columns). Convert data to correct types after + # reading in. + kwargs = dict(sep=separator, header=header, skiprows=skiprows, dtype=dtype, **kwargs) + + if nrows is not None: + kwargs["nrows"] = nrows + + try: + df = df_lib.read_csv(data_fp, **kwargs) + except ParserError: + logger.warning("Failed to parse the CSV with pandas default way, trying \\ as escape character.") + df = df_lib.read_csv(data_fp, escapechar="\\", **kwargs) + + return df + + +read_csv = functools.partial(read_xsv, separator=",") +read_tsv = functools.partial(read_xsv, separator="\t") + + +@DeveloperAPI +@spread +def read_json(data_fp, df_lib, normalize=False, **kwargs): + # Not supported unless lines=True + kwargs.pop("nrows", None) + + if normalize: + return df_lib.json_normalize(load_json(data_fp)) + else: + return df_lib.read_json(data_fp, **kwargs) + + +@DeveloperAPI +@spread +def read_jsonl(data_fp, df_lib, **kwargs): + return df_lib.read_json(data_fp, lines=True, **kwargs) + + +@DeveloperAPI +@spread +def read_excel(data_fp, df_lib, **kwargs): + fp_split = os.path.splitext(data_fp) + if fp_split[1] == ".xls": + excel_engine = "xlrd" + else: + excel_engine = "openpyxl" + + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + logger.warning("Falling back to pd.read_excel() since dask backend does not support it") + return dd.from_pandas(pd.read_excel(data_fp, engine=excel_engine, **kwargs), npartitions=1) + return df_lib.read_excel(data_fp, engine=excel_engine, **kwargs) + + +@DeveloperAPI +@spread +def read_parquet(data_fp, df_lib, nrows=None, **kwargs): + if nrows is not None: + import pyarrow.parquet as pq + + from ludwig.utils.fs_utils import get_fs_and_path + + fs, _ = get_fs_and_path(data_fp) + dataset = pq.ParquetDataset(data_fp, filesystem=fs).fragments[0] + + preview = dataset.head(nrows).to_pandas() + + if is_dask_lib(df_lib): + return df_lib.from_pandas(preview, npartitions=1) + return preview + + return df_lib.read_parquet(data_fp, **kwargs) + + +@DeveloperAPI +@spread +def read_pickle(data_fp, df_lib, **kwargs): + # Pickle deserializes arbitrary Python objects — only load files you trust. + # Auto-dispatch from file extension is intentionally disabled; this function + # is only reachable via explicit data_format="pickle". + logger.warning( + "Loading a pickle dataset (%s). Pickle deserialization executes arbitrary Python code — " + "only load files from sources you trust.", + data_fp, + ) + + # Chunking is not supported for pickle files: + kwargs.pop("nrows", None) + + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + return dd.from_pandas(pd.read_pickle(data_fp), npartitions=1) + return df_lib.read_pickle(data_fp) + + +@DeveloperAPI +@spread +def read_fwf(data_fp, df_lib, **kwargs): + return df_lib.read_fwf(data_fp, **kwargs) + + +@DeveloperAPI +@spread +def read_feather(data_fp, df_lib, **kwargs): + # Chunking is not supported for feather files: + kwargs.pop("nrows", None) + + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + logger.warning("Falling back to pd.read_feather() since dask backend does not support it") + return dd.from_pandas(pd.read_feather(data_fp), npartitions=1) + return df_lib.read_feather(data_fp) + + +@DeveloperAPI +@spread +def read_html(data_fp, df_lib, **kwargs): + # Chunking is not supported for html files: + kwargs.pop("nrows", None) + + # Wrap literal HTML strings in StringIO to avoid pandas FutureWarning + from io import StringIO + + if isinstance(data_fp, str) and not os.path.isfile(data_fp): + data_fp = StringIO(data_fp) + + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + logger.warning("Falling back to pd.read_html() since dask backend does not support it") + return dd.from_pandas(pd.read_html(data_fp)[0], npartitions=1) + return df_lib.read_html(data_fp)[0] + + +@DeveloperAPI +@spread +def read_orc(data_fp, df_lib, **kwargs): + # Chunking is not supported for orc files: + kwargs.pop("nrows", None) + + return df_lib.read_orc(data_fp, **kwargs) + + +@DeveloperAPI +@spread +def read_sas(data_fp, df_lib, **kwargs): + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + logger.warning("Falling back to pd.read_sas() since dask backend does not support it") + return dd.from_pandas(read_nrows_via_chunksize(data_fp, df_lib.read_sas, **kwargs), npartitions=1) + return read_nrows_via_chunksize(data_fp, df_lib.read_sas, **kwargs) + + +@DeveloperAPI +@spread +def read_spss(data_fp, df_lib, **kwargs): + # Chunking is not supported for spss files: + kwargs.pop("nrows", None) + + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + logger.warning("Falling back to pd.read_spss() since dask backend does not support it") + return dd.from_pandas(pd.read_spss(data_fp), npartitions=1) + return df_lib.read_spss(data_fp) + + +@DeveloperAPI +@spread +def read_stata(data_fp, df_lib, **kwargs): + # https://github.com/dask/dask/issues/9055 + if is_dask_lib(df_lib): + logger.warning("Falling back to pd.read_stata() since dask backend does not support it") + return dd.from_pandas(read_nrows_via_chunksize(data_fp, df_lib.read_stata, **kwargs), npartitions=1) + return read_nrows_via_chunksize(data_fp, df_lib.read_stata, **kwargs) + + +@DeveloperAPI +@spread +def read_hdf5(data_fp, **_kwargs): + return load_hdf5(data_fp, clean_cols=True) + + +@DeveloperAPI +@spread +def read_buffer(buf, fname): + """Reads data in from a binary buffer by first writing the data to a temporary file, and then processes it + based on its format (hdf5, csv, tsv etc). + + Useful if object is a binary buffer coming from streaming data. + """ + data_format = figure_data_format_dataset(fname) + reader_fn = data_reader_registry[data_format] + with tempfile.TemporaryDirectory() as tmpdir: + temp_name = os.path.join(tmpdir, "dataset") + with open(temp_name, "wb") as f: + f.write(buf.read()) + return reader_fn(temp_name, pd) + + +@DeveloperAPI +@spread +def read_fname(fname, data_format=None, df_lib=pd, **kwargs): + """This function reads data from fname using the df_lib data processing library (defaults to pandas). + + Useful if you don't know the file type extension in advance. + """ + data_format = data_format or figure_data_format_dataset(fname) + reader_fn = data_reader_registry[data_format] + return reader_fn(fname, df_lib, **kwargs) + + +@DeveloperAPI +def save_csv(data_fp, data): + with open_file(data_fp, "w", encoding="utf-8") as csv_file: + writer = csv.writer(csv_file) + for row in data: + if not isinstance(row, collections.abc.Iterable) or isinstance(row, str): + row = [row] + writer.writerow(row) + + +@DeveloperAPI +def csv_contains_column(data_fp, column_name): + return column_name in read_csv(data_fp, nrows=0) # only loads header + + +@DeveloperAPI +def load_yaml(yaml_fp): + with open_file(yaml_fp, "r") as f: + return yaml.safe_load(f) + + +@DeveloperAPI +def load_config_from_str(config): + """Load the config as either a serialized string or a path to a YAML file.""" + config = yaml.safe_load(config) + if isinstance(config, str): + # Assume the caller provided a path name + with open(config, encoding="utf-8") as f: + config = yaml.safe_load(f) + return config + + +@DeveloperAPI +def load_json(data_fp): + with open_file(data_fp, "r") as input_file: + data = json.load(input_file) + return data + + +@DeveloperAPI +def save_json(data_fp, data, sort_keys=True, indent=4): + with open_file(data_fp, "w") as output_file: + json.dump(data, output_file, cls=NumpyEncoder, sort_keys=sort_keys, indent=indent) + + +@DeveloperAPI +def hash_dict(d: dict, max_length: int | None = 6) -> bytes: + """Function that maps a dictionary into a unique hash. + + Known limitation: All values and keys of the dict must have an ordering. If not, there's no guarantee to obtain the + same hash. For instance, values that are sets will potentially lead to different hashed when run on different + machines or in different python sessions. Replacing them with sorted lists is suggested. + """ + s = json.dumps(d, cls=NumpyEncoder, sort_keys=True, ensure_ascii=True) + h = hashlib.md5(s.encode()) + d = h.digest() + b = base64.b64encode(d, altchars=b"__") + return b[:max_length] + + +@DeveloperAPI +def numpy_to_python(obj: Any) -> Any: + """Recursively convert numpy scalars and arrays to plain Python types. + + Suitable for making objects JSON-serializable without a full JSON round-trip. + """ + if isinstance(obj, np.integer): + return int(obj) + if isinstance(obj, np.floating): + return float(obj) + if isinstance(obj, np.bool_): + return bool(obj) + if isinstance(obj, np.ndarray): + return obj.tolist() + if isinstance(obj, dict): + return {k: numpy_to_python(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple, set)): + return [numpy_to_python(v) for v in obj] + return obj + + +@DeveloperAPI +def to_json_dict(d): + """Converts Python dict to pure JSON ready format.""" + return json.loads(json.dumps(d, cls=NumpyEncoder)) + + +@DeveloperAPI +def chunk_dict(data, chunk_size=100): + """Split large dictionary into chunks. + + Source: https://stackoverflow.com/a/22878842 + """ + it = iter(data) + for _ in range(0, len(data), chunk_size): + yield {k: data[k] for k in islice(it, chunk_size)} + + +@DeveloperAPI +def flatten_dict(d, parent_key="", sep="."): + """Based on https://www.geeksforgeeks.org/python-convert-nested-dictionary-into-flattened-dictionary/""" + items = [] + for k, v in d.items(): + new_key = parent_key + sep + k if parent_key else k + + if isinstance(v, collections.abc.MutableMapping): + items.extend(flatten_dict(v, new_key, sep=sep).items()) + elif isinstance(v, list): + list_mapping = {str(i): item for i, item in enumerate(v)} + items.extend(flatten_dict(list_mapping, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + +@DeveloperAPI +def save_hdf5(data_fp, data): + numpy_dataset = to_numpy_dataset(data) + with upload_h5(data_fp) as h5_file: + h5_file.create_dataset(HDF5_COLUMNS_KEY, data=np.array(data.columns.values, dtype="S")) + for column in data.columns: + h5_file.create_dataset(column, data=numpy_dataset[column]) + + +@DeveloperAPI +def load_hdf5(data_fp, clean_cols: bool = False): + with download_h5(data_fp) as hdf5_data: + columns = [s.decode("utf-8") for s in hdf5_data[HDF5_COLUMNS_KEY][()].tolist()] + + numpy_dataset = {} + for column in columns: + # Column names from training hdf5 will be in the form 'Survived_a2fv4' + np_col = column.rsplit("_", 1)[0] if clean_cols else column + numpy_dataset[np_col] = hdf5_data[column][()] + + return from_numpy_dataset(numpy_dataset) + + +@DeveloperAPI +def load_object(object_fp): + with open_file(object_fp, "rb") as f: + return pickle.load(f) + + +@DeveloperAPI +def save_object(object_fp, obj): + with open_file(object_fp, "wb") as f: + pickle.dump(obj, f) + + +@DeveloperAPI +def load_array(data_fp, dtype=float): + list_num = [] + with open_file(data_fp, "r") as input_file: + for x in input_file: + list_num.append(dtype(x.strip())) + return np.array(list_num) + + +@DeveloperAPI +def load_matrix(data_fp, dtype=float): + list_num = [] + with open_file(data_fp, "r") as input_file: + for row in input_file: + list_num.append([dtype(elem) for elem in row.strip().split()]) + return np.squeeze(np.array(list_num)) + + +@DeveloperAPI +def save_array(data_fp, array): + with open_file(data_fp, "w") as output_file: + for x in np.nditer(array): + output_file.write(str(x) + "\n") + + +# TODO(shreya): Confirm types of args +@DeveloperAPI +def load_pretrained_embeddings(embeddings_path: str, vocab: list[str]) -> np.ndarray: + """Create an embedding matrix of all words in vocab.""" + embeddings, embeddings_size = load_glove(embeddings_path, return_embedding_size=True) + + # calculate an average embedding, to use for initializing missing words + avg_embedding = [embeddings[w] for w in vocab if w in embeddings] + avg_embedding = sum(avg_embedding) / len(avg_embedding) + + # create the embedding matrix + embeddings_vectors = [] + for word in vocab: + if word in embeddings: + embeddings_vectors.append(embeddings[word]) + else: + embeddings_vectors.append(avg_embedding + np.random.uniform(-0.01, 0.01, embeddings_size)) + embeddings_matrix = np.stack(embeddings_vectors) + + # let's help the garbage collector free some memory + embeddings = None + + return embeddings_matrix + + +@DeveloperAPI +@functools.lru_cache(1) +def load_glove(file_path: str, return_embedding_size: bool = False) -> dict[str, np.ndarray]: + """Loads Glove embeddings for each word. + + Returns: + Mapping between word and numpy array of size embedding_size as set by + first line of file. + """ + logger.info(f" Loading Glove format file {file_path}") + embeddings = {} + embedding_size = 0 + + # collect embeddings size assuming the first line is correct + with open_file(file_path, "r", encoding="utf-8") as f: + found_line = False + while not found_line: + line = f.readline() + if line: + embedding_size = len(line.split()) - 1 + found_line = True + + # collect embeddings + with open_file(file_path, "r", encoding="utf-8") as f: + for line_number, line in enumerate(f): + if line: + try: + split = line.split() + if len(split) != embedding_size + 1: + raise ValueError( + f"Line {line_number} is of length {len(split)}, " + f"while expected length is {embedding_size + 1}." + ) + word = split[0] + embedding = np.array([float(val) for val in split[-embedding_size:]]) + embeddings[word] = embedding + except ValueError: + logger.warning(f"Line {line_number} in the GloVe file {file_path} is malformed, skipping it") + logger.info(f" {len(embeddings)} embeddings loaded") + + if return_embedding_size: + return embeddings, embedding_size + return embeddings + + +@DeveloperAPI +def split_data(split: float, data: list) -> tuple[list, list]: + split_length = int(round(split * len(data))) + random.shuffle(data) + return data[:split_length], data[split_length:] + + +@DeveloperAPI +def split_by_slices(slices: list[Any], n: int, probabilities: list[float]) -> list[Any]: + splits = [] + indices = cumsum([int(x * n) for x in probabilities]) + start = 0 + for end in indices: + splits.append(slices[start:end]) + start = end + return splits + + +@DeveloperAPI +def shuffle_unison_inplace(list_of_lists, random_state=None): + if list_of_lists: + if not all(len(single_list) == len(list_of_lists[0]) for single_list in list_of_lists): + lengths = [len(lst) for lst in list_of_lists] + raise ValueError(f"All lists must have the same length for unison shuffling, got lengths: {lengths}.") + if random_state is not None: + p = random_state.permutation(len(list_of_lists[0])) + else: + p = np.random.permutation(len(list_of_lists[0])) + return [single_list[p] for single_list in list_of_lists] + return None + + +@DeveloperAPI +def shuffle_dict_unison_inplace(np_dict, random_state=None): + keys = list(np_dict.keys()) + list_of_lists = list(np_dict.values()) + + # shuffle up the list of lists according to previous fct + shuffled_list = shuffle_unison_inplace(list_of_lists, random_state) + + recon = {} + for ii, dkey in enumerate(keys): + recon[dkey] = shuffled_list[ii] + + # we've shuffled the dictionary in place! + return recon + + +@DeveloperAPI +def split_dataset_ttv(dataset, split): + # Obtain distinct splits from the split column. If + # a split is not present in this set, then we can skip generating + # the dataframe for that split. + if dataset[split].dtype != int: + dataset[split] = dataset[split].astype(int) + + distinct_values = dataset[split].drop_duplicates() + if hasattr(distinct_values, "compute"): + distinct_values = distinct_values.compute() + distinct_values = set(distinct_values.values.tolist()) + + training_set = split_dataset(dataset, split, 0) if 0 in distinct_values else None + validation_set = split_dataset(dataset, split, 1) if 1 in distinct_values else None + test_set = split_dataset(dataset, split, 2) if 2 in distinct_values else None + return training_set, test_set, validation_set + + +@DeveloperAPI +def split_dataset(dataset, split, value_to_split=0): + split_df = dataset[dataset[split] == value_to_split] + return split_df + + +@DeveloperAPI +def collapse_rare_labels(labels, labels_limit): + if labels_limit > 0: + labels[labels >= labels_limit] = labels_limit + return labels + + +@DeveloperAPI +def class_counts(dataset, labels_field): + return np.bincount(dataset[labels_field].flatten()).tolist() + + +@DeveloperAPI +def load_from_file(file_name, field=None, dtype=int, ground_truth_split=2): + """Load experiment data from supported file formats. + + Experiment data can be test/train statistics, model predictions, probability, ground truth, ground truth metadata. + + Args: + file_name: Path to file to be loaded. + field: Target Prediction field. + dtype: dtype to use when loading matrix data. + ground_truth_split: Ground truth split filter where 0 is train 1 is validation and 2 is test split. By + default test split is used when loading ground truth from hdf5. + + Returns: + Experiment data as array. + """ + if file_name.endswith(".hdf5") and field is not None: + dataset = pd.read_hdf(file_name, key=HDF5_COLUMNS_KEY) + column = dataset[field] + array = column[dataset[SPLIT] == ground_truth_split].values # ground truth + elif file_name.endswith(".npy"): + array = np.load(file_name) + elif file_name.endswith(".csv"): + array = read_csv(file_name, header=None).values + else: + array = load_matrix(file_name, dtype) + return array + + +@DeveloperAPI +def replace_file_extension(file_path, extension): + """Return a file path for a file with same name but different format. a.csv, json -> a.json a.csv, hdf5 -> a.hdf5. + + Args: + file_path: original file path + extension: file extension + + Returns: + file path with same name but different format + """ + if file_path is None: + return None + extension = extension.strip() + if extension.startswith("."): + # Handle the case if the user calls with '.hdf5' instead of 'hdf5' + extension = extension[1:] + + return os.path.splitext(file_path)[0] + "." + extension + + +@DeveloperAPI +def file_exists_with_diff_extension(file_path, extension): + return file_path is None or os.path.isfile(replace_file_extension(file_path, extension)) + + +@DeveloperAPI +def add_sequence_feature_column(df, col_name, seq_length): + """Adds a new column to the dataframe computed from an existing column. Values in the new column are space- + delimited strings composed of preceding values of the same column up to seq_length. For example values of the + i-th row of the new column will be a space-delimited string of df[col_name][i-seq_length]. + + Args: + df: input dataframe + col_name: column name containing sequential data + seq_length: length of an array of preceeding column values to use + """ + if col_name not in df.columns.values: + logger.error(f"{col_name} column does not exist") + return + + new_col_name = col_name + "_feature" + if new_col_name in df.columns.values: + logger.warning(f"{new_col_name} column already exists, values will be overridden") + + new_data = [None] * seq_length + old_data = np.array(df[col_name]) + + for i in range(seq_length, len(df)): + new_data.append(" ".join(str(j) for j in old_data[i - seq_length : i])) + + df[new_col_name] = new_data + df[new_col_name] = df[new_col_name].bfill() + + +@DeveloperAPI +def override_in_memory_flag(input_features, override_value): + num_overrides = 0 + for feature in input_features: + if PREPROCESSING in feature: + if "in_memory" in feature[PREPROCESSING]: + feature[PREPROCESSING]["in_memory"] = override_value + num_overrides += 1 + return num_overrides + + +@DeveloperAPI +def normalize_numpy(obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return normalize_numpy(obj.tolist()) + elif isinstance(obj, list): + return [normalize_numpy(v) for v in obj] + else: + return obj + + +@DeveloperAPI +class NumpyEncoder(json.JSONEncoder): + """Custom JSON encoder for handling NumPy objects. + + This encoder extends the `json.JSONEncoder` class and provides + custom serialization for NumPy objects. It converts NumPy arrays, + sets, tuples, integers, floating-point numbers, booleans, and + dataclasses to their JSON serializable equivalents. + + Attributes: + None + + Methods: + default: Overrides the default method of `json.JSONEncoder` + to provide custom serialization for NumPy objects. + + Usage: + Use this encoder when serializing objects that contain NumPy + arrays or other NumPy objects to JSON. + + Example: + encoder = NumpyEncoder() + json_data = encoder.encode(data) + """ + + def default(self, o): + if isinstance(o, (set, tuple)): + return list(o) + elif isinstance(o, np.bool_): + return bool(o) + elif isinstance(o, np.integer): + return int(o) + elif isinstance(o, np.floating): + return float(o) + elif isinstance(o, np.ndarray): + return o.tolist() + elif dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + elif hasattr(o, "to_dict"): + return o.to_dict() + else: + return json.JSONEncoder.default(self, o) + + +@DeveloperAPI +def generate_kfold_splits(data_df, num_folds, random_state): + kf = KFold(n_splits=num_folds, shuffle=True, random_state=random_state) + fold_num = 0 + for train_indices, test_indices in kf.split(data_df): + fold_num += 1 + yield train_indices, test_indices, fold_num + + +@DeveloperAPI +def get_path_size(start_path, regex_accept=None, regex_reject=None): + total_size = 0 + pattern_accept = re.compile(regex_accept) if regex_accept else None + pattern_reject = re.compile(regex_reject) if regex_reject else None + + for dirpath, _, filenames in os.walk(start_path): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + if not os.path.islink(filepath): + accepted = True + if pattern_accept: + accepted = accepted and pattern_accept.match(filename) + if pattern_reject: + accepted = accepted and not pattern_reject.match(filename) + if accepted: + total_size += os.path.getsize(filepath) + + return total_size + + +@DeveloperAPI +def clear_data_cache(): + """Clears any cached data objects (e.g., embeddings)""" + load_glove.cache_clear() + + +@DeveloperAPI +def figure_data_format_dataset(dataset): + from ludwig.data.format_registry import detect_format + + if isinstance(dataset, CacheableDataset): + return figure_data_format_dataset(dataset.unwrap()) + elif isinstance(dataset, pd.DataFrame): + return pd.DataFrame + elif dd and isinstance(dataset, dd.DataFrame): + return dd.DataFrame + elif isinstance(dataset, dict): + return dict + elif isinstance(dataset, str): + dataset_str = dataset.strip() + if dataset_str.startswith("ludwig://"): + return "ludwig" + if dataset_str.startswith("hf://"): + return "hf" + + # Delegate to format registry for extension-based detection + fmt = detect_format(dataset_str) + if fmt is not None: + return fmt + + # Legacy fallback for extensions not in registry. + # .p / .pkl / .pickle are intentionally absent — pickle auto-dispatch is + # disabled (CWE-502). Pass data_format="pickle" explicitly to opt in. + lower = dataset_str.lower() + if lower.endswith((".xlsm", ".xlsb", ".odf", ".ods", ".odt")): + return "excel" + elif lower.endswith(".sas"): + return "sas" + elif lower.endswith(".spss"): + return "spss" + elif lower.endswith(".stata"): + return "stata" + else: + raise ValueError(f"Dataset path string {dataset} does not contain a valid extension") + else: + raise ValueError(f"Cannot figure out the format of dataset {dataset}") + + +@DeveloperAPI +def figure_data_format(dataset=None, training_set=None, validation_set=None, test_set=None): + if dataset is not None: + data_format = figure_data_format_dataset(dataset) + elif training_set is not None: + data_formats = [figure_data_format_dataset(training_set)] + if validation_set is not None: + data_formats.append(figure_data_format_dataset(validation_set)) + if test_set is not None: + data_formats.append(figure_data_format_dataset(test_set)) + data_formats_set = set(data_formats) + if len(data_formats_set) > 1: + error_message = "Datasets have different formats. Training: " + error_message += str(data_formats[0]) + if validation_set: + error_message = ", Validation: " + error_message += str(data_formats[1]) + if test_set: + error_message = ", Test: " + error_message += str(data_formats[-1]) + raise ValueError(error_message) + else: + data_format = next(iter(data_formats_set)) + else: + raise ValueError("At least one between dataset and training_set must be not None") + return data_format + + +@DeveloperAPI +def is_model_dir(path: str) -> bool: + hyperparameters_fn = os.path.join(path, MODEL_HYPERPARAMETERS_FILE_NAME) + ts_metadata_fn = os.path.join(path, TRAIN_SET_METADATA_FILE_NAME) + is_a_model_dir = False + if os.path.isdir(path) and os.path.isfile(hyperparameters_fn) and os.path.isfile(ts_metadata_fn): + for file_name in os.listdir(path): + if file_name.startswith(MODEL_WEIGHTS_FILE_NAME): + is_a_model_dir = True + break + return is_a_model_dir + + +@DeveloperAPI +def ndarray2string(parm_array): + # convert numpy.ndarray to ludwig custom string format + if isinstance(parm_array, np.ndarray): + return f"__ndarray__{json.dumps(parm_array.tolist())}" + else: + raise ValueError(f"Argument must be numpy.ndarray. Instead argument found to be {type(parm_array)}") + + +@DeveloperAPI +def string2ndarray(parm_string): + # convert ludwig custom ndarray string to numpy.ndarray + if isinstance(parm_string, str) and parm_string[:11] == "__ndarray__": + return np.array(json.loads(parm_string[11:])) + else: + raise ValueError("Argument must be Ludwig custom string format for numpy.ndarray") + + +@DeveloperAPI +def is_ludwig_ndarray_string(parm_string): + # tests if parameter is a Ludwig custom ndarray string + return isinstance(parm_string, str) and parm_string[:11] == "__ndarray__" + + +@DeveloperAPI +def get_pa_dtype(obj: Any): + if np.isscalar(obj): + return pa.from_numpy_dtype(np.array(obj).dtype) + elif isinstance(obj, np.ndarray) or isinstance(obj, list) or isinstance(obj, tuple): + return pa.list_(get_pa_dtype(obj[0])) + else: + raise ValueError(f"Unsupported type for pyarrow dtype: {type(obj)}") + + +@DeveloperAPI +def get_pa_schema(df: DataFrame): + """Gets the pyarrow schema associated with a given DataFrame. + + This will fail in very specific conditions worth enumerating: + 1. If the DataFrame is a Dask DataFrame which has a partition of size 1 and its only sample is a NaN, then the + `schema` dict will not contain the associated key. The value in this case will be inferred (likely incorrectly) + as a float64 downstream. + 2. If the DataFrame contains NaNs in some column and the presence of NaNs changes the overall dtype of the column. + For example, if a number feature column contains some NaN-like value, then its dtype will be changed by the + below `fillna` call from float32 to float64. This will cause `to_parquet` to fail downstream. + """ + head = df.head(100) + + schema = {} + for k, v in head.items(): + if sum(v.isna()) > 0: + v = v.fillna(np.nan).replace([np.nan], [None]) # Only fill NaNs if they are present + v = v.values + + for val in v: + if val is not None and k not in schema: + schema[k] = get_pa_dtype(val) + break + return pa.schema(list(schema.items())) + + +data_reader_registry = { + **dict.fromkeys(CSV_FORMATS, read_csv), + **dict.fromkeys(TSV_FORMATS, read_tsv), + **dict.fromkeys(JSON_FORMATS, read_json), + **dict.fromkeys(JSONL_FORMATS, read_jsonl), + **dict.fromkeys(EXCEL_FORMATS, read_excel), + **dict.fromkeys(PARQUET_FORMATS, read_parquet), + **dict.fromkeys(PICKLE_FORMATS, read_pickle), + **dict.fromkeys(FWF_FORMATS, read_fwf), + **dict.fromkeys(FEATHER_FORMATS, read_feather), + **dict.fromkeys(HTML_FORMATS, read_html), + **dict.fromkeys(ORC_FORMATS, read_orc), + **dict.fromkeys(SAS_FORMATS, read_sas), + **dict.fromkeys(SPSS_FORMATS, read_spss), + **dict.fromkeys(STATA_FORMATS, read_stata), + **dict.fromkeys(HDF5_FORMATS, read_hdf5), +} + + +@DeveloperAPI +def load_dataset(dataset, data_format=None, df_lib=PANDAS_DF): + if not data_format or data_format == "auto": + data_format = figure_data_format(dataset) + + # use appropriate reader to create dataframe + if data_format in DATAFRAME_FORMATS: + return dataset + elif data_format in DICT_FORMATS: + return pd.DataFrame(dataset) + elif data_format in CACHEABLE_FORMATS: + data_reader = get_from_registry(data_format, data_reader_registry) + return data_reader(dataset, df_lib) + else: + raise ValueError(f"{data_format} format is not supported") + + +@DeveloperAPI +@contextlib.contextmanager +def use_credentials(creds): + if creds is None: + with contextlib.nullcontext(): + yield + return + + # https://filesystem-spec.readthedocs.io/en/latest/features.html#configuration + # This allows us to avoid having to plumb the `storage_options` kwargs through + # every remote FS call in Ludwig. This implementation is restricted to one thread + # in the process acquiring the lock at once. + with GLOBAL_CRED_LOCK: + with tempfile.TemporaryDirectory() as tmpdir: + fname = os.path.join(tmpdir, "conf.json") + with open(fname, "w", encoding="utf-8") as f: + json.dump(creds, f) + + # Backup any existing credentials + old_conf = dict(**conf) + + conf.clear() + set_conf_files(tmpdir, conf) + try: + yield + finally: + # Restore previous credentials + with open(fname, "w", encoding="utf-8") as f: + json.dump(old_conf, f) + conf.clear() + set_conf_files(tmpdir, conf) + + +def get_sanitized_feature_name(feature_name: str) -> str: + """Replaces non-word characters (anything other than alphanumeric or _) with _. + + Used in model config initialization and sanitize_column_names(), which is called during dataset building. + """ + return re.sub(r"[(){}.:\"\"\'\'\[\]]", "_", feature_name) + + +def sanitize_column_names(df: DataFrame) -> DataFrame: + """Renames df columns with non-word characters (anything other than alphanumeric or _) to _.""" + safe_column_names = [get_sanitized_feature_name(col) for col in df.columns] + return df.rename(columns=dict(zip(df.columns, safe_column_names))) diff --git a/ludwig/utils/dataframe_utils.py b/ludwig/utils/dataframe_utils.py new file mode 100644 index 0000000..405356c --- /dev/null +++ b/ludwig/utils/dataframe_utils.py @@ -0,0 +1,159 @@ +from typing import Optional + +import numpy as np +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import DASK_MODULE_NAME +from ludwig.data.dataframe.base import DataFrameEngine +from ludwig.utils.types import DataFrame + + +@DeveloperAPI +def is_dask_lib(df_lib) -> bool: + """Returns whether the dataframe library is dask.""" + return df_lib.__name__ == DASK_MODULE_NAME + + +@DeveloperAPI +def is_dask_backend(backend: Optional["Backend"]) -> bool: # noqa: F821 + """Returns whether the backend's dataframe is dask.""" + return backend is not None and is_dask_lib(backend.df_engine.df_lib) + + +@DeveloperAPI +def is_dask_series_or_df(df: DataFrame, backend: Optional["Backend"]) -> bool: # noqa: F821 + if is_dask_backend(backend): + import dask.dataframe as dd + + return isinstance(df, dd.Series) or isinstance(df, dd.DataFrame) + return False + + +@DeveloperAPI +def flatten_df(df: DataFrame, df_engine: DataFrameEngine) -> tuple[DataFrame, dict[str, tuple]]: + """Returns a flattened dataframe with a dictionary of the original shapes, keyed by dataframe columns.""" + # Workaround for: https://issues.apache.org/jira/browse/ARROW-5645 + column_shapes = {} + for c in df.columns: + df = df_engine.persist(df) + shape = df_engine.compute( + df_engine.map_objects( + df[c], + lambda x: np.array(x).shape, + ).max() + ) + + if len(shape) > 1: + column_shapes[c] = shape + df[c] = df_engine.map_objects(df[c], lambda x: np.array(x).reshape(-1)) + return df, column_shapes + + +@DeveloperAPI +def unflatten_df(df: DataFrame, column_shapes: dict[str, tuple], df_engine: DataFrameEngine) -> DataFrame: + """Returns an unflattened dataframe, the reverse of flatten_df.""" + for c in df.columns: + shape = column_shapes.get(c) + if shape: + df[c] = df_engine.map_objects(df[c], lambda x: np.array(x).reshape(shape)) + return df + + +@DeveloperAPI +def to_numpy_dataset(df: DataFrame, backend: Optional["Backend"] = None) -> dict[str, np.ndarray]: # noqa: F821 + """Returns a dictionary of numpy arrays, keyed by the columns of the given dataframe. + + Also accepts a dict of numpy arrays (returned as-is after ensuring values are numpy arrays). + """ + # If already a dict (e.g., from _load_parquet), ensure values are numpy arrays and return + if isinstance(df, dict): + dataset = {} + for k, v in df.items(): + if not isinstance(v, np.ndarray): + v = np.array(v) + dataset[k] = v + return dataset + + # Compute Dask DataFrames to pandas first to avoid issues with extension dtypes + # (e.g. TensorDtype) that Dask-expr's metadata system cannot handle. + if backend and is_dask_backend(backend): + df = backend.df_engine.compute(df) + dataset = {} + for col in df.columns: + if len(df.index) != 0: + dataset[col] = np.stack(df[col].to_numpy()) + else: + # Dataframe is empty. + # Use to_list() directly, as np.stack() requires at least one array to stack. + dataset[col] = df[col].to_list() + return dataset + + +@DeveloperAPI +def from_numpy_dataset(dataset) -> pd.DataFrame: + """Returns a pandas dataframe from the dataset.""" + import numpy as np + + col_mapping = {} + for k, v in dataset.items(): + if not isinstance(v, np.ndarray): + v = np.array(v) + if len(v.shape) > 1: + # unstacking, needed for ndarrays of dimension 2 and more + (*vals,) = v + else: + # not unstacking. Needed because otherwise pandas casts types + # the way it wants, like converting a list of float32 scalats + # to a column of float64 + vals = v + col_mapping[k] = vals + if not col_mapping: + # pandas 2.x: pd.DataFrame.from_dict({}) returns RangeIndex (int) columns; + # explicitly return object-dtype Index so .str accessor works downstream. + return pd.DataFrame(columns=pd.Index([], dtype="object")) + return pd.DataFrame.from_dict(col_mapping) + + +@DeveloperAPI +def set_index_name(pd_df: pd.DataFrame, name: str) -> pd.DataFrame: + pd_df.index.name = name + return pd_df + + +@DeveloperAPI +def to_batches(df: pd.DataFrame, batch_size: int) -> list[pd.DataFrame]: + n_rows = len(df) + return [df[i : i + batch_size].copy() for i in range(0, n_rows, batch_size)] + + +@DeveloperAPI +def from_batches(batches: list[pd.DataFrame]) -> pd.DataFrame: + return pd.concat(batches) + + +@DeveloperAPI +def to_scalar_df(df: pd.DataFrame) -> pd.DataFrame: + """Converts all columns in a pd.DataFrame to be scalar types. + + For object columns of lists, each element of the list is expanded into its own column named {column}_{index}. We + assume all object columns are lists of the same length (i.e., tensor format output from preprocessing). It's also + important that the relative order of the columns is preserved, to maintain consistency with other conversions like + the one for Hummingbird. + """ + scalar_df = df + column_ordering = [] + for c, s in df.items(): + if s.dtype == "object": + s_list = s.to_list() + try: + ncols = s_list[0].shape[0] + split_cols = [f"{c}_{k}" for k in range(ncols)] + sdf = pd.DataFrame(s_list, columns=split_cols) + scalar_df = pd.concat([scalar_df, sdf], axis=1) + column_ordering += split_cols + except AttributeError as e: + raise ValueError(f"Expected series of lists, but found {s_list[0]}") from e + else: + column_ordering.append(c) + return scalar_df[column_ordering] diff --git a/ludwig/utils/dataset_quality.py b/ludwig/utils/dataset_quality.py new file mode 100644 index 0000000..a896e35 --- /dev/null +++ b/ludwig/utils/dataset_quality.py @@ -0,0 +1,497 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Dataset quality checks for Ludwig training data. + +Usage:: + + from ludwig.utils.dataset_quality import check_dataset_quality + + report = check_dataset_quality(df, target_column="label") + print(report.summary()) # one-line PASS/FAIL + counts + for f in report.failures: + print(f.name, f.message) # actionable failure details + +Checks performed +---------------- +minimum_size, missing_values, high_cardinality_target, single_class, +class_imbalance, id_columns, near_duplicate_columns, low_variance_columns, +target_leakage. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from enum import Enum + +import numpy as np +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI + +logger = logging.getLogger(__name__) + + +class CheckStatus(str, Enum): + PASS = "pass" + WARN = "warn" + FAIL = "fail" + + +@DeveloperAPI +@dataclass +class CheckResult: + name: str + status: CheckStatus + message: str + details: dict = field(default_factory=dict) + + +@DeveloperAPI +@dataclass +class DatasetQualityReport: + dataset_name: str + n_rows: int + n_cols: int + checks: list[CheckResult] + + @property + def passed(self) -> bool: + """True if no FAIL checks.""" + return all(c.status != CheckStatus.FAIL for c in self.checks) + + @property + def warnings(self) -> list[CheckResult]: + return [c for c in self.checks if c.status == CheckStatus.WARN] + + @property + def failures(self) -> list[CheckResult]: + return [c for c in self.checks if c.status == CheckStatus.FAIL] + + def summary(self) -> str: + """One-line summary string.""" + n_fail = sum(1 for c in self.checks if c.status == CheckStatus.FAIL) + n_warn = sum(1 for c in self.checks if c.status == CheckStatus.WARN) + n_pass = len(self.checks) - n_fail - n_warn + overall = "PASS" if n_fail == 0 else "FAIL" + return ( + f"[{overall}] Dataset '{self.dataset_name}' " + f"({self.n_rows} rows, {self.n_cols} cols): " + f"{n_pass} passed, {n_warn} warnings, {n_fail} failures" + ) + + +# --------------------------------------------------------------------------- +# Individual check helpers +# --------------------------------------------------------------------------- + + +def _check_minimum_size(df: pd.DataFrame, min_rows: int) -> CheckResult: + n = len(df) + if n < min_rows: + return CheckResult( + name="minimum_size", + status=CheckStatus.FAIL, + message=f"Dataset has only {n} rows; at least {min_rows} are required.", + details={"n_rows": n, "min_rows": min_rows}, + ) + return CheckResult( + name="minimum_size", + status=CheckStatus.PASS, + message=f"Dataset has {n} rows (minimum {min_rows}).", + details={"n_rows": n, "min_rows": min_rows}, + ) + + +def _check_minimum_features(df: pd.DataFrame, target_column: str | None, min_features: int) -> CheckResult: + feature_cols = [c for c in df.columns if c != target_column] + n = len(feature_cols) + if n < min_features: + return CheckResult( + name="minimum_features", + status=CheckStatus.FAIL, + message=(f"Dataset has only {n} non-target feature column(s); at least {min_features} are required."), + details={"n_features": n, "min_features": min_features}, + ) + return CheckResult( + name="minimum_features", + status=CheckStatus.PASS, + message=f"Dataset has {n} non-target feature column(s) (minimum {min_features}).", + details={"n_features": n, "min_features": min_features}, + ) + + +def _check_missing_values(df: pd.DataFrame, max_missing_pct: float) -> CheckResult: + total_cells = df.size + if total_cells == 0: + missing_pct = 0.0 + else: + missing_pct = df.isnull().sum().sum() / total_cells + + if missing_pct > max_missing_pct: + return CheckResult( + name="missing_values", + status=CheckStatus.WARN, + message=(f"Dataset has {missing_pct:.1%} missing values overall (threshold {max_missing_pct:.1%})."), + details={"missing_pct": missing_pct, "max_missing_pct": max_missing_pct}, + ) + return CheckResult( + name="missing_values", + status=CheckStatus.PASS, + message=f"Missing values: {missing_pct:.1%} (threshold {max_missing_pct:.1%}).", + details={"missing_pct": missing_pct, "max_missing_pct": max_missing_pct}, + ) + + +def _check_constant_columns(df: pd.DataFrame) -> CheckResult: + constant_cols = [c for c in df.columns if df[c].nunique(dropna=True) <= 1] + if constant_cols: + return CheckResult( + name="constant_columns", + status=CheckStatus.WARN, + message=f"Found {len(constant_cols)} constant column(s): {constant_cols}.", + details={"constant_columns": constant_cols}, + ) + return CheckResult( + name="constant_columns", + status=CheckStatus.PASS, + message="No constant columns detected.", + details={"constant_columns": []}, + ) + + +_MAX_CORR_COLS = 50 + + +def _check_near_duplicate_columns(df: pd.DataFrame, threshold: float) -> CheckResult: + """Finds pairs of numeric columns whose Pearson |r| exceeds *threshold*.""" + numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist() + if len(numeric_cols) > _MAX_CORR_COLS: + logger.warning( + f"Skipping near-duplicate column check: {len(numeric_cols)} numeric columns exceeds " + f"the {_MAX_CORR_COLS}-column cap to avoid O(n²) runtime." + ) + return CheckResult( + name="near_duplicate_columns", + status=CheckStatus.WARN, + message=f"Too many numeric columns ({len(numeric_cols)}) to check for near-duplicates efficiently.", + details={"pairs": [], "threshold": threshold}, + ) + near_dup_pairs: list[tuple[str, str, float]] = [] + + for i in range(len(numeric_cols)): + for j in range(i + 1, len(numeric_cols)): + col_a = numeric_cols[i] + col_b = numeric_cols[j] + try: + r = df[[col_a, col_b]].dropna().corr().iloc[0, 1] + except (ValueError, TypeError): + continue + if pd.notna(r) and abs(r) > threshold: + near_dup_pairs.append((col_a, col_b, float(r))) + + if near_dup_pairs: + pair_strs = [f"({a}, {b}, r={r:.3f})" for a, b, r in near_dup_pairs] + return CheckResult( + name="near_duplicate_columns", + status=CheckStatus.WARN, + message=(f"Found {len(near_dup_pairs)} near-duplicate column pair(s) with |r| > {threshold}: {pair_strs}."), + details={"pairs": near_dup_pairs, "threshold": threshold}, + ) + return CheckResult( + name="near_duplicate_columns", + status=CheckStatus.PASS, + message=f"No near-duplicate column pairs found (threshold |r| > {threshold}).", + details={"pairs": [], "threshold": threshold}, + ) + + +def _check_target_leakage(df: pd.DataFrame, target_column: str, threshold: float = 0.99) -> CheckResult: + """Detects features that are almost perfectly correlated with the target.""" + if target_column not in df.columns: + return CheckResult( + name="target_leakage", + status=CheckStatus.WARN, + message=f"Target column '{target_column}' not found in DataFrame; skipping leakage check.", + details={}, + ) + + target_series = df[target_column] + if not pd.api.types.is_numeric_dtype(target_series): + # Encode categorically for correlation purposes. + target_encoded = target_series.astype("category").cat.codes.replace(-1, np.nan) + else: + target_encoded = target_series + + feature_cols = [c for c in df.select_dtypes(include=[np.number]).columns if c != target_column] + leaking: list[tuple[str, float]] = [] + + for col in feature_cols: + try: + r = df[[col]].join(target_encoded.rename("__target__")).dropna().corr().iloc[0, 1] + except (ValueError, TypeError): + continue + if pd.notna(r) and abs(r) > threshold: + leaking.append((col, float(r))) + + if leaking: + leaking_strs = [f"{c} (r={r:.3f})" for c, r in leaking] + return CheckResult( + name="target_leakage", + status=CheckStatus.FAIL, + message=( + f"Possible target leakage: {len(leaking)} feature(s) are nearly perfectly " + f"correlated with '{target_column}': {leaking_strs}." + ), + details={"leaking_columns": leaking, "threshold": threshold}, + ) + return CheckResult( + name="target_leakage", + status=CheckStatus.PASS, + message=f"No target leakage detected (threshold |r| > {threshold}).", + details={"leaking_columns": [], "threshold": threshold}, + ) + + +def _check_id_columns(df: pd.DataFrame) -> CheckResult: + """Identifies columns that look like identifiers.""" + id_cols: list[str] = [] + for col in df.columns: + series = df[col].dropna() + n = len(series) + if n == 0: + continue + n_distinct = series.nunique() + if n_distinct == n: + id_cols.append(col) + continue + if pd.api.types.is_integer_dtype(series): + unique_sorted = sorted(series.unique()) + if len(unique_sorted) >= 2: + # Check sequential without allocating a full range list + lo, hi = int(unique_sorted[0]), int(unique_sorted[-1]) + if hi - lo + 1 == len(unique_sorted): + id_cols.append(col) + + if id_cols: + return CheckResult( + name="id_columns", + status=CheckStatus.WARN, + message=f"Found {len(id_cols)} likely ID column(s): {id_cols}.", + details={"id_columns": id_cols}, + ) + return CheckResult( + name="id_columns", + status=CheckStatus.PASS, + message="No ID columns detected.", + details={"id_columns": []}, + ) + + +def _check_class_imbalance(df: pd.DataFrame, target_column: str) -> CheckResult: + """Warns if the minority class represents less than 1% of total rows.""" + if target_column not in df.columns: + return CheckResult( + name="class_imbalance", + status=CheckStatus.WARN, + message=f"Target column '{target_column}' not found; skipping imbalance check.", + details={}, + ) + + target_series = df[target_column].dropna() + if len(target_series) == 0: + return CheckResult( + name="class_imbalance", + status=CheckStatus.WARN, + message=f"Target column '{target_column}' has no non-null values; skipping imbalance check.", + details={}, + ) + if pd.api.types.is_numeric_dtype(target_series) and target_series.nunique() > 20: + # Continuous target — not a classification problem. + return CheckResult( + name="class_imbalance", + status=CheckStatus.PASS, + message="Target appears continuous; class imbalance check skipped.", + details={}, + ) + + counts = target_series.value_counts() + total = len(target_series) + minority_count = int(counts.min()) + minority_class = counts.idxmin() + minority_pct = minority_count / total if total > 0 else 0.0 + + if minority_pct < 0.01: + return CheckResult( + name="class_imbalance", + status=CheckStatus.WARN, + message=( + f"Class imbalance detected: minority class '{minority_class}' " + f"has only {minority_count} sample(s) ({minority_pct:.2%} of rows)." + ), + details={ + "minority_class": str(minority_class), + "minority_count": minority_count, + "minority_pct": minority_pct, + }, + ) + return CheckResult( + name="class_imbalance", + status=CheckStatus.PASS, + message=f"Class balance is acceptable (minority class: {minority_pct:.2%}).", + details={ + "minority_class": str(minority_class), + "minority_count": minority_count, + "minority_pct": minority_pct, + }, + ) + + +def _check_single_class(df: pd.DataFrame, target_column: str) -> CheckResult: + """Fails if the target column has only one distinct value.""" + if target_column not in df.columns: + return CheckResult( + name="single_class", + status=CheckStatus.WARN, + message=f"Target column '{target_column}' not found; skipping single-class check.", + details={}, + ) + + n_distinct = df[target_column].nunique(dropna=True) + if n_distinct <= 1: + return CheckResult( + name="single_class", + status=CheckStatus.FAIL, + message=( + f"Target column '{target_column}' has only {n_distinct} distinct value(s). " + "A model cannot be trained on a single-class target." + ), + details={"n_distinct": n_distinct}, + ) + return CheckResult( + name="single_class", + status=CheckStatus.PASS, + message=f"Target column '{target_column}' has {n_distinct} distinct value(s).", + details={"n_distinct": n_distinct}, + ) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def check_dataset_quality( + df: pd.DataFrame, + target_column: str | None = None, + dataset_name: str = "unnamed", + min_rows: int = 200, + min_features: int = 2, + max_missing_pct: float = 0.5, + max_correlation_threshold: float = 0.99, +) -> DatasetQualityReport: + """Runs all quality checks on a DataFrame and returns a report. + + Checks performed: + + 1. **minimum_size**: at least *min_rows* rows → FAIL if not. + 2. **minimum_features**: at least *min_features* non-target columns → FAIL if not. + 3. **missing_values**: overall missing value % > *max_missing_pct* → WARN. + 4. **constant_columns**: columns with only 1 distinct value → WARN (lists them). + 5. **near_duplicate_columns**: column pairs with Pearson |r| > *max_correlation_threshold* → WARN. + 6. **target_leakage**: any non-target feature with |r| > 0.99 with target → FAIL + (only when *target_column* is provided). + 7. **id_columns**: columns that look like IDs (all-unique values or sequential ints) → WARN. + 8. **class_imbalance**: if target is categorical and minority class < 1% of rows → WARN + (only when *target_column* is provided). + 9. **single_class**: if target has only 1 distinct value → FAIL + (only when *target_column* is provided). + + # Inputs + :param df: (pd.DataFrame) the dataset to check. + :param target_column: (str | None) name of the target column, if known. + :param dataset_name: (str) a label used in the report summary. + :param min_rows: (int) minimum acceptable number of rows. + :param min_features: (int) minimum acceptable number of non-target features. + :param max_missing_pct: (float) maximum acceptable fraction of missing cells. + :param max_correlation_threshold: (float) |r| above which two numeric columns + are considered near-duplicates (also used for target leakage detection). + + # Return + :return: (DatasetQualityReport) report containing all check results. + """ + checks: list[CheckResult] = [] + + checks.append(_check_minimum_size(df, min_rows)) + checks.append(_check_minimum_features(df, target_column, min_features)) + checks.append(_check_missing_values(df, max_missing_pct)) + checks.append(_check_constant_columns(df)) + checks.append(_check_near_duplicate_columns(df, max_correlation_threshold)) + checks.append(_check_id_columns(df)) + + if target_column is not None: + checks.append(_check_target_leakage(df, target_column, threshold=max_correlation_threshold)) + checks.append(_check_class_imbalance(df, target_column)) + checks.append(_check_single_class(df, target_column)) + + report = DatasetQualityReport( + dataset_name=dataset_name, + n_rows=len(df), + n_cols=len(df.columns), + checks=checks, + ) + + logger.info(report.summary()) + for failure in report.failures: + logger.warning("Quality FAIL — %s: %s", failure.name, failure.message) + for warning in report.warnings: + logger.info("Quality WARN — %s: %s", warning.name, warning.message) + + return report + + +@DeveloperAPI +def drop_quality_issues(df: pd.DataFrame, report: DatasetQualityReport) -> pd.DataFrame: + """Returns a cleaned DataFrame with constant columns and detected ID columns removed. + + Columns removed: + + - Constant columns (identified by the *constant_columns* check). + - Likely ID columns (identified by the *id_columns* check). + + # Inputs + :param df: (pd.DataFrame) the original dataset. + :param report: (DatasetQualityReport) quality report produced by + :func:`check_dataset_quality`. + + # Return + :return: (pd.DataFrame) cleaned copy of *df* with offending columns dropped. + """ + cols_to_drop: set[str] = set() + + for check in report.checks: + if check.name == "constant_columns": + cols_to_drop.update(check.details.get("constant_columns", [])) + elif check.name == "id_columns": + cols_to_drop.update(check.details.get("id_columns", [])) + + # Only drop columns that actually exist in the DataFrame. + cols_to_drop = cols_to_drop.intersection(df.columns) + + if cols_to_drop: + logger.info("Dropping %d column(s) due to quality issues: %s", len(cols_to_drop), sorted(cols_to_drop)) + + return df.drop(columns=list(cols_to_drop)) diff --git a/ludwig/utils/dataset_utils.py b/ludwig/utils/dataset_utils.py new file mode 100644 index 0000000..80ccb7e --- /dev/null +++ b/ludwig/utils/dataset_utils.py @@ -0,0 +1,118 @@ +import pandas as pd +from sklearn.model_selection import train_test_split + +from ludwig.api_annotations import PublicAPI +from ludwig.constants import TEST_SPLIT, TRAIN_SPLIT, VALIDATION_SPLIT +from ludwig.data.dataset.base import Dataset +from ludwig.utils.defaults import default_random_seed + + +@PublicAPI +def get_repeatable_train_val_test_split( + df_input, stratify_colname="", random_seed=default_random_seed, frac_train=0.7, frac_val=0.1, frac_test=0.2 +): + """Return df_input with split column containing (if possible) non-zero rows in the train, validation, and test + data subset categories. + + If the input dataframe does not contain an existing split column or if the + number of rows in both the validation and test split is 0 and non-empty + stratify_colname specified, return df_input with split column set according + to frac_ and stratify_colname. + + Else stratify_colname is ignored, and: + If the input dataframe contains an existing split column and non-zero row + counts for all three split types, return df_input. + If the input dataframe contains an existing split column but only one of + validation and test split has non-zero row counts, return df_input with + missing split getting rows from train split as per frac_. + + Parameters + ---------- + df_input : Pandas dataframe + Input dataframe to be split. + stratify_colname : str + The column used for stratification (if desired); usually the label column. + random_seed : int + Seed used to get repeatable split. + frac_train : float + frac_val : float + frac_test : float + The ratios with which to split the dataframe into train, val, and test data; + should sum to 1.0. + + Returns + ------- + df_split : + Dataframe containing the three splits. + """ + + if frac_train + frac_val + frac_test != 1.0: + raise ValueError(f"fractions {frac_train:f}, {frac_val:f}, {frac_test:f} do not add up to 1.0") + if stratify_colname: + do_stratify_split = True + if stratify_colname not in df_input.columns: + raise ValueError(f"{stratify_colname} is not a column in the dataframe") + else: + do_stratify_split = False + if "split" not in df_input.columns: + df_input["split"] = 0 # set up for non-stratified split path + + if "split" in df_input.columns: + df_train = df_input[df_input["split"] == TRAIN_SPLIT].copy() + df_val = df_input[df_input["split"] == VALIDATION_SPLIT].copy() + df_test = df_input[df_input["split"] == TEST_SPLIT].copy() + if not do_stratify_split or len(df_val) != 0 or len(df_test) != 0: + if len(df_val) == 0: + df_val = df_train.sample(frac=frac_val, replace=False, random_state=random_seed) + df_train = df_train.drop(df_val.index) + if len(df_test) == 0: + df_test = df_train.sample(frac=frac_test, replace=False, random_state=random_seed) + df_train = df_train.drop(df_test.index) + do_stratify_split = False + + if do_stratify_split: + # Make sure the `stratify_colname` doesn't have any NaNs. + df_input = df_input[df_input[stratify_colname].notna()] + + # Split original dataframe into train and temp dataframes. + y = df_input[[stratify_colname]] # Dataframe of just the column on which to stratify. + df_train, df_temp, y_train, y_temp = train_test_split( + df_input, y, stratify=y, test_size=(1.0 - frac_train), random_state=random_seed + ) + # Split the temp dataframe into val and test dataframes. + relative_frac_test = frac_test / (frac_val + frac_test) + df_val, df_test, y_val, y_test = train_test_split( + df_temp, y_temp, stratify=y_temp, test_size=relative_frac_test, random_state=random_seed + ) + + total_split = len(df_train) + len(df_val) + len(df_test) + if len(df_input) != total_split: + raise RuntimeError( + f"Dataset split produced {total_split} rows but input had {len(df_input)} rows. " + f"This is an internal error — please report it." + ) + df_train["split"] = TRAIN_SPLIT + df_val["split"] = VALIDATION_SPLIT + df_test["split"] = TEST_SPLIT + df_split = pd.concat([df_train, df_val, df_test], ignore_index=True) + return df_split + + +def generate_dataset_statistics( + training_set: Dataset, + validation_set: str | dict | pd.DataFrame | Dataset | None, + test_set: str | dict | pd.DataFrame | Dataset | None, +) -> list[tuple[str, int, int]]: + from ludwig.benchmarking.utils import format_memory + + dataset_statistics = [ + ["Dataset", "Size (Rows)", "Size (In Memory)"], + ["Training", len(training_set), format_memory(training_set.in_memory_size_bytes)], + ] + if validation_set is not None: + dataset_statistics.append( + ["Validation", len(validation_set), format_memory(validation_set.in_memory_size_bytes)] + ) + if test_set is not None: + dataset_statistics.append(["Test", len(test_set), format_memory(test_set.in_memory_size_bytes)]) + return dataset_statistics diff --git a/ludwig/utils/date_utils.py b/ludwig/utils/date_utils.py new file mode 100644 index 0000000..587757a --- /dev/null +++ b/ludwig/utils/date_utils.py @@ -0,0 +1,95 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import time +from datetime import date, datetime, UTC + +import numpy as np +from dateutil.parser import parse, ParserError + +from ludwig.api_annotations import DeveloperAPI + +SCALE_S = np.floor(np.log10(time.time())) + + +@DeveloperAPI +def create_vector_from_datetime_obj(datetime_obj): + yearday = datetime_obj.toordinal() - date(datetime_obj.year, 1, 1).toordinal() + 1 + + midnight = datetime_obj.replace(hour=0, minute=0, second=0, microsecond=0) + second_of_day = (datetime_obj - midnight).seconds + + return [ + datetime_obj.year, + datetime_obj.month, + datetime_obj.day, + datetime_obj.weekday(), + yearday, + datetime_obj.hour, + datetime_obj.minute, + datetime_obj.second, + second_of_day, + ] + + +@DeveloperAPI +def parse_datetime(timestamp: float | int | str) -> datetime: + """Parse a datetime from a string or a numeric timestamp. + + Args: + timestamp: A datetime string or numeric timestamp. + + Returns: + A datetime representation of `timestamp`. + """ + try: + dt = parse(timestamp) + except (OverflowError, ParserError, TypeError): + dt = convert_number_to_datetime(timestamp) + + return dt + + +@DeveloperAPI +def convert_number_to_datetime(timestamp: float | int | str) -> datetime: + """Convert a numeric timestamp to a datetime object. + + `datetime` objects can be created from POSIX timestamps like those returned by `time.time()`. + + Args: + timestamp: A numeric timestamp. + + Returns: + A datetime representation of `timestamp`. + + Raises: + ValueError: Raised if `timestamp` is not a number or not a valid datetime. + """ + try: + timestamp = float(timestamp) + except TypeError: + raise ValueError(f"Provided value {timestamp} is not a valid numeric timestamp") + + # Determine the unit of the timestamp + ts_scale = np.floor(np.log10(timestamp)) + + # `datetime.datetime.fromtimestamp` expects a timestamp in seconds. Rescale the timestamp if it is not in seconds. + if ts_scale > SCALE_S: + delta = ts_scale - SCALE_S + timestamp = timestamp / np.power(10, delta) + + # Convert the timestamp to a datetime object. If it is not a valid timestamp, `ValueError` is raised. + dt = datetime.fromtimestamp(timestamp, tz=UTC).replace(tzinfo=None) + return dt diff --git a/ludwig/utils/defaults.py b/ludwig/utils/defaults.py new file mode 100644 index 0000000..a623011 --- /dev/null +++ b/ludwig/utils/defaults.py @@ -0,0 +1,98 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import argparse +import copy +import logging + +import yaml + +from ludwig.api_annotations import DeveloperAPI +from ludwig.contrib import add_contrib_callback_args +from ludwig.features.feature_registries import get_input_type_registry +from ludwig.globals import LUDWIG_VERSION +from ludwig.schema.model_config import ModelConfig +from ludwig.schema.preprocessing import PreprocessingConfig +from ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version +from ludwig.utils.data_utils import load_config_from_str, load_yaml +from ludwig.utils.fs_utils import open_file +from ludwig.utils.print_utils import print_ludwig + +logger = logging.getLogger(__name__) + +default_random_seed = 42 + +# Still needed for preprocessing TODO(Connor): Refactor ludwig/data/preprocessing to use schema +# TODO(travis): remove this, make type a protected string for each subclass +default_feature_specific_preprocessing_parameters = { + name: preproc_sect.get_schema_cls()(name="__tmp__", type=name).preprocessing.to_dict() + for name, preproc_sect in get_input_type_registry().items() +} + +default_training_preprocessing_parameters = copy.deepcopy(default_feature_specific_preprocessing_parameters) +default_training_preprocessing_parameters.update(PreprocessingConfig().to_dict()) + +default_prediction_preprocessing_parameters = copy.deepcopy(default_feature_specific_preprocessing_parameters) + + +@DeveloperAPI +def render_config(config=None, output=None, **kwargs): + upgraded_config = upgrade_config_dict_to_latest_version(config) + output_config = ModelConfig.from_dict(upgraded_config).to_dict() + + if output is None: + print(yaml.safe_dump(output_config, None, sort_keys=False)) + else: + with open_file(output, "w") as f: + yaml.safe_dump(output_config, f, sort_keys=False) + + +@DeveloperAPI +def cli_render_config(sys_argv): + parser = argparse.ArgumentParser( + description="This script renders the full config from a user config.", + prog="ludwig render_config", + usage="%(prog)s [options]", + ) + parser.add_argument( + "-c", + "--config", + type=load_yaml, + help="Path to the YAML file containing the model configuration", + ) + parser.add_argument( + "-cs", + "--config_str", + dest="config", + type=load_config_from_str, + help="JSON or YAML serialized string of the model configuration", + ) + parser.add_argument( + "-o", + "--output", + type=str, + help="output rendered YAML config path", + required=False, + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("render_config", *sys_argv) + + print_ludwig("Render Config", LUDWIG_VERSION) + render_config(**vars(args)) diff --git a/ludwig/utils/entmax/LICENSE b/ludwig/utils/entmax/LICENSE new file mode 100644 index 0000000..6dcfe88 --- /dev/null +++ b/ludwig/utils/entmax/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DeepSPIN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ludwig/utils/entmax/README.md b/ludwig/utils/entmax/README.md new file mode 100644 index 0000000..766cfcf --- /dev/null +++ b/ludwig/utils/entmax/README.md @@ -0,0 +1,94 @@ +# entmax + +______________________________________________________________________ + +This package provides a pytorch implementation of entmax and entmax losses: +a sparse family of probability mappings and corresponding loss functions, +generalizing softmax / cross-entropy. + +*Features:* + +- Exact partial-sort algorithms for 1.5-entmax and 2-entmax (sparsemax). +- A bisection-based algorithm for generic alpha-entmax. +- Gradients w.r.t. alpha for adaptive, learned sparsity! + +*Requirements:* python 3, pytorch >= 1.0 (and pytest for unit tests) + +## Example + +```python +import torch +from torch.nn.functional import softmax + +from entmax import sparsemax, entmax15 + +x = torch.tensor([-2, 0, 0.5]) + +print(softmax(x, dim=0)) +# tensor([0.0486, 0.3592, 0.5922]) + +print(sparsemax(x, dim=0)) +# tensor([0.0000, 0.2500, 0.7500]) + +print(entmax15(x, dim=0)) +# tensor([0.0000, 0.3260, 0.6740]) +``` + +Gradients w.r.t. alpha (continued): + +```python +import torch +from torch.autograd import grad + +from entmax import entmax_bisect + +x = torch.tensor([[-1, 0, 0.5], [1, 2, 3.5]]) + +alpha = torch.tensor(1.33, requires_grad=True) + +p = entmax_bisect(x, alpha) + +print(p) +# tensor([[0.0460, 0.3276, 0.6264], +# [0.0026, 0.1012, 0.8963]], grad_fn=) + +print(grad(p[0, 0], alpha)) +# (tensor(-0.2562),) +``` + +## Installation + +``` +pip install entmax +``` + +## Citations + +[Sparse Sequence-to-Sequence Models](https://www.aclweb.org/anthology/P19-1146) + +``` +@inproceedings{entmax, + author = {Peters, Ben and Niculae, Vlad and Martins, Andr{\'e} FT}, + title = {Sparse Sequence-to-Sequence Models}, + booktitle = {Proc. ACL}, + year = {2019}, + url = {https://www.aclweb.org/anthology/P19-1146} +} +``` + +[Adaptively Sparse Transformers](https://arxiv.org/pdf/1909.00015.pdf) + +``` +@inproceedings{correia19adaptively, + author = {Correia, Gon\c{c}alo M and Niculae, Vlad and Martins, Andr{\'e} FT}, + title = {Adaptively Sparse Transformers}, + booktitle = {Proc. EMNLP-IJCNLP (to appear)}, + year = {2019}, +} +``` + +Further reading: + +- Blondel, Martins, and Niculae, 2019. [Learning with Fenchel-Young Losses](https://arxiv.org/abs/1901.02324). +- Martins and Astudillo, 2016. [From Softmax to Sparsemax: A Sparse Model of Attention and Multi-Label Classification](https://arxiv.org/abs/1602.02068). +- Peters and Martins, 2019 [IT-IST at the SIGMORPHON 2019 Shared Task: Sparse Two-headed Models for Inflection](https://www.aclweb.org/anthology/W19-4207). diff --git a/ludwig/utils/entmax/__init__.py b/ludwig/utils/entmax/__init__.py new file mode 100644 index 0000000..6d6f100 --- /dev/null +++ b/ludwig/utils/entmax/__init__.py @@ -0,0 +1,33 @@ +__version__ = "1.1.dev0" + +from ludwig.utils.entmax.activations import Entmax15, entmax15, Sparsemax, sparsemax +from ludwig.utils.entmax.losses import ( + entmax15_loss, + Entmax15Loss, + entmax_bisect_loss, + EntmaxBisectLoss, + sparsemax_bisect_loss, + sparsemax_loss, + SparsemaxBisectLoss, + SparsemaxLoss, +) +from ludwig.utils.entmax.root_finding import entmax_bisect, EntmaxBisect, sparsemax_bisect, SparsemaxBisect + +__all__ = [ + "Entmax15", + "Entmax15Loss", + "EntmaxBisect", + "EntmaxBisectLoss", + "Sparsemax", + "SparsemaxBisect", + "SparsemaxBisectLoss", + "SparsemaxLoss", + "entmax15", + "entmax15_loss", + "entmax_bisect", + "entmax_bisect_loss", + "sparsemax", + "sparsemax_bisect", + "sparsemax_bisect_loss", + "sparsemax_loss", +] diff --git a/ludwig/utils/entmax/activations.py b/ludwig/utils/entmax/activations.py new file mode 100644 index 0000000..10c0cb8 --- /dev/null +++ b/ludwig/utils/entmax/activations.py @@ -0,0 +1,329 @@ +"""An implementation of entmax (Peters et al., 2019). See https://arxiv.org/pdf/1905.05702 for detailed +description. + +This builds on previous work with sparsemax (Martins & Astudillo, 2016). See https://arxiv.org/pdf/1602.02068. +""" + +# Author: Ben Peters +# Author: Vlad Niculae +# License: MIT + +import torch +import torch.nn as nn +from torch.autograd import Function + + +def _make_ix_like(X, dim): + d = X.size(dim) + rho = torch.arange(1, d + 1, device=X.device, dtype=X.dtype) + view = [1] * X.dim() + view[0] = -1 + return rho.view(view).transpose(0, dim) + + +def _roll_last(X, dim): + if dim == -1: + return X + elif dim < 0: + dim = X.dim() - dim + + perm = [i for i in range(X.dim()) if i != dim] + [dim] + return X.permute(perm) + + +def _sparsemax_threshold_and_support(X, dim=-1, k=None): + """Core computation for sparsemax: optimal threshold and support size. + + Parameters + ---------- + X : torch.Tensor + The input tensor to compute thresholds over. + + dim : int + The dimension along which to apply sparsemax. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + + Returns + ------- + tau : torch.Tensor like `X`, with all but the `dim` dimension intact + the threshold value for each vector + support_size : torch LongTensor, shape like `tau` + the number of nonzeros in each vector. + """ + + if k is None or k >= X.shape[dim]: # do full sort + topk, _ = torch.sort(X, dim=dim, descending=True) + else: + topk, _ = torch.topk(X, k=k, dim=dim) + + topk_cumsum = topk.cumsum(dim) - 1 + rhos = _make_ix_like(topk, dim) + support = rhos * topk > topk_cumsum + + support_size = support.sum(dim=dim).unsqueeze(dim) + tau = topk_cumsum.gather(dim, support_size - 1) + tau /= support_size.to(X.dtype) + + if k is not None and k < X.shape[dim]: + unsolved = (support_size == k).squeeze(dim) + + if torch.any(unsolved): + in_ = _roll_last(X, dim)[unsolved] + tau_, ss_ = _sparsemax_threshold_and_support(in_, dim=-1, k=2 * k) + _roll_last(tau, dim)[unsolved] = tau_ + _roll_last(support_size, dim)[unsolved] = ss_ + + return tau, support_size + + +def _entmax_threshold_and_support(X, dim=-1, k=None): + """Core computation for 1.5-entmax: optimal threshold and support size. + + Parameters + ---------- + X : torch.Tensor + The input tensor to compute thresholds over. + + dim : int + The dimension along which to apply 1.5-entmax. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + + Returns + ------- + tau : torch.Tensor like `X`, with all but the `dim` dimension intact + the threshold value for each vector + support_size : torch LongTensor, shape like `tau` + the number of nonzeros in each vector. + """ + + if k is None or k >= X.shape[dim]: # do full sort + Xsrt, _ = torch.sort(X, dim=dim, descending=True) + else: + Xsrt, _ = torch.topk(X, k=k, dim=dim) + + rho = _make_ix_like(Xsrt, dim) + mean = Xsrt.cumsum(dim) / rho + mean_sq = (Xsrt**2).cumsum(dim) / rho + ss = rho * (mean_sq - mean**2) + delta = (1 - ss) / rho + + # NOTE this is not exactly the same as in reference algo + # Fortunately it seems the clamped values never wrongly + # get selected by tau <= sorted_z. Prove this! + delta_nz = torch.clamp(delta, 0) + tau = mean - torch.sqrt(delta_nz) + + support_size = (tau <= Xsrt).sum(dim).unsqueeze(dim) + tau_star = tau.gather(dim, support_size - 1) + + if k is not None and k < X.shape[dim]: + unsolved = (support_size == k).squeeze(dim) + + if torch.any(unsolved): + X_ = _roll_last(X, dim)[unsolved] + tau_, ss_ = _entmax_threshold_and_support(X_, dim=-1, k=2 * k) + _roll_last(tau_star, dim)[unsolved] = tau_ + _roll_last(support_size, dim)[unsolved] = ss_ + + return tau_star, support_size + + +class SparsemaxFunction(Function): + @classmethod + def forward(cls, ctx, X, dim=-1, k=None): + ctx.dim = dim + output, backwards_kwargs = _sparsemax_forward(X, dim, k) + ctx.save_for_backward(backwards_kwargs["supp_size"], output) + return output + + @classmethod + def backward(cls, ctx, grad_output): + supp_size, output = ctx.saved_tensors + dim = ctx.dim + grad_input = grad_output.clone() + grad_input[output == 0] = 0 + + v_hat = grad_input.sum(dim=dim) / supp_size.to(output.dtype).squeeze(dim) + v_hat = v_hat.unsqueeze(dim) + grad_input = torch.where(output != 0, grad_input - v_hat, grad_input) + return grad_input, None, None + + +def _sparsemax_forward(X, dim, k): + max_val, _ = X.max(dim=dim, keepdim=True) + X = X - max_val # same numerical stability trick as softmax + tau, supp_size = _sparsemax_threshold_and_support(X, dim=dim, k=k) + output = torch.clamp(X - tau, min=0) + return output, {"supp_size": supp_size} + + +class Entmax15Function(Function): + @classmethod + def forward(cls, ctx, X, dim=0, k=None): + ctx.dim = dim + Y, _ = _entmax15_forward(X, dim, k) + ctx.save_for_backward(Y) + return Y + + @classmethod + def backward(cls, ctx, dY): + (Y,) = ctx.saved_tensors + gppr = Y.sqrt() # = 1 / g'' (Y) + dX = dY * gppr + q = dX.sum(ctx.dim) / gppr.sum(ctx.dim) + q = q.unsqueeze(ctx.dim) + dX -= q * gppr + return dX, None, None + + +def _entmax15_forward(X, dim, k): + max_val, _ = X.max(dim=dim, keepdim=True) + X = X - max_val # same numerical stability trick as for softmax + X = X / 2 # divide by 2 to solve actual Entmax + + tau_star, _ = _entmax_threshold_and_support(X, dim=dim, k=k) + + Y = torch.clamp(X - tau_star, min=0) ** 2 + return Y, {} + + +def sparsemax(X, dim=-1, k=None, training=True): + """sparsemax: normalizing sparse transform (a la softmax). + + Solves the projection: + + min_p ||x - p||_2 s.t. p >= 0, sum(p) == 1. + + Parameters + ---------- + X : torch.Tensor + The input tensor. + + dim : int + The dimension along which to apply sparsemax. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + + Returns + ------- + P : torch tensor, same shape as X + The projection result, such that P.sum(dim=dim) == 1 elementwise. + """ + # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility + # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053 + if not training: + output, _ = _sparsemax_forward(X, dim, k) + return output + return SparsemaxFunction.apply(X, dim, k) + + +def entmax15(X, dim=-1, k=None, training=True): + """1.5-entmax: normalizing sparse transform (a la softmax). + + Solves the optimization problem: + + max_p - H_1.5(p) s.t. p >= 0, sum(p) == 1. + + where H_1.5(p) is the Tsallis alpha-entropy with alpha=1.5. + + Parameters + ---------- + X : torch.Tensor + The input tensor. + + dim : int + The dimension along which to apply 1.5-entmax. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + + Returns + ------- + P : torch tensor, same shape as X + The projection result, such that P.sum(dim=dim) == 1 elementwise. + """ + # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility + # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053 + if not training: + output, _ = _entmax15_forward(X, dim, k) + return output + return Entmax15Function.apply(X, dim, k) + + +class Sparsemax(nn.Module): + def __init__(self, dim=-1, k=None): + """sparsemax: normalizing sparse transform (a la softmax). + + Solves the projection: + + min_p ||x - p||_2 s.t. p >= 0, sum(p) == 1. + + Parameters + ---------- + dim : int + The dimension along which to apply sparsemax. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + """ + self.dim = dim + self.k = k + super().__init__() + + def forward(self, X): + return sparsemax(X, dim=self.dim, k=self.k, training=self.training) + + +class Entmax15(nn.Module): + def __init__(self, dim=-1, k=None): + """1.5-entmax: normalizing sparse transform (a la softmax). + + Solves the optimization problem: + + max_p - H_1.5(p) s.t. p >= 0, sum(p) == 1. + + where H_1.5(p) is the Tsallis alpha-entropy with alpha=1.5. + + Parameters + ---------- + dim : int + The dimension along which to apply 1.5-entmax. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + """ + self.dim = dim + self.k = k + super().__init__() + + def forward(self, X): + return entmax15(X, dim=self.dim, k=self.k, training=self.training) diff --git a/ludwig/utils/entmax/losses.py b/ludwig/utils/entmax/losses.py new file mode 100644 index 0000000..ad3dc5d --- /dev/null +++ b/ludwig/utils/entmax/losses.py @@ -0,0 +1,284 @@ +import torch +import torch.nn as nn +from torch.autograd import Function + +from ludwig.constants import IGNORE_INDEX_TOKEN_ID +from ludwig.utils.entmax.activations import entmax15, sparsemax +from ludwig.utils.entmax.root_finding import entmax_bisect, sparsemax_bisect + + +class _GenericLoss(nn.Module): + def __init__(self, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction="elementwise_mean"): + if reduction not in ("elementwise_mean", "sum", "none"): + raise ValueError( + f"Invalid reduction mode '{reduction}'. Expected one of: 'elementwise_mean', 'sum', 'none'." + ) + self.reduction = reduction + self.ignore_index = ignore_index + super().__init__() + + def forward(self, X, target): + loss = self.loss(X, target) + if self.ignore_index >= 0: + ignored_positions = target == self.ignore_index + size = (target.size(0) - ignored_positions.sum()).item() + loss.masked_fill_(ignored_positions, 0.0) + else: + size = target.size(0) + if self.reduction == "sum": + loss = loss.sum() + elif self.reduction == "elementwise_mean": + if size == 0: + # Returns zero loss and zero gradient in the rare case that all row targets are ignored. + loss = loss.sum() * 0.0 + else: + loss = loss.sum() / float(size) + return loss + + +class _GenericLossFunction(Function): + @classmethod + def forward(cls, ctx, X, target, alpha, proj_args): + """X (FloatTensor): n x num_classes target (LongTensor): n, the indices of the target classes.""" + if X.shape[0] != target.shape[0]: + raise ValueError( + f"X and target batch sizes must match, got X.shape[0]={X.shape[0]}, target.shape[0]={target.shape[0]}." + ) + + p_star = cls.project(X, alpha, **proj_args) + loss = cls.omega(p_star, alpha) + + p_star.scatter_add_(1, target.unsqueeze(1), torch.full_like(p_star, -1)) + loss += torch.einsum("ij,ij->i", p_star, X) + ctx.save_for_backward(p_star) + + return loss + + @classmethod + def backward(cls, ctx, grad_output): + (p_star,) = ctx.saved_tensors + grad = grad_output.unsqueeze(1) * p_star + ret = (grad,) + + # pad with as many Nones as needed + return ret + (None,) * (1 + cls.n_fwd_args) + + +class SparsemaxLossFunction(_GenericLossFunction): + n_fwd_args = 1 + + @classmethod + def project(cls, X, alpha, k): + return sparsemax(X, dim=-1, k=k) + + @classmethod + def omega(cls, p_star, alpha): + return (1 - (p_star**2).sum(dim=1)) / 2 + + @classmethod + def forward(cls, ctx, X, target, k=None): + return super().forward(ctx, X, target, alpha=2, proj_args={"k": k}) + + +class SparsemaxBisectLossFunction(_GenericLossFunction): + n_fwd_args = 1 + + @classmethod + def project(cls, X, alpha, n_iter): + return sparsemax_bisect(X, n_iter=n_iter) + + @classmethod + def omega(cls, p_star, alpha): + return (1 - (p_star**2).sum(dim=1)) / 2 + + @classmethod + def forward(cls, ctx, X, target, n_iter=50): + return super().forward(ctx, X, target, alpha=2, proj_args={"n_iter": n_iter}) + + +class Entmax15LossFunction(_GenericLossFunction): + n_fwd_args = 1 + + @classmethod + def project(cls, X, alpha, k=None): + return entmax15(X, dim=-1, k=k) + + @classmethod + def omega(cls, p_star, alpha): + return (1 - (p_star * torch.sqrt(p_star)).sum(dim=1)) / 0.75 + + @classmethod + def forward(cls, ctx, X, target, k=None): + return super().forward(ctx, X, target, alpha=1.5, proj_args={"k": k}) + + +class EntmaxBisectLossFunction(_GenericLossFunction): + n_fwd_args = 2 + + @classmethod + def project(cls, X, alpha, n_iter): + return entmax_bisect(X, alpha=alpha, n_iter=n_iter, ensure_sum_one=True) + + @classmethod + def omega(cls, p_star, alpha): + return (1 - (p_star**alpha).sum(dim=1)) / (alpha * (alpha - 1)) + + @classmethod + def forward(cls, ctx, X, target, alpha=1.5, n_iter=50): + return super().forward(ctx, X, target, alpha, proj_args={"n_iter": n_iter}) + + +def sparsemax_loss(X, target, k=None): + """sparsemax loss: sparse alternative to cross-entropy. + + Computed using a partial sorting strategy. + + Parameters + ---------- + X : torch.Tensor, shape=(n_samples, n_classes) + The input 2D tensor of predicted scores + + target : torch.LongTensor, shape=(n_samples,) + The ground truth labels, 0 <= target < n_classes. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + + Returns + ------- + losses, torch.Tensor, shape=(n_samples,) + The loss incurred at each sample. + """ + return SparsemaxLossFunction.apply(X, target, k) + + +def sparsemax_bisect_loss(X, target, n_iter=50): + """sparsemax loss: sparse alternative to cross-entropy. + + Computed using bisection. + + Parameters + ---------- + X : torch.Tensor, shape=(n_samples, n_classes) + The input 2D tensor of predicted scores + + target : torch.LongTensor, shape=(n_samples,) + The ground truth labels, 0 <= target < n_classes. + + n_iter : int + Number of bisection iterations. For float32, 24 iterations should + suffice for machine precision. + + Returns + ------- + losses, torch.Tensor, shape=(n_samples,) + The loss incurred at each sample. + """ + return SparsemaxBisectLossFunction.apply(X, target, n_iter) + + +def entmax15_loss(X, target, k=None): + """1.5-entmax loss: sparse alternative to cross-entropy + + Computed using a partial sorting strategy. + + Parameters + ---------- + X : torch.Tensor, shape=(n_samples, n_classes) + The input 2D tensor of predicted scores + + target : torch.LongTensor, shape=(n_samples,) + The ground truth labels, 0 <= target < n_classes. + + k : int or None + number of largest elements to partial-sort over. For optimal + performance, should be slightly bigger than the expected number of + nonzeros in the solution. If the solution is more than k-sparse, + this function is recursively called with a 2*k schedule. + If `None`, full sorting is performed from the beginning. + + Returns + ------- + losses, torch.Tensor, shape=(n_samples,) + The loss incurred at each sample. + """ + return Entmax15LossFunction.apply(X, target, k) + + +def entmax_bisect_loss(X, target, alpha=1.5, n_iter=50): + """alpha-entmax loss: sparse alternative to cross-entropy. + + Computed using bisection, supporting arbitrary alpha > 1. + + Parameters + ---------- + X : torch.Tensor, shape=(n_samples, n_classes) + The input 2D tensor of predicted scores + + target : torch.LongTensor, shape=(n_samples,) + The ground truth labels, 0 <= target < n_classes. + + alpha : float or torch.Tensor + Tensor of alpha parameters (> 1) to use for each row of X. If scalar + or python float, the same value is used for all rows. A value of + alpha=2 corresponds to sparsemax, and alpha=1 would in theory recover + softmax. For numeric reasons, this algorithm does not work with `alpha=1`: + if you want softmax, we recommend `torch.nn.softmax` + + n_iter : int + Number of bisection iterations. For float32, 24 iterations should + suffice for machine precision. + + Returns + ------- + losses, torch.Tensor, shape=(n_samples,) + The loss incurred at each sample. + """ + return EntmaxBisectLossFunction.apply(X, target, alpha, n_iter) + + +class SparsemaxBisectLoss(_GenericLoss): + def __init__(self, n_iter=50, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction="elementwise_mean"): + self.n_iter = n_iter + super().__init__(ignore_index, reduction) + + def loss(self, X, target): + return sparsemax_bisect_loss(X, target, self.n_iter) + + +class SparsemaxLoss(_GenericLoss): + def __init__(self, k=None, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction="elementwise_mean"): + self.k = k + super().__init__(ignore_index, reduction) + + def loss(self, X, target): + return sparsemax_loss(X, target, self.k) + + +class EntmaxBisectLoss(_GenericLoss): + def __init__( + self, + alpha=1.5, + n_iter=50, + ignore_index=IGNORE_INDEX_TOKEN_ID, + reduction="elementwise_mean", + ): + self.alpha = alpha + self.n_iter = n_iter + super().__init__(ignore_index, reduction) + + def loss(self, X, target): + return entmax_bisect_loss(X, target, self.alpha, self.n_iter) + + +class Entmax15Loss(_GenericLoss): + def __init__(self, k=100, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction="elementwise_mean"): + self.k = k + super().__init__(ignore_index, reduction) + + def loss(self, X, target): + return entmax15_loss(X, target, self.k) diff --git a/ludwig/utils/entmax/root_finding.py b/ludwig/utils/entmax/root_finding.py new file mode 100644 index 0000000..dec9416 --- /dev/null +++ b/ludwig/utils/entmax/root_finding.py @@ -0,0 +1,298 @@ +"""Bisection implementation of alpha-entmax (Peters et al., 2019). + +Backward pass wrt alpha per (Correia et al., 2019). See https://arxiv.org/pdf/1905.05702 for detailed description. +""" + +# Author: Goncalo M Correia +# Author: Ben Peters +# Author: Vlad Niculae + +import torch +import torch.nn as nn +from torch.autograd import Function + + +class EntmaxBisectFunction(Function): + @classmethod + def _gp(cls, x, alpha): + return x ** (alpha - 1) + + @classmethod + def _gp_inv(cls, y, alpha): + return y ** (1 / (alpha - 1)) + + @classmethod + def _p(cls, X, alpha): + return cls._gp_inv(torch.clamp(X, min=0), alpha) + + @classmethod + def forward(cls, ctx, X, alpha=1.5, dim=-1, n_iter=50, ensure_sum_one=True): + p_m, backward_kwargs = _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one, cls) + + ctx.alpha = backward_kwargs["alpha"] + ctx.dim = backward_kwargs["dim"] + ctx.save_for_backward(p_m) + return p_m + + @classmethod + def backward(cls, ctx, dY): + (Y,) = ctx.saved_tensors + + gppr = torch.where(Y > 0, Y ** (2 - ctx.alpha), Y.new_zeros(1)) + + dX = dY * gppr + q = dX.sum(ctx.dim) / gppr.sum(ctx.dim) + q = q.unsqueeze(ctx.dim) + dX -= q * gppr + + d_alpha = None + if ctx.needs_input_grad[1]: + # alpha gradient computation + # d_alpha = (partial_y / partial_alpha) * dY + # NOTE: ensure alpha is not close to 1 + # since there is an indetermination + # batch_size, _ = dY.shape + + # shannon terms + S = torch.where(Y > 0, Y * torch.log(Y), Y.new_zeros(1)) + # shannon entropy + ent = S.sum(ctx.dim).unsqueeze(ctx.dim) + Y_skewed = gppr / gppr.sum(ctx.dim).unsqueeze(ctx.dim) + + d_alpha = dY * (Y - Y_skewed) / ((ctx.alpha - 1) ** 2) + d_alpha -= dY * (S - Y_skewed * ent) / (ctx.alpha - 1) + d_alpha = d_alpha.sum(ctx.dim).unsqueeze(ctx.dim) + + return dX, d_alpha, None, None, None + + +def _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one, cls=EntmaxBisectFunction): + if not isinstance(alpha, torch.Tensor): + alpha = torch.tensor(alpha, dtype=X.dtype, device=X.device) + + alpha_shape = list(X.shape) + alpha_shape[dim] = 1 + alpha = alpha.expand(*alpha_shape) + + d = X.shape[dim] + + max_val, _ = X.max(dim=dim, keepdim=True) + X = X * (alpha - 1) + max_val = max_val * (alpha - 1) + + # Note: when alpha < 1, tau_lo > tau_hi. This still works since dm < 0. + tau_lo = max_val - cls._gp(1, alpha) + tau_hi = max_val - cls._gp(1 / d, alpha) + + f_lo = cls._p(X - tau_lo, alpha).sum(dim) - 1 + + dm = tau_hi - tau_lo + + for _it in range(n_iter): + dm /= 2 + tau_m = tau_lo + dm + p_m = cls._p(X - tau_m, alpha) + f_m = p_m.sum(dim) - 1 + + mask = (f_m * f_lo >= 0).unsqueeze(dim) + tau_lo = torch.where(mask, tau_m, tau_lo) + + if ensure_sum_one: + p_m /= p_m.sum(dim=dim).unsqueeze(dim=dim) + + return p_m, {"alpha": alpha, "dim": dim} + + +# slightly more efficient special case for sparsemax +class SparsemaxBisectFunction(EntmaxBisectFunction): + @classmethod + def _gp(cls, x, alpha): + return x + + @classmethod + def _gp_inv(cls, y, alpha): + return y + + @classmethod + def _p(cls, x, alpha): + return torch.clamp(x, min=0) + + @classmethod + def forward(cls, ctx, X, dim=-1, n_iter=50, ensure_sum_one=True): + p_m, backward_kwargs = _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one) + + ctx.alpha = backward_kwargs["alpha"] + ctx.dim = backward_kwargs["dim"] + ctx.save_for_backward(p_m) + return p_m + + @classmethod + def backward(cls, ctx, dY): + (Y,) = ctx.saved_tensors + gppr = (Y > 0).to(dtype=dY.dtype) + dX = dY * gppr + q = dX.sum(ctx.dim) / gppr.sum(ctx.dim) + q = q.unsqueeze(ctx.dim) + dX -= q * gppr + return dX, None, None, None + + +def _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one): + return _entmax_bisect_forward(X, alpha=2, dim=dim, n_iter=50, ensure_sum_one=True, cls=SparsemaxBisectFunction) + + +def entmax_bisect(X, alpha=1.5, dim=-1, n_iter=50, ensure_sum_one=True, training=True): + """alpha-entmax: normalizing sparse transform (a la softmax). + + Solves the optimization problem: + + max_p - H_a(p) s.t. p >= 0, sum(p) == 1. + + where H_a(p) is the Tsallis alpha-entropy with custom alpha >= 1, + using a bisection (root finding, binary search) algorithm. + + This function is differentiable with respect to both X and alpha. + + Parameters + ---------- + X : torch.Tensor + The input tensor. + + alpha : float or torch.Tensor + Tensor of alpha parameters (> 1) to use. If scalar + or python float, the same value is used for all rows, otherwise, + it must have shape (or be expandable to) + alpha.shape[j] == (X.shape[j] if j != dim else 1) + A value of alpha=2 corresponds to sparsemax, and alpha=1 would in theory recover + softmax. For numeric reasons, this algorithm does not work with `alpha=1`: if you + want softmax, we recommend `torch.nn.softmax`. + + dim : int + The dimension along which to apply alpha-entmax. + + n_iter : int + Number of bisection iterations. For float32, 24 iterations should + suffice for machine precision. + + ensure_sum_one : bool, + Whether to divide the result by its sum. If false, the result might + sum to close but not exactly 1, which might cause downstream problems. + + Returns + ------- + P : torch tensor, same shape as X + The projection result, such that P.sum(dim=dim) == 1 elementwise. + """ + # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility + # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053 + if not training: + output, _ = _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one) + return output + return EntmaxBisectFunction.apply(X, alpha, dim, n_iter, ensure_sum_one) + + +def sparsemax_bisect(X, dim=-1, n_iter=50, ensure_sum_one=True, training=True): + """sparsemax: normalizing sparse transform (a la softmax), via bisection. + + Solves the projection: + + min_p ||x - p||_2 s.t. p >= 0, sum(p) == 1. + + Parameters + ---------- + X : torch.Tensor + The input tensor. + + dim : int + The dimension along which to apply sparsemax. + + n_iter : int + Number of bisection iterations. For float32, 24 iterations should + suffice for machine precision. + + ensure_sum_one : bool, + Whether to divide the result by its sum. If false, the result might + sum to close but not exactly 1, which might cause downstream problems. + + Note: This function does not yet support normalizing along anything except + the last dimension. Please use transposing and views to achieve more + general behavior. + + Returns + ------- + P : torch tensor, same shape as X + The projection result, such that P.sum(dim=dim) == 1 elementwise. + """ + # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility + # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053 + if not training: + output, _ = _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one) + return output + return SparsemaxBisectFunction.apply(X, dim, n_iter, ensure_sum_one) + + +class SparsemaxBisect(nn.Module): + def __init__(self, dim=-1, n_iter=None): + """sparsemax: normalizing sparse transform (a la softmax) via bisection + + Solves the projection: + + min_p ||x - p||_2 s.t. p >= 0, sum(p) == 1. + + Parameters + ---------- + dim : int + The dimension along which to apply sparsemax. + + n_iter : int + Number of bisection iterations. For float32, 24 iterations should + suffice for machine precision. + """ + self.dim = dim + self.n_iter = n_iter + super().__init__() + + def forward(self, X): + return sparsemax_bisect(X, dim=self.dim, n_iter=self.n_iter, training=self.training) + + +class EntmaxBisect(nn.Module): + def __init__(self, alpha=1.5, dim=-1, n_iter=50): + """alpha-entmax: normalizing sparse map (a la softmax) via bisection. + + Solves the optimization problem: + + max_p - H_a(p) s.t. p >= 0, sum(p) == 1. + + where H_a(p) is the Tsallis alpha-entropy with custom alpha >= 1, + using a bisection (root finding, binary search) algorithm. + + Parameters + ---------- + alpha : float or torch.Tensor + Tensor of alpha parameters (> 1) to use. If scalar + or python float, the same value is used for all rows, otherwise, + it must have shape (or be expandable to) + alpha.shape[j] == (X.shape[j] if j != dim else 1) + A value of alpha=2 corresponds to sparsemax; and alpha=1 would in theory recover + softmax. For numeric reasons, this algorithm does not work with `alpha=1`; if you + want softmax, we recommend `torch.nn.softmax`. + + dim : int + The dimension along which to apply alpha-entmax. + + n_iter : int + Number of bisection iterations. For float32, 24 iterations should + suffice for machine precision. + + """ + super().__init__() + self.dim = dim + self.n_iter = n_iter + if isinstance(alpha, torch.Tensor): + self.register_buffer("alpha", alpha) + else: + self.alpha = alpha + + def forward(self, X): + return entmax_bisect(X, alpha=self.alpha, dim=self.dim, n_iter=self.n_iter, training=self.training) diff --git a/ludwig/utils/error_handling_utils.py b/ludwig/utils/error_handling_utils.py new file mode 100644 index 0000000..cc1f856 --- /dev/null +++ b/ludwig/utils/error_handling_utils.py @@ -0,0 +1,18 @@ +import logging +from functools import partial + +from retry.api import retry, retry_call + +import ludwig.constants as const + +logger = logging.getLogger(__name__) + + +default_retry_call = partial( + retry_call, tries=const.TRIES, backoff=const.BACKOFF, delay=const.DELAY, jitter=const.JITTER, logger=logger +) + + +default_retry = partial( + retry, tries=const.TRIES, backoff=const.BACKOFF, delay=const.DELAY, jitter=const.JITTER, logger=logger +) diff --git a/ludwig/utils/eval_utils.py b/ludwig/utils/eval_utils.py new file mode 100644 index 0000000..e26fb13 --- /dev/null +++ b/ludwig/utils/eval_utils.py @@ -0,0 +1,269 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from collections import OrderedDict + +import numpy as np +from sklearn import metrics +from sklearn.metrics import confusion_matrix + +logger = logging.getLogger(__name__) + + +class ConfusionMatrix: + def __init__(self, conditions, predictions, labels=None, sample_weight=None): + # assert (len(predictions) == len(conditions)) + min_length = min(len(predictions), len(conditions)) + self.predictions = predictions[:min_length] + self.conditions = conditions[:min_length] + + if labels is not None: + self.label2idx = {label: idx for idx, label in enumerate(labels)} + self.idx2label = dict(enumerate(labels)) + labels = list(range(len(labels))) + else: + self.label2idx = { + str(label): idx for idx, label in enumerate(np.unique([self.predictions, self.conditions])) + } + self.idx2label = { + idx: str(label) for idx, label in enumerate(np.unique([self.predictions, self.conditions])) + } + self.cm = confusion_matrix(self.conditions, self.predictions, labels=labels, sample_weight=sample_weight) + + # if labels is not None: + # self.labels_dict = {label: idx for idx, label in enumerate(labels)} + # else: + # if conditions.dtype.char == 'S': # it's an array of strings + # self.labels_dict = {str(label): idx for idx, label in + # enumerate(np.unique([predictions, conditions]))} + # else: # number + # max_label = np.concatenate([predictions, conditions]).max() + # self.labels_dict = {str(i): i for i in range(max_label + 1)} + # labels = [str(i) for i in range(max_label + 1)] + # self.cm = confusion_matrix(conditions, predictions, labels, sample_weight) + + self.sum_predictions = np.sum(self.cm, axis=0) + self.sum_conditions = np.sum(self.cm, axis=1) + self.all = np.sum(self.cm) + + def label_to_idx(self, label): + return self.label2idx[label] + + def true_positives(self, idx): + return self.cm[idx, idx] + + def true_negatives(self, idx): + return self.all - self.sum_predictions[idx] - self.sum_conditions[idx] + self.true_positives(idx) + + def false_positives(self, idx): + return self.sum_predictions[idx] - self.true_positives(idx) + + def false_negatives(self, idx): + return self.sum_conditions[idx] - self.true_positives(idx) + + def true_positive_rate(self, idx): + nom = self.true_positives(idx) + den = self.sum_conditions[idx] + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def true_negative_rate(self, idx): + nom = tn = self.true_negatives(idx) + den = tn + self.false_positives(idx) + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def positive_predictive_value(self, idx): + nom = self.true_positives(idx) + den = self.sum_predictions[idx] + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def negative_predictive_value(self, idx): + nom = tn = self.true_negatives(idx) + den = tn + self.false_negatives(idx) + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def false_negative_rate(self, idx): + return 1.0 - self.true_positive_rate(idx) + + def false_positive_rate(self, idx): + return 1.0 - self.true_negative_rate(idx) + + def false_discovery_rate(self, idx): + return 1.0 - self.positive_predictive_value(idx) + + def false_omission_rate(self, idx): + return 1.0 - self.negative_predictive_value(idx) + + def accuracy(self, idx): + nom = self.true_positives(idx) + self.true_negatives(idx) + den = self.all + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def precision(self, idx): + return self.positive_predictive_value(idx) + + def recall(self, idx): + return self.true_positive_rate(idx) + + def fbeta_score(self, beta, idx): + beta_2 = np.power(beta, 2) + precision = self.precision(idx) + recall = self.recall(idx) + nom = (1 + beta_2) * precision * recall + den = (beta_2 * precision) + recall + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def f1_score(self, idx): + return self.fbeta_score(1, idx) + + def sensitivity(self, idx): + return self.true_positive_rate(idx) + + def specificity(self, idx): + return self.true_negative_rate(idx) + + def hit_rate(self, idx): + return self.true_positive_rate(idx) + + def miss_rate(self, idx): + return self.false_negative_rate(idx) + + def fall_out(self, idx): + return self.false_positive_rate(idx) + + def matthews_correlation_coefficient(self, idx): + tp = self.true_positives(idx) + tn = self.true_negatives(idx) + fp = self.false_positives(idx) + fn = self.false_negatives(idx) + nom = tp * tn - fp * fn + den = np.sqrt((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn)) + if den == 0 or den == np.nan: + return 0 + else: + return nom / den + + def informedness(self, idx): + return self.true_positive_rate(idx) + self.true_negative_rate(idx) - 1 + + def markedness(self, idx): + return self.positive_predictive_value(idx) + self.negative_predictive_value(idx) - 1 + + def token_accuracy(self): + return metrics.accuracy_score(self.conditions, self.predictions) + + def avg_precision(self, average="macro"): + return metrics.precision_score(self.conditions, self.predictions, average=average) + + def avg_recall(self, average="macro"): + return metrics.recall_score(self.conditions, self.predictions, average=average) + + def avg_f1_score(self, average="macro"): + return metrics.f1_score(self.conditions, self.predictions, average=average) + + def avg_fbeta_score(self, beta, average="macro"): + return metrics.fbeta_score(self.conditions, self.predictions, beta=beta, average=average) + + def kappa_score(self): + return metrics.cohen_kappa_score(self.conditions, self.predictions) + + def class_stats(self, idx): + return { + "true_positives": self.true_positives(idx), + "true_negatives": self.true_negatives(idx), + "false_positives": self.false_positives(idx), + "false_negatives": self.false_negatives(idx), + "true_positive_rate": self.true_positive_rate(idx), + "true_negative_rate": self.true_negative_rate(idx), + "positive_predictive_value": self.positive_predictive_value(idx), + "negative_predictive_value": self.negative_predictive_value(idx), + "false_negative_rate": self.false_negative_rate(idx), + "false_positive_rate": self.false_positive_rate(idx), + "false_discovery_rate": self.false_discovery_rate(idx), + "false_omission_rate": self.false_omission_rate(idx), + "accuracy": self.accuracy(idx), + "precision": self.precision(idx), + "recall": self.recall(idx), + "f1_score": self.f1_score(idx), + "sensitivity": self.sensitivity(idx), + "specificity": self.specificity(idx), + "hit_rate": self.hit_rate(idx), + "miss_rate": self.miss_rate(idx), + "fall_out": self.fall_out(idx), + "matthews_correlation_coefficient": self.matthews_correlation_coefficient(idx), + "informedness": self.informedness(idx), + "markedness": self.markedness(idx), + } + + def per_class_stats(self): + stats = OrderedDict() + for idx in sorted(self.idx2label.keys()): + stats[self.idx2label[idx]] = self.class_stats(idx) + return stats + + def stats(self): + return { + "token_accuracy": self.token_accuracy(), + "avg_precision_macro": self.avg_precision(average="macro"), + "avg_recall_macro": self.avg_recall(average="macro"), + "avg_f1_score_macro": self.avg_f1_score(average="macro"), + "avg_precision_micro": self.avg_precision(average="micro"), + "avg_recall_micro": self.avg_recall(average="micro"), + "avg_f1_score_micro": self.avg_f1_score(average="micro"), + "avg_precision_weighted": self.avg_precision(average="micro"), + "avg_recall_weighted": self.avg_recall(average="micro"), + "avg_f1_score_weighted": self.avg_f1_score(average="weighted"), + "kappa_score": self.kappa_score(), + } + + +def roc_curve(conditions, prediction_scores, pos_label=None, sample_weight=None): + return metrics.roc_curve(conditions, prediction_scores, pos_label=pos_label, sample_weight=sample_weight) + + +def roc_auc_score(conditions, prediction_scores, average="micro", sample_weight=None): + try: + return metrics.roc_auc_score(conditions, prediction_scores, average=average, sample_weight=sample_weight) + except ValueError as ve: + logger.info(ve) + + +def precision_recall_curve(conditions, prediction_scores, pos_label=None, sample_weight=None): + return metrics.precision_recall_curve( + conditions, prediction_scores, pos_label=pos_label, sample_weight=sample_weight + ) + + +def average_precision_score(conditions, prediction_scores, average="micro", sample_weight=None): + # average == [micro, macro, sampled, weidhted] + return metrics.average_precision_score(conditions, prediction_scores, average=average, sample_weight=sample_weight) diff --git a/ludwig/utils/fs_utils.py b/ludwig/utils/fs_utils.py new file mode 100644 index 0000000..a7e9c44 --- /dev/null +++ b/ludwig/utils/fs_utils.py @@ -0,0 +1,412 @@ +#! /usr/bin/env python +# Copyright (c) 2021 Linux Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import contextlib +import errno +import functools +import logging +import os +import pathlib +import shutil +import tempfile +import uuid +from urllib.parse import unquote, urlparse + +import certifi +import fsspec +import pyarrow.fs + +try: + import h5py +except ImportError: + h5py = None +import urllib3 +from filelock import FileLock +from fsspec.core import split_protocol + +from ludwig.api_annotations import DeveloperAPI + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def get_default_cache_location() -> str: + """Returns a path to the default LUDWIG_CACHE location, or $HOME/.ludwig_cache.""" + cache_path = None + if os.environ.get("LUDWIG_CACHE"): + cache_path = os.environ["LUDWIG_CACHE"] + else: + cache_path = str(pathlib.Path.home().joinpath(".ludwig_cache")) + + # Check if the cache path exists, if not create it + if not os.path.exists(cache_path): + os.makedirs(cache_path) + return cache_path + + +@DeveloperAPI +def get_fs_and_path(url): + protocol, path = split_protocol(url) + # Parse the url to get only the escaped url path + path = unquote(urlparse(path).path) + # Create a windows compatible path from url path + path = os.fspath(pathlib.PurePosixPath(path)) + fs = fsspec.filesystem(protocol) + return fs, path + + +@DeveloperAPI +def has_remote_protocol(url): + protocol, _ = split_protocol(url) + return protocol and protocol != "file" + + +@DeveloperAPI +def is_http(urlpath): + protocol, _ = split_protocol(urlpath) + return protocol == "http" or protocol == "https" + + +@DeveloperAPI +def upgrade_http(urlpath): + protocol, url = split_protocol(urlpath) + if protocol == "http": + return "https://" + url + return None + + +@DeveloperAPI +@functools.lru_cache(maxsize=32) +def get_bytes_obj_from_path(path: str) -> bytes | None: + if is_http(path): + try: + return get_bytes_obj_from_http_path(path) + except Exception: + logger.warning(f"Failed to fetch bytes from HTTP path: {path}", exc_info=True) + return None + else: + try: + with open_file(path) as f: + return f.read() + except OSError as e: + logger.warning(e) + return None + + +@DeveloperAPI +def stream_http_get_request(path: str) -> urllib3.response.HTTPResponse: + if upgrade_http(path): + http = urllib3.PoolManager() + else: + http = urllib3.PoolManager(ca_certs=certifi.where()) + resp = http.request("GET", path, preload_content=False) + return resp + + +@DeveloperAPI +@functools.lru_cache(maxsize=32) +def get_bytes_obj_from_http_path(path: str) -> bytes: + resp = stream_http_get_request(path) + if resp.status == 404: + upgraded = upgrade_http(path) + if upgraded: + logger.info(f"reading url {path} failed. upgrading to https and retrying") + return get_bytes_obj_from_http_path(upgraded) + else: + raise urllib3.exceptions.HTTPError(f"reading url {path} failed and cannot be upgraded to https") + + # stream data + data = b"" + for chunk in resp.stream(1024): + data += chunk + return data + + +@DeveloperAPI +def find_non_existing_dir_by_adding_suffix(directory_name): + fs, _ = get_fs_and_path(directory_name) + suffix = 0 + curr_directory_name = directory_name + while fs.exists(curr_directory_name): + curr_directory_name = directory_name + "_" + str(suffix) + suffix += 1 + return curr_directory_name + + +@DeveloperAPI +def abspath(url): + protocol, _ = split_protocol(url) + if protocol is not None: + # we assume any path containing an explicit protovol is fully qualified + return url + return os.path.abspath(url) + + +@DeveloperAPI +def path_exists(url): + fs, path = get_fs_and_path(url) + return fs.exists(path) + + +@DeveloperAPI +def listdir(url): + fs, path = get_fs_and_path(url) + return fs.listdir(path) + + +@DeveloperAPI +def safe_move_file(src, dst): + """Rename a file from `src` to `dst`. Inspired by: https://alexwlchan.net/2019/03/atomic-cross-filesystem- + moves-in-python/ + + * Moves must be atomic. `shutil.move()` is not atomic. + + * Moves must work across filesystems. Sometimes temp directories and the + model directories live on different filesystems. `os.replace()` will + throw errors if run across filesystems. + + So we try `os.replace()`, but if we detect a cross-filesystem copy, we + switch to `shutil.move()` with some wrappers to make it atomic. + """ + try: + os.replace(src, dst) + except OSError as err: + if err.errno == errno.EXDEV: + # Generate a unique ID, and copy `` to the target directory with a temporary name `..tmp`. + # Because we're copying across a filesystem boundary, this initial copy may not be atomic. We insert a + # random UUID so if different processes are copying into ``, they don't overlap in their tmp copies. + copy_id = uuid.uuid4() + tmp_dst = f"{dst}.{copy_id}.tmp" + shutil.copyfile(src, tmp_dst) + + # Atomic replace file onto the new name, and clean up original source file. + os.replace(tmp_dst, dst) + os.unlink(src) + else: + raise + + +@DeveloperAPI +def safe_move_directory(src, dst): + """Recursively moves files from src directory to dst directory and removes src directory. + + If dst directory does not exist, it will be created. + """ + try: + os.replace(src, dst) + except OSError as err: + if err.errno == errno.EXDEV: + # Generate a unique ID, and copy `` to the target directory with a temporary name `..tmp`. + # Because we're copying across a filesystem boundary, this initial copy may not be atomic. We insert a + # random UUID so if different processes are copying into ``, they don't overlap in their tmp copies. + copy_id = uuid.uuid4() + tmp_dst = f"{dst}.{copy_id}.tmp" + shutil.copytree(src, tmp_dst) + + # Atomic replace directory name onto the new name, and clean up original source directory. + os.replace(tmp_dst, dst) + os.unlink(src) + else: + raise + + +@DeveloperAPI +def rename(src, tgt): + protocol, _ = split_protocol(tgt) + if protocol is not None: + fs = fsspec.filesystem(protocol) + fs.mv(src, tgt, recursive=True) + else: + safe_move_file(src, tgt) + + +@DeveloperAPI +def upload_file(src, tgt): + protocol, _ = split_protocol(tgt) + fs = fsspec.filesystem(protocol) + fs.put(src, tgt) + + +@DeveloperAPI +def copy(src, tgt, recursive=False): + protocol, _ = split_protocol(tgt) + fs = fsspec.filesystem(protocol) + fs.copy(src, tgt, recursive=recursive) + + +@DeveloperAPI +def makedirs(url, exist_ok=False): + fs, path = get_fs_and_path(url) + fs.makedirs(path, exist_ok=exist_ok) + + +@DeveloperAPI +def delete(url, recursive=False): + fs, path = get_fs_and_path(url) + return fs.delete(path, recursive=recursive) + + +@DeveloperAPI +def upload(lpath, rpath): + fs, path = get_fs_and_path(rpath) + pyarrow.fs.copy_files(lpath, path, destination_filesystem=pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(fs))) + + +@DeveloperAPI +def download(rpath, lpath): + fs, path = get_fs_and_path(rpath) + pyarrow.fs.copy_files(path, lpath, source_filesystem=pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(fs))) + + +@DeveloperAPI +def checksum(url): + fs, path = get_fs_and_path(url) + return fs.checksum(path) + + +@DeveloperAPI +def to_url(path): + protocol, _ = split_protocol(path) + if protocol is not None: + return path + return pathlib.Path(os.path.abspath(path)).as_uri() + + +@DeveloperAPI +@contextlib.contextmanager +def upload_output_directory(url): + if url is None: + yield None, None + return + + if has_remote_protocol(url): + # To avoid extra network load, write all output files locally at runtime, + # then upload to the remote fs at the end. + with tempfile.TemporaryDirectory() as tmpdir: + fs, remote_path = get_fs_and_path(url) + + # In cases where we are resuming from a previous run, we first need to download + # the artifacts from the remote filesystem + if path_exists(url): + fs.get(url, tmpdir + "/", recursive=True) + + def put_fn(): + # Use pyarrow API here as fs.put() is inconsistent in where it uploads the file + # See: https://github.com/fsspec/filesystem_spec/issues/1062 + pyarrow.fs.copy_files( + tmpdir, remote_path, destination_filesystem=pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(fs)) + ) + + # Write to temp directory locally + yield tmpdir, put_fn + + # Upload to remote when finished + put_fn() + else: + # For local paths (including file:// URIs), use the path directly. + _, local_path = get_fs_and_path(url) + makedirs(local_path, exist_ok=True) + yield local_path, None + + +@DeveloperAPI +@contextlib.contextmanager +def open_file(url, *args, **kwargs): + fs, path = get_fs_and_path(url) + with fs.open(path, *args, **kwargs) as f: + yield f + + +@DeveloperAPI +@contextlib.contextmanager +def download_h5(url): + """Legacy HDF5 download. + + Requires h5py (pip install h5py). + """ + if h5py is None: + raise ImportError("h5py is required to read legacy HDF5 files. Install with: pip install h5py") + with tempfile.TemporaryDirectory() as tmpdir: + local_path = os.path.join(tmpdir, os.path.basename(url)) + fs, path = get_fs_and_path(url) + fs.get(path, local_path) + with h5py.File(local_path, "r") as f: + yield f + + +@DeveloperAPI +@contextlib.contextmanager +def upload_h5(url): + """Legacy HDF5 upload. + + Requires h5py (pip install h5py). + """ + if h5py is None: + raise ImportError("h5py is required to write legacy HDF5 files. Install with: pip install h5py") + with upload_output_file(url) as local_fname: + mode = "w" + if url == local_fname and path_exists(url): + mode = "r+" + + with h5py.File(local_fname, mode) as f: + yield f + + +@DeveloperAPI +@contextlib.contextmanager +def upload_output_file(url): + """Takes a remote URL as input, returns a temp filename, then uploads it when done.""" + protocol, _ = split_protocol(url) + if protocol is not None: + fs = fsspec.filesystem(protocol) + with tempfile.TemporaryDirectory() as tmpdir: + local_fname = os.path.join(tmpdir, "tmpfile") + yield local_fname + fs.put(local_fname, url, recursive=True) + else: + yield url + + +@DeveloperAPI +class file_lock(contextlib.AbstractContextManager): + """File lock based on filelock package.""" + + def __init__(self, path: str, ignore_remote_protocol: bool = True, lock_file: str = ".lock") -> None: + if not isinstance(path, (str, os.PathLike, pathlib.Path)): + self.lock = None + else: + path = os.path.join(path, lock_file) if os.path.isdir(path) else f"{path}./{lock_file}" + if ignore_remote_protocol and has_remote_protocol(path): + self.lock = None + else: + self.lock = FileLock(path, timeout=-1) + + def __enter__(self, *args, **kwargs): + if self.lock: + return self.lock.__enter__(*args, **kwargs) + + def __exit__(self, *args, **kwargs): + if self.lock: + return self.lock.__exit__(*args, **kwargs) + + +@DeveloperAPI +def list_file_names_in_directory(directory_name: str) -> list[str]: + file_path: pathlib.Path # noqa [F842] # incorrect flagging of "local variable is annotated but never used" + file_names: list[str] = [ + file_path.name for file_path in pathlib.Path(directory_name).iterdir() if file_path.is_file() + ] + return file_names diff --git a/ludwig/utils/h3_util.py b/ludwig/utils/h3_util.py new file mode 100644 index 0000000..9e035e0 --- /dev/null +++ b/ludwig/utils/h3_util.py @@ -0,0 +1,113 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from typing import NamedTuple + + +class H3Data(NamedTuple): + mode: int + edge: int + resolution: int + base_cell: int + cells: list[int] + + +def set_bit(v, index, x): + """Set the index:th bit of v to 1 if x is truthy, else to 0, and return the new value.""" + mask = 1 << index # Compute mask, an integer with just bit 'index' set. + v &= ~mask # Clear the bit indicated by the mask (if x is False) + if x: + v |= mask # If x was True, set the bit indicated by the mask. + return v # Return the result, we're done. + + +def set_bits(v, start_bit, slice_length, x): + bin_x = bin(x) + for i, index in enumerate(range(start_bit, start_bit + slice_length)): + val = int(bin_x[-(i + 1)]) if 2 + i < len(bin_x) else 0 + v = set_bit(v, index, val) + return v + + +def components_to_h3(components): + h3 = 18446744073709551615 + h3 = set_bits(h3, 64 - 5, 4, components["mode"]) + h3 = set_bits(h3, 64 - 8, 3, components["edge"]) + h3 = set_bits(h3, 64 - 12, 4, components["resolution"]) + h3 = set_bits(h3, 64 - 19, 7, components["base_cell"]) + for i, cell in enumerate(components["cells"]): + h3 = set_bits(h3, 64 - 19 - (i + 1) * 3, 3, cell) + h3 = set_bits(h3, 64 - 1, 4, 0) + return h3 + + +def bitslice(x: int, start_bit: int, slice_length: int) -> int: + ones_mask: int = int(2**slice_length - 1) + return (x & (ones_mask << start_bit)) >> start_bit + + +def h3_index_mode(h3_long: int) -> int: + return bitslice(h3_long, 64 - 5, 4) + + +def h3_edge(h3_long: int) -> int: + return bitslice(h3_long, 64 - 8, 3) + + +def h3_resolution(h3_long: int) -> int: + return bitslice(h3_long, 64 - 12, 4) + + +def h3_base_cell(h3_long: int) -> int: + return bitslice(h3_long, 64 - 19, 7) + + +def h3_octal_components(h3_long): + res = h3_resolution(h3_long) + return "{0:0{w}o}".format(bitslice(h3_long + 2**63, 64 - 19 - 3 * res, 3 * res), w=res) + + +def h3_component(h3_long: int, i: int) -> int: + return bitslice(h3_long, 64 - 19 - 3 * i, 3) + + +def h3_components(h3_long: int) -> list[int]: + return [h3_component(h3_long, i) for i in range(1, h3_resolution(h3_long) + 1)] + + +def h3_to_components(h3_value: int) -> H3Data: + """Extract the values from an H3 hexadecimal value Refer to this for the bit layout: + + https://uber.github.io/h3/#/documentation/core-library/h3-index-representations + """ + # lat_long = (0, 0) # h3ToGeo(h3_value) + return H3Data( + mode=h3_index_mode(h3_value), + edge=h3_edge(h3_value), + resolution=h3_resolution(h3_value), + base_cell=h3_base_cell(h3_value), + cells=h3_components(h3_value), + ) + + +if __name__ == "__main__": + value = 622236723497533439 + components = h3_to_components(value) + h3 = components_to_h3(components) + components2 = h3_to_components(h3) + print(value) + print(components) + print(h3) + print(components2) diff --git a/ludwig/utils/heuristics.py b/ludwig/utils/heuristics.py new file mode 100644 index 0000000..dd44a10 --- /dev/null +++ b/ludwig/utils/heuristics.py @@ -0,0 +1,26 @@ +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.config_utils import has_pretrained_encoder, has_trainable_encoder, has_unstructured_input_feature + + +def get_auto_learning_rate(config: ModelConfig) -> float: + """Uses config heuristics to determine an appropriate learning rate. + + The main idea behind the following heuristics is that smaller learning rates are more + suitable for features with larger encoders, which are typically used with unstructured features. + Note that these are meant to be rough heuristics that are solely based on feature types and the + type of the corresponding encoder. More factors could be taken into consideration such as model + size, dataset size, batch size, number of features, etc. + + Args: + config: Ludwig config used to train the model. + """ + if not has_unstructured_input_feature(config): + return 0.001 + + if not has_pretrained_encoder(config): + return 0.0001 + + if has_trainable_encoder(config): + return 0.00001 + + return 0.00002 diff --git a/ludwig/utils/hf_utils.py b/ludwig/utils/hf_utils.py new file mode 100644 index 0000000..23c5288 --- /dev/null +++ b/ludwig/utils/hf_utils.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import logging +import os +import tempfile +from os import PathLike +from typing import TYPE_CHECKING + +from transformers import AutoTokenizer +from transformers.tokenization_utils import PreTrainedTokenizer + +if TYPE_CHECKING: + from transformers import PreTrainedModel + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils.error_handling_utils import default_retry +from ludwig.utils.fs_utils import download, path_exists +from ludwig.utils.upload_utils import hf_hub_login + +logger = logging.getLogger(__name__) + + +@default_retry() +def load_pretrained_hf_model_from_hub( + model_class: type, + pretrained_model_name_or_path: str | PathLike | None, + **pretrained_kwargs, +) -> PreTrainedModel: + """Download a HuggingFace model. + + Downloads a model from the HuggingFace zoo with retry on failure. + Args: + model_class: Class of the model to download. + pretrained_model_name_or_path: Name of the model to download. + pretrained_kwargs: Additional arguments to pass to the model constructor. + Returns: + The pretrained model object. + """ + return model_class.from_pretrained(pretrained_model_name_or_path, **pretrained_kwargs) + + +@default_retry() +def load_pretrained_hf_tokenizer( + pretrained_model_name_or_path: str | PathLike | None, **pretrained_kwargs +) -> PreTrainedTokenizer: + """Download a HuggingFace tokenizer. + + Args: + pretrained_model_name_or_path: Name of the tokenizer to download. + pretrained_kwargs: Additional arguments to pass to the tokenizer constructor. + Returns: + The pretrained tokenizer object. + """ + return AutoTokenizer.from_pretrained(pretrained_model_name_or_path, **pretrained_kwargs) + + +def _load_pretrained_hf_model_from_dir( + model_class: type, + pretrained_model_name_or_path: str | PathLike | None, + **pretrained_kwargs, +) -> PreTrainedModel: + """Downloads a model to a local temporary directory, and Loads a pretrained HF model from a local directory.""" + with tempfile.TemporaryDirectory() as tmpdir: + download(pretrained_model_name_or_path, tmpdir) + return model_class.from_pretrained(tmpdir, **pretrained_kwargs) + + +@DeveloperAPI +def load_pretrained_hf_model_with_hub_fallback( + model_class: type, + pretrained_model_name_or_path: str | PathLike | None, + **pretrained_kwargs, +) -> tuple[PreTrainedModel, bool]: + """Returns the model and a boolean indicating whether the model was downloaded from the HuggingFace hub. + + If the `LUDWIG_PRETRAINED_MODELS_DIR` environment variable is set, we attempt to load the HF model from this + directory, falling back to downloading from the HF hub if the model is not found, downloading fails, or if model + initialization fails. + + `LUDWIG_PRETRAINED_MODELS_DIR` can be an s3 path. Weights are copied to a local temporary directory, and the model + is loaded from there. + + The expected structure of the `LUDWIG_PRETRAINED_MODELS_DIR` directory is: + {LUDWIG_PRETRAINED_MODELS_DIR}/{pretrained_model_name_or_path}/pytorch_model.bin + {LUDWIG_PRETRAINED_MODELS_DIR}/{pretrained_model_name_or_path}/config.json + + For example, if `LUDWIG_PRETRAINED_MODELS_DIR` is set to `s3://my-bucket/pretrained-models`, and + `pretrained_model_name_or_path` is set to `bert-base-uncased`, we expect to find the following files: + s3://my-bucket/bert-base-uncased/ + - pytorch_model.bin + - config.json + + If the `LUDWIG_PRETRAINED_MODELS_DIR` environment variable is not set, we download the model from the HF hub. + """ + pretrained_models_dir = os.environ.get("LUDWIG_PRETRAINED_MODELS_DIR") + if pretrained_models_dir: + pretrained_model_path = os.path.join(pretrained_models_dir, pretrained_model_name_or_path) + if path_exists(pretrained_model_path): + try: + logger.info( + f"Found existing pretrained model artifact {pretrained_model_name_or_path} in directory " + f"{pretrained_models_dir}. Downloading." + ) + return ( + _load_pretrained_hf_model_from_dir(model_class, pretrained_model_path, **pretrained_kwargs), + False, + ) + except Exception: + logger.warning( + f"Failed to download pretrained model from {pretrained_models_dir}. " + "Falling back to HuggingFace model hub.", + exc_info=True, + ) + + # Fallback to HF hub. + return load_pretrained_hf_model_from_hub(model_class, pretrained_model_name_or_path, **pretrained_kwargs), True + + +def upload_folder_to_hfhub( + repo_id: str, + folder_path: str, + repo_type: str | None = "model", + private: bool | None = False, + path_in_repo: str | None = None, # defaults to root of repo + commit_message: str | None = None, + commit_description: str | None = None, +) -> None: + """Uploads a local folder to the Hugging Face Model Hub. + + Args: + repo_id (str): The ID of the target repository on the Hugging Face Model Hub. + folder_path (str): The local path to the folder to be uploaded. + repo_type (str, optional): The type of the repository ('model', 'dataset', or 'space'). + Defaults to 'model'. + private (bool, optional): If True, the repository will be private; otherwise, it will be public. + Defaults to False. + path_in_repo (str, optional): The relative path within the repository where the folder should be uploaded. + Defaults to None, which means the root of the repository. + commit_message (str, optional): A message for the commit associated with the upload. + commit_description (str, optional): A description for the commit associated with the upload. + + Raises: + FileNotFoundError: If the specified folder does not exist. + ValueError: If the specified folder is empty, a file, or if an invalid 'repo_type' is provided. + ValueError: If the upload process fails for any reason. + + Returns: + None + """ + # Make sure the folder exists + if not os.path.exists(folder_path): + raise FileNotFoundError(f"Folder {folder_path} does not exist.") + + # Make sure the folder is not a file + if os.path.isfile(folder_path): + raise ValueError(f"Folder {folder_path} is a file. Please provide a folder.") + + # Make sure the folder is not empty + if not os.listdir(folder_path): + raise ValueError(f"Folder {folder_path} is empty.") + + if repo_type not in {"model", "dataset", "space"}: + raise ValueError(f"Invalid repo_type {repo_type}. Valid values are 'model', 'dataset', and 'space'.") + + # Login to the hub + api = hf_hub_login() + + # Create the repo if it doesn't exist. This is a no-op if the repo already exists + # This is required because the API doesn't allow uploading to a non-existent repo + if not api.repo_exists(repo_id, repo_type=repo_type): + logger.info(f"{repo_id} does not exist. Creating.") + api.create_repo(repo_id, private=private, exist_ok=True, repo_type=repo_type) + + # Upload the folder + try: + logger.info(f"Uploading folder {folder_path} to repo {repo_id}.") + api.upload_folder( + repo_id=repo_id, + folder_path=folder_path, + repo_type=repo_type, + path_in_repo=path_in_repo, + commit_message=commit_message, + commit_description=commit_description, + ) + except Exception as e: + raise ValueError(f"Failed to upload folder {folder_path} to repo {repo_id}") from e diff --git a/ludwig/utils/html_utils.py b/ludwig/utils/html_utils.py new file mode 100644 index 0000000..7784457 --- /dev/null +++ b/ludwig/utils/html_utils.py @@ -0,0 +1,95 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import re +from html.parser import HTMLParser + +from ludwig.utils import strings_utils + +logger = logging.getLogger(__name__) + + +class HTMLStripper(HTMLParser): + def __init__(self): + super().__init__() + self.reset() + self.strict = False + self.convert_charrefs = True + self.fed = [] + + def handle_data(self, data): + self.fed.append(data) + + def get_data(self): + return "".join(self.fed) + + def error(self, message): + logger.error(message) + + +def strip_tags(html): + stripper = HTMLStripper() + stripper.feed(html) + return stripper.get_data() + + +# regular expressions for cleaning text +res_pre = [(re.compile(r"([^.:;\?\!>])(
)"), r"\1.\2"), (re.compile(r"
"), r" ")] +res_post = [ + (re.compile(r"[ \t\0]"), r" "), + (re.compile(r"[–_]"), r"-"), + ( + re.compile(r"[\’\‘]"), + r"""), + (re.compile(r'[”“]]'), r""", + ), + (re.compile(r"℅"), r"%"), + (re.compile(r"([^.>])(
)"), r"\1.\2"), + (re.compile(r"\\\\[NnRr]"), r" "), + (re.compile(r"\\[NnRr]"), r" "), + (re.compile(r"[\n\r]"), r" "), + (re.compile(r"\\\\"), r" / "), + (re.compile(r"
"), r" "), + (re.compile(r"\\\\" ""), r"\'"), + (re.compile(r"^\'([^\']+)$"), r"\1"), + (re.compile(r"([\<\>\{\}\[\]\(\)\-\+\=:;,\./\?\!\$%&£#@\'₹ ])\1+"), r"\1"), + ( + re.compile( + r"[^qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\<\>\{\}\[\]\(\)\-\+\=:;,\./\?\!\$%&£#@\'₹ ]" + ), + r" ", + ), + (re.compile(r"\s{2,}"), r" "), +] + + +def clean_html(html_text): + # print() + # print(html_text) + html_text, matched = strings_utils.match_replace(html_text, res_pre) + # print(html_text) + html_text = strip_tags(html_text) + # print(html_text) + html_text = strings_utils.strip_accents(html_text) + # print(html_text) + # result = html_text.strip( + # 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\<\>\{\}\[\]\(\)\-\+\=:;,\./\?\!\$%&€£#@'₹\' ') + # if result: + # print(result) + html_text, matched = strings_utils.match_replace(html_text, res_post) + # print(matched) + # print(html_text) + return html_text diff --git a/ludwig/utils/image_utils.py b/ludwig/utils/image_utils.py new file mode 100644 index 0000000..b315714 --- /dev/null +++ b/ludwig/utils/image_utils.py @@ -0,0 +1,537 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import warnings +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from io import BytesIO + +import numpy as np +import torch +import torchvision.transforms.functional as F +from torchvision.io import decode_image, ImageReadMode +from torchvision.models._api import WeightsEnum + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import CROP_OR_PAD, IMAGE_MAX_CLASSES, INTERPOLATE +from ludwig.utils.data_utils import get_abs_path +from ludwig.utils.fs_utils import get_bytes_obj_from_path +from ludwig.utils.registry import Registry + + +@dataclass +class TVModelVariant: + # Model variant identifier + variant_id: str | int + + # TorchVision function to create model class + create_model_function: Callable + + # Torchvision class for model weights + model_weights: WeightsEnum + + +logger = logging.getLogger(__name__) + +IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".tiff", ".tif", ".bmp", ".gif") + + +@DeveloperAPI +class ResizeChannels(torch.nn.Module): + def __init__(self, num_channels: int): + super().__init__() + self.num_channels = num_channels + + def forward(self, imgs: torch.Tensor): + original_imgs_shape = imgs.shape + if len(original_imgs_shape) == 3: # if shape is (C, H, W), add batch dimension + imgs = imgs.unsqueeze(0) + + channels = imgs.shape[1] + if channels > self.num_channels: + # take the first `self.num_channels` channels + imgs = imgs[:, : self.num_channels, :, :] + elif channels < self.num_channels: + # repeat and use the first `self.num_channels` channels + imgs = imgs.repeat(1, (self.num_channels // channels) + 1, 1, 1)[:, : self.num_channels, :, :] + + if len(original_imgs_shape) == 3: # if shape was (C, H, W), remove batch dimension + return imgs[0] + return imgs + + +@DeveloperAPI +def get_gray_default_image(num_channels: int, height: int, width: int) -> np.ndarray: + return np.full((num_channels, height, width), 128, dtype=np.float32) + + +@DeveloperAPI +def get_average_image(image_lst: list[np.ndarray]) -> np.array: + return np.mean([x for x in image_lst if x is not None], axis=(0), dtype=np.float32) + + +@DeveloperAPI +def is_bytes_image(bytes_obj) -> bool: + """Check if a bytes object is an image using PIL.""" + try: + from io import BytesIO + + from PIL import Image + + if isinstance(bytes_obj, bytes): + bytes_obj = BytesIO(bytes_obj) + Image.open(bytes_obj).verify() + return True + except (OSError, SyntaxError, ValueError): + return False + + +def is_image(src_path: str, img_entry: bytes | str, column: str) -> bool: + if not isinstance(img_entry, str): + return False + try: + from io import BytesIO + + from PIL import Image + + path = get_abs_path(src_path, img_entry) + bytes_obj = get_bytes_obj_from_path(path) + if isinstance(bytes_obj, bytes): + bytes_obj = BytesIO(bytes_obj) + Image.open(bytes_obj).verify() + return True + except Exception as e: + logger.warning(f"While assessing potential image in is_image() for column {column}, encountered exception: {e}") + return False + + +@DeveloperAPI +def is_image_score(path): + return int(isinstance(path, str) and path.lower().endswith(IMAGE_EXTENSIONS)) + + +@DeveloperAPI +def get_image_read_mode_from_num_channels(num_channels: int) -> ImageReadMode: + """Returns the torchvision.io.ImageReadMode corresponding to the number of channels. + + If num_channels is not recognized, returns ImageReadMode.UNCHANGED. + """ + mode = ImageReadMode.UNCHANGED + if num_channels == 1: + mode = ImageReadMode.GRAY + elif num_channels == 2: + mode = ImageReadMode.GRAY_ALPHA + elif num_channels == 3: + mode = ImageReadMode.RGB + elif num_channels == 4: + mode = ImageReadMode.RGB_ALPHA + return mode + + +@DeveloperAPI +def read_image_from_path( + path: str, num_channels: int | None = None, return_num_bytes=False +) -> torch.Tensor | None | tuple[torch.Tensor | None, int]: + """Reads image from path. + + Useful for reading from a small number of paths. For more intensive reads, use backend.read_binary_files instead. If + `return_num_bytes` is True, returns a tuple of (image, num_bytes). + """ + bytes_obj = get_bytes_obj_from_path(path) + image = read_image_from_bytes_obj(bytes_obj, num_channels) + if return_num_bytes: + if bytes_obj is not None: + num_bytes = len(bytes_obj) + else: + num_bytes = None + return image, num_bytes + else: + return image + + +@DeveloperAPI +def read_image_from_bytes_obj(bytes_obj: bytes | None = None, num_channels: int | None = None) -> torch.Tensor | None: + """Tries to read image as a tensor from the path. + + If the path is not decodable as a PNG, attempts to read as a numpy file. If neither of these work, returns None. + """ + if bytes_obj is None: + return None + mode = get_image_read_mode_from_num_channels(num_channels) + + image = read_image_as_png(bytes_obj, mode) + if image is None: + image = read_image_as_numpy(bytes_obj) + if image is None: + image = read_image_as_tif(bytes_obj) + if image is None: + warnings.warn("Unable to read image from bytes object.") + return image + + +@DeveloperAPI +def read_image_as_png(bytes_obj: bytes, mode: ImageReadMode = ImageReadMode.UNCHANGED) -> torch.Tensor | None: + """Reads image from bytes object from a PNG file.""" + try: + with BytesIO(bytes_obj) as buffer: + buffer_view = buffer.getbuffer() + if len(buffer_view) == 0: + del buffer_view + raise Exception("Bytes object is empty. This could be due to a failed load from storage.") + image = decode_image(torch.frombuffer(buffer_view, dtype=torch.uint8), mode=mode) + del buffer_view + return image + except Exception: + logger.warning("Failed to read image from PNG file.", exc_info=True) + return None + + +@DeveloperAPI +def read_image_as_numpy(bytes_obj: bytes) -> torch.Tensor | None: + """Reads image from bytes object from a numpy file.""" + try: + with BytesIO(bytes_obj) as buffer: + image = np.load(buffer) + return torch.from_numpy(image) + except Exception: + logger.warning("Failed to read image from numpy file.", exc_info=True) + return None + + +@DeveloperAPI +def read_image_as_tif(bytes_obj: bytes) -> torch.Tensor | None: + """Reads image from bytes object from a tif file.""" + try: + import tifffile + + with BytesIO(bytes_obj) as buffer: + image = tifffile.imread(buffer) + if image.dtype == np.uint16: + image = image.astype(np.int32) + image = torch.from_numpy(image) + if len(image.shape) == 2: + image = torch.unsqueeze(image, dim=0) + return image + except Exception: + logger.warning("Failed to read image from tif file.", exc_info=True) + return None + + +@DeveloperAPI +def pad( + img: torch.Tensor, + new_size: int | tuple[int, int], +) -> torch.Tensor: + """Torchscript-compatible implementation of pad. + + Args: + img (torch.Tensor): image with shape [..., height, width] to pad + new_size (Union[int, Tuple[int, int]]): size to pad to. If int, resizes to square image of that size. + + Returns: + torch.Tensor: padded image of size [..., size[0], size[1]] or [..., size, size] if size is int. + """ + new_size = to_tuple(new_size) + old_size = img.shape[-2:] + pad_size = (torch.tensor(new_size) - torch.tensor(old_size)) / 2 + padding = torch.cat((torch.floor(pad_size), torch.ceil(pad_size))) + padding[padding < 0] = 0 + padding = [int(x) for x in padding] + return F.pad(img, padding=padding, padding_mode="edge") + + +@DeveloperAPI +def crop( + img: torch.Tensor, + new_size: int | tuple[int, int], +) -> torch.Tensor: + """Torchscript-compatible implementation of crop. + + Args: + img (torch.Tensor): image with shape [..., height, width] to crop + size (Union[int, Tuple[int, int]]): size to crop to. If int, crops to square image of that size. + + Returns: + torch.Tensor: cropped image of size [..., size[0], size[1]] or [..., size, size] if size is int. + """ + new_size = to_tuple(new_size) + return F.center_crop(img, output_size=new_size) + + +@DeveloperAPI +def crop_or_pad(img: torch.Tensor, new_size: int | tuple[int, int]): + """Torchscript-compatible implementation of resize using constants.CROP_OR_PAD. + + Args: + img (torch.Tensor): image with shape [..., height, width] to resize + new_size (Union[int, Tuple[int, int]]): size to resize to. If int, resizes to square image of that size. + + Returns: + torch.Tensor: resized image of size [..., size[0], size[1]] or [..., size, size] if size is int. + """ + new_size = to_tuple(new_size) + if list(new_size) == list(img.shape[-2:]): + return img + img = pad(img, new_size) + img = crop(img, new_size) + return img + + +@DeveloperAPI +def resize_image( + img: torch.Tensor, + new_size: int | tuple[int, int], + resize_method: str, + crop_or_pad_constant: str = CROP_OR_PAD, + interpolate_constant: str = INTERPOLATE, +) -> torch.Tensor: + """Torchscript-compatible implementation of resize. + + Args: + img (torch.Tensor): image with shape [..., height, width] to resize + new_size (Union[int, Tuple[int, int]]): size to resize to. If int, resizes to square image of that size. + resize_method (str): method to use for resizing. Either constants.CROP_OR_PAD or constants.INTERPOLATE. + + Returns: + torch.Tensor: resized image of size [..., size[0], size[1]] or [..., size, size] if size is int. + """ + new_size = to_tuple(new_size) + if list(img.shape[-2:]) != list(new_size): + if resize_method == crop_or_pad_constant: + return crop_or_pad(img, new_size) + elif resize_method == interpolate_constant: + return F.resize(img, new_size) + raise ValueError(f"Invalid image resize method: {resize_method}") + return img + + +@DeveloperAPI +def grayscale(img: torch.Tensor) -> torch.Tensor: + """Grayscales RGB image.""" + return F.rgb_to_grayscale(img) + + +@DeveloperAPI +def num_channels_in_image(img: torch.Tensor): + """Returns number of channels in image.""" + if img is None or img.ndim < 2: + raise ValueError( + f"Cannot determine number of channels: expected a 2D or 3D image tensor, " + f"got {'None' if img is None else f'{img.ndim}D tensor with shape {tuple(img.shape)}'}." + ) + + if img.ndim == 2: + return 1 + else: + return img.shape[0] + + +@DeveloperAPI +def get_unique_channels( + image_sample: list[torch.Tensor], + num_channels: int, + num_classes: int | None = None, +) -> torch.Tensor: + """Returns a tensor of unique channel values from a list of images. + Args: + image_sample: A list of images of dimensions [C x H x W] or [H x W], where C is the channel dimension + num_channels: The expected number of channels + num_classes: The expected number of classes or None + + Return: + channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class. + """ + n_images = 0 + no_new_class = 0 + channel_class_map = None + for img in image_sample: + if img.ndim < 2: + raise ValueError("Invalid image dimensions {img.ndim}") + if img.ndim == 2: + img = img.unsqueeze(0) + if num_channels == 1 and num_channels_in_image(img) != 1: + img = grayscale(img) + if num_classes == 2 and num_channels_in_image(img) == 1: + img = img.type(torch.float32) / 255 + img = img.round() * 255 + img = img.type(torch.uint8) + + img = img.flatten(1, 2) + img = img.permute(1, 0) + uniq_chans = img.unique(dim=0) + + if channel_class_map is None: + channel_class_map = uniq_chans + else: + channel_class_map = torch.concat((channel_class_map, uniq_chans)).unique(dim=0) + if channel_class_map.shape[0] > IMAGE_MAX_CLASSES: + raise ValueError( + f"Images inferred num classes {channel_class_map.shape[0]} exceeds max classes {IMAGE_MAX_CLASSES}." + ) + + n_images += 1 + if n_images % 25 == 0: + logger.info(f"Processed the first {n_images} images inferring {channel_class_map.shape[0]} classes...") + + if channel_class_map.shape[0] == uniq_chans.shape[0]: + no_new_class += 1 + if no_new_class >= 4 and channel_class_map.shape[0] == num_classes: + break # early loop exit + else: + no_new_class = 0 + + logger.info(f"Inferred {channel_class_map.shape[0]} classes from the first {n_images} images.") + return channel_class_map.type(torch.uint8) + + +@DeveloperAPI +def get_class_mask_from_image( + channel_class_map: torch.Tensor, + img: torch.Tensor, +) -> torch.Tensor: + """Returns a masked image where each mask value is the channel class of the input. + Args: + channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class. + img: An input image of dimensions [C x H x W] or [H x W], where C is the channel dimension + + Return: + [mask] A masked image of dimensions [H x W] where each value is the channel class of the input + """ + num_classes = channel_class_map.shape[0] + mask = torch.full((img.shape[-2], img.shape[-1]), num_classes, dtype=torch.uint8) + if img.ndim == 2: + img = img.unsqueeze(0) + if num_classes == 2 and num_channels_in_image(img) == 1: + img = img.type(torch.float32) / 255 + img = img.round() * 255 + img = img.type(torch.uint8) + img = img.permute(1, 2, 0) + for nclass, value in enumerate(channel_class_map): + mask[(img == value).all(-1)] = nclass + + if torch.any(mask.ge(num_classes)): + # Map any unseen colors to class 0 (background). This can happen when the color sample + # used to build the class map (infer_image_sample_size) missed rare colors present in + # other split images. Silently remapping avoids crashing at the cost of minor label noise. + import warnings + + warnings.warn( + f"Image channel could not be mapped to a class for {torch.sum(mask.ge(num_classes)).item()} pixels. " + f"{num_classes} classes were inferred from the initial sample. Remapping unseen colors to class 0. " + f"To avoid this, increase infer_image_sample_size in preprocessing.", + UserWarning, + stacklevel=2, + ) + mask[mask.ge(num_classes)] = 0 + + return mask + + +@DeveloperAPI +def get_image_from_class_mask( + channel_class_map: torch.Tensor, + mask: np.ndarray, +) -> np.ndarray: + """Returns an image with channel values determined from a corresponding mask. + Args: + channel_class_map: An tensor mapping channel values to classes, where dim=0 is the class. + mask: A masked image of dimensions [H x W] where each value is the channel class of the final image + + Return: + [img] An image of dimensions [C x H x W], where C is the channel dimension + """ + mask = torch.from_numpy(mask) + img = torch.zeros(channel_class_map.shape[1], mask.shape[-2], mask.shape[-1], dtype=torch.uint8) + img = img.permute(1, 2, 0) + mask = mask.unsqueeze(0) + mask = mask.permute(1, 2, 0) + for nclass, value in enumerate(channel_class_map): + img[(mask == nclass).all(-1)] = value + img = img.permute(2, 0, 1) + + return img.numpy() + + +@DeveloperAPI +def to_tuple(v: int | tuple[int, int]) -> tuple[int, int]: + """Converts int or tuple to tuple of ints.""" + if torch.jit.isinstance(v, int): + return v, v + else: + return v + + +@DeveloperAPI +def to_np_tuple(prop: int | Iterable) -> np.ndarray: + """Creates a np array of length 2 from a Conv2D property. + + E.g., stride=(2, 3) gets converted into np.array([2, 3]), where the height_stride = 2 and width_stride = 3. stride=2 + gets converted into np.array([2, 2]). + """ + if isinstance(prop, int): + return np.ones(2).astype(int) * prop + elif isinstance(prop, np.ndarray) and prop.size == 2: + return prop.astype(int) + elif isinstance(prop, Iterable) and len(prop) == 2: + return np.array(list(prop)).astype(int) + else: + raise TypeError(f"prop must be int or iterable of length 2, but is {prop}.") + + +@DeveloperAPI +def get_img_output_shape( + img_height: int, + img_width: int, + kernel_size: int | tuple[int], + stride: int | tuple[int], + padding: int | tuple[int] | str, + dilation: int | tuple[int], +) -> tuple[int]: + """Returns the height and width of an image after a 2D img op. + + Currently supported for Conv2D, MaxPool2D and AvgPool2d ops. + """ + if padding == "same": + return (img_height, img_width) + elif padding == "valid": + padding = np.zeros(2) + else: + padding = to_np_tuple(padding) + + kernel_size = to_np_tuple(kernel_size) + stride = to_np_tuple(stride) + dilation = to_np_tuple(dilation) + shape = np.array([img_height, img_width]) + + out_shape = np.floor(((shape + 2 * padding - dilation * (kernel_size - 1) - 1) / stride) + 1) + + return tuple(out_shape.astype(int)) + + +torchvision_model_registry = Registry() + + +def register_torchvision_model_variants(variants: list[TVModelVariant]): + def wrap(cls): + # prime with empty placeholder + torchvision_model_registry[cls.torchvision_model_type] = {} + + # register each variant + for variant in variants: + torchvision_model_registry[cls.torchvision_model_type][variant.variant_id] = variant + return cls + + return wrap diff --git a/ludwig/utils/llm_quantization_utils.py b/ludwig/utils/llm_quantization_utils.py new file mode 100644 index 0000000..895d7c6 --- /dev/null +++ b/ludwig/utils/llm_quantization_utils.py @@ -0,0 +1,61 @@ +import torch +from torch import nn + +try: + from bitsandbytes.functional import dequantize_4bit + from bitsandbytes.nn.modules import Linear4bit +except ImportError: + dequantize_4bit = None + Linear4bit = None + +from ludwig.api_annotations import DeveloperAPI + + +@DeveloperAPI +def linear4bit_to_linear(linear4bit_layer): + """Converts a Linear4Bit layer to a standard Linear layer by dequantizing the weight values and copying the + dequantized weights to a new Linear layer. + + Args: + linear4bit_layer (Linear4bit): The input Linear4Bit layer. + + Returns: + nn.Linear: A new Linear layer with dequantized weights and biases. + """ + # Create a new Linear layer with the same shape + new_linear_layer = nn.Linear( + linear4bit_layer.in_features, + linear4bit_layer.out_features, + bias=linear4bit_layer.bias is not None, + dtype=torch.float16, + ) + + # Dequantize the weight and bias from the Linear4bit layer and perform an in-place tensor replacement + # to update the weights and bias in the new Linear layer. This is done to avoid creating a new tensor + # and copying the data, which is slow. + new_linear_layer.weight.data.copy_( + dequantize_4bit(linear4bit_layer.weight.data, linear4bit_layer.weight.quant_state) + ) + if linear4bit_layer.bias is not None: + new_linear_layer.bias.data.copy_(linear4bit_layer.bias.data) + + return new_linear_layer + + +@DeveloperAPI +def convert_quantized_linear_to_linear(module): + """Recursively converts Linear4Bit layers to standard Linear layers in a given module. + + Args: + module (nn.Module): The input module containing potentially nested Linear4Bit layers. + + Returns: + None + """ + for name, child in module.named_children(): + if isinstance(child, Linear4bit): + # Replace Linear4Bit layer with a new Linear layer + setattr(module, name, linear4bit_to_linear(child)) + else: + # Recursively apply the conversion for nested modules + convert_quantized_linear_to_linear(child) diff --git a/ludwig/utils/llm_utils.py b/ludwig/utils/llm_utils.py new file mode 100644 index 0000000..8c8b0ff --- /dev/null +++ b/ludwig/utils/llm_utils.py @@ -0,0 +1,830 @@ +from __future__ import annotations + +import copy +import logging +import tempfile +from typing import TYPE_CHECKING, Union + +import torch +import torch.nn.functional as F +import transformers +from packaging import version + +try: + from bitsandbytes.nn.modules import Embedding as BnbEmbedding +except ImportError: + BnbEmbedding = None +from transformers import AutoModelForCausalLM, TextStreamer + +from ludwig.constants import IGNORE_INDEX_TOKEN_ID, LOGITS, PREDICTIONS, PROBABILITIES +from ludwig.schema.trainer import LLMTrainerConfig +from ludwig.utils.error_handling_utils import default_retry +from ludwig.utils.logging_utils import log_once +from ludwig.utils.model_utils import find_embedding_layer_with_path + +if TYPE_CHECKING: + from transformers import AutoConfig, PreTrainedModel, PreTrainedTokenizer + + from ludwig.schema.encoders.text_encoders import LLMEncoderConfig + from ludwig.schema.model_types.llm import LLMModelConfig + + +logger = logging.getLogger(__name__) + +transformers_436 = version.parse(transformers.__version__) >= version.parse("4.36.0") + +FALLBACK_CONTEXT_LEN = 2048 + +_MODELS_WITH_DEVICE_MAP_AUTO_EXCLUSION = set() + + +@default_retry(tries=8, exceptions=OSError) +def load_pretrained_from_config( + config_obj: LLMModelConfig | LLMEncoderConfig, + model_config: AutoConfig | None = None, + weights_save_path: str | None = None, +) -> PreTrainedModel: + load_kwargs = {} + quantization = config_obj.quantization + if quantization and getattr(quantization, "backend", "bitsandbytes") == "bitsandbytes": + # Apply bitsandbytes quantization configuration at model load time. + load_kwargs["dtype"] = getattr(torch, quantization.bnb_4bit_compute_dtype) + load_kwargs["quantization_config"] = quantization.to_bitsandbytes() + load_kwargs["device_map"] = "auto" + + if transformers_436: + load_kwargs["attn_implementation"] = "eager" + else: + # Either no quantization, or torchao — which quantizes the model *after* load, not + # via transformers' BitsAndBytesConfig. Load in float32 by default to avoid CUBLAS + # errors with small hidden sizes and to ensure numerical stability during training + # without mixed-precision. + load_kwargs["dtype"] = torch.float32 + + config_modified = False + if config_obj.model_parameters: + # Add any model specific parameters to the load kwargs + for param_name, param_value in config_obj.model_parameters.to_dict().items(): + # Not all parameters are supported by all models, so we only add the parameter to the load kwargs + # if it is supported by the model. + if param_value is None: + continue + + if hasattr(model_config, param_name): + if isinstance(param_value, dict): + # For nested dict params (e.g. rope_scaling), merge with existing + # config values to preserve defaults like rope_theta. + existing = getattr(model_config, param_name, {}) or {} + existing.update(param_value) + setattr(model_config, param_name, existing) + config_modified = True + else: + load_kwargs[param_name] = param_value + else: + logger.warning(f"Parameter {param_name} is not supported by {config_obj.base_model}. Skipping.") + + # Only pass config= when we've directly modified it (e.g. rope_scaling merge). + if config_modified: + load_kwargs["config"] = model_config + + logger.info("Loading large language model...") + pretrained_model_name_or_path = weights_save_path or config_obj.base_model + trust_remote_code = getattr(config_obj, "trust_remote_code", False) + is_multimodal = getattr(config_obj, "is_multimodal", False) + if is_multimodal: + # VLMs — Qwen2-VL, LLaVA, InternVL, Idefics etc. — register their own vision-to-sequence + # head. AutoModelForVision2Seq picks the right class automatically so the vision tower, + # projector, and LM head all come along together. + from transformers import AutoModelForVision2Seq + + logger.info("Loading multimodal (VLM) base model via AutoModelForVision2Seq") + model: PreTrainedModel = AutoModelForVision2Seq.from_pretrained( + pretrained_model_name_or_path, trust_remote_code=trust_remote_code, **load_kwargs + ) + else: + model: PreTrainedModel = AutoModelForCausalLM.from_pretrained( + pretrained_model_name_or_path, trust_remote_code=trust_remote_code, **load_kwargs + ) + return model + + +def to_device( + model: PreTrainedModel, + device: str | torch.DeviceObjType, + config_obj: LLMModelConfig, + curr_device: torch.DeviceObjType, +) -> tuple[PreTrainedModel, torch.DeviceObjType]: + """Move an LLM to the requested device, accounting for sharding and adapters. + + Args: + model: Pretrained model to put on device + config_obj: LLM config + curr_device: The current device that the model is on + + Returns: + `model` moved to `device` + """ + device = torch.device(device) + + if device.type == curr_device.type: + log_once(f"Model already on device'{device}'.") + return model, device + else: + log_once(f"Moving LLM from '{curr_device}' to '{device}'.") + + model_kwargs = {} + num_gpus = torch.cuda.device_count() + if device == torch.device("cuda") and num_gpus > 1: + # TODO: make this configurable in the future. These parameters are from FastChat: + # https://github.com/lm-sys/FastChat/blob/0e958b852a14f4bef5f0e9d7a5e7373477329cf2/fastchat/serve/inference.py#L90 + # TODO: Wrap device_map="auto" in a try-except block since it may not be supported for all models (E.g. BertLMHead) + # We don't add quantization here (float16 or bfloat16) since we may not always want to quantize. We should + # make quantization configurable in the future via the trainer config. + model_kwargs.update( + { + "low_cpu_mem_usage": True, + "max_memory": dict.fromkeys(range(num_gpus), "13GiB"), + } + ) + + if config_obj.base_model not in _MODELS_WITH_DEVICE_MAP_AUTO_EXCLUSION: + model_kwargs["device_map"] = "auto" + + if config_obj.quantization: + model_kwargs["quantization_config"] = config_obj.quantization.to_bitsandbytes() + + # we save and reload the weights to ensure that they can be sharded across the GPUs using `from_pretrained` + with tempfile.TemporaryDirectory() as tmpdir: + model.save_pretrained(tmpdir) + + if config_obj.adapter: + model = AutoModelForCausalLM.from_pretrained( + config_obj.base_model, + trust_remote_code=getattr(config_obj, "trust_remote_code", False), + **model_kwargs, + ) + + # Leave this import inline to support a minimal install of Ludwig + from peft import PeftModel + + model = PeftModel.from_pretrained( + model, + tmpdir, + torch_dtype=torch.float16, + ) + else: + model = AutoModelForCausalLM.from_pretrained( + tmpdir, + trust_remote_code=getattr(config_obj, "trust_remote_code", False), + **model_kwargs, + ) + else: + model = model.to(device) + + return model, device + + +def _load_peft_config(pretrained_adapter_weights: str): + """Load a PeftConfig, fixing known compatibility issues with newer PEFT versions.""" + import json + + from huggingface_hub import hf_hub_download + from peft import PeftConfig + + config_file = hf_hub_download(pretrained_adapter_weights, "adapter_config.json") + with open(config_file) as f: + config_dict = json.load(f) + + # AdaLoRA requires total_step > 0 in newer PEFT versions, but pretrained + # configs may have total_step=None. + if config_dict.get("peft_type") == "ADALORA" and not config_dict.get("total_step"): + config_dict["total_step"] = 10000 + + return PeftConfig.from_peft_type(**config_dict) + + +def initialize_adapter(model: PreTrainedModel, config_obj: "LLMModelConfig") -> Union["PeftModel", PreTrainedModel]: # noqa F821 + """Wrap a pretrained model with a PEFT model for fine-tuning. + + Dispatches to the multi-adapter path when ``config_obj.adapters`` is set (several + named adapters registered on the same base, optional weighted merge, runtime + switching via ``set_adapter``) and to the single-adapter path for ``config_obj.adapter``. + The two fields are mutually exclusive at the schema layer. + + Args: + model: Pretrained model to fine-tune with an adapter. + config_obj: LLM config + + Returns: + ``model`` wrapped in a PEFT model if an adapter config was provided, otherwise + ``model`` unaltered. + """ + if getattr(config_obj, "adapters", None) is not None: + return _initialize_multi_adapters(model, config_obj) + + if config_obj.adapter: + if config_obj.adapter.pretrained_adapter_weights: + # Load pretrained adapter weights if specified. + logger.info(f"Using pretrained adapter weights: {config_obj.adapter.pretrained_adapter_weights}") + + # Leave this import inline to support a minimal install of Ludwig + from peft import MODEL_TYPE_TO_PEFT_MODEL_MAPPING, PeftConfig # noqa + + peft_config = _load_peft_config(config_obj.adapter.pretrained_adapter_weights) + + model = MODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type].from_pretrained( + model, config_obj.adapter.pretrained_adapter_weights, config=peft_config + ) + else: + # Leave this import inline to support a minimal install of Ludwig + from peft import get_peft_model, TaskType + + # If no pretrained adapter is provided, we want to load untrained weights into the model + peft_config = config_obj.adapter.to_config( + task_type=TaskType.CAUSAL_LM, tokenizer_name_or_path=config_obj.base_model + ) + + model = get_peft_model(model, peft_config) + + return model + + +def _initialize_multi_adapters(model: PreTrainedModel, config_obj: "LLMModelConfig") -> "PeftModel": # noqa F821 + """Attach several named PEFT adapters to ``model`` and (optionally) a merged one. + + PEFT's public multi-adapter surface: + * ``get_peft_model(base, cfg, adapter_name=...)`` creates a PeftModel with one named + adapter. We use the first configured adapter here as the anchor. + * ``peft_model.add_adapter(adapter_name, cfg)`` registers additional adapters on the + same PeftModel. + * ``peft_model.add_weighted_adapter(source_names, weights, name, combination_type, + density)`` produces a new adapter by combining existing ones. Used when + ``adapters.merge`` is set. + * ``peft_model.set_adapter(name)`` designates the default adapter. Only one adapter + is active at a time — users switch explicitly at inference, or ask for the merged + adapter as the default. + """ + from peft import get_peft_model, TaskType # imported inline for minimal installs + + adapters_cfg = config_obj.adapters + items = list(adapters_cfg.adapters.items()) # insertion order, validated non-empty by schema + first_name, first_cfg = items[0] + + if not hasattr(first_cfg, "to_config"): + # The schema stores entries as raw dicts; re-materialize them into adapter configs via + # the adapter registry so each has a working `to_config()` method. + items = [(name, _materialize_adapter_config(cfg)) for name, cfg in items] + first_name, first_cfg = items[0] + + first_peft_config = first_cfg.to_config(task_type=TaskType.CAUSAL_LM, tokenizer_name_or_path=config_obj.base_model) + model = get_peft_model(model, first_peft_config, adapter_name=first_name) + + for name, adapter_cfg in items[1:]: + peft_config = adapter_cfg.to_config(task_type=TaskType.CAUSAL_LM, tokenizer_name_or_path=config_obj.base_model) + model.add_adapter(name, peft_config) + + if adapters_cfg.merge is not None: + merge = adapters_cfg.merge + weights = merge.weights if merge.weights is not None else [1.0] * len(merge.sources) + kwargs = { + "adapters": merge.sources, + "weights": weights, + "adapter_name": merge.name, + "combination_type": merge.combination_type, + } + if merge.combination_type in ("ties", "dare_linear", "dare_ties", "magnitude_prune"): + kwargs["density"] = merge.density + model.add_weighted_adapter(**kwargs) + logger.info( + "Merged adapters %s (weights=%s) via %s into %r", + merge.sources, + weights, + merge.combination_type, + merge.name, + ) + + active = adapters_cfg.active or first_name + model.set_adapter(active) + logger.info("Registered adapters: %s (active=%s)", [n for n, _ in items], active) + + return model + + +def _materialize_adapter_config(raw): + """Turn a raw dict from ``adapters.adapters`` into a BaseAdapterConfig instance.""" + from ludwig.schema.llms.peft import adapter_registry + + if hasattr(raw, "to_config"): + return raw + if not isinstance(raw, dict): + raise TypeError(f"Expected dict adapter config, got {type(raw).__name__}") + adapter_type = raw.get("type") + if adapter_type is None: + raise ValueError("Adapter config is missing required `type` field.") + if adapter_type not in adapter_registry: + raise ValueError(f"Unknown adapter type {adapter_type!r}. Known: {list(adapter_registry.keys())}") + return adapter_registry[adapter_type].model_validate(raw) + + +def get_context_len(model_config: AutoConfig): + """Determines the maximum length of the context (input + output tokens) based on the provided model + configuration. + + Args: + model_config (AutoConfig): The model configuration object containing information about the model's properties. + + Returns: + int: The maximum context length, which can be derived from the model configuration. If no relevant attribute + is found, the default value of 2048 is returned. + + This function examines the provided model configuration object to identify the attribute that specifies the maximum + context length. It checks for attributes in the following order of preference: + 1. 'max_sequence_length': If this attribute is present in the model configuration, its value is returned. + 2. 'max_position_embeddings': If 'max_sequence_length' is not found but 'max_position_embeddings' is present, its + value is returned. + 3. 'n_positions': If neither 'max_sequence_length' nor 'max_position_embeddings' are found, and 'n_positions' is + present, its value is returned. + 4. Default: If none of the relevant attributes are present, the function returns a default value of 2048. + + Note: + - The maximum context length is important for defining the size of input and output sequences in a model. + + Example Usage: + >>> config = AutoConfig.from_pretrained("bert-base-uncased") + >>> context_len = get_context_len(config) + >>> print(context_len) + 512 + """ + if hasattr(model_config, "max_sequence_length"): + return model_config.max_sequence_length + elif hasattr(model_config, "max_position_embeddings"): + return model_config.max_position_embeddings + elif hasattr(model_config, "n_positions"): + return model_config.n_positions + else: + return FALLBACK_CONTEXT_LEN + + +def has_padding_token(input_tensor: torch.Tensor, tokenizer: PreTrainedTokenizer): + """Checks if the input tensor contains any padding tokens. + + Args: + input_tensor (torch.Tensor): The input tensor. + tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input. + + Returns: + bool: True if the input tensor contains any padding tokens, False otherwise. + + Example: + >>> import torch + >>> from transformers import PreTrainedTokenizer + >>> tokenizer = PreTrainedTokenizer.from_pretrained('bert-base-uncased') + >>> input_sentence = "This is an example sentence." + >>> input_ids = tokenizer.encode(input_sentence, add_special_tokens=True) + >>> padded_input_ids = torch.nn.functional.pad(input_ids, (0, 10 - len(input_ids))) + >>> has_padding = has_padding_token(padded_input_ids, tokenizer) + >>> has_padding + True + """ + if input_tensor.dim() == 1: + return torch.any(input_tensor == tokenizer.pad_token_id).item() + elif input_tensor.dim() == 2: + return torch.any(input_tensor == tokenizer.pad_token_id, dim=-1).item() + else: + raise ValueError( + f"Input tensor must be 1D (single sequence) or 2D (batch of sequences), got {input_tensor.dim()}D tensor." + ) + + +def remove_left_padding(input_ids_sample: torch.Tensor, tokenizer: PreTrainedTokenizer): + """Removes left padding and other tokens until the first BOS token from the input_ids tensor. + + Args: + input_ids_sample (torch.Tensor): The input tensor with padding and other tokens. + tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input. + + Returns: + torch.Tensor: The input tensor without left padding and other tokens until the first BOS token. + + Example: + >>> import torch + >>> from transformers import PreTrainedTokenizer + >>> tokenizer = PreTrainedTokenizer.from_pretrained('bert-base-uncased') + >>> input_sentence = "This is an example sentence." + >>> input_ids = tokenizer.encode(input_sentence, add_special_tokens=True) + >>> padded_input_ids = torch.nn.functional.pad(input_ids, (10 - len(input_ids), 0)) + >>> input_ids_no_padding = remove_left_padding(padded_input_ids, tokenizer) + >>> input_ids_no_padding + tensor([[1, 2, 3]]) + """ + # Remove all PAD tokens + pad_idxs = torch.where(input_ids_sample == tokenizer.pad_token_id)[0] # all PAD token locations + input_ids_no_padding = input_ids_sample + if len(pad_idxs) != 0: + pad_idx = pad_idxs[-1] # get last PAD token location + input_ids_no_padding = input_ids_sample[pad_idx + 1 :] + + # Start from the first BOS token + bos_idxs = torch.where(input_ids_no_padding == tokenizer.bos_token_id)[0] # all BOS token locations + if len(bos_idxs) != 0: + bos_idx = bos_idxs[0] # get first BOS token location + else: + bos_idx = 0 + + input_ids_no_bos = input_ids_no_padding[bos_idx:].unsqueeze(0) + return input_ids_no_bos + + +def add_left_padding(input_ids, max_length, pad_value=0): + """Adds left padding to the input_ids tensor. + + Args: + input_ids (torch.Tensor): The input tensor. + max_length (int): The maximum length of the tensor after padding. + pad_value (int, optional): The value used for padding. Defaults to 0. + + Returns: + torch.Tensor: The input_ids tensor with left padding. + + Example: + >>> input_ids = torch.tensor([1, 2, 3]) + >>> max_length = 5 + >>> padded_tensor = add_left_padding(input_ids, max_length) + >>> padded_tensor + tensor([0, 0, 1, 2, 3]) + """ + padding = torch.tensor([pad_value] * (max_length - input_ids.shape[0]), dtype=torch.int64, device=input_ids.device) + return torch.cat((padding, input_ids), dim=-1) + + +def create_attention_mask(input_ids: torch.Tensor, tokenizer: PreTrainedTokenizer): + """Creates an attention mask for the input_ids tensor. This also sets the last padding token ID to 1 if it + exists. + + Args: + input_ids (torch.Tensor): The input tensor. + tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input. + + Returns: + torch.Tensor: The attention mask tensor. + + Example: + >>> import torch # noqa + >>> from transformers import PreTrainedTokenizer + >>> tokenizer = PreTrainedTokenizer.from_pretrained('bert-base-uncased') + >>> input_sentence = "This is an example sentence." + >>> input_ids = tokenizer.encode(input_sentence, add_special_tokens=True) + >>> attention_mask = create_attention_mask(input_ids, tokenizer) + >>> attention_mask + tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + """ + attention_mask = input_ids != tokenizer.pad_token_id + # Last token may not be padding if we've already hit the max sequence length + if not attention_mask[-1]: + # last token is padding, always attended to even if it is padding + attention_mask[-1] = 1 + attention_mask = attention_mask.to(torch.int64) + return attention_mask + + +def find_last_matching_index(tensor_a: torch.Tensor, tensor_b: torch.Tensor): + """Returns the last index of `tensor_a` that matches `tensor_b`. Specifically, this checks whether the tensor_b + is in the last tensor_b.shape[0] elements of tensor_a. + + Args: + tensor_a (torch.Tensor): The first tensor. + tensor_b (torch.Tensor): The second tensor. + + Returns: + int: The last index of `tensor_a` that matches `tensor_b`. Returns -1 if there is no matching index. + + Example: + >>> import torch + >>> tensor_a = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]) + >>> tensor_b = torch.tensor([6, 7, 8]) + >>> last_matching_index = find_last_matching_index(tensor_a, tensor_b) + >>> last_matching_index + 5 + """ + last_index = -1 + + tensor_a_length = tensor_a.shape[0] + tensor_b_length = tensor_b.shape[0] + + # Get the last tensor_b_length elements of tensor_a. + tensor_a_truncated = tensor_a[-tensor_b_length:] + + # Find the last matching index. + for i in range(tensor_b_length): + if torch.equal(tensor_a_truncated[i:], tensor_b[: tensor_b_length - i]): + last_index = tensor_a_length - tensor_b_length + i + break + + return last_index + + +def pad_target_tensor_for_fine_tuning( + targets: dict[str, torch.Tensor], + predictions: dict[str, torch.Tensor], + model_inputs: torch.Tensor, + of_name: str, +) -> dict[str, torch.Tensor]: + """Pad and adjust target tensors for fine-tuning LLMS models. + + This function is used to pad and adjust the target tensors with IGNORE_INDEX_TOKEN_ID based on the model inputs and + predictions during the fine-tuning process of Language Models. Here's what this function does: + 1. If none of the tokens from the target were in the model inputs, we create a tensor of the length of model + inputs with value IGNORE_INDEX_TOKEN_IDs. This ignores this row from affecting loss. + 2. If the target tokens were entirely inside the model inputs, we want to pad all the tokens in model_inputs + coming from the input with IGNORE_INDEX_TOKEN_IDs and leave the target tokens as is. This ensures that all + of the target tokens are used during loss computation. + 3. In the scenario that only some part of the target tokens were in the model inputs, we want to pad the model + inputs until that point and only leave the partial tokens of the target as is. This ensures that we will + only compute loss on the target tokens that were in the model inputs. + + Args: + targets (Dict[str, torch.Tensor]): A dictionary containing the target tensors. + predictions (Dict[str, torch.Tensor]): A dictionary containing the predicted tensors. + model_inputs (torch.Tensor): The input tensor passed into the model's forward pass. + of_name (str): The name of the target tensor to be padded and adjusted. + + Returns: + Dict[str, torch.Tensor]: A dictionary containing the updated target + dictionaries. + """ + target_length = targets.get(of_name).size()[1] + prediction_length = predictions[of_name].get(PREDICTIONS).size()[1] + + if target_length == prediction_length: + return targets + + updated_targets = [] + for idx, target in enumerate(targets[of_name]): + # Remove any leading IGNORE_INDEX_TOKEN_IDs in the target that were temporarily added for alignment + end_index = (target != IGNORE_INDEX_TOKEN_ID).nonzero()[0] + target = target[end_index:] + target_device = target.device + + # See if any part of the target was in the tensor passed into the model's forward pass + last_matching_index = find_last_matching_index(model_inputs[idx], target) + + # If the last matching index is -1, it means that the input tensor passed into the model was truncated + # and did not contain the target tensor. In this case, we need to truncate the target tensors as well + # and just set it to a tensor of IGNORE_INDEX_TOKEN_ID so that we don't compute loss on this target tensor. + if last_matching_index == -1: + updated_targets.append(torch.full((prediction_length,), IGNORE_INDEX_TOKEN_ID).to(device=target_device)) + + # If the last matching index is not -1, it means that the input tensor passed into the model was not + # truncated and contained either a part of the target tensor or the entire target tensor. In this case, + # we need to set the target tensor to the part of the target tensor that was passed into the model while + # also padding it to the correct length with IGNORE_INDEX_TOKEN_ID. + else: + padding = torch.full((last_matching_index,), IGNORE_INDEX_TOKEN_ID).to(device=target_device) + updated_targets.append(torch.cat((padding, target), dim=-1)[:prediction_length]) + + targets[of_name] = torch.stack(updated_targets).to(device=targets.get(of_name).device, dtype=torch.int64) + + return targets + + +def generate_merged_ids( + input_ids: torch.tensor, + target_ids: torch.tensor, + tokenizer: PreTrainedTokenizer, + max_sequence_length: int | None = None, +): + """Generate merged input and target IDs tensor. + + This function merges the input_ids and target_ids together to create a unified tensor + to pass into the model. It also returns attention masks for the merged tensors. + + Args: + input_ids (torch.Tensor): The input IDs tensor. + target_ids (torch.Tensor or None): The target IDs tensor or None. + max_sequence_length (int or None): The maximum sequence length to pad or truncate to. + tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input_ids and target_ids. + + Returns: + torch.Tensor: The merged input and target IDs tensor. + torch.Tensor: The attention masks for the merged tensor. + """ + merged_input_and_targets = [] + lengths = [] + + eos_tensor = torch.tensor([tokenizer.eos_token_id]).to(target_ids[0].device) + + # Merge input_ids and target_ids by concatenating them together. + # We remove the left padding from both input_ids and target_ids before concatenating them. + for input_id_sample, target_id_sample in zip(input_ids, target_ids): + input_id_sample_no_padding = remove_left_padding(input_id_sample, tokenizer)[0] + target_id_sample_no_padding = remove_left_padding(target_id_sample, tokenizer)[0] + target_id_sample_no_padding = torch.cat((target_id_sample_no_padding, eos_tensor), dim=-1) + + merged_sample_ids = torch.cat((input_id_sample_no_padding, target_id_sample_no_padding), dim=-1) + # If the merged tensor is longer than the maximum sequence length, we truncate it. + if max_sequence_length and merged_sample_ids.shape[0] > max_sequence_length: + merged_sample_ids = merged_sample_ids[:max_sequence_length] + + merged_input_and_targets.append(merged_sample_ids) + lengths.append(merged_sample_ids.shape[0]) + + # Since we remove the left padding from the target_ids, the merged input_ids and target_ids + # may not have the same lengths. We need to align them to the same length by adding left padding + # and generate an attention mask for just the part of the input that is not padding. + max_length = max(lengths) + attention_masks = [] + for i, merged_sample_ids in enumerate(merged_input_and_targets): + merged_input_and_targets[i] = add_left_padding(merged_sample_ids, max_length) + attention_masks.append(create_attention_mask(merged_input_and_targets[i], tokenizer)) + + return torch.stack(merged_input_and_targets), torch.stack(attention_masks) + + +def _get_decoded_targets_and_predictions( + targets: dict[str, torch.Tensor], + predictions: dict[str, dict[str, torch.Tensor]], + tokenizer: PreTrainedTokenizer, + of_name: str, +): + """Returns the decoded targets and predictions, accounting for IGNORE_INDEX_TOKEN_ID.""" + target_tensor = targets[of_name] + pred_tensor = predictions[of_name][PREDICTIONS] + # Ensure targets and predictions are on the same device + if target_tensor.device != pred_tensor.device: + target_tensor = target_tensor.to(pred_tensor.device) + sanitized_targets = torch.where(target_tensor != IGNORE_INDEX_TOKEN_ID, target_tensor, tokenizer.pad_token_id) + sanitized_predictions = torch.where( + pred_tensor != IGNORE_INDEX_TOKEN_ID, + pred_tensor, + tokenizer.pad_token_id, + ) + decoded_targets = tokenizer.batch_decode(sanitized_targets, skip_special_tokens=True) + decoded_predictions = tokenizer.batch_decode(sanitized_predictions, skip_special_tokens=True) + return decoded_targets, decoded_predictions + + +def get_realigned_target_and_prediction_tensors_for_inference( + targets: dict[str, torch.Tensor], + predictions: dict[str, dict[str, torch.Tensor]], + of_name: str, + tokenizer: PreTrainedTokenizer, + pad_value: int | None = None, +) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]: + """Realigns the target tensor with the predictions. + + This is necessary for text metrics that require the target and prediction to be of the same length. + + Args: + targets: The target tensor. + predictions: The prediction tensor. + of_name: The output feature's name. + tokenizer: The HF tokenizer. + pad_direction: The direction to pad the tensors. Can be 'left' or 'right'. + Defaults to 'right'. + + Returns: + Tuple of realigned (targets, decoded_targets, predictions, decoded_predictions). + - targets is a map of feature name -> tensor of token ids. + - predictions is a map from output feature name -> map of tensors with the following items: + - "predictions": tensor of token ids. + - "probabilities": tensor of probabilities. + - "logits": tensor of logits. + """ + target_length = targets.get(of_name).size()[1] + prediction_length = predictions[of_name].get(PREDICTIONS).size()[1] + + if target_length == prediction_length: + return targets, predictions + + if not pad_value: + pad_value = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id + + zeros_to_add = ( + target_length - prediction_length if target_length > prediction_length else prediction_length - target_length + ) + + # We don't want to modify the original targets and predictions tensors, so we create a copy of them. + _targets = copy.deepcopy(targets) + _predictions = copy.deepcopy(predictions) + + # Align target and prediction tensors for text to text metric computation + if target_length > prediction_length: + # Pad the predictions. + _predictions[of_name][PREDICTIONS] = F.pad( + _predictions[of_name][PREDICTIONS], (0, zeros_to_add), value=pad_value + ).to(torch.int64) + + _predictions[of_name][PROBABILITIES] = F.pad(_predictions[of_name][PROBABILITIES], (0, 0, 0, zeros_to_add)).to( + torch.float32 + ) + + _predictions[of_name][LOGITS] = F.pad(_predictions[of_name][LOGITS], (0, 0, 0, zeros_to_add)).to(torch.float32) + else: + _targets[of_name] = F.pad(_targets[of_name], (0, zeros_to_add), value=pad_value).to(torch.int64) + + return _targets, _predictions + + +def update_embedding_layer(model: AutoModelForCausalLM, config_obj: LLMTrainerConfig) -> AutoModelForCausalLM: + """Updates the embedding layer of the model to use the 8-bit embedding layer from bitsandbytes.nn.modules. + + This is necessary when using 8-bit optimizers from bitsandbytes. + See: https://github.com/TimDettmers/bitsandbytes#tldr + """ + # If we're using an 8-bit optimizer, we need to replace the embedding layer with a custom embedding layer from + # bnb.nn.modules.Embedding. + if hasattr(config_obj, "optimizer") and config_obj.optimizer.is_8bit: + embedding_layer, module_path = find_embedding_layer_with_path(model) + if embedding_layer is None: + raise ValueError( + "Could not find an embedding layer in the model. This is required when using 8-bit optimizers" + " since a custom 8-bit embedding layer is used in place of the original embedding layer." + ) + + # Initialize the BNB embedding layer with the same parameters and weights as the original embedding layer. + bnb_embedding = BnbEmbedding( + num_embeddings=embedding_layer.num_embeddings, + embedding_dim=embedding_layer.embedding_dim, + padding_idx=embedding_layer.padding_idx, + max_norm=embedding_layer.max_norm, + norm_type=embedding_layer.norm_type, + scale_grad_by_freq=embedding_layer.scale_grad_by_freq, + sparse=embedding_layer.sparse, + _weight=embedding_layer.weight, + device=model.device, + ) + + # Update the model's original embedding layer to use the BNB embedding layer using the module_path + # returned by find_embedding_layer_with_path. + module_path = module_path.split(".") + module = model + for module_name in module_path[:-1]: + module = getattr(module, module_name) + setattr(module, module_path[-1], bnb_embedding) + + # Set the get input embeddings lambda function to return the BNB embedding layer + model.get_input_embeddings = lambda: bnb_embedding + + logger.info("Updated the pretrained embedding layer to use the embedding layer from bitsandbytes.") + + return model + + +def create_text_streamer(tokenizer: PreTrainedTokenizer) -> TextStreamer: + """Creates a TextStreamer object for streaming text to stdout during generation.""" + return TextStreamer(tokenizer=tokenizer, skip_prompt=True) + + +def generate_merged_ids_packed( + input_ids: torch.tensor, + target_ids: torch.tensor, + tokenizer, + max_sequence_length: int | None = None, + max_sequences_per_pack: int = 8, +): + """Generate merged IDs with sequence packing for throughput improvement. + + Instead of padding each sequence to the same length, packs multiple short + sequences into a single batch entry with block-diagonal attention masks + to prevent cross-sequence attention. + + Args: + input_ids: [batch, input_len] token IDs for prompts + target_ids: [batch, target_len] token IDs for completions + tokenizer: HuggingFace tokenizer + max_sequence_length: Maximum pack length + max_sequences_per_pack: Maximum sequences per pack + + Returns: + packed_ids: [num_packs, max_seq_len] packed token IDs + packed_attention_mask: [num_packs, max_seq_len, max_seq_len] block-diagonal attention + """ + from ludwig.utils.sequence_packing import pack_sequences + + # First, merge each input+target pair into a single sequence (without padding) + merged_sequences = [] + eos_tensor = torch.tensor([tokenizer.eos_token_id]).to(target_ids[0].device) + pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0 + + for input_id_sample, target_id_sample in zip(input_ids, target_ids): + input_no_pad = remove_left_padding(input_id_sample, tokenizer)[0] + target_no_pad = remove_left_padding(target_id_sample, tokenizer)[0] + target_no_pad = torch.cat((target_no_pad, eos_tensor), dim=-1) + merged = torch.cat((input_no_pad, target_no_pad), dim=-1) + if max_sequence_length and merged.shape[0] > max_sequence_length: + merged = merged[:max_sequence_length] + merged_sequences.append(merged) + + # Create dummy attention masks (all ones, actual masking done by block-diagonal) + attention_masks = [torch.ones(seq.shape[0]) for seq in merged_sequences] + + # Pack sequences using greedy bin packing + packed_ids, packed_attn, _, _ = pack_sequences( + merged_sequences, + attention_masks, + max_length=max_sequence_length or max(s.shape[0] for s in merged_sequences), + pad_token_id=pad_token_id, + max_sequences_per_pack=max_sequences_per_pack, + ) + + return packed_ids, packed_attn diff --git a/ludwig/utils/logging_utils.py b/ludwig/utils/logging_utils.py new file mode 100644 index 0000000..f5bfa43 --- /dev/null +++ b/ludwig/utils/logging_utils.py @@ -0,0 +1,15 @@ +_logged = set() + + +def log_once(key: str) -> bool: + """Returns True if this is the "first" call for a given key. + + Example: + if log_once("some_key"): + logger.info("Some verbose logging statement") # noqa + """ + + if key not in _logged: + _logged.add(key) + return True + return False diff --git a/ludwig/utils/loss_utils.py b/ludwig/utils/loss_utils.py new file mode 100644 index 0000000..3dd0946 --- /dev/null +++ b/ludwig/utils/loss_utils.py @@ -0,0 +1,23 @@ +import torch + + +def rmspe_loss(targets: torch.Tensor, predictions: torch.Tensor) -> torch.Tensor: + """Root mean square percentage error. + + Bad predictions can lead to arbitrarily large RMSPE values, especially if some values of targets are very close to + zero. We return a large value instead of inf when (some) targets are zero. + """ + epsilon = 1e-4 + # add epsilon if targets are zero to avoid division by zero + denominator = targets + epsilon * (targets == 0).float() + loss = torch.sqrt(torch.mean(((targets - predictions).float() / denominator) ** 2)) + return loss + + +def mean_confidence_penalty(probabilities: torch.Tensor, num_classes: int) -> torch.Tensor: + max_entropy = torch.log(torch.tensor(num_classes)) + # clipping needed for avoiding log(0) = -inf + entropy_per_class, _ = torch.max(-probabilities * torch.log(torch.clamp(probabilities, 1e-10, 1)), dim=0) + entropy = torch.sum(entropy_per_class, -1) + penalty = (max_entropy - entropy) / max_entropy + return torch.mean(penalty) diff --git a/ludwig/utils/math_utils.py b/ludwig/utils/math_utils.py new file mode 100644 index 0000000..44972f7 --- /dev/null +++ b/ludwig/utils/math_utils.py @@ -0,0 +1,65 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import math + +import numpy as np + + +def softmax(x, temperature=1.0): + e_x = np.exp((x - np.max(x)) / temperature) + return e_x / e_x.sum() + + +def int_type(number): + if number <= np.iinfo(np.int8).max: + return np.int8 + elif number <= np.iinfo(np.int16).max: + return np.int16 + elif number <= np.iinfo(np.int32).max: + return np.int32 + else: # if number <= np.iinfo(np.int64).max: + return np.int64 + + +def convert_size(size_bytes): + if size_bytes == 0: + return "0B" + size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + i = int(math.floor(math.log(size_bytes, 1024))) + p = math.pow(1024, i) + s = round(size_bytes / p, 2) + return f"{s} {size_name[i]}" + + +def round2precision(val, precision: int = 0, which: str = ""): + if precision < 0: + raise ValueError(f"precision must be non-negative, got {precision}") + val *= 10**precision + round_callback = round + if which.lower() == "up": + round_callback = math.ceil + if which.lower() == "down": + round_callback = math.floor + return "{1:.{0}f}".format(precision, round_callback(val) / 10**precision) + + +def cumsum(x: list[int]) -> list[int]: + results = [] + j = 0 + for i in range(0, len(x)): + j += x[i] + results.append(j) + return results diff --git a/ludwig/utils/metric_utils.py b/ludwig/utils/metric_utils.py new file mode 100644 index 0000000..ea8a54c --- /dev/null +++ b/ludwig/utils/metric_utils.py @@ -0,0 +1,125 @@ +from collections import defaultdict, namedtuple + +import torch +from torch import Tensor +from torchmetrics.metric import Metric + +from ludwig.constants import COMBINED, LOSS, NAME, TYPE +from ludwig.modules.metric_registry import get_metric_names_for_type +from ludwig.types import FeatureConfigDict + + +def sequence_mask(lengths: Tensor, maxlen: int | None = None, dtype=torch.bool) -> Tensor: + """Implements tf.sequence_mask in torch. + + From https://discuss.pytorch.org/t/pytorch-equivalent-for-tf-sequence-mask/39036/2. + """ + if maxlen is None: + maxlen = lengths.max() + row_vector = torch.arange(0, maxlen, 1).to(lengths.device) + matrix = torch.unsqueeze(lengths, dim=-1) + mask = row_vector < matrix + + return mask.type(dtype) + + +def dynamic_partition(data: Tensor, partitions: Tensor, num_partitions: int) -> list[Tensor]: + """Implements tf.dynamic_partition in torch. + + From https://discuss.pytorch.org/t/equivalent-of-tf-dynamic-partition/53735. + """ + if data.size() != partitions.size(): + raise ValueError( + f"data and partitions must have the same size, got data={data.size()} partitions={partitions.size()}." + ) + + # Flatten data into 1D vectors to do partitioning correctly. + data = data.view(-1) + partitions = partitions.view(-1) + result = [] + for i in range(num_partitions): + result += [data[(partitions == i).nonzero().squeeze(1)]] + return result + + +def masked_correct_predictions(targets: Tensor, preds: Tensor, targets_sequence_lengths: Tensor) -> Tensor: + """Masks out special symbols, and returns tensor of correct predictions. + + Args: + targets: 2D tensor [batch_size, sequence_length] + preds: 2D tensor [batch_size, sequence_length] + + Returns: + 1D tensor of all correct predictions. + """ + correct_preds = preds == targets + + mask = sequence_mask(lengths=targets_sequence_lengths, maxlen=correct_preds.shape[1], dtype=torch.int32) + _, masked_correct_preds = dynamic_partition(data=correct_preds, partitions=mask, num_partitions=2) + + return masked_correct_preds.type(torch.float32) + + +def get_scalar_from_ludwig_metric(metric: Metric) -> float: + """Returns the scalar value of a Ludwig metric. + + Params: + metric: Metric object + + Returns: + float: scalar value of the metric + """ + return metric.compute().detach().cpu().numpy().item() + + +# Data for training and evaluation metrics. +TrainerMetric = namedtuple("TrainerMetric", ("epoch", "step", "value")) + + +def reduce_trainer_metrics_dict( + dict_dict_trainer_metrics: dict[str, dict[str, list[TrainerMetric]]], +) -> dict[str, dict[str, list[float]]]: + """Reduces Dict[feature_name, Dict[metric_name, List[TrainerMetric]]] to Dict[feature_name, Dict[metric_name, + List[float]]]. + + Used for flattening the results returned by trainer.py::train(), which come from ProgressTracker. + """ + flattened_dict = defaultdict(lambda: defaultdict(list)) + for feature_name, trainer_metric_dict in dict_dict_trainer_metrics.items(): + for metric_name, trainer_metrics in trainer_metric_dict.items(): + for trainer_metric in trainer_metrics: + flattened_dict[feature_name][metric_name].append(trainer_metric[-1]) + # Convert defaultdict to dict so JSON serialization works with dataclasses.asdict(). + return {k: dict(v) for k, v in flattened_dict.items()} + + +def get_metric_names(output_features: dict[str, "OutputFeature"]) -> dict[str, list[str]]: # noqa + """Returns a dict of output_feature_name -> list of metric names.""" + metrics_names = {} + for output_feature_name, output_feature in output_features.items(): + metrics_names[output_feature_name] = sorted(list(get_metric_names_for_type(output_feature.type()))) + # Add combined loss. + metrics_names[COMBINED] = [LOSS] + return metrics_names + + +def get_feature_to_metric_names_map(output_features: list[FeatureConfigDict]) -> dict[str, list[str]]: + """Returns a dict of output_feature_name -> list of metric names.""" + metrics_names = {} + for output_feature in output_features: + output_feature_name = output_feature[NAME] + output_feature_type = output_feature[TYPE] + metrics_names[output_feature_name] = get_metric_names_for_type(output_feature_type) + metrics_names[COMBINED] = [LOSS] + return metrics_names + + +def get_feature_to_metric_names_map_from_feature_collection( + output_features: "FeatureCollection", # noqa +) -> dict[str, list[str]]: + """Returns a dict of output_feature_name -> list of metric names.""" + metrics_names = { + output_feature.name: get_metric_names_for_type(output_feature.type) for output_feature in output_features + } + metrics_names[COMBINED] = [LOSS] + return metrics_names diff --git a/ludwig/utils/metrics_printed_table.py b/ludwig/utils/metrics_printed_table.py new file mode 100644 index 0000000..6fd7260 --- /dev/null +++ b/ludwig/utils/metrics_printed_table.py @@ -0,0 +1,98 @@ +import logging + +from tabulate import tabulate + +from ludwig.constants import COMBINED, LOSS +from ludwig.utils.metric_utils import TrainerMetric + +logger = logging.getLogger(__name__) + + +def get_metric_value_or_empty(metrics_log: dict[str, list[TrainerMetric]], metric_name: str): + """Returns the metric value if it exists or empty.""" + if metric_name not in metrics_log: + return "" + return metrics_log[metric_name][-1][-1] + + +def print_table_for_single_output_feature( + train_metrics_log: dict[str, list[TrainerMetric]], + validation_metrics_log: dict[str, list[TrainerMetric]], + test_metrics_log: dict[str, list[TrainerMetric]], + combined_loss_for_each_split: list[float], +) -> None: + """Prints the metrics table for a single output feature. + + Args: + train_metrics_log: Dict from metric name to list of TrainerMetric. + validation_metrics_log: Dict from metric name to list of TrainerMetric. + test_metrics_log: Dict from metric name to list of TrainerMetric. + """ + # Get the superset of metric names across all splits. + all_metric_names = set() + all_metric_names.update(train_metrics_log.keys()) + all_metric_names.update(validation_metrics_log.keys()) + all_metric_names.update(test_metrics_log.keys()) + all_metric_names = sorted(list(all_metric_names)) + + # Assemble the printed table. + # Each item in the printed_table corresponds to a row in the printed table. + printed_table = [["train", "validation", "test"]] + for metric_name in all_metric_names: + metrics_for_each_split = [ + get_metric_value_or_empty(train_metrics_log, metric_name), + get_metric_value_or_empty(validation_metrics_log, metric_name), + get_metric_value_or_empty(test_metrics_log, metric_name), + ] + printed_table.append([metric_name] + metrics_for_each_split) + + # Add combined loss. + printed_table.append(["combined_loss"] + combined_loss_for_each_split) + + logger.info(tabulate(printed_table, headers="firstrow", tablefmt="fancy_grid", floatfmt=".4f")) + + +def print_metrics_table( + output_features: dict[str, "OutputFeature"], # noqa + train_metrics_log: dict[str, dict[str, list[TrainerMetric]]], + validation_metrics_log: dict[str, dict[str, list[TrainerMetric]]], + test_metrics_log: dict[str, dict[str, list[TrainerMetric]]], +): + """Prints a table of metrics table for each output feature, for each split. + + Example: + ╒═══════════════╤═════════╤══════════════╤════════╕ + │ │ train │ validation │ test │ + ╞═══════════════╪═════════╪══════════════╪════════╡ + │ accuracy │ 0.8157 │ 0.6966 │ 0.8090 │ + ├───────────────┼─────────┼──────────────┼────────┤ + │ loss │ 0.4619 │ 0.5039 │ 0.4488 │ + ├───────────────┼─────────┼──────────────┼────────┤ + │ precision │ 0.8274 │ 0.6250 │ 0.7818 │ + ├───────────────┼─────────┼──────────────┼────────┤ + │ recall │ 0.6680 │ 0.4545 │ 0.6615 │ + ├───────────────┼─────────┼──────────────┼────────┤ + │ roc_auc │ 0.8471 │ 0.7706 │ 0.8592 │ + ├───────────────┼─────────┼──────────────┼────────┤ + │ specificity │ 0.9105 │ 0.8393 │ 0.8938 │ + ├───────────────┼─────────┼──────────────┼────────┤ + │ combined_loss │ 0.4619 │ 0.5039 │ 0.4488 │ + ╘═══════════════╧═════════╧══════════════╧════════╛ + """ + # Obtain the combined loss, which is the same across all output features. + combined_loss_for_each_split = [ + get_metric_value_or_empty(train_metrics_log[COMBINED], LOSS), + get_metric_value_or_empty(validation_metrics_log[COMBINED], LOSS), + get_metric_value_or_empty(test_metrics_log[COMBINED], LOSS), + ] + + for output_feature_name in sorted(output_features.keys()): + if output_feature_name == COMBINED: + # Skip the combined output feature. The combined loss will be added to each output feature's table. + continue + print_table_for_single_output_feature( + train_metrics_log[output_feature_name], + validation_metrics_log[output_feature_name], + test_metrics_log[output_feature_name], + combined_loss_for_each_split, + ) diff --git a/ludwig/utils/misc_utils.py b/ludwig/utils/misc_utils.py new file mode 100644 index 0000000..0e4f00e --- /dev/null +++ b/ludwig/utils/misc_utils.py @@ -0,0 +1,220 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import copy +import functools +import os +import random +import subprocess +import weakref +from collections import OrderedDict +from collections.abc import Mapping +from typing import Any, TYPE_CHECKING + +import numpy +import torch + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import PROC_COLUMN +from ludwig.globals import DESCRIPTION_FILE_NAME, MODEL_FILE_NAME +from ludwig.utils import fs_utils +from ludwig.utils.fs_utils import find_non_existing_dir_by_adding_suffix + +if TYPE_CHECKING: + from ludwig.schema.model_types.base import ModelConfig + + +@DeveloperAPI +def set_random_seed(random_seed): + os.environ["PYTHONHASHSEED"] = str(random_seed) + random.seed(random_seed) + numpy.random.seed(random_seed) + torch.manual_seed(random_seed) + if torch.cuda.is_available() and torch.cuda.device_count() > 0: + torch.cuda.manual_seed(random_seed) + + +@DeveloperAPI +def merge_dict(dct, merge_dct): + """Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys, + dict_merge recurses down into dicts nested to an arbitrary depth, updating keys. The ``merge_dct`` is merged + into ``dct``. + + Args: + dct: Dict onto which the merge is executed. + merge_dct: Dict merged into dct. + """ + dct = copy.deepcopy(dct) + for k, _v in merge_dct.items(): + if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], Mapping): + dct[k] = merge_dict(dct[k], merge_dct[k]) + else: + dct[k] = merge_dct[k] + return dct + + +@DeveloperAPI +def sum_dicts(dicts, dict_type=dict): + summed_dict = dict_type() + for d in dicts: + for key, value in d.items(): + if key in summed_dict: + prev_value = summed_dict[key] + if isinstance(value, (dict, OrderedDict)): + summed_dict[key] = sum_dicts([prev_value, value], dict_type=type(value)) + elif isinstance(value, numpy.ndarray): + summed_dict[key] = numpy.concatenate((prev_value, value)) + else: + summed_dict[key] = prev_value + value + else: + summed_dict[key] = value + return summed_dict + + +@DeveloperAPI +def get_from_registry(key, registry): + if hasattr(key, "lower"): + key = key.lower() + if key in registry: + return registry[key] + else: + raise ValueError(f"Key '{key}' not in registry, available options: {registry.keys()}") + + +@DeveloperAPI +def set_default_value(dictionary, key, value): + if key not in dictionary: + dictionary[key] = value + + +@DeveloperAPI +def set_default_values(dictionary: dict, default_value_dictionary: dict): + """This function sets multiple default values recursively for various areas of the config. By using the helper + function set_default_value, It parses input values that contain nested dictionaries, only setting values for + parameters that have not already been defined by the user. + + Args: + dictionary (dict): The dictionary to set default values for, generally a section of the config. + default_value_dictionary (dict): The dictionary containing the default values for the config. + """ + for key, value in default_value_dictionary.items(): + if key not in dictionary: # Event where the key is not in the dictionary yet + dictionary[key] = value + elif value == {}: # Event where dict is empty + set_default_value(dictionary, key, value) + elif isinstance(value, dict) and value: # Event where dictionary is nested - recursive call + set_default_values(dictionary[key], value) + else: + set_default_value(dictionary, key, value) + + +@DeveloperAPI +def get_class_attributes(c): + return {i for i in dir(c) if not callable(getattr(c, i)) and not i.startswith("_")} + + +@DeveloperAPI +def get_output_directory(output_directory, experiment_name, model_name="run"): + base_dir_name = os.path.join(output_directory, experiment_name + ("_" if model_name else "") + (model_name or "")) + return fs_utils.abspath(find_non_existing_dir_by_adding_suffix(base_dir_name)) + + +@DeveloperAPI +def get_file_names(output_directory): + description_fn = os.path.join(output_directory, DESCRIPTION_FILE_NAME) + training_stats_fn = os.path.join(output_directory, "training_statistics.json") + + model_dir = os.path.join(output_directory, MODEL_FILE_NAME) + + return description_fn, training_stats_fn, model_dir + + +@DeveloperAPI +def get_combined_features(config): + return config["input_features"] + config["output_features"] + + +@DeveloperAPI +def get_proc_features(config): + return get_proc_features_from_lists(config["input_features"], config["output_features"]) + + +@DeveloperAPI +def get_proc_features_from_lists(*args): + return {feature[PROC_COLUMN]: feature for features in args for feature in features} + + +@DeveloperAPI +def set_saved_weights_in_checkpoint_flag(config_obj: "ModelConfig"): + """Adds a flag to all input feature encoder configs indicating that the weights are saved in the checkpoint. + + Next time the model is loaded we will restore pre-trained encoder weights from ludwig model (and not load from cache + or model hub). + """ + for input_feature in config_obj.input_features: + encoder_obj = input_feature.encoder + encoder_obj.saved_weights_in_checkpoint = True + + +@DeveloperAPI +def remove_empty_lines(str): + return "\n".join([line.rstrip() for line in str.split("\n") if line.rstrip()]) + + +@DeveloperAPI +def memoized_method(*lru_args, **lru_kwargs): + def decorator(func): + @functools.wraps(func) + def wrapped_func(self, *args, **kwargs): + # We're storing the wrapped method inside the instance. If we had + # a strong reference to self the instance would never die. + self_weak = weakref.ref(self) + + @functools.wraps(func) + @functools.lru_cache(*lru_args, **lru_kwargs) + def cached_method(*args, **kwargs): + return func(self_weak(), *args, **kwargs) + + setattr(self, func.__name__, cached_method) + return cached_method(*args, **kwargs) + + return wrapped_func + + return decorator + + +@DeveloperAPI +def get_commit_hash(): + """If Ludwig is run from a git repository, get the commit hash of the current HEAD. + + Returns None if git is not executable in the current environment or Ludwig is not run in a git repo. + """ + try: + with open(os.devnull, "w") as devnull: + is_a_git_repo = subprocess.call(["git", "branch"], stderr=subprocess.STDOUT, stdout=devnull) == 0 + if is_a_git_repo: + commit_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("utf-8") + return commit_hash + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + pass + return None + + +@DeveloperAPI +def scrub_creds(config_dict: dict[str, Any]) -> dict[str, Any]: + """Returns a copy of a config dict with all sensitive fields scrubbed.""" + if config_dict.get("backend", {}) and "credentials" in config_dict.get("backend", {}): + config_dict["backend"]["credentials"] = {} + return config_dict diff --git a/ludwig/utils/model_card.py b/ludwig/utils/model_card.py new file mode 100644 index 0000000..39f838c --- /dev/null +++ b/ludwig/utils/model_card.py @@ -0,0 +1,189 @@ +"""Auto-generate a model card from Ludwig training artifacts. + +Produces a Markdown file summarizing the model architecture, training configuration, dataset characteristics, and +evaluation metrics. Saved automatically to the output directory after training. +""" + +import logging +import platform +from datetime import datetime, UTC + +logger = logging.getLogger(__name__) + + +def generate_model_card( + config: dict, + training_set_metadata: dict, + train_stats=None, + model_dir: str | None = None, + dataset_statistics: list | None = None, +) -> str: + """Generate a Markdown model card from Ludwig training artifacts. + + Args: + config: The full Ludwig config dict. + training_set_metadata: Feature metadata computed during preprocessing. + train_stats: Training statistics (train/validation/test metrics per epoch). + model_dir: Path where the model is saved. + dataset_statistics: Dataset split sizes. + + Returns: + Markdown string. + """ + lines = [] + lines.append("# Model Card") + lines.append("") + lines.append(f"Generated by Ludwig on {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')}") + lines.append("") + + # Model type + model_type = config.get("model_type", "ecd") + lines.append("## Model Type") + lines.append("") + lines.append(f"- **Type:** {model_type.upper()}") + if "base_model" in config: + lines.append(f"- **Base model:** {config['base_model']}") + lines.append("") + + # Architecture + lines.append("## Architecture") + lines.append("") + combiner = config.get("combiner", {}) + lines.append(f"- **Combiner:** {combiner.get('type', 'concat')}") + for k, v in combiner.items(): + if k != "type": + lines.append(f" - {k}: {v}") + lines.append("") + + # Input features + lines.append("## Input Features") + lines.append("") + lines.append("| Name | Type | Encoder |") + lines.append("|------|------|---------|") + for feat in config.get("input_features", []): + encoder = feat.get("encoder", {}) + enc_type = encoder.get("type", "default") if isinstance(encoder, dict) else str(encoder) + lines.append(f"| {feat['name']} | {feat['type']} | {enc_type} |") + lines.append("") + + # Output features + lines.append("## Output Features") + lines.append("") + lines.append("| Name | Type | Decoder |") + lines.append("|------|------|---------|") + for feat in config.get("output_features", []): + decoder = feat.get("decoder", {}) + dec_type = decoder.get("type", "default") if isinstance(decoder, dict) else str(decoder) + lines.append(f"| {feat['name']} | {feat['type']} | {dec_type} |") + lines.append("") + + # Training configuration + trainer = config.get("trainer", {}) + if trainer: + lines.append("## Training Configuration") + lines.append("") + for k in ["epochs", "batch_size", "learning_rate", "early_stop", "optimizer", "loss_balancing"]: + if k in trainer: + val = trainer[k] + if isinstance(val, dict): + val = val.get("type", str(val)) + lines.append(f"- **{k}:** {val}") + lines.append("") + + # Dataset statistics + if dataset_statistics: + lines.append("## Dataset") + lines.append("") + for row in dataset_statistics: + if isinstance(row, (list, tuple)) and len(row) >= 2: + lines.append(f"- **{row[0]}:** {row[1]}") + lines.append("") + + # Feature metadata summary + if training_set_metadata: + lines.append("## Feature Statistics") + lines.append("") + for feat in config.get("input_features", []): + feat_name = feat["name"] + meta = training_set_metadata.get(feat_name, {}) + if isinstance(meta, dict): + if "mean" in meta: + lines.append(f"- **{feat_name}:** mean={meta['mean']:.4f}, std={meta.get('std', 0):.4f}") + elif "idx2str" in meta: + vocab_size = len(meta["idx2str"]) + lines.append(f"- **{feat_name}:** {vocab_size} categories") + lines.append("") + + # Evaluation metrics + if train_stats is not None: + lines.append("## Evaluation Metrics") + lines.append("") + for split_name, split_attr in [("Validation", "validation"), ("Test", "test")]: + split_data = getattr(train_stats, split_attr, None) + if split_data: + lines.append(f"### {split_name}") + lines.append("") + for feat_name, feat_metrics in split_data.items(): + if isinstance(feat_metrics, dict): + lines.append(f"**{feat_name}:**") + for metric_name, values in feat_metrics.items(): + if isinstance(values, list) and values: + if "loss" in metric_name or "error" in metric_name: + best = min(values) + else: + best = max(values) + if isinstance(best, float): + lines.append(f"- {metric_name}: {best:.6f}") + lines.append("") + + # Environment + lines.append("## Environment") + lines.append("") + try: + import ludwig + + lines.append(f"- **Ludwig version:** {ludwig.__version__}") + except ImportError: + pass + try: + import torch + + lines.append(f"- **PyTorch version:** {torch.__version__}") + if torch.cuda.is_available(): + lines.append(f"- **GPU:** {torch.cuda.get_device_name(0)}") + except ImportError: + pass + lines.append(f"- **Python:** {platform.python_version()}") + lines.append(f"- **Platform:** {platform.platform()}") + if model_dir: + lines.append(f"- **Model path:** {model_dir}") + lines.append("") + + return "\n".join(lines) + + +def save_model_card( + output_directory: str, + config: dict, + training_set_metadata: dict, + train_stats=None, + model_dir: str | None = None, + dataset_statistics: list | None = None, +): + """Generate and save a model card to the output directory.""" + import os + + card = generate_model_card( + config=config, + training_set_metadata=training_set_metadata, + train_stats=train_stats, + model_dir=model_dir, + dataset_statistics=dataset_statistics, + ) + + card_path = os.path.join(output_directory, "model_card.md") + with open(card_path, "w") as f: + f.write(card) + + logger.info(f"Model card saved to {card_path}") + return card_path diff --git a/ludwig/utils/model_export.py b/ludwig/utils/model_export.py new file mode 100644 index 0000000..1d721f4 --- /dev/null +++ b/ludwig/utils/model_export.py @@ -0,0 +1,199 @@ +"""Modern model export utilities using torch.export, ONNX, and SafeTensors. + +Replaces the deprecated TorchScript export pipeline with: +- torch.export: PyTorch 2.x native export producing ExportedProgram (ATen-level IR) +- ONNX: via torch.onnx.export(dynamo=True) for cross-platform deployment +- SafeTensors: secure, zero-copy weight serialization (already default for ECD) + +TorchScript is fully deprecated as of PyTorch 2.9 and was removed from Ludwig in +v0.15. torch.export is the official replacement that captures the full computation +graph as an ExportedProgram. Migration guide: +https://pytorch.org/docs/stable/export.html + +Usage: + from ludwig.utils.model_export import ModelExporter + + exporter = ModelExporter(model) + exporter.export_torch(path, sample_input) # torch.export format (.pt2) + exporter.export_onnx(path, sample_input) # ONNX via dynamo + exporter.export_safetensors(path) # weights only +""" + +import logging +import os + +import torch + +logger = logging.getLogger(__name__) + + +class ModelExporter: + """Unified model export interface for Ludwig models.""" + + def __init__(self, model: torch.nn.Module): + self.model = model + + def export_torch(self, save_path: str, sample_input: dict[str, torch.Tensor] | None = None): + """Export model via torch.export for deployment. + + Produces an ExportedProgram that captures the full computation graph at the + ATen operator level. This is the official replacement for TorchScript. + + The exported program can be: + - Loaded back via torch.export.load() + - Compiled with torch.compile() for runtime optimization + - Used as input to torch.onnx.export(dynamo=True) + - Deployed via ExecuTorch for on-device inference + + Args: + save_path: Directory to save the exported model. + sample_input: Example input dict for tracing. If None, generates one. + + Returns: + Path to the saved .pt2 file. + """ + os.makedirs(save_path, exist_ok=True) + export_path = os.path.join(save_path, "model.pt2") + + self.model.eval() + + if sample_input is None: + sample_input = self._generate_sample_input() + + try: + # torch.export captures the full computation graph + exported = torch.export.export(self.model, args=(sample_input,)) + torch.export.save(exported, export_path) + logger.info(f"Model exported via torch.export to {export_path}") + except Exception as e: + logger.warning( + f"torch.export failed: {e}. This can happen with dynamic control flow " + f"or unsupported operations. Falling back to torch.jit.trace." + ) + # Fallback to tracing for models that can't be exported + traced = torch.jit.trace(self.model, (sample_input,), strict=False) + traced.save(export_path) + logger.info(f"Model exported via torch.jit.trace (fallback) to {export_path}") + + return export_path + + def export_onnx(self, save_path: str, sample_input: dict[str, torch.Tensor] | None = None): + """Export model to ONNX format via dynamo-based exporter. + + Uses torch.onnx.export(dynamo=True) which is built on torch.export + and produces more accurate ONNX graphs than the legacy TorchScript-based + ONNX exporter. + + Args: + save_path: Directory to save the ONNX model. + sample_input: Example input dict for tracing. + + Returns: + Path to the saved .onnx file. + """ + os.makedirs(save_path, exist_ok=True) + onnx_path = os.path.join(save_path, "model.onnx") + + self.model.eval() + + if sample_input is None: + sample_input = self._generate_sample_input() + + try: + # Dynamo-based ONNX export (recommended for PyTorch 2.x) + torch.onnx.export( + self.model, + (sample_input,), + onnx_path, + dynamo=True, + ) + logger.info(f"Model exported to ONNX (dynamo) at {onnx_path}") + except Exception as e: + logger.warning(f"Dynamo ONNX export failed: {e}. Trying legacy exporter.") + try: + torch.onnx.export( + self.model, + (sample_input,), + onnx_path, + opset_version=17, + input_names=list(sample_input.keys()), + ) + logger.info(f"Model exported to ONNX (legacy) at {onnx_path}") + except Exception as e2: + logger.error(f"ONNX export failed: {e2}") + raise + + return onnx_path + + def export_safetensors(self, save_path: str): + """Export model weights in SafeTensors format. + + SafeTensors provides secure, zero-copy weight serialization. + Already the default format for Ludwig ECD models. + + Args: + save_path: Directory to save the weights. + + Returns: + Path to the saved .safetensors file. + """ + from safetensors.torch import save_model + + os.makedirs(save_path, exist_ok=True) + weights_path = os.path.join(save_path, "model.safetensors") + save_model(self.model, weights_path) + logger.info(f"Model weights exported to SafeTensors at {weights_path}") + return weights_path + + def _generate_sample_input(self) -> dict[str, torch.Tensor]: + """Generate a sample input for export tracing. + + Uses the model's create_sample_input() if available, otherwise creates dummy tensors based on input feature + shapes. + """ + if hasattr(self.model, "create_sample_input"): + return self.model.create_sample_input() + + # Fallback: create dummy inputs from input features + sample = {} + if hasattr(self.model, "input_features"): + for name, feature in self.model.input_features.items(): + if hasattr(feature, "create_sample_input"): + sample[name] = feature.create_sample_input(batch_size=2) + else: + sample[name] = torch.zeros(2, 1) + else: + logger.warning("Cannot generate sample input: model has no input_features") + sample = {"input": torch.zeros(2, 1)} + + return sample + + +def load_exported_model(path: str) -> torch.nn.Module: + """Load an exported model from disk. + + Supports torch.export (.pt2), TorchScript (.pt), and ONNX (.onnx) formats. + + Args: + path: Path to the exported model file. + + Returns: + Loaded model or ExportedProgram. + """ + if path.endswith(".pt2"): + try: + return torch.export.load(path) + except (RuntimeError, OSError): + # Fallback for traced models saved with .pt2 extension + return torch.jit.load(path) + elif path.endswith(".pt"): + return torch.jit.load(path) + elif path.endswith(".onnx"): + try: + import onnxruntime as ort + + return ort.InferenceSession(path) + except ImportError: + raise ImportError("onnxruntime is required to load ONNX models. pip install onnxruntime") + else: + raise ValueError(f"Unknown model format: {path}. Supported: .pt2, .pt, .onnx") diff --git a/ludwig/utils/model_soup.py b/ludwig/utils/model_soup.py new file mode 100644 index 0000000..5b0c9c2 --- /dev/null +++ b/ludwig/utils/model_soup.py @@ -0,0 +1,81 @@ +"""Model soup: average multiple checkpoint weights for better generalization. + +Based on Wortsman et al., "Model soups: averaging weights of multiple fine-tuned models +improves accuracy without increasing inference time", ICML 2022. +""" + +import logging +from collections.abc import Callable + +import torch + +logger = logging.getLogger(__name__) + + +def uniform_soup(state_dicts: list[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]: + """Average all state dicts uniformly. + + Args: + state_dicts: List of model state dicts to average. + + Returns: + Averaged state dict. + """ + if not state_dicts: + raise ValueError("No state dicts provided for model soup") + if len(state_dicts) == 1: + return state_dicts[0] + + avg = {} + n = len(state_dicts) + for key in state_dicts[0]: + stacked = torch.stack([sd[key].float() for sd in state_dicts if key in sd]) + avg[key] = (stacked.sum(dim=0) / n).to(state_dicts[0][key].dtype) + return avg + + +def greedy_soup( + state_dicts: list[dict[str, torch.Tensor]], + model: torch.nn.Module, + eval_fn: Callable[[], float], + is_minimize: bool, +) -> dict[str, torch.Tensor]: + """Greedily add checkpoints that improve validation metric. + + Args: + state_dicts: List of state dicts sorted by individual performance (best first). + model: The model to load state dicts into for evaluation. + eval_fn: Callable that evaluates current model and returns metric value. + is_minimize: If True, lower metric is better. + + Returns: + Best soup state dict found. + """ + if not state_dicts: + raise ValueError("No state dicts provided for model soup") + if len(state_dicts) == 1: + return state_dicts[0] + + # Start with the best individual checkpoint + ingredients = [state_dicts[0]] + model.load_state_dict(state_dicts[0], strict=False) + best_metric = eval_fn() + best_sd = state_dicts[0] + logger.info(f"Model soup: starting with best checkpoint, metric={best_metric:.6f}") + + for i, sd in enumerate(state_dicts[1:], 1): + candidate = uniform_soup(ingredients + [sd]) + model.load_state_dict(candidate, strict=False) + metric = eval_fn() + + improved = (is_minimize and metric < best_metric) or (not is_minimize and metric > best_metric) + if improved: + ingredients.append(sd) + best_sd = candidate + best_metric = metric + logger.info(f"Model soup: added checkpoint {i}, metric improved to {best_metric:.6f}") + else: + logger.info(f"Model soup: skipped checkpoint {i}, metric={metric:.6f} (no improvement)") + + logger.info(f"Model soup: final soup uses {len(ingredients)} of {len(state_dicts)} checkpoints") + return best_sd diff --git a/ludwig/utils/model_utils.py b/ludwig/utils/model_utils.py new file mode 100644 index 0000000..d29bd3f --- /dev/null +++ b/ludwig/utils/model_utils.py @@ -0,0 +1,126 @@ +import logging +from collections import OrderedDict + +import numpy as np +import torch + +logger = logging.getLogger(__name__) + +NUMPY_TO_TORCH_DTYPE = { + bool: torch.bool, + np.bool_: torch.bool, + np.uint8: torch.uint8, + np.int8: torch.int8, + np.int16: torch.int16, + np.int32: torch.int32, + np.int64: torch.int64, + np.float16: torch.float16, + np.float32: torch.float32, + np.float64: torch.float64, + np.complex64: torch.complex64, + np.complex128: torch.complex128, +} + + +def extract_tensors(model: torch.nn.Module) -> tuple[torch.nn.Module, list[dict]]: + """Remove the tensors from a PyTorch model, convert them to NumPy arrays, and return the stripped model and + tensors. + + Reference implementation: https://medium.com/ibm-data-ai/how-to-load-pytorch-models-340-times-faster-with- + ray-8be751a6944c # noqa + """ + + tensors = [] + for _, module in model.named_modules(): + # Store the tensors as numpy arrays in Python dictionaries + # Delete the same tensors since we no longer need them and we want to reduce memory pressure. + # This ensures that throughout this process, we keep memory nearly linear w.r.t model parameters. + params = OrderedDict() + buffers = OrderedDict() + for name, param in module.named_parameters(recurse=False): + params[name] = torch.clone(param).detach().numpy() + del param + for name, buf in module.named_buffers(recurse=False): + buffers[name] = torch.clone(buf).detach().numpy() + del buf + tensors.append({"params": params, "buffers": buffers}) + + # Strip all tensors and buffers out of the original model. + for _, module in model.named_modules(): + for name in [name for name, _ in module.named_parameters(recurse=False)] + [ + name for name, _ in module.named_buffers(recurse=False) + ]: + setattr(module, name, None) + + return model, tensors + + +def replace_tensors(m: torch.nn.Module, tensors: list[dict], device: torch.device): + """Restore the tensors that extract_tensors() stripped out of a PyTorch model. This operation is performed in + place. + + Reference implementation: https://medium.com/ibm-data-ai/how-to-load-pytorch-models-340-times-faster-with- + ray-8be751a6944c # noqa + """ + modules = [module for _, module in m.named_modules()] + for module, tensor_dict in zip(modules, tensors): + # There are separate APIs to set parameters and buffers. + for name, array in tensor_dict["params"].items(): + module.register_parameter( + name, + torch.nn.Parameter(torch.as_tensor(array, device=device, dtype=NUMPY_TO_TORCH_DTYPE.get(array.dtype))), + ) + + for name, array in tensor_dict["buffers"].items(): + module.register_buffer( + name, + torch.as_tensor(array, device=device, dtype=NUMPY_TO_TORCH_DTYPE.get(array.dtype)), + ) + + +def find_embedding_layer_with_path(module, module_names: list[str] | None = None): + """Recursively search through a module to find an embedding layer and its module path. + + Returns a tuple containing the embedding layer and its module path. + """ + if module_names is None: + module_names = [] + for name, child_module in module.named_children(): + if isinstance(child_module, torch.nn.Embedding): + # If an embedding layer is found, return it along with the module path + return child_module, ".".join(module_names + [name]) + else: + # Recursively search in the child module and update the module_names list + found, path = find_embedding_layer_with_path(child_module, module_names + [name]) + if found is not None: + return found, path + return None, None + + +def contains_nan_or_inf_tensors(module: torch.nn.Module) -> bool: + """Check for NaN or infinity (inf) values in the tensors (parameters and buffers) of a PyTorch module. This + function recursively inspects the module's parameters and buffers to identify NaN or inf values. It is designed + to ensure the numerical stability of the model by detecting any irregularities in the tensor values. + + Parameters: + module (torch.nn.Module): The PyTorch module to check for NaN or inf values. + + Returns: + bool: Returns True if any NaN or inf values are found in the module's tensors. Otherwise, returns False. + """ + for name, param in module.named_parameters(): + if param.requires_grad and (torch.isnan(param).any() or torch.isinf(param).any()): + logger.info(f"Found NaN or inf values in parameter '{name}' of module '{module.__class__.__name__}'") + return True + + for name, buffer in module.named_buffers(): + if torch.isnan(buffer).any() or torch.isinf(buffer).any(): + logger.info(f"Found NaN or inf values in buffer '{name}' of module '{module.__class__.__name__}'") + return True + + for name, submodule in module.named_children(): + if contains_nan_or_inf_tensors(submodule): + logger.info(f"Found NaN or inf values in submodule '{name}' of module '{module.__class__.__name__}'") + return True + + return False diff --git a/ludwig/utils/nlp_utils.py b/ludwig/utils/nlp_utils.py new file mode 100644 index 0000000..1c0a363 --- /dev/null +++ b/ludwig/utils/nlp_utils.py @@ -0,0 +1,214 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import sys + +logger = logging.getLogger(__name__) + +nlp_pipelines = { + "en": None, + "it": None, + "es": None, + "de": None, + "fr": None, + "pt": None, + "nl": None, + "el": None, + "nb": None, + "lt": None, + "da": None, + "pl": None, + "ro": None, + "ja": None, + "zh": None, + "xx": None, +} +language_module_registry = { + "en": "en_core_web_sm", + "it": "it_core_news_sm", + "es": "es_core_news_sm", + "de": "de_core_news_sm", + "fr": "fr_core_news_sm", + "pt": "pt_core_news_sm", + "nl": "nl_core_news_sm", + "el": "el_core_news_sm", + "nb": "nb_core_news_sm", + "lt": "lt_core_news_sm", + "da": "da_core_news_sm", + "pl": "pl_core_news_sm", + "ro": "ro_core_news_sm", + "ja": "ja_core_news_sm", + "zh": "zh_core_web_sm", + "xx": "xx_ent_wiki_sm", +} +default_characters = [ + " ", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "8", + "9", + "-", + ",", + ";", + ".", + "!", + "?", + ":", + "'", + "'", + "/", + "\\", + "|", + "_", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "~", + "`", + "+", + "-", + "=", + "<", + ">", + "(", + ")", + "[", + "]", + "{", + "}", +] +punctuation = {".", ",", "@", "$", "%", "/", ":", ";", "+", "="} + + +def load_nlp_pipeline(language="xx"): + if language not in language_module_registry: + logger.error(f"Language {language} is not supported.Suported languages are: {language_module_registry.keys()}") + raise ValueError + else: + spacy_module_name = language_module_registry[language] + if nlp_pipelines[language] is None: + logger.info("Loading NLP pipeline") + try: + import spacy + except ImportError: + logger.error( + " spacy is not installed. " + "In order to install all text feature dependencies run " + "pip install ludwig[text]" + ) + sys.exit(-1) + + try: + nlp_pipelines[language] = spacy.load(spacy_module_name, disable=["parser", "tagger", "ner"]) + except OSError: + logger.info(" spaCy {} model is missing, downloading it (this will only happen once)") + from spacy.cli import download + + download(spacy_module_name) + nlp_pipelines[language] = spacy.load(spacy_module_name, disable=["parser", "tagger", "ner"]) + + return nlp_pipelines[language] + + +def pass_filters( + token, filter_numbers=False, filter_punctuation=False, filter_short_tokens=False, filter_stopwords=False +): + passes_filters = True + if filter_numbers: + passes_filters = not token.like_num + if passes_filters and filter_punctuation: + passes_filters = not bool(set(token.orth_) & punctuation) + if passes_filters and filter_short_tokens: + passes_filters = len(token) > 2 + if passes_filters and filter_stopwords: + passes_filters = not token.is_stop + return passes_filters + + +def process_text( + text, + nlp_pipeline, + return_lemma=False, + filter_numbers=False, + filter_punctuation=False, + filter_short_tokens=False, + filter_stopwords=False, +): + doc = nlp_pipeline(text) + return [ + token.lemma_ if return_lemma else token.text + for token in doc + if pass_filters(token, filter_numbers, filter_punctuation, filter_short_tokens, filter_stopwords) + ] + + +if __name__ == "__main__": + text = ( + "Hello John, how are you doing my good old friend? Are you still number 732 in the list? Did you pay $32.43 or " + "54.21 for the book?" + ) + print(process_text(text, load_nlp_pipeline())) + print( + process_text(text, load_nlp_pipeline(), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True) + ) + print(process_text(text, load_nlp_pipeline(), filter_stopwords=True)) + print(process_text(text, load_nlp_pipeline(), return_lemma=True)) + print( + process_text( + text, + load_nlp_pipeline(), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + ) + print(process_text(text, load_nlp_pipeline(), return_lemma=True, filter_stopwords=True)) diff --git a/ludwig/utils/numerical_test_utils.py b/ludwig/utils/numerical_test_utils.py new file mode 100644 index 0000000..1709dc0 --- /dev/null +++ b/ludwig/utils/numerical_test_utils.py @@ -0,0 +1,71 @@ +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from typing import Any + +import numpy as np + + +def _dict_like(x): + """Returns true if an object is a dict or convertible to one, false if not.""" + try: + _ = dict(x) + except (TypeError, ValueError, KeyError): + # Objects that implement ``__getitem__`` without ``__iter__`` / ``keys()`` (e.g. + # dataclasses with a string-keyed accessor like ``TrainingStats``) make + # ``dict(x)`` fall back to integer-index iteration, which raises ``KeyError`` — + # not ``IndexError`` — when the ``__getitem__`` only accepts string keys. Treat + # those as not-dict-like rather than letting the KeyError bubble up. + return False + return True + + +def _enumerable(x): + """Returns true if an object is enumerable, false if not.""" + try: + _ = enumerate(x) + except (TypeError, ValueError, KeyError): + # Same rationale as _dict_like: an object exposing only ``__getitem__`` falls + # back to integer-index iteration, which raises KeyError against a string-keyed + # accessor. Treat those as not-enumerable here. + return False + return True + + +def assert_all_finite(x: Any, keypath=""): + """Ensures that all scalars at all levels of the dictionary, list, array, or scalar are finite. + + keypath is only used for logging error messages, to indicate where the non-finite value was detected. + """ + path_description = f" at {keypath} " if keypath else " " + if np.isscalar(x): + assert np.isfinite(x), f"Value{path_description}should be finite, but is {x!s}." + elif isinstance(x, np.ndarray): + non_finite_indices = np.nonzero(~np.isfinite(x)) + non_finite_values = x[non_finite_indices] + assert np.all(np.isfinite(x)), ( + f"All values{path_description}should be finite, but found {non_finite_values!s} " + "at positions {str(np.array(non_finite_indices).flatten())}." + ) + elif _dict_like(x): + # x is either a dict or convertible to one + for k, v in dict(x).items(): + assert_all_finite(v, keypath=keypath + "." + str(k) if keypath else str(k)) + elif _enumerable(x): + # x is a list, set or other enumerable type, but not a string, dict, or numpy array. + for i, v in enumerate(x): + assert_all_finite(v, keypath=keypath + f"[{i}]") + else: + raise AssertionError(f"Unhandled type {type(x)!s} for value{path_description}") diff --git a/ludwig/utils/output_feature_utils.py b/ludwig/utils/output_feature_utils.py new file mode 100644 index 0000000..e623b3b --- /dev/null +++ b/ludwig/utils/output_feature_utils.py @@ -0,0 +1,122 @@ +"""Utilities used for managing output feature dicts.""" + +import numpy as np +import torch + +from ludwig.utils.torch_utils import sequence_length_3D, sequence_mask + + +def get_feature_concat_name(feature_name: str, tensor_name: str) -> str: + return feature_name + "::" + tensor_name + + +def get_tensor_name_from_concat_name(concat_name: str) -> str: + return concat_name.split("::")[-1] + + +def get_feature_name_from_concat_name(concat_name: str) -> str: + return "::".join(concat_name.split("::")[:-1]) + + +def get_single_output_feature_tensors( + output_feature_dict: dict[str, torch.Tensor], feature_name: str +) -> dict[str, torch.Tensor]: + """Returns a map of tensors related to the given feature_name.""" + single_output_feature_tensors = {} + for concat_name, tensor in output_feature_dict.items(): + if get_feature_name_from_concat_name(concat_name) == feature_name: + single_output_feature_tensors[get_tensor_name_from_concat_name(concat_name)] = tensor + return single_output_feature_tensors + + +def get_output_feature_tensor( + output_dict: dict[str, torch.Tensor], feature_name: str, tensor_name: str +) -> torch.Tensor: + """Returns a tensor related for the given feature_name and tensor_name.""" + concat_name = get_feature_concat_name(feature_name, tensor_name) + if concat_name not in output_dict: + raise ValueError( + f"Could not find {tensor_name} for {feature_name} in the output_dict with keys: {output_dict.keys()}" + ) + return output_dict[get_feature_concat_name(feature_name, tensor_name)] + + +def set_output_feature_tensor( + output_dict: dict[str, torch.Tensor], feature_name: str, tensor_name: str, tensor: torch.Tensor +): + """Adds tensor for the given feature_name and tensor_name to the tensor dict.""" + output_dict[get_feature_concat_name(feature_name, tensor_name)] = tensor + + +def concat_dependencies( + feature_name: str, + dependencies: list[str], + dependency_reducers: torch.ModuleDict, + combiner_hidden_state: torch.Tensor, + other_output_feature_states: dict[str, torch.Tensor], +) -> torch.Tensor: + """Concatenates combiner_hidden_state with other output feature hidden states based on listed dependencies.""" + # No dependencies. + if not dependencies: + return combiner_hidden_state + + dependency_hidden_states = [] + for feature_name in dependencies: + # The dependent feature should be present since ECD does a topological sort over output features. + feature_hidden_state = other_output_feature_states[feature_name] + + # This feature is sequential. + if len(combiner_hidden_state.shape) > 2: + if len(feature_hidden_state.shape) > 2: + # The dependent feature is also sequential. + # matrix matrix -> concat + if combiner_hidden_state.shape[1] != feature_hidden_state.shape[1]: + raise ValueError( + f"Sequence length mismatch between combiner output ({combiner_hidden_state.shape[1]}) " + f"and dependent feature '{feature_name}' ({feature_hidden_state.shape[1]}). " + f"Both sequential features must have the same sequence length." + ) + dependency_hidden_states.append(feature_hidden_state) + else: + # The dependent feature is not sequential. + # matrix vector -> tile concat + sequence_max_length = combiner_hidden_state.shape[1] + multipliers = (1, sequence_max_length, 1) + tiled_representation = torch.tile(torch.unsqueeze(feature_hidden_state, 1), multipliers) + + sequence_length = sequence_length_3D(combiner_hidden_state) + mask = sequence_mask(sequence_length, sequence_max_length) + tiled_representation = torch.mul( + tiled_representation, + mask[:, :, np.newaxis].type(torch.float32), + ) + + dependency_hidden_states.append(tiled_representation) + + else: + # This feature is not sequential. + if len(feature_hidden_state.shape) > 2: + # The dependent feature is sequential. + # vector matrix -> reduce concat + reducer = dependency_reducers[feature_name] + dependency_hidden_states.append(reducer(feature_hidden_state)) + else: + # The dependent feature is not sequential. + # vector vector -> concat + dependency_hidden_states.append(feature_hidden_state) + + try: + hidden = torch.cat([combiner_hidden_state] + dependency_hidden_states, dim=-1) + except Exception as e: + raise ValueError( + f"Shape mismatch {e} while concatenating dependent features of {feature_name}: " + f"{dependencies}. Concatenating the feature activations tensor {combiner_hidden_state} " + f"with activation tensors of dependencies: {dependency_hidden_states}. The error is " + "likely due to a mismatch of the second dimension (sequence length) or a " + "difference in ranks. Likely solutions are setting the maximum_sequence_length " + "of all sequential features to be the same, or reduce the output of some " + "features, or disabling the bucketing setting bucketing_field to None / null, " + "as activating it will reduce the length of the field the bucketing is " + "performed on." + ) + return hidden diff --git a/ludwig/utils/package_utils.py b/ludwig/utils/package_utils.py new file mode 100644 index 0000000..284e2bc --- /dev/null +++ b/ludwig/utils/package_utils.py @@ -0,0 +1,39 @@ +import importlib +import types + + +class LazyLoader(types.ModuleType): + """Lazily import a module, mainly to avoid pulling in large dependencies. + + `contrib`, and `ffmpeg` are examples of modules that are large and not always + needed, and this allows them to only be loaded when they are used. + + Copied from: https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/util/lazy_loader.py + """ + + # The lint error here is incorrect. + def __init__(self, local_name, parent_module_globals, name): # pylint: disable=super-on-old-class + self._local_name = local_name + self._parent_module_globals = parent_module_globals + + super().__init__(name) + + def _load(self): + # Import the target module and insert it into the parent's namespace + module = importlib.import_module(self.__name__) + self._parent_module_globals[self._local_name] = module + + # Update this object's dict so that if someone keeps a reference to the + # LazyLoader, lookups are efficient (__getattr__ is only called on lookups + # that fail). + self.__dict__.update(module.__dict__) + + return module + + def __getattr__(self, item): + module = self._load() + return getattr(module, item) + + def __dir__(self): + module = self._load() + return dir(module) diff --git a/ludwig/utils/preference_data.py b/ludwig/utils/preference_data.py new file mode 100644 index 0000000..6a34713 --- /dev/null +++ b/ludwig/utils/preference_data.py @@ -0,0 +1,131 @@ +"""Preference data handling for DPO/KTO/ORPO/GRPO training. + +Provides utilities for processing preference data where each example has: +- A prompt (input) +- A chosen completion (preferred response) +- A rejected completion (dispreferred response) + +In Ludwig's data model, preference data is represented as: +- Input feature: the prompt text +- Output feature: the chosen completion +- Additional column: the rejected completion (specified via trainer config) + +The DPO/KTO/ORPO trainers access rejected completions during training to +compute preference losses. +""" + +import logging + +import torch + +logger = logging.getLogger(__name__) + + +def tokenize_preference_pair( + tokenizer, + prompt_ids: torch.Tensor, + chosen_ids: torch.Tensor, + rejected_ids: torch.Tensor, + max_length: int, +) -> dict[str, torch.Tensor]: + """Tokenize and merge prompt with chosen and rejected completions separately. + + Returns merged sequences for both chosen and rejected, each concatenating + the prompt with its respective completion. + + Args: + tokenizer: HuggingFace tokenizer + prompt_ids: [batch, prompt_len] token IDs for the prompt + chosen_ids: [batch, chosen_len] token IDs for the chosen completion + rejected_ids: [batch, rejected_len] token IDs for the rejected completion + max_length: Maximum sequence length + + Returns: + Dict with: + - chosen_input_ids: [batch, max_length] + - chosen_attention_mask: [batch, max_length] + - chosen_labels: [batch, max_length] (with -100 for prompt tokens) + - rejected_input_ids: [batch, max_length] + - rejected_attention_mask: [batch, max_length] + - rejected_labels: [batch, max_length] (with -100 for prompt tokens) + """ + batch_size = prompt_ids.shape[0] + device = prompt_ids.device + pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else 0 + + def merge_and_pad(prompt, completion): + """Merge prompt + completion, pad to max_length, create labels with prompt masked.""" + merged_ids = [] + merged_masks = [] + merged_labels = [] + + for i in range(batch_size): + # Remove padding from both + p = prompt[i][prompt[i] != pad_token_id] + c = completion[i][completion[i] != pad_token_id] + + # Concatenate + combined = torch.cat([p, c])[:max_length] + seq_len = combined.shape[0] + + # Pad to max_length (left padding) + pad_len = max_length - seq_len + padded_ids = torch.cat([torch.full((pad_len,), pad_token_id, device=device), combined]) + padded_mask = torch.cat([torch.zeros(pad_len, device=device), torch.ones(seq_len, device=device)]) + + # Labels: mask prompt tokens with -100, keep completion tokens + prompt_len = min(p.shape[0], max_length) + labels = padded_ids.clone() + labels[: pad_len + prompt_len] = -100 # mask padding + prompt + + merged_ids.append(padded_ids) + merged_masks.append(padded_mask) + merged_labels.append(labels) + + return ( + torch.stack(merged_ids).long(), + torch.stack(merged_masks).long(), + torch.stack(merged_labels).long(), + ) + + chosen_input_ids, chosen_attention_mask, chosen_labels = merge_and_pad(prompt_ids, chosen_ids) + rejected_input_ids, rejected_attention_mask, rejected_labels = merge_and_pad(prompt_ids, rejected_ids) + + return { + "chosen_input_ids": chosen_input_ids, + "chosen_attention_mask": chosen_attention_mask, + "chosen_labels": chosen_labels, + "rejected_input_ids": rejected_input_ids, + "rejected_attention_mask": rejected_attention_mask, + "rejected_labels": rejected_labels, + } + + +def validate_preference_data(df, chosen_column: str, rejected_column: str): + """Validate that a dataframe has the required preference columns. + + Args: + df: Pandas DataFrame + chosen_column: Name of the column with chosen completions + rejected_column: Name of the column with rejected completions + + Raises: + ValueError if columns are missing or empty + """ + if chosen_column not in df.columns: + raise ValueError( + f"Preference training requires a '{chosen_column}' column in the data. " + f"Available columns: {list(df.columns)}" + ) + if rejected_column not in df.columns: + raise ValueError( + f"Preference training requires a '{rejected_column}' column in the data. " + f"Available columns: {list(df.columns)}" + ) + + null_chosen = df[chosen_column].isna().sum() + null_rejected = df[rejected_column].isna().sum() + if null_chosen > 0: + logger.warning(f"{null_chosen} rows have null values in '{chosen_column}' column") + if null_rejected > 0: + logger.warning(f"{null_rejected} rows have null values in '{rejected_column}' column") diff --git a/ludwig/utils/print_utils.py b/ludwig/utils/print_utils.py new file mode 100644 index 0000000..1ed1c94 --- /dev/null +++ b/ludwig/utils/print_utils.py @@ -0,0 +1,102 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +from collections import OrderedDict +from pprint import pformat + +from ludwig.api_annotations import DeveloperAPI + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def get_logging_level_registry() -> dict[str, int]: + return { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, + "notset": logging.NOTSET, + } + + +@DeveloperAPI +def get_logo(message, ludwig_version): + return "\n".join( + [ + "███████████████████████", + "█ █ █ █ ▜█ █ █ █ █ █", + "█ █ █ █ █ █ █ █ █ █ ███", + "█ █ █ █ █ █ █ █ █ ▌ █", + "█ █████ █ █ █ █ █ █ █ █", + "█ █ ▟█ █ █ █", + "███████████████████████", + f"ludwig v{ludwig_version} - {message}", + "", + ] + ) + + +@DeveloperAPI +def print_ludwig(message, ludwig_version): + logger.info(get_logo(message, ludwig_version)) + + +@DeveloperAPI +def print_boxed(text, print_fun=logger.info): + box_width = len(text) + 2 + print_fun("") + print_fun("╒{}╕".format("═" * box_width)) + print_fun(f"│ {text.upper()} │") + print_fun("╘{}╛".format("═" * box_width)) + print_fun("") + + +@DeveloperAPI +def repr_ordered_dict(d: OrderedDict): + return "{" + ",\n ".join(f"{x}: {pformat(y, indent=4)}" for x, y in d.items()) + "}" + + +@DeveloperAPI +def query_yes_no(question: str, default: str | None = "yes"): + """Ask a yes/no question via raw_input() and return their answer. + + Args: + question: String presented to the user + default: The presumed answer from the user. Must be "yes", "no", or None (Answer is required) + + Returns: Boolean based on prompt response + """ + valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False} + if default is None: + prompt = " [y/n] " + elif default == "yes": + prompt = " [Y/n] " + elif default == "no": + prompt = " [y/N] " + else: + raise ValueError(f"invalid default answer: '{default}'") + + while True: + logger.info(question + prompt) + choice = input().lower() + if default is not None and choice == "": + return valid[default] + elif choice in valid: + return valid[choice] + else: + logger.info("Please respond with 'yes' or 'no' (or 'y' or 'n').\n") diff --git a/ludwig/utils/quantization.py b/ludwig/utils/quantization.py new file mode 100644 index 0000000..a09cf05 --- /dev/null +++ b/ludwig/utils/quantization.py @@ -0,0 +1,184 @@ +"""PyTorch-native quantization via torchao. + +Provides int4, int8, and float8 weight quantization without requiring +bitsandbytes. Exposes three operations: + +* :func:`quantize_model` — post-training quantization (PTQ). Call on a trained + fp16/bf16/fp32 model to produce a quantized model. +* :func:`prepare_qat_model` — insert fake-quant observers before training + (quantization-aware training). Train as usual in the target low-precision + regime. +* :func:`convert_qat_model` — after QAT training, convert the observed model + to actually-quantized weights for inference. + +Usage in Ludwig config (Post-training quantization / PTQ): + + quantization: + backend: torchao + mode: int4_weight_only # or int8_weight_only, int8_dynamic, float8 + +Usage in Ludwig config (Quantization-aware training / QAT): + + quantization: + backend: torchao + mode: int4_weight_only + qat: true +""" + +import logging + +logger = logging.getLogger(__name__) + + +# Canonical mode list. Keep in sync with ``_TORCHAO_MODES`` in ``ludwig/schema/llms/quantization.py``. +_VALID_MODES = ("int4_weight_only", "int8_weight_only", "int8_dynamic", "float8") + + +def _import_torchao_ptq(): + """Import torchao's PTQ API or raise a clear error if torchao is missing.""" + try: + from torchao.quantization import ( + float8_weight_only, + int4_weight_only, + int8_dynamic_activation_int8_weight, + int8_weight_only, + quantize_, + ) + except ImportError as exc: + raise ImportError( + "torchao is required for the 'torchao' quantization backend. " + "Install with: pip install 'ludwig[llm]' or pip install torchao." + ) from exc + + return quantize_, { + "int4_weight_only": int4_weight_only, + "int8_weight_only": int8_weight_only, + "int8_dynamic": int8_dynamic_activation_int8_weight, + "float8": float8_weight_only, + } + + +def _import_torchao_qat(): + """Import torchao's QAT API or raise a clear error if torchao is missing. + + The QAT submodule has moved between torchao releases, so we try the modern namespace first and fall back to the + pre-0.9 location. + """ + try: + from torchao.quantization.qat import ( + FakeQuantizeConfig, + from_intx_quantization_aware_training, + intx_quantization_aware_training, + ) + except ImportError: + try: + from torchao.quantization.prototype.qat import ( # type: ignore[no-redef] + FakeQuantizeConfig, + from_intx_quantization_aware_training, + intx_quantization_aware_training, + ) + except ImportError as exc: + raise ImportError( + "torchao QAT is required for `quantization.qat: true`. " + "Install torchao >= 0.9 with: pip install 'ludwig[llm]'." + ) from exc + + return intx_quantization_aware_training, from_intx_quantization_aware_training, FakeQuantizeConfig + + +def _validate_mode(mode: str) -> None: + if mode not in _VALID_MODES: + raise ValueError(f"Unknown quantization mode '{mode}'. Options: {list(_VALID_MODES)}") + + +def quantize_model(model, quantization_type: str): + """Apply torchao post-training quantization (PTQ) to ``model`` in-place. + + Args: + model: PyTorch model to quantize. + quantization_type: One of :data:`_VALID_MODES`. + + Returns: + The quantized model (modified in-place). + """ + _validate_mode(quantization_type) + quantize_, config_map = _import_torchao_ptq() + logger.info("Applying %s quantization via torchao (PTQ)", quantization_type) + quantize_(model, config_map[quantization_type]()) + logger.info("Quantization complete") + return model + + +def _qat_bit_width(mode: str) -> int: + if mode.startswith("int4"): + return 4 + if mode.startswith("int8"): + return 8 + # float8 isn't a standard IntXQAT path; reject explicitly rather than silently quantize wrong. + raise ValueError( + f"QAT is not supported for quantization mode '{mode}'. " + f"Use PTQ (qat: false) for 'float8', or pick an int4/int8 mode for QAT." + ) + + +def prepare_qat_model(model, quantization_type: str, group_size: int = 32): + """Insert fake-quant observers into ``model`` for quantization-aware training. + + Call this once on the unquantized model **before** training starts. Ludwig's + ``LLM.prepare_for_training`` handles this automatically when + ``quantization.qat: true`` is set in the user's config. + + After training completes, call :func:`convert_qat_model` to convert the + observed model to actually-quantized weights for inference / export. + + Args: + model: PyTorch model to prepare for QAT. + quantization_type: One of :data:`_VALID_MODES` (int4/int8 only; float8 + is PTQ-only in torchao). + group_size: Group size for per-group weight quantization. 32 is a sane + default for small / medium LMs. + + Returns: + The model modified in-place with fake-quant observers inserted. + """ + _validate_mode(quantization_type) + intx_qat, _, FakeQuantizeConfig = _import_torchao_qat() + + bit_width = _qat_bit_width(quantization_type) + weight_cfg = FakeQuantizeConfig(dtype=f"int{bit_width}", group_size=group_size) + activation_cfg = None + if quantization_type == "int8_dynamic": + activation_cfg = FakeQuantizeConfig(dtype="int8", is_dynamic=True) + + logger.info("Preparing model for QAT via torchao (%s, group_size=%d)", quantization_type, group_size) + # intx_quantization_aware_training returns a transform that quantize_ applies. + from torchao.quantization import quantize_ + + quantize_(model, intx_qat(activation_cfg, weight_cfg)) + return model + + +def convert_qat_model(model, quantization_type: str): + """Convert a QAT-prepared model to actually-quantized weights. + + Call after QAT training finishes — typically from the LLM save / export path. + Undoes the fake-quant observers inserted by :func:`prepare_qat_model` and replaces + them with real quantized tensors matching the original ``quantization_type``. + + Args: + model: Model previously prepared via :func:`prepare_qat_model`. + quantization_type: The same mode that was passed to ``prepare_qat_model``. + + Returns: + The model modified in-place. + """ + _validate_mode(quantization_type) + _, from_intx_qat, _ = _import_torchao_qat() + quantize_, config_map = _import_torchao_ptq() + + logger.info("Converting QAT-prepared model to %s quantized weights", quantization_type) + # First strip the fake-quant observers back to plain Linear... + quantize_(model, from_intx_qat()) + # ...then apply real PTQ to get the final quantized tensors. + quantize_(model, config_map[quantization_type]()) + return model diff --git a/ludwig/utils/registry.py b/ludwig/utils/registry.py new file mode 100644 index 0000000..f5580c7 --- /dev/null +++ b/ludwig/utils/registry.py @@ -0,0 +1,117 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from collections import UserDict +from typing import Generic, TypeVar + +# Legacy default keys for backward compatibility. +# New code should use the explicit `default` parameter instead. +DEFAULT_KEYS = ["None", "none", "null", None] + + +T = TypeVar("T") + + +class Registry(UserDict, Generic[T]): + """Type-safe registry with optional parent delegation and mock support. + + Items are considered to exist in the registry if they are in either the + registry itself or its parent. Supports: + - Generic typing: Registry[EncoderType], Registry[CombinerType] + - Parent delegation for hierarchical registries + - register() decorator for clean registration + - unregister() for testing and dynamic removal + - Mock support via context manager + """ + + def __init__(self, source=None): + init_data = None + parent = {} + if isinstance(source, Registry): + parent = source + else: + init_data = source + + self.parent = parent + super().__init__(init_data) + + def __getitem__(self, key: str) -> T: + if self.parent and key not in self.data: + return self.parent.__getitem__(key) + return self.data.__getitem__(key) + + def __contains__(self, key: str): + return key in self.data or key in self.parent + + def __len__(self) -> int: + return len(self.data) + len(self.parent) + + def __iter__(self): + return self._merged().__iter__() + + def keys(self): + return self._merged().keys() + + def values(self): + return self._merged().values() + + def items(self): + return self._merged().items() + + def _merged(self): + return {**self.parent, **self.data} + + def register(self, name: str, default: bool = False): + """Register a class in the registry via decorator. + + Args: + name: Registration key. + default: If True, also register under None/"none"/"null" keys. + """ + + def wrap(cls): + self[name] = cls + if default: + for key in DEFAULT_KEYS: + self[key] = cls + return cls + + return wrap + + def unregister(self, name: str): + """Remove a registered item. Useful for testing. + + Args: + name: Key to remove. + + Raises: + KeyError if name is not registered. + """ + if name in self.data: + del self.data[name] + else: + raise KeyError(f"'{name}' is not registered") + + def get_default(self) -> T | None: + """Get the default registered item (registered with default=True).""" + for key in DEFAULT_KEYS: + if key in self.data: + return self.data[key] + return None + + def list_registered(self) -> list[str]: + """List all registered names (excluding default key aliases).""" + return [k for k in self._merged() if k not in DEFAULT_KEYS] diff --git a/ludwig/utils/sequence_packing.py b/ludwig/utils/sequence_packing.py new file mode 100644 index 0000000..1acffbf --- /dev/null +++ b/ludwig/utils/sequence_packing.py @@ -0,0 +1,151 @@ +"""Sequence packing for efficient LLM training. + +Packs multiple short sequences into a single batch entry to maximize GPU utilization. +Instead of padding every sequence to max_length (wasting compute on pad tokens), +packing concatenates multiple sequences and uses a block-diagonal attention mask +to prevent cross-sequence attention. + +Two strategies: +- "greedy": Simple first-fit decreasing (FFD) bin packing +- "full": Pack all sequences without gaps (may split sequences) + +Config: + trainer: + packing: true + packing_max_sequences_per_pack: 8 # max sequences in one pack + +Based on: Krell et al., "Efficient Sequence Packing without Cross-contamination", 2021 +""" + +import logging + +import torch + +logger = logging.getLogger(__name__) + + +def pack_sequences( + input_ids_list: list[torch.Tensor], + attention_mask_list: list[torch.Tensor], + labels_list: list[torch.Tensor] | None = None, + max_length: int = 2048, + pad_token_id: int = 0, + max_sequences_per_pack: int = 8, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor]: + """Pack multiple variable-length sequences into fixed-length packs. + + Args: + input_ids_list: List of 1D token ID tensors (unpadded). + attention_mask_list: List of 1D attention mask tensors. + labels_list: Optional list of 1D label tensors for loss computation. + max_length: Maximum pack length. + pad_token_id: Token ID used for padding. + max_sequences_per_pack: Maximum number of sequences in one pack. + + Returns: + Tuple of: + - packed_input_ids: [num_packs, max_length] + - packed_attention_mask: [num_packs, max_length, max_length] (2D block-diagonal) + - packed_labels: [num_packs, max_length] or None (with -100 for non-label tokens) + - sequence_ids: [num_packs, max_length] (which sequence each token belongs to) + """ + # Sort by length descending for greedy bin packing + lengths = [ids.shape[0] for ids in input_ids_list] + sorted_indices = sorted(range(len(lengths)), key=lambda i: lengths[i], reverse=True) + + # Greedy first-fit decreasing bin packing + packs = [] # list of lists of (seq_index, start_pos) + pack_remaining = [] # remaining space in each pack + + for idx in sorted_indices: + seq_len = lengths[idx] + if seq_len > max_length: + # Sequence too long, truncate it and put in its own pack + packs.append([(idx, 0)]) + pack_remaining.append(0) + continue + + # Find first pack with enough space + placed = False + for pack_idx, remaining in enumerate(pack_remaining): + if remaining >= seq_len and len(packs[pack_idx]) < max_sequences_per_pack: + start = max_length - remaining + packs[pack_idx].append((idx, start)) + pack_remaining[pack_idx] -= seq_len + placed = True + break + + if not placed: + packs.append([(idx, 0)]) + pack_remaining.append(max_length - seq_len) + + # Build packed tensors + device = input_ids_list[0].device + num_packs = len(packs) + + packed_input_ids = torch.full((num_packs, max_length), pad_token_id, dtype=torch.long, device=device) + packed_attention_mask = torch.zeros((num_packs, max_length, max_length), dtype=torch.bool, device=device) + sequence_ids = torch.full((num_packs, max_length), -1, dtype=torch.long, device=device) + packed_labels = None + if labels_list is not None: + packed_labels = torch.full((num_packs, max_length), -100, dtype=torch.long, device=device) + + for pack_idx, pack_contents in enumerate(packs): + for seq_in_pack, (seq_idx, start_pos) in enumerate(pack_contents): + seq_len = min(lengths[seq_idx], max_length - start_pos) + end_pos = start_pos + seq_len + + # Copy token IDs + packed_input_ids[pack_idx, start_pos:end_pos] = input_ids_list[seq_idx][:seq_len] + + # Block-diagonal attention: each sequence attends only to itself + # This is a causal mask within each sequence block + for i in range(start_pos, end_pos): + for j in range(start_pos, i + 1): # causal: attend to current and previous positions + packed_attention_mask[pack_idx, i, j] = True + + # Sequence IDs + sequence_ids[pack_idx, start_pos:end_pos] = seq_in_pack + + # Labels + if packed_labels is not None and labels_list is not None: + packed_labels[pack_idx, start_pos:end_pos] = labels_list[seq_idx][:seq_len] + + logger.debug( + f"Packed {len(input_ids_list)} sequences into {num_packs} packs " + f"({len(input_ids_list) / max(num_packs, 1):.1f}x packing ratio)" + ) + + return packed_input_ids, packed_attention_mask, packed_labels, sequence_ids + + +def create_block_diagonal_mask( + sequence_lengths: list[int], + max_length: int, + causal: bool = True, + device: torch.device | None = None, +) -> torch.Tensor: + """Create a block-diagonal attention mask for packed sequences. + + Args: + sequence_lengths: Length of each sequence in the pack. + max_length: Total pack length. + causal: If True, apply causal masking within each block. + device: Target device. + + Returns: + [max_length, max_length] boolean attention mask. + """ + mask = torch.zeros(max_length, max_length, dtype=torch.bool, device=device) + offset = 0 + for length in sequence_lengths: + end = min(offset + length, max_length) + if causal: + for i in range(offset, end): + mask[i, offset : i + 1] = True + else: + mask[offset:end, offset:end] = True + offset = end + if offset >= max_length: + break + return mask diff --git a/ludwig/utils/server_utils.py b/ludwig/utils/server_utils.py new file mode 100644 index 0000000..bfa1be9 --- /dev/null +++ b/ludwig/utils/server_utils.py @@ -0,0 +1,168 @@ +import json +import os +import tempfile +from typing import Any + +import numpy as np +import pandas as pd +from starlette.datastructures import UploadFile +from starlette.responses import JSONResponse + +from ludwig.utils.data_utils import NumpyEncoder + + +def serialize_payload(data_source: pd.DataFrame | pd.Series) -> tuple: + """ + Generates two dictionaries to be sent via REST API for Ludwig prediction + service. + First dictionary created is payload_dict. Keys found in payload_dict: + raw_data: this is json string created by pandas to_json() method + source_type: indicates if the data_source is either a pandas dataframe or + pandas series. This is needed to know how to rebuild the structure. + ndarray_dtype: this is a dictionary where each entry is for any ndarray + data found in the data_source. This could be an empty dictioinary if no + ndarray objects are present in data_source. Key for this dictionary is + column name if data_source is dataframe or index name if data_source is + series. The value portion of the dictionary is the dtype of the + ndarray. This value is used to set the correct dtype when rebuilding + the entry. + + Second dictionary created is called payload_files, this contains information + and content for files to be sent to the server. NOTE: if no files are to be + sent, this will be an empty dictionary. + Entries in this dictionary: + Key: file path string for file to be sent to server + Value: tuple(file path string, byte encoded file content, + 'application/octet-stream') + + Args: + data_source: input features to be sent to Ludwig server + + Returns: tuple(payload_dict, payload_files) + + """ + payload_dict = {} + payload_dict["ndarray_dtype"] = {} + payload_files = {} + if isinstance(data_source, pd.DataFrame): + payload_dict["raw_data"] = data_source.to_json(orient="columns") + payload_dict["source_type"] = "dataframe" + for col in data_source.columns: + if isinstance(data_source[col].iloc[0], np.ndarray): + # if we have any ndarray columns, record dtype + payload_dict["ndarray_dtype"][col] = str(data_source[col].iloc[0].dtype) + elif isinstance(data_source[col].iloc[0], str) and os.path.exists(data_source[col].iloc[0]): + # if we have file path feature, prepare file for transport + for v in data_source[col]: + payload_files[v] = (v, open(v, "rb"), "application/octet-stream") + elif isinstance(data_source, pd.Series): + payload_dict["raw_data"] = data_source.to_json(orient="index") + payload_dict["source_type"] = "series" + for col in data_source.index: + if isinstance(data_source[col], np.ndarray): + # for ndarrays record dtype for reconstruction + payload_dict["ndarray_dtype"][col] = str(data_source[col].dtype) + elif isinstance(data_source[col], str) and os.path.exists(data_source[col]): + # if we have file path feature, prepare file for transport + v = data_source[col] + payload_files[v] = (v, open(v, "rb"), "application/octet-stream") + else: + ValueError(f'"data_source" must be either a pandas DataFrame or Series, format found to be {type(data_source)}') + + return payload_dict, payload_files + + +def _write_file(v, files): + # Convert UploadFile to a NamedTemporaryFile to ensure it's on the disk + suffix = os.path.splitext(v.filename)[1] + named_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) + files.append(named_file) + named_file.write(v.file.read()) + named_file.close() + return named_file.name + + +def deserialize_payload(json_string: str) -> pd.DataFrame: + """This function performs the inverse of the serialize_payload function and rebuilds the object represented in + json_string to a pandas DataFrame. + + Args: + json_string: representing object to be rebuilt. + + Returns: pandas.DataFrame + """ + payload_dict = json.loads(json_string) + + # extract raw data from json string + raw_data_dict = json.loads(payload_dict["raw_data"]) + # rebuild based on original data source + if payload_dict["source_type"] == "dataframe": + # reconstitute the pandas dataframe + df = pd.DataFrame.from_dict(raw_data_dict, orient="columns") + elif payload_dict["source_type"] == "series": + # reconstitute series into single row dataframe + df = pd.DataFrame(pd.Series(raw_data_dict)).T + else: + ValueError( + 'Unknown "source_type" found. Valid values are "dataframe" or "series". Instead found {}'.format( + payload_dict["source_type"] + ) + ) + + # if source has ndarrays, rebuild those from list and set + # original dtype. + if payload_dict["ndarray_dtype"]: + # yes, now covert list representation to ndarray representation + for col in payload_dict["ndarray_dtype"]: + dtype = payload_dict["ndarray_dtype"][col] + df[col] = df[col].apply(lambda x: np.array(x).astype(dtype)) + + return df + + +def deserialize_request(form) -> tuple: + """This function will deserialize the REST API request packet to create a pandas dataframe that is input to the + Ludwig predict method and a list of files that will be cleaned up at the end of processing. + + Args: + form: REST API provide form data + + Returns: tuple(pandas.DataFrame, list of temporary files to clean up) + """ + files = [] + file_index = {} + for _k, v in form.multi_items(): + if type(v) is UploadFile: + file_index[v.filename] = _write_file(v, files) + + # reconstruct the dataframe + df = deserialize_payload(form["payload"]) + + # insert files paths of the temporary files in place of the original + # file paths specified by the user. + # pd.DataFrame.replace() method is used to replace file path string + # specified by the user context with the file path string where a + # temporary file containing the same content. + # parameters for replace() method: + # to_replace: list of file path strings that the user provided + # value: list of temporary files created for each input file + # + # IMPORTANT: There is a one-to-one correspondence of the to_replace list + # and the value list. Each list must be the same size. + df.replace(to_replace=list(file_index.keys()), value=list(file_index.values()), inplace=True) + + return df, files + + +class NumpyJSONResponse(JSONResponse): + def render(self, content: dict[str, Any]) -> str: + """Override the default JSONResponse behavior to encode numpy arrays. + + Args: + content: JSON object to be serialized. + + Returns: str + """ + return json.dumps( + content, ensure_ascii=False, allow_nan=False, indent=None, separators=(",", ":"), cls=NumpyEncoder + ).encode("utf-8") diff --git a/ludwig/utils/state_dict_backward_compatibility.py b/ludwig/utils/state_dict_backward_compatibility.py new file mode 100644 index 0000000..3b673bb --- /dev/null +++ b/ludwig/utils/state_dict_backward_compatibility.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + + +def _update_transformers_to_freeze_module(state_dict): + """Updates pre-trained encoders which were saved prior to the addition of FreezeModule.""" + return { + ( + k.replace("encoder_obj.transformer.", "encoder_obj.transformer.module.") + if "encoder_obj.transformer.module" not in k + else k + ): v + for k, v in state_dict.items() + } + + +def _update_combiner_no_input_features(state_dict): + """Removed combiner.input_features from state_dict.""" + return {k: v for k, v in state_dict.items() if not k.startswith("combiner.input_features.")} + + +def _update_combiner_no_device_tensor(state_dict): + """Removed device_tensor from state_dict.""" + return {k: v for k, v in state_dict.items() if not k.endswith("device_tensor")} + + +def update_state_dict(state_dict): + """Checks state_dict on load, updates state dict if needed.""" + state_dict = _update_transformers_to_freeze_module(state_dict) + state_dict = _update_combiner_no_input_features(state_dict) + state_dict = _update_combiner_no_device_tensor(state_dict) + return state_dict diff --git a/ludwig/utils/strings_utils.py b/ludwig/utils/strings_utils.py new file mode 100644 index 0000000..79d5d6d --- /dev/null +++ b/ludwig/utils/strings_utils.py @@ -0,0 +1,591 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import re +import unicodedata +from collections import Counter +from dataclasses import dataclass +from enum import Enum + +import numpy as np +from dateutil.parser import parse as parse_datetime + +from ludwig.constants import PADDING_SYMBOL, START_SYMBOL, STOP_SYMBOL, UNKNOWN_SYMBOL +from ludwig.data.dataframe.base import DataFrameEngine +from ludwig.data.dataframe.pandas import PANDAS +from ludwig.utils.fs_utils import open_file +from ludwig.utils.math_utils import int_type +from ludwig.utils.tokenizers import get_tokenizer_from_registry +from ludwig.utils.types import Series + +PANDAS_TRUE_STRS = {"true"} +PANDAS_FALSE_STRS = {"false"} + +BOOL_TRUE_STRS = {"yes", "y", "true", "t", "1", "1.0"} +BOOL_FALSE_STRS = {"no", "n", "false", "f", "0", "0.0", "-1", "-1.0"} + +logger = logging.getLogger(__name__) + + +class SpecialSymbol(Enum): + """Special symbols used for text features.""" + + STOP = 0 + START = 1 + PADDING = 2 + UNKNOWN = 3 + + +def all_bool_strs(): + """Returns all valid boolean strings, with varied capitalization.""" + fns = [lambda x: x, lambda x: x.upper(), lambda x: x.capitalize()] + return sorted({fn(x) for fn in fns for x in BOOL_TRUE_STRS | BOOL_FALSE_STRS}) + + +def make_safe_filename(s): + def safe_char(c): + if c.isalnum(): + return c + else: + return "_" + + return "".join(safe_char(c) for c in s).rstrip("_") + + +def strip_accents(s): + return "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn") + + +def str2bool(v: str, fallback_true_label=None) -> bool: + """Returns bool representation of the given value v. + + Check the value against global bool string lists. + Fallback to using fallback_true_label as True if the value isn't in the global bool lists. + + args: + v: Value to get the bool representation for. + fallback_true_label: (str) label to use as 'True'. + """ + v_str = str(v).lower() + if v_str in BOOL_TRUE_STRS: + return True + if v_str in BOOL_FALSE_STRS: + return False + if fallback_true_label is None: + raise ValueError( + f"Cannot automatically map value '{v}' to a boolean and no `preprocessing.fallback_true_label` specified" + ) + return v == fallback_true_label + + +def values_are_pandas_numbers(values: list[str]): + """Returns True if values would be read by pandas as dtype float or int.""" + for v in values: + try: + float(v) + except ValueError: + return False + return True + + +def values_are_pandas_bools(values: list[str]): + """Returns True if values would be read by pandas as dtype bool.""" + lowercase_values_set = {str(v).lower() for v in values} + return lowercase_values_set.issubset(PANDAS_FALSE_STRS | PANDAS_TRUE_STRS) + + +def are_conventional_bools(values: list[str | bool]) -> bool: + """Returns whether all values are conventional booleans.""" + for value in values: + lower_value = str(value).lower() + if lower_value not in BOOL_TRUE_STRS and lower_value not in BOOL_FALSE_STRS: + return False + return True + + +def is_number(s: str | int | float): + """Returns whether specified value is number.""" + if isinstance(s, str) and s.lower() == "nan": + return True + try: + float(s) + return True + except ValueError: + return False + + +def is_datetime(s: str | int | float): + """Returns whether specified value is datetime.""" + if is_number(s): + return False + + try: + parse_datetime(s) + return True + except (ValueError, OverflowError): + return False + + +def are_all_datetimes(values: list[str | int | float]): + """Returns whether all values are datetimes.""" + for value in values: + if not is_datetime(value): + return False + return True + + +def are_all_numbers(values: list[str | int | float]): + """Returns whether all values are numbers.""" + for value in values: + if not is_number(value): + return False + return True + + +def is_integer(s: str | int | float): + """Returns whether specified value is an integer.""" + try: + float(s) + except ValueError: + return False + else: + return float(s).is_integer() and not np.isnan(float(s)) + + +def are_sequential_integers(values: list[str | int | float]): + """Returns whether distinct values form sequential integer list.""" + int_list = [] + for value in values: + if not is_integer(value): + return False + int_list.append(int(float(value))) + return (max(int_list) - min(int_list) + 1) == len(int_list) + + +def match_replace(string_to_match, list_regex): + """Matches strings against regular expressions. + + arguments: + string_to_match -- the string to match + + returns: + string_to_match -- the cleaned string + matched -- the list of regular expressions that matched + """ + matched = [] + for regex in list_regex: + match = re.search(regex[0], string_to_match) + if match: + string_to_match = re.sub(regex[0], regex[1], string_to_match) + matched.append(regex[0].pattern) + return string_to_match, matched + + +def load_vocabulary(vocab_file): + with open_file(vocab_file, "r", encoding="utf-8") as f: + vocabulary = [] + for line in f: + line = line.strip() + if " " in line: + line = line.split(" ")[0] + vocabulary.append(line) + return vocabulary + + +def add_or_move_symbol(vocab_list: list[str], vocab_set: set[str], symbol: str, index: int): + """Inserts or moves the symbol to the specified index.""" + if symbol in vocab_set: + vocab_list.remove(symbol) + vocab_list.insert(index, symbol) + + +@dataclass +class Vocabulary: + vocab: list[str] + """List of strings representing the computed vocabulary.""" + + str2idx: dict[str, int] + """Map of symbol to index.""" + + str2freq: dict[str, int] + """Map of symbol to frequency.""" + + str2idf: dict[str, int] | None + """Map of symbol to inverse document frequency.""" + + max_sequence_length: int + """Maximum sequence length.""" + + sequence_length_99ptile: int + """99th percentile of maximum sequence length.""" + + pad_idx: int + """Index to padding symbol.""" + + padding_symbol: str + """Actual padding symbol.""" + + unknown_symbol: str + """Actual unknown symbol.""" + + prompt_template_num_tokens: int = 0 + """The number of tokens in the prompt template. + + If -1, then there is no prompt template. + """ + + +def _get_vocab_from_dict(vocab: dict[str, int]) -> list[str]: + """Returns a vocab in list format from a vocab token=>idx dictionary.""" + vocab_values = list(vocab.values()) + if len(set(vocab_values)) != len(vocab_values): + raise ValueError("Vocabulary has duplicate mappings in its vocabulary. This should never happen.") + + # construct a vocab that is a list that reflects the token=>index mapping in HF's vocab + # pre-allocate a list to make sure each index is inited to prevent OBO errors caused by missing indices + max_idx = max(vocab_values) + vocab_list = [None for _ in range(max_idx + 1)] + for token, idx in vocab.items(): + vocab_list[idx] = token + return vocab_list + + +def _get_vocabulary( + tokenizer_type: str, + tokenizer, + vocab_file: str, + unknown_symbol: str, + add_special_symbols: bool, + padding_symbol: str, + unit_counts: Counter, + num_most_frequent: int, +) -> list[str] | None: + """Returns the vocabulary from the tokenizer_type, tokenizer, or vocab_file. + + If the `tokenizer_type` is 'hf_tokenizer', then the set vocabulary from the tokenizer is used. + + If there's no vocab_file or if the tokenizer has no set vocabulary (e.g. space_punct), then the vocabulary is + determined from the tokenized data (unit_counts). + + The UNKNOWN special symbol is always included in the final vocabulary. Additional special symbols (PADDING, START, + STOP) are added if add_special_symbols=True. If the tokenizer is a pre-trained huggingface tokenizer, then the + special symbols are taken from the tokenizer's vocabulary. + """ + # Pre-trained huggingface tokenizer. Use the pre-existing vocabulary and special symbols. + if tokenizer_type == "hf_tokenizer": + try: + return _get_vocab_from_dict(tokenizer.get_vocab()) + except NotImplementedError: + logger.warning( + "HuggingFace tokenizer does not have a get_vocab() method. " + + "Using tokenizer.tokenizer.vocab_size and tokenizer.tokenizer._convert_id_to_token " + + "to build the vocabulary." + ) + vocab = [] + for idx in range(tokenizer.tokenizer.vocab_size): + vocab.append(tokenizer.tokenizer._convert_id_to_token(idx)) + vocab += tokenizer.tokenizer.added_tokens_encoder.keys() + return vocab + + # The tokenizer has a preset vocabulary. + if hasattr(tokenizer, "get_vocab"): + return _get_vocab_from_dict(tokenizer.get_vocab()) + + # Load the vocabulary from the vocab file. + if vocab_file is not None: + return load_vocabulary(vocab_file) + + # The tokenizer had no preset vocabulary, for example space_punct. + # Compute the vocabulary from tokenized data. + return [unit for unit, _ in unit_counts.most_common(num_most_frequent)] + + +def remove_bracketed_elements(prompt_template: str) -> str: + """Example: -> .""" + pattern = r"\{.*?\}" + return re.sub(pattern, "", prompt_template) + + +def create_vocabulary( + data: Series, + tokenizer_type: str = "space", + lowercase: bool = True, + num_most_frequent: int | None = None, + vocab_file: str | None = None, + add_special_symbols: bool = True, + unknown_symbol: str = UNKNOWN_SYMBOL, + padding_symbol: str = PADDING_SYMBOL, + start_symbol: str = START_SYMBOL, + stop_symbol: str = STOP_SYMBOL, + pretrained_model_name_or_path: str | None = None, + ngram_size: int | None = None, + compute_idf: bool = False, + processor: DataFrameEngine = PANDAS, + prompt_template: str = "", +) -> Vocabulary: + """Computes a vocabulary over the provided data frame. + + This function is used when the data consists of multiple tokens within one example. E.g., words in a text feature, + items in a set feature, etc. If the feature only contains a single token like for category features, + `create_vocabulary_single_token` should be used instead, as it is more efficient. + + A tokenizer is specified using the `tokenizer_type`. The tokenizer will be used to process all of the data + provided, producing an indexed vocabulary with frequency counts. If the `tokenizer_type` is 'hf_tokenizer', + then a pre-trained huggingface tokenizer is loaded from `pretrained_model_name_or_path` and that vocabulary is + used directly. + + The UNKNOWN special symbol is always included in the final vocabulary. Additional special symbols (PADDING, START, + STOP) are added if add_special_symbols=True. If the tokenizer is a pre-trained huggingface tokenizer, then the + special symbols are taken from the tokenizer's vocabulary. + + Args: + prompt_template: The prompt template for the model. Applicable only to LLMs. + data: Series of string data. + tokenizer_type: Tokenizer type. Can be a tokenizer registry value or 'hf_tokenizer' for huggingface. + lowercase: Whether to lowercase all strings. + num_most_frequent: Upper limit on vocabulary size., + add_special_symbols: If True, START, STOP, PADDING special symbols are added to the vocabulary. UNKNOWN is + always added. + unknown_symbol: String representation for the UNKNOWN symbol. + padding_symbol: String representation for the PADDING symbol. + start_symbol: String representation for the START symbol. + stop_symbol: String representation for the STOP symbol. + pretrained_model_name_or_path: Name/path to huggingface model. + ngram_size: Size of the n-gram when using `ngram` tokenizer. + compute_idf: If True, computes the inverse document frequency for each token. + processor: Which processor to use to process data. + + Returns: + Vocabulary object containing metadata about the vocab. + + TODO(Justin): Clean up pad_idx, padding_symbol, unknown_symbol return, as no one seems to be using it. + """ + tokenizer = get_tokenizer_from_registry(tokenizer_type)( + vocab_file=vocab_file, + pretrained_model_name_or_path=pretrained_model_name_or_path, + ngram_size=ngram_size, + ) + + # Number of tokens in template. + prompt_template_num_tokens = -1 + if prompt_template: + prompt_without_bracketed_elements = remove_bracketed_elements(prompt_template) + prompt_template_num_tokens = len(tokenizer(prompt_without_bracketed_elements)) + + # Tokenize the data. + def process_line(line): + return tokenizer(line.lower() if lowercase else line) + + processed_lines = processor.map_objects(data, process_line) + processed_counts = processed_lines.explode().value_counts(sort=False) + processed_counts = processor.compute(processed_counts) + unit_counts = Counter(dict(processed_counts)) + lengths = processor.map_objects(processed_lines, len, meta=(processed_lines.name, int)) + max_sequence_length = processor.compute(lengths.max()) + sequence_length_99ptile = processor.compute(lengths.quantile(0.99)) + + if tokenizer_type != "hf_tokenizer": + # For non-HF tokenizers, add 2 for start and stop symbols. + max_sequence_length += 2 + sequence_length_99ptile += 2 + + pad_idx = None + if tokenizer_type == "hf_tokenizer": + # Replace the special symbols with the ones from the tokenizer. + unknown_symbol = tokenizer.get_unk_token() + padding_symbol = tokenizer.get_pad_token() + pad_idx = tokenizer.convert_token_to_id(padding_symbol) + + vocab: list[str] = _get_vocabulary( + tokenizer_type, + tokenizer, + vocab_file, + unknown_symbol, + add_special_symbols, + padding_symbol, + unit_counts, + num_most_frequent, + ) + vocab_set = set(vocab) + + doc_unit_counts = None + if compute_idf: + # The document frequency used for TF-IDF. Similar to unit_counts, but de-duped by document. + document_counts = processor.map_objects(processed_lines, set).explode().value_counts(sort=False) + document_counts = processor.compute(document_counts) + doc_unit_counts = Counter(dict(document_counts)) + + if tokenizer_type != "hf_tokenizer": + if add_special_symbols: + add_or_move_symbol(vocab, vocab_set, stop_symbol, SpecialSymbol.STOP.value) + add_or_move_symbol(vocab, vocab_set, start_symbol, SpecialSymbol.START.value) + add_or_move_symbol(vocab, vocab_set, padding_symbol, SpecialSymbol.PADDING.value) + # Always add the UNKNOWN symbol if we're using our own tokenizer. + add_or_move_symbol(vocab, vocab_set, unknown_symbol, SpecialSymbol.UNKNOWN.value) + + str2idx = {unit: i for i, unit in enumerate(vocab)} + str2freq = {unit: unit_counts.get(unit) if unit in unit_counts else 0 for unit in vocab} + str2idf = ( + {unit: np.log(len(vocab) / (1 + doc_unit_counts.get(unit))) if unit in doc_unit_counts else 0 for unit in vocab} + if compute_idf + else None + ) + + if pad_idx is None and padding_symbol in str2idx: + pad_idx = str2idx[padding_symbol] + + return Vocabulary( + vocab=vocab, + str2idx=str2idx, + str2freq=str2freq, + str2idf=str2idf, + max_sequence_length=max_sequence_length, + sequence_length_99ptile=sequence_length_99ptile, + pad_idx=pad_idx, + padding_symbol=padding_symbol, + unknown_symbol=unknown_symbol, + prompt_template_num_tokens=prompt_template_num_tokens, + ) + + +def create_vocabulary_single_token( + data: Series, + num_most_frequent: int | None = None, + processor: DataFrameEngine = PANDAS, + unknown_symbol: str = UNKNOWN_SYMBOL, +): + """Computes a vocabulary over the provided data frame. + + This function should be used iff the values in each row of data should be considered as a single token, e.g., + category features ("interested", "not interested", "somewhat interested"). + + This assumption allows us to be more efficient than `create_vocabulary()` as we can skip tokenization and + computing the maximum sequence length, which are unnecessary for category features. + + Args: + data: Series of string data. + num_most_frequent: Upper limit on vocabulary size. + unknown_symbol: String representation for the UNKNOWN symbol. + processor: Which processor to use to process data. + + Returns: + Tuple of: + vocab: List of strings representing the computed vocabulary. + str2idx: Map of symbol to index. + str2freq: Map of symbol to frequency. + """ + processed_counts = data.str.strip().value_counts(sort=True) + processed_counts = processor.compute(processed_counts) + full_vocab = processed_counts.index.tolist() + # Only add unknown symbol if num most frequent tokens is less than total number of unique tokens + if num_most_frequent < len(full_vocab): + vocab = [unknown_symbol] + full_vocab[:num_most_frequent] + else: + vocab = full_vocab + str2idx = {unit: i for i, unit in enumerate(vocab)} + str2freq = processed_counts.to_dict() + str2freq = {k: str2freq.get(k, 0) for k in vocab} + return vocab, str2idx, str2freq + + +def _get_sequence_vector( + sequence, tokenizer, tokenizer_type, format_dtype, unit_to_id, lowercase=True, unknown_symbol=UNKNOWN_SYMBOL +) -> np.ndarray: + unit_sequence = tokenizer(sequence.lower() if lowercase else sequence) + + unit_indices_vector = np.empty(len(unit_sequence), dtype=format_dtype) + for i in range(len(unit_sequence)): + curr_unit = unit_sequence[i] + if tokenizer_type == "hf_tokenizer": + unit_indices_vector[i] = curr_unit + else: + if curr_unit in unit_to_id: + unit_indices_vector[i] = unit_to_id[curr_unit] + else: + unit_indices_vector[i] = unit_to_id[unknown_symbol] + + # Add start and stop symbols. + # Huggingface's pretrained tokenizers take care of this implicitly: + # https://huggingface.co/docs/transformers/preprocessing + if tokenizer_type != "hf_tokenizer": + unit_indices_vector = np.append(unit_indices_vector, unit_to_id[STOP_SYMBOL]) + unit_indices_vector = np.insert(unit_indices_vector, 0, unit_to_id[START_SYMBOL]) + return unit_indices_vector + + +def build_sequence_matrix( + sequences, # pd.core.series.Series + inverse_vocabulary, + tokenizer_type, + length_limit, + padding_symbol=PADDING_SYMBOL, + padding="right", + unknown_symbol=UNKNOWN_SYMBOL, + lowercase=True, + tokenizer_vocab_file=None, + pretrained_model_name_or_path=None, + processor=PANDAS, +) -> np.ndarray: + tokenizer = get_tokenizer_from_registry(tokenizer_type)( + vocab_file=tokenizer_vocab_file, + pretrained_model_name_or_path=pretrained_model_name_or_path, + ) + + format_dtype = int_type(len(inverse_vocabulary) - 1) + + unit_vectors = processor.map_objects( + sequences, + lambda sequence: _get_sequence_vector( + sequence, + tokenizer, + tokenizer_type, + format_dtype, + inverse_vocabulary, + lowercase=lowercase, + unknown_symbol=unknown_symbol, + ), + ) + + max_length = processor.compute(processor.map_objects(unit_vectors, len, meta=(unit_vectors.name, int)).max()) + if max_length < length_limit: + logger.debug(f"max length of {format}: {max_length} < limit: {length_limit}") + max_length = length_limit + + if tokenizer_type == "hf_tokenizer": + padding_symbol = tokenizer.get_pad_token() + pad_token_id = tokenizer.convert_token_to_id(padding_symbol) + else: + pad_token_id = inverse_vocabulary[padding_symbol] + + def pad(vector): + sequence = np.full((int(max_length),), pad_token_id, dtype=format_dtype) + limit = min(vector.shape[0], max_length) + if padding == "right": + sequence[:limit] = vector[:limit] + else: # if padding == 'left + sequence[max_length - limit :] = vector[:limit] + return sequence + + padded = processor.map_objects(unit_vectors, pad) + return padded + + +def get_tokenizer(tokenizer_type: str, tokenizer_vocab_file: str, pretrained_model_name_or_path: str): + """Returns a tokenizer object based on the tokenizer type.""" + return get_tokenizer_from_registry(tokenizer_type)( + vocab_file=tokenizer_vocab_file, + pretrained_model_name_or_path=pretrained_model_name_or_path, + ) diff --git a/ludwig/utils/structural_warning.py b/ludwig/utils/structural_warning.py new file mode 100644 index 0000000..7be3f19 --- /dev/null +++ b/ludwig/utils/structural_warning.py @@ -0,0 +1,29 @@ +import warnings + +from ludwig.utils.logging_utils import log_once + + +def warn_structure_refactor(old_module: str, new_module: str, direct: bool = True) -> None: + """Create structure refactor warning to indicate modules new location post. + + Only creates a warning once per module. + """ + old_module = old_module.replace(".py", "") + if log_once(old_module): + warning = ( + f"The module `{old_module}` has been moved to `{new_module}` and the old " + f"location will be deprecated soon. Please adjust your imports to point " + f"to the new location." + ) + + if direct: + warning += f" Example: Do a global search and replace `{old_module}` with `{new_module}`." + else: + warning += ( + f"\nATTENTION: This module may have been split or refactored. Please " + f"check the contents of `{new_module}` before making changes." + ) + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn(warning, DeprecationWarning, stacklevel=3) diff --git a/ludwig/utils/system_utils.py b/ludwig/utils/system_utils.py new file mode 100644 index 0000000..e9fcc3d --- /dev/null +++ b/ludwig/utils/system_utils.py @@ -0,0 +1,26 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from dataclasses import dataclass + +from ludwig.api_annotations import DeveloperAPI + + +@DeveloperAPI +@dataclass +class Resources: + cpus: int + gpus: int diff --git a/ludwig/utils/time_utils.py b/ludwig/utils/time_utils.py new file mode 100644 index 0000000..fda2cdb --- /dev/null +++ b/ludwig/utils/time_utils.py @@ -0,0 +1,98 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import time +from datetime import datetime, timedelta + +logger = logging.getLogger(__name__) + + +class WithTimer: + def __init__(self, title="", quiet=False): + self.title = title + self.quiet = quiet + + def elapsed(self): + return time.time() - self.wall, time.process_time() - self.proc + + def enter(self): + """Manually trigger enter.""" + self.__enter__() + + def __enter__(self): + self.proc = time.process_time() + self.wall = time.time() + return self + + def __exit__(self, *args): + if not self.quiet: + elapsed_wp = self.elapsed() + logger.info(f"Elapsed {self.title}: wall {elapsed_wp[0]:.06f}, sys {elapsed_wp[1]:.06f}") + + +class Timer: + def __init__(self): + self.reset() + + def reset(self): + self._proc = time.process_time() + self._wall = time.time() + + def elapsed(self): + return self.wall(), self.proc() + + def elapsed_str(self): + return strdelta(self.wall() * 1000.0), strdelta(self.proc() * 1000.0) + + def wall(self): + return time.time() - self._wall + + def proc(self): + return time.process_time() - self._proc + + def tic(self): + """Like Matlab tic/toc for wall time and processor time.""" + self.reset() + + def toc(self): + """Like Matlab tic/toc for wall time.""" + return self.wall() + + def tocproc(self): + """Like Matlab tic/toc, but for processor time.""" + return self.proc() + + +def timestamp(): + return f"{datetime.now():%Y_%m_%d_%H_%M_%S}" + + +def strdelta(tdelta): + if isinstance(tdelta, (int, float)): + tdelta = timedelta(milliseconds=tdelta) + d = {"D": tdelta.days} + d["H"], rem = divmod(tdelta.seconds, 3600) + d["M"], d["S"] = divmod(rem, 60) + d["f"] = str(tdelta.microseconds)[0:4] + if d["D"] > 0: + t = "{D}d {H}h {M}m {S}.{f}s" + elif d["H"] > 0: + t = "{H}h {M}m {S}.{f}s" + elif d["M"] > 0: + t = "{M}m {S}.{f}s" + else: + t = "{S}.{f}s" + return t.format(**d) diff --git a/ludwig/utils/tokenizers.py b/ludwig/utils/tokenizers.py new file mode 100644 index 0000000..b45988e --- /dev/null +++ b/ludwig/utils/tokenizers.py @@ -0,0 +1,1088 @@ +"""Ludwig string tokenizers including string-based, spacy-based, and huggingface-based implementations. + +To add a new tokenizer, 1) implement a subclass of BaseTokenizer and 2) add it to the tokenizer_registry. + +Once it's in the registry, tokenizers can be used in a ludwig config, e.g.. + +``` +input_features: + - name: title + type: text + preprocessing: + tokenizer: +``` +""" + +import logging +import re +from abc import abstractmethod +from typing import Any + +import torch + +from ludwig.utils.nlp_utils import load_nlp_pipeline, process_text + +logger = logging.getLogger(__name__) + + +SPACE_PUNCTUATION_REGEX = re.compile(r"\w+|[^\w\s]") +COMMA_REGEX = re.compile(r"\s*,\s*") +UNDERSCORE_REGEX = re.compile(r"\s*_\s*") + +TORCHSCRIPT_COMPATIBLE_TOKENIZERS = {"space", "space_punct"} + + +class BaseTokenizer: + @abstractmethod + def __init__(self, **kwargs): + pass + + @abstractmethod + def __call__(self, text: str): + pass + + +class CharactersToListTokenizer(BaseTokenizer): + def __call__(self, text): + return list(text) + + +class SpaceStringToListTokenizer(torch.nn.Module): + """Implements torchscript-compatible whitespace tokenization.""" + + def __init__(self, **kwargs): + super().__init__() + + def forward(self, v: str | list[str] | torch.Tensor) -> Any: + if isinstance(v, torch.Tensor): + raise ValueError(f"Unsupported input: {v}") + + inputs: list[str] = [] + # Ludwig calls map on List[str] objects, so we need to handle individual strings as well. + if isinstance(v, str): + inputs.append(v) + else: + inputs.extend(v) + + tokens: list[list[str]] = [] + for sequence in inputs: + split_sequence = sequence.strip().split(" ") + token_sequence: list[str] = [] + for token in split_sequence: + if len(token) > 0: + token_sequence.append(token) + tokens.append(token_sequence) + + return tokens[0] if isinstance(v, str) else tokens + + +class SpacePunctuationStringToListTokenizer(torch.nn.Module): + """Implements torchscript-compatible space_punct tokenization.""" + + def __init__(self, **kwargs): + super().__init__() + + def is_regex_w(self, c: str) -> bool: + return c.isalnum() or c == "_" + + def forward(self, v: str | list[str] | torch.Tensor) -> Any: + if isinstance(v, torch.Tensor): + raise ValueError(f"Unsupported input: {v}") + + inputs: list[str] = [] + # Ludwig calls map on List[str] objects, so we need to handle individual strings as well. + if isinstance(v, str): + inputs.append(v) + else: + inputs.extend(v) + + tokens: list[list[str]] = [] + for sequence in inputs: + token_sequence: list[str] = [] + word: list[str] = [] + for c in sequence: + if self.is_regex_w(c): + word.append(c) + elif len(word) > 0: # if non-empty word and non-alphanumeric char, append word to token sequence + token_sequence.append("".join(word)) + word.clear() + + if not self.is_regex_w(c) and not c.isspace(): # non-alphanumeric, non-space char is punctuation + token_sequence.append(c) + + if len(word) > 0: # add last word + token_sequence.append("".join(word)) + + tokens.append(token_sequence) + + return tokens[0] if isinstance(v, str) else tokens + + +class StringSplitTokenizer(BaseTokenizer): + """Splits a string by a given separator.""" + + def __init__(self, separator: str = " ", **kwargs): + self.separator = separator + + def __call__(self, text): + return text.split(self.separator) + + +class NgramTokenizer(BaseTokenizer): + """Tokenizes text into unigrams + ngrams up to n.""" + + def __init__(self, n: int = 2, **kwargs): + self.n = n + + def __call__(self, text): + tokens = text.strip().split() + result = list(tokens) + for i in range(2, self.n + 1): + for j in range(len(tokens) - i + 1): + result.append(" ".join(tokens[j : j + i])) + return result + + +class UnderscoreStringToListTokenizer(BaseTokenizer): + def __call__(self, text): + return UNDERSCORE_REGEX.split(text.strip()) + + +class CommaStringToListTokenizer(BaseTokenizer): + def __call__(self, text): + return COMMA_REGEX.split(text.strip()) + + +class UntokenizedStringToListTokenizer(BaseTokenizer): + def __call__(self, text): + return [text] + + +class StrippedStringToListTokenizer(BaseTokenizer): + def __call__(self, text): + return [text.strip()] + + +class EnglishTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("en")) + + +class EnglishFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("en"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class EnglishRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("en"), filter_stopwords=True) + + +class EnglishLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + process_text(text, load_nlp_pipeline("en"), return_lemma=True) + + +class EnglishLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("en"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class EnglishLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("en"), return_lemma=True, filter_stopwords=True) + + +class ItalianTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("it")) + + +class ItalianFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("it"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class ItalianRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("it"), filter_stopwords=True) + + +class ItalianLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("it"), return_lemma=True) + + +class ItalianLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("it"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class ItalianLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("it"), return_lemma=True, filter_stopwords=True) + + +class SpanishTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("es")) + + +class SpanishFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("es"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class SpanishRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("es"), filter_stopwords=True) + + +class SpanishLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("es"), return_lemma=True) + + +class SpanishLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("es"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class SpanishLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("es"), return_lemma=True, filter_stopwords=True) + + +class GermanTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("de")) + + +class GermanFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("de"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class GermanRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("de"), filter_stopwords=True) + + +class GermanLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("de"), return_lemma=True) + + +class GermanLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("de"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class GermanLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("de"), return_lemma=True, filter_stopwords=True) + + +class FrenchTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("fr")) + + +class FrenchFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("fr"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class FrenchRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("fr"), filter_stopwords=True) + + +class FrenchLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("fr"), return_lemma=True) + + +class FrenchLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("fr"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class FrenchLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("fr"), return_lemma=True, filter_stopwords=True) + + +class PortugueseTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pt")) + + +class PortugueseFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("pt"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class PortugueseRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pt"), filter_stopwords=True) + + +class PortugueseLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pt"), return_lemma=True) + + +class PortugueseLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("pt"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class PortugueseLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pt"), return_lemma=True, filter_stopwords=True) + + +class DutchTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nl")) + + +class DutchFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("nl"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class DutchRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nl"), filter_stopwords=True) + + +class DutchLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nl"), return_lemma=True) + + +class DutchLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("nl"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class DutchLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nl"), return_lemma=True, filter_stopwords=True) + + +class GreekTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("el")) + + +class GreekFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("el"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class GreekRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("el"), filter_stopwords=True) + + +class GreekLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("el"), return_lemma=True) + + +class GreekLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("el"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class GreekLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("el"), return_lemma=True, filter_stopwords=True) + + +class NorwegianTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nb")) + + +class NorwegianFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("nb"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class NorwegianRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nb"), filter_stopwords=True) + + +class NorwegianLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nb"), return_lemma=True) + + +class NorwegianLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("nb"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class NorwegianLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("nb"), return_lemma=True, filter_stopwords=True) + + +class LithuanianTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("lt")) + + +class LithuanianFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("lt"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class LithuanianRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("lt"), filter_stopwords=True) + + +class LithuanianLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("lt"), return_lemma=True) + + +class LithuanianLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("lt"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class LithuanianLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("lt"), return_lemma=True, filter_stopwords=True) + + +class DanishTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("da")) + + +class DanishFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("da"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class DanishRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("da"), filter_stopwords=True) + + +class DanishLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("da"), return_lemma=True) + + +class DanishLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("da"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class DanishLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("da"), return_lemma=True, filter_stopwords=True) + + +class PolishTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pl")) + + +class PolishFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("pl"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class PolishRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pl"), filter_stopwords=True) + + +class PolishLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pl"), return_lemma=True) + + +class PolishLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("pl"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class PolishLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("pl"), return_lemma=True, filter_stopwords=True) + + +class RomanianTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("ro")) + + +class RomanianFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("ro"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class RomanianRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("ro"), filter_stopwords=True) + + +class RomanianLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("ro"), return_lemma=True) + + +class RomanianLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("ro"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class RomanianLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("ro"), return_lemma=True, filter_stopwords=True) + + +class JapaneseTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("jp")) + + +class JapaneseFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("jp"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class JapaneseRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("jp"), filter_stopwords=True) + + +class JapaneseLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("jp"), return_lemma=True) + + +class JapaneseLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("jp"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class JapaneseLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("jp"), return_lemma=True, filter_stopwords=True) + + +class ChineseTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("zh")) + + +class ChineseFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("zh"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class ChineseRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("zh"), filter_stopwords=True) + + +class ChineseLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("zh"), return_lemma=True) + + +class ChineseLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("zh"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class ChineseLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("zh"), return_lemma=True, filter_stopwords=True) + + +class MultiTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("xx"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class MultiFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, load_nlp_pipeline("xx"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True + ) + + +class MultiRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("xx"), filter_stopwords=True) + + +class MultiLemmatizeTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("xx"), return_lemma=True) + + +class MultiLemmatizeFilterTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text( + text, + load_nlp_pipeline("xx"), + return_lemma=True, + filter_numbers=True, + filter_punctuation=True, + filter_short_tokens=True, + ) + + +class MultiLemmatizeRemoveStopwordsTokenizer(BaseTokenizer): + def __call__(self, text): + return process_text(text, load_nlp_pipeline("xx"), return_lemma=True, filter_stopwords=True) + + +class HFTokenizer(BaseTokenizer): + def __init__(self, pretrained_model_name_or_path, **kwargs): + super().__init__() + from transformers import AutoTokenizer + + self.tokenizer = AutoTokenizer.from_pretrained( + pretrained_model_name_or_path, + trust_remote_code=kwargs.get("trust_remote_code", False), + ) + # Some models (e.g. LLaMA) don't have a pad_token by default. + # Set it to eos_token to avoid NoneType errors in preprocessing. + if self.tokenizer.pad_token is None and self.tokenizer.eos_token is not None: + self.tokenizer.pad_token = self.tokenizer.eos_token + + def __call__(self, text): + return self.tokenizer.encode(text, truncation=True) + + def get_vocab(self): + return self.tokenizer.get_vocab() + + def get_pad_token(self) -> str: + return self.tokenizer.pad_token + + def get_unk_token(self) -> str: + return self.tokenizer.unk_token + + def convert_token_to_id(self, token: str) -> int: + if token is None: + return 0 + return self.tokenizer.convert_tokens_to_ids(token) + + +tokenizer_registry = { + # Torchscript-compatible tokenizers. + "space": SpaceStringToListTokenizer, + "space_punct": SpacePunctuationStringToListTokenizer, + # Tokenizers not compatible with torchscript + "characters": CharactersToListTokenizer, + "underscore": UnderscoreStringToListTokenizer, + "comma": CommaStringToListTokenizer, + "untokenized": UntokenizedStringToListTokenizer, + "stripped": StrippedStringToListTokenizer, + "english_tokenize": EnglishTokenizer, + "english_tokenize_filter": EnglishFilterTokenizer, + "english_tokenize_remove_stopwords": EnglishRemoveStopwordsTokenizer, + "english_lemmatize": EnglishLemmatizeTokenizer, + "english_lemmatize_filter": EnglishLemmatizeFilterTokenizer, + "english_lemmatize_remove_stopwords": EnglishLemmatizeRemoveStopwordsTokenizer, + "italian_tokenize": ItalianTokenizer, + "italian_tokenize_filter": ItalianFilterTokenizer, + "italian_tokenize_remove_stopwords": ItalianRemoveStopwordsTokenizer, + "italian_lemmatize": ItalianLemmatizeTokenizer, + "italian_lemmatize_filter": ItalianLemmatizeFilterTokenizer, + "italian_lemmatize_remove_stopwords": ItalianLemmatizeRemoveStopwordsTokenizer, + "spanish_tokenize": SpanishTokenizer, + "spanish_tokenize_filter": SpanishFilterTokenizer, + "spanish_tokenize_remove_stopwords": SpanishRemoveStopwordsTokenizer, + "spanish_lemmatize": SpanishLemmatizeTokenizer, + "spanish_lemmatize_filter": SpanishLemmatizeFilterTokenizer, + "spanish_lemmatize_remove_stopwords": SpanishLemmatizeRemoveStopwordsTokenizer, + "german_tokenize": GermanTokenizer, + "german_tokenize_filter": GermanFilterTokenizer, + "german_tokenize_remove_stopwords": GermanRemoveStopwordsTokenizer, + "german_lemmatize": GermanLemmatizeTokenizer, + "german_lemmatize_filter": GermanLemmatizeFilterTokenizer, + "german_lemmatize_remove_stopwords": GermanLemmatizeRemoveStopwordsTokenizer, + "french_tokenize": FrenchTokenizer, + "french_tokenize_filter": FrenchFilterTokenizer, + "french_tokenize_remove_stopwords": FrenchRemoveStopwordsTokenizer, + "french_lemmatize": FrenchLemmatizeTokenizer, + "french_lemmatize_filter": FrenchLemmatizeFilterTokenizer, + "french_lemmatize_remove_stopwords": FrenchLemmatizeRemoveStopwordsTokenizer, + "portuguese_tokenize": PortugueseTokenizer, + "portuguese_tokenize_filter": PortugueseFilterTokenizer, + "portuguese_tokenize_remove_stopwords": PortugueseRemoveStopwordsTokenizer, + "portuguese_lemmatize": PortugueseLemmatizeTokenizer, + "portuguese_lemmatize_filter": PortugueseLemmatizeFilterTokenizer, + "portuguese_lemmatize_remove_stopwords": PortugueseLemmatizeRemoveStopwordsTokenizer, + "dutch_tokenize": DutchTokenizer, + "dutch_tokenize_filter": DutchFilterTokenizer, + "dutch_tokenize_remove_stopwords": DutchRemoveStopwordsTokenizer, + "dutch_lemmatize": DutchLemmatizeTokenizer, + "dutch_lemmatize_filter": DutchLemmatizeFilterTokenizer, + "dutch_lemmatize_remove_stopwords": DutchLemmatizeRemoveStopwordsTokenizer, + "greek_tokenize": GreekTokenizer, + "greek_tokenize_filter": GreekFilterTokenizer, + "greek_tokenize_remove_stopwords": GreekRemoveStopwordsTokenizer, + "greek_lemmatize": GreekLemmatizeTokenizer, + "greek_lemmatize_filter": GreekLemmatizeFilterTokenizer, + "greek_lemmatize_remove_stopwords": GreekLemmatizeRemoveStopwordsFilterTokenizer, + "norwegian_tokenize": NorwegianTokenizer, + "norwegian_tokenize_filter": NorwegianFilterTokenizer, + "norwegian_tokenize_remove_stopwords": NorwegianRemoveStopwordsTokenizer, + "norwegian_lemmatize": NorwegianLemmatizeTokenizer, + "norwegian_lemmatize_filter": NorwegianLemmatizeFilterTokenizer, + "norwegian_lemmatize_remove_stopwords": NorwegianLemmatizeRemoveStopwordsFilterTokenizer, + "lithuanian_tokenize": LithuanianTokenizer, + "lithuanian_tokenize_filter": LithuanianFilterTokenizer, + "lithuanian_tokenize_remove_stopwords": LithuanianRemoveStopwordsTokenizer, + "lithuanian_lemmatize": LithuanianLemmatizeTokenizer, + "lithuanian_lemmatize_filter": LithuanianLemmatizeFilterTokenizer, + "lithuanian_lemmatize_remove_stopwords": LithuanianLemmatizeRemoveStopwordsFilterTokenizer, + "danish_tokenize": DanishTokenizer, + "danish_tokenize_filter": DanishFilterTokenizer, + "danish_tokenize_remove_stopwords": DanishRemoveStopwordsTokenizer, + "danish_lemmatize": DanishLemmatizeTokenizer, + "danish_lemmatize_filter": DanishLemmatizeFilterTokenizer, + "danish_lemmatize_remove_stopwords": DanishLemmatizeRemoveStopwordsFilterTokenizer, + "polish_tokenize": PolishTokenizer, + "polish_tokenize_filter": PolishFilterTokenizer, + "polish_tokenize_remove_stopwords": PolishRemoveStopwordsTokenizer, + "polish_lemmatize": PolishLemmatizeTokenizer, + "polish_lemmatize_filter": PolishLemmatizeFilterTokenizer, + "polish_lemmatize_remove_stopwords": PolishLemmatizeRemoveStopwordsFilterTokenizer, + "romanian_tokenize": RomanianTokenizer, + "romanian_tokenize_filter": RomanianFilterTokenizer, + "romanian_tokenize_remove_stopwords": RomanianRemoveStopwordsTokenizer, + "romanian_lemmatize": RomanianLemmatizeTokenizer, + "romanian_lemmatize_filter": RomanianLemmatizeFilterTokenizer, + "romanian_lemmatize_remove_stopwords": RomanianLemmatizeRemoveStopwordsFilterTokenizer, + "japanese_tokenize": JapaneseTokenizer, + "japanese_tokenize_filter": JapaneseFilterTokenizer, + "japanese_tokenize_remove_stopwords": JapaneseRemoveStopwordsTokenizer, + "japanese_lemmatize": JapaneseLemmatizeTokenizer, + "japanese_lemmatize_filter": JapaneseLemmatizeFilterTokenizer, + "japanese_lemmatize_remove_stopwords": JapaneseLemmatizeRemoveStopwordsFilterTokenizer, + "chinese_tokenize": ChineseTokenizer, + "chinese_tokenize_filter": ChineseFilterTokenizer, + "chinese_tokenize_remove_stopwords": ChineseRemoveStopwordsTokenizer, + "chinese_lemmatize": ChineseLemmatizeTokenizer, + "chinese_lemmatize_filter": ChineseLemmatizeFilterTokenizer, + "chinese_lemmatize_remove_stopwords": ChineseLemmatizeRemoveStopwordsFilterTokenizer, + "multi_tokenize": MultiTokenizer, + "multi_tokenize_filter": MultiFilterTokenizer, + "multi_tokenize_remove_stopwords": MultiRemoveStopwordsTokenizer, + "multi_lemmatize": MultiLemmatizeTokenizer, + "multi_lemmatize_filter": MultiLemmatizeFilterTokenizer, + "multi_lemmatize_remove_stopwords": MultiLemmatizeRemoveStopwordsTokenizer, +} + + +class SentencePieceTokenizer(torch.nn.Module): + """SentencePiece tokenizer using HuggingFace transformers (XLMR-based).""" + + def __init__(self, pretrained_model_name_or_path: str | None = None, **kwargs): + super().__init__() + from transformers import AutoTokenizer + + if pretrained_model_name_or_path is None: + pretrained_model_name_or_path = "xlm-roberta-base" + self.tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path) + + def forward(self, v: str | list[str] | torch.Tensor): + if isinstance(v, torch.Tensor): + raise ValueError(f"Unsupported input: {v}") + if isinstance(v, str): + return self.tokenizer.tokenize(v) + return [self.tokenizer.tokenize(s) for s in v] + + +class CLIPTokenizer(torch.nn.Module): + """CLIP tokenizer using HuggingFace transformers.""" + + def __init__(self, pretrained_model_name_or_path: str | None = None, **kwargs): + super().__init__() + from transformers import CLIPTokenizer as HFCLIPTokenizer + + if pretrained_model_name_or_path is None: + pretrained_model_name_or_path = "openai/clip-vit-base-patch32" + self.tokenizer = HFCLIPTokenizer.from_pretrained(pretrained_model_name_or_path) + + def __call__(self, text): + if isinstance(text, str): + return self.tokenizer.tokenize(text) + return [self.tokenizer.tokenize(t) for t in text] + + def get_vocab(self): + return self.tokenizer.get_vocab() + + +class GPT2BPETokenizer(torch.nn.Module): + """GPT-2 BPE tokenizer using HuggingFace transformers.""" + + def __init__(self, pretrained_model_name_or_path: str | None = None, **kwargs): + super().__init__() + from transformers import GPT2Tokenizer + + if pretrained_model_name_or_path is None: + pretrained_model_name_or_path = "gpt2" + self.tokenizer = GPT2Tokenizer.from_pretrained(pretrained_model_name_or_path) + + def __call__(self, text): + if isinstance(text, str): + return self.tokenizer.tokenize(text) + return [self.tokenizer.tokenize(t) for t in text] + + def get_vocab(self): + return self.tokenizer.get_vocab() + + +class BERTTokenizer(torch.nn.Module): + """BERT tokenizer using HuggingFace transformers.""" + + def __init__( + self, + vocab_file: str | None = None, + pretrained_model_name_or_path: str | None = None, + is_hf_tokenizer: bool | None = False, + do_lower_case: bool | None = None, + **kwargs, + ): + super().__init__() + from transformers import BertTokenizer + + if pretrained_model_name_or_path is None: + pretrained_model_name_or_path = "bert-base-uncased" + tokenizer_kwargs = {} + if do_lower_case is not None: + tokenizer_kwargs["do_lower_case"] = do_lower_case + self.tokenizer = BertTokenizer.from_pretrained(pretrained_model_name_or_path, **tokenizer_kwargs) + self.is_hf_tokenizer = is_hf_tokenizer + self.pad_token = self.tokenizer.pad_token + self.unk_token = self.tokenizer.unk_token + self.cls_token_id = self.tokenizer.cls_token_id + self.sep_token_id = self.tokenizer.sep_token_id + + def __call__(self, text): + if isinstance(text, str): + texts = [text] + else: + texts = text + + if self.is_hf_tokenizer: + results = [self.tokenizer.encode(t) for t in texts] + else: + results = [self.tokenizer.tokenize(t) for t in texts] + + return results[0] if isinstance(text, str) else results + + def get_vocab(self): + return self.tokenizer.get_vocab() + + def get_pad_token(self) -> str: + return self.pad_token + + def get_unk_token(self) -> str: + return self.unk_token + + def convert_token_to_id(self, token: str) -> int: + if token is None: + return 0 + return self.tokenizer.convert_tokens_to_ids(token) + + +tokenizer_registry.update( + { + "sentencepiece": SentencePieceTokenizer, + "clip": CLIPTokenizer, + "gpt2bpe": GPT2BPETokenizer, + "bert": BERTTokenizer, + } +) + + +def get_hf_tokenizer(pretrained_model_name_or_path, **kwargs): + """Gets a HuggingFace-based tokenizer that follows HF convention. + + Args: + pretrained_model_name_or_path: Name of the model in the HF repo. Example: "bert-base-uncased". + Returns: + A HF tokenizer. + """ + model_name_lower = pretrained_model_name_or_path.lower() + # Use BERTTokenizer only for actual BERT models, not for models like albert/roberta + # that have "bert" in their name but use different tokenization (SentencePiece, BPE, etc.) + if "bert" in model_name_lower and not any( + x in model_name_lower for x in ("albert", "roberta", "distilbert", "modernbert") + ): + logger.info(f"Loading BERT tokenizer for {pretrained_model_name_or_path}") + return BERTTokenizer(pretrained_model_name_or_path=pretrained_model_name_or_path, is_hf_tokenizer=True) + + logger.info(f"Loading HuggingFace tokenizer for {pretrained_model_name_or_path}") + return HFTokenizer(pretrained_model_name_or_path) + + +tokenizer_registry.update( + { + "hf_tokenizer": get_hf_tokenizer, + } +) + + +def get_tokenizer_from_registry(tokenizer_name: str) -> torch.nn.Module: + """Returns the appropriate tokenizer from the tokenizer registry.""" + if tokenizer_name in tokenizer_registry: + return tokenizer_registry[tokenizer_name] + raise KeyError(f"Invalid tokenizer name: '{tokenizer_name}'. Available tokenizers: {tokenizer_registry.keys()}") diff --git a/ludwig/utils/torch_utils.py b/ludwig/utils/torch_utils.py new file mode 100644 index 0000000..a21dbbb --- /dev/null +++ b/ludwig/utils/torch_utils.py @@ -0,0 +1,421 @@ +import math +import os +import warnings +from abc import abstractmethod +from functools import lru_cache + +import torch +from torch import nn +from torch.nn import Module, ModuleDict + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ENCODER_OUTPUT +from ludwig.utils.entmax.activations import Entmax15, Sparsemax +from ludwig.utils.strings_utils import SpecialSymbol + +_TORCH_INIT_PARAMS: tuple | None = None + + +@DeveloperAPI +def get_torch_device(): + if torch.cuda.is_available() and torch.cuda.device_count() > 0: + # Use cublasLt for batched GEMM operations. The default cublas library has known + # bugs with cublasSgemmStridedBatched on certain GPU/driver combinations. + torch.backends.cuda.preferred_blas_library("cublaslt") + return "cuda" + + if bool(os.environ.get("LUDWIG_ENABLE_MPS")): + if torch.backends.mps.is_available() and torch.backends.mps.is_built(): + if not bool(os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK")): + warnings.warn( + "LUDWIG_ENABLE_MPS is set and MPS is available, but PYTORCH_ENABLE_MPS_FALLBACK has not been set. " + "Depending on your model config, some operations may not be compatible. If errors occur, try " + "setting `PYTORCH_ENABLE_MPS_FALLBACK=1` and resubmitting." + ) + return "mps" + else: + warnings.warn("LUDWIG_ENABLE_MPS is set but MPS is not available, falling back to CPU.") + + return "cpu" + + +DEVICE = get_torch_device() + + +@DeveloperAPI +def place_on_device(x, device): + """Recursively places the input on the specified device.""" + if isinstance(x, list): + return [place_on_device(xi, device) for xi in x] + elif isinstance(x, dict): + return {k: place_on_device(v, device) for k, v in x.items()} + elif isinstance(x, set): + return {place_on_device(xi, device) for xi in x} + elif isinstance(x, tuple): + return tuple(place_on_device(xi, device) for xi in x) + elif isinstance(x, torch.Tensor): + return x.to(device) + else: + return x + + +@DeveloperAPI +def sequence_length_2D(sequence: torch.Tensor) -> torch.Tensor: + """Returns the number of non-padding elements per sequence in batch. + + Args: + sequence: A 2D tensor of shape [batch size x max sequence length]. + + Returns: + The count of non-zero elements per sequence. + """ + used = (sequence != SpecialSymbol.PADDING.value).type(torch.int32) + length = torch.sum(used, 1) + return length + + +@DeveloperAPI +def sequence_length_3D(sequence: torch.Tensor) -> torch.Tensor: + """Returns the number of non-zero elements per sequence in batch. + + Args: + sequence: A 3D tensor of shape [batch size x max sequence length x hidden size]. + + Returns: + The count of non-zero elements per sequence. + """ + used = torch.sign(torch.amax(torch.abs(sequence), dim=2)) + length = torch.sum(used, 1) + length = length.int() + return length + + +@DeveloperAPI +def sequence_mask(lengths: torch.Tensor, maxlen: int | None = None, dtype: torch.dtype = torch.bool): + """Returns a mask of shape (batch_size x maxlen), where mask[i] is True for each element up to lengths[i], + otherwise False i.e. if maxlen=5 and lengths[i] = 3, mask[i] = [True, True True, False False]. + + Args: + lengths: A 1d integer tensor of shape [batch size]. + maxlen: The maximum sequence length. If not specified, the max(lengths) is used. + dtype: The type to output. + + Returns: + A sequence mask tensor of shape (batch_size x maxlen). + """ + if maxlen is None: + maxlen = lengths.max() + matrix = torch.unsqueeze(lengths, dim=-1) + row_vector = torch.arange(0, maxlen, 1, device=lengths.device) + mask = row_vector < matrix + mask = mask.type(dtype) + return mask + + +@DeveloperAPI +def periodic(inputs: torch.Tensor, period: int) -> torch.Tensor: + """Returns periodic representation assuming 0 is start of period.""" + return torch.cos(inputs * 2 * math.pi / period) + + +initializer_registry = { + "uniform": nn.init.uniform_, + "normal": nn.init.normal_, + "constant": nn.init.constant_, + "ones": nn.init.ones_, + "zeros": nn.init.zeros_, + "eye": nn.init.eye_, + "dirac": nn.init.dirac_, + "xavier_uniform": nn.init.xavier_uniform_, + "xavier_normal": nn.init.xavier_normal_, + "kaiming_uniform": nn.init.kaiming_uniform_, + "kaiming_normal": nn.init.kaiming_normal_, + "orthogonal": nn.init.orthogonal_, + "sparse": nn.init.sparse_, + "identity": nn.init.eye_, +} + + +class SwiGLU(nn.Module): + """SiLU-Gated Linear Unit (Shazeer, 2020). + + Used in LLaMA/PaLM. + Splits input in half along last dim, applies SiLU to gate half, multiplies. + Note: input_size must be 2x the desired output_size. + """ + + def forward(self, x): + x1, x2 = x.chunk(2, dim=-1) + return nn.functional.silu(x1) * x2 + + +class GeGLU(nn.Module): + """GELU-Gated Linear Unit (Shazeer, 2020). + + Used in T5 v1.1. Splits input in half along last dim, applies GELU to gate half, multiplies. + """ + + def forward(self, x): + x1, x2 = x.chunk(2, dim=-1) + return nn.functional.gelu(x1) * x2 + + +class ReGLU(nn.Module): + """ReLU-Gated Linear Unit (Shazeer, 2020). + + Splits input in half along last dim, applies ReLU to gate half, multiplies. + """ + + def forward(self, x): + x1, x2 = x.chunk(2, dim=-1) + return nn.functional.relu(x1) * x2 + + +activations = { + "elu": nn.ELU, + "leakyRelu": nn.LeakyReLU, + "logSigmoid": nn.LogSigmoid, + "relu": nn.ReLU, + "sigmoid": nn.Sigmoid, + "tanh": nn.Tanh, + "softmax": nn.Softmax, + "gelu": nn.GELU, + "silu": nn.SiLU, + "swish": nn.SiLU, + "mish": nn.Mish, + "selu": nn.SELU, + "prelu": nn.PReLU, + "relu6": nn.ReLU6, + "hardswish": nn.Hardswish, + "hardsigmoid": nn.Hardsigmoid, + "softplus": nn.Softplus, + "celu": nn.CELU, + "swiglu": SwiGLU, + "geglu": GeGLU, + "reglu": ReGLU, + "sparsemax": Sparsemax, + "entmax15": Entmax15, + None: nn.Identity, +} + + +@DeveloperAPI +def get_activation(activation): + return activations[activation]() + + +@DeveloperAPI +def reg_loss(model: nn.Module, regularizer: str, l1: float = 0.01, l2: float = 0.01): + """Computes the regularization loss for a given model. + + Parameters: + model: torch.nn.Module object to compute regularization loss for. + regularizer: regularizer to use (currently l1, l2 and l1_l2 supported). + l1: L1 regularization coefficient. + l2: L2 regularization coefficient. + + Returns: + Regularization loss for the model (float). + """ + + if regularizer == "l1": + l1_reg = l1 * sum(torch.abs(p).sum() for p in model.parameters()) + return l1_reg + if regularizer == "l2": + l2_reg = l2 * sum(torch.square(p).sum() for p in model.parameters()) + return l2_reg + if regularizer == "l1_l2": + l1_reg = l1 * sum(torch.abs(p).sum() for p in model.parameters()) + l2_reg = l2 * sum(torch.square(p).sum() for p in model.parameters()) + return l1_reg + l2_reg + + +@DeveloperAPI +class LudwigModule(Module): + def __init__(self): + super().__init__() + self._losses = {} + self.register_buffer("device_tensor", torch.zeros(0), persistent=False) + + @property + def device(self): + return self.device_tensor.device + + def prepare_for_training(self): + """This is called from within the Trainer object to do any final instantiation before model training.""" + + def losses(self): + collected_losses = [] + for loss in self._losses.values(): + collected_losses.append(loss) + + for child in self.children(): + if isinstance(child, LudwigModule): + collected_losses.extend(child.losses()) + elif isinstance(child, ModuleDict): + for c in child.values(): + if hasattr(c, "losses"): # Some modules, i.e. SequenceReducers, don't have losses. + collected_losses.extend(c.losses()) + elif isinstance(child, Module): + pass + else: + raise ValueError + + return collected_losses + + def update_loss(self, key: str, loss: torch.Tensor): + """This should be called in the forward pass to add a custom loss term to the combined loss.""" + self._losses[key] = loss + + @property + def input_dtype(self): + return torch.float32 + + @property + @abstractmethod + def input_shape(self) -> torch.Size: + """Returns size of the input tensor without the batch dimension.""" + # raise NotImplementedError("Abstract class.") + + @property + def output_shape(self) -> torch.Size: + """Returns size of the output tensor without the batch dimension.""" + return self._computed_output_shape() + + @lru_cache(maxsize=1) + def _computed_output_shape(self) -> torch.Size: + dummy_input = torch.rand(2, *self.input_shape, device=self.device) + output_tensor = self.forward(dummy_input.type(self.input_dtype)) + + if isinstance(output_tensor, torch.Tensor): + return output_tensor.size()[1:] + elif isinstance(output_tensor, dict) and ENCODER_OUTPUT in output_tensor: + return output_tensor[ENCODER_OUTPUT].size()[1:] + else: + raise ValueError( + f"Cannot determine output shape from encoder output of type {type(output_tensor).__name__}.\n" + f"Expected a torch.Tensor or a dict with key '{ENCODER_OUTPUT}'." + ) + + +def freeze_parameters(module: nn.Module): + """Freezes the parameters of a torch module.""" + for p in module.parameters(): + p.requires_grad = False + + +@DeveloperAPI +class FreezeModule(nn.Module): + def __init__(self, module: nn.Module, frozen: bool): + super().__init__() + if frozen: + freeze_parameters(module) + module.eval() + else: + module.train() + self.module = module + self.frozen = frozen + + def train(self, mode: bool = True): + if self.frozen: + # Ignores any attempt to set params trainable + return self + + return super().train(mode) + + +@DeveloperAPI +class Dense(LudwigModule): + def __init__( + self, + input_size, + output_size, + use_bias=True, + weights_initializer="xavier_uniform", + bias_initializer="zeros", + ): + super().__init__() + self.dense = nn.Linear(in_features=input_size, out_features=output_size, bias=use_bias) + weights_initializer = initializer_registry[weights_initializer] + weights_initializer(self.dense.weight) + + if use_bias: + bias_initializer = initializer_registry[bias_initializer] + bias_initializer(self.dense.bias) + + @property + def input_shape(self) -> torch.Size: + return self.dense.input_shape + + def forward(self, input: torch.Tensor) -> torch.Tensor: + output = torch.squeeze(self.dense(input), dim=-1) + return output + + +@DeveloperAPI +def initialize_pytorch( + gpus: int | str | list[int] | None = None, + gpu_memory_limit: float | None = None, + allow_parallel_threads: bool = True, +): + param_tuple = (gpus, gpu_memory_limit, allow_parallel_threads) + if _TORCH_INIT_PARAMS is not None: + if param_tuple != _TORCH_INIT_PARAMS: + warnings.warn( + "PyTorch has already been initialized. Changes to `gpus`, " + "`gpu_memory_limit`, and `allow_parallel_threads` will be ignored. " + "Start a new Python process to modify these values." + ) + return + + # For reproducivility / determinism, set parallel threads to 1. + # For performance, leave unset to allow PyTorch to select the best value automatically. + if not allow_parallel_threads: + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + if torch.cuda.is_available() and torch.cuda.device_count() > 0: + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + if isinstance(gpus, int): + gpus = [gpus] + elif isinstance(gpus, str): + gpus = gpus.strip() + gpus = [int(g) for g in gpus.split(",")] + + if gpus and len(gpus) == 1 and gpus[0] == -1: + # CUDA_VISIBLE_DEVICES syntax for disabling all GPUs + os.environ["CUDA_VISIBLE_DEVICES"] = "" + elif torch.cuda.is_available() and torch.cuda.device_count() > 0: + # Set visible devices so GPU utilization is isolated + # (no GPU contention between workers). + if gpus is not None: + if len(gpus) == 1: + torch.cuda.set_device(gpus[0]) + elif len(gpus) > 1: + os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(str(i) for i in gpus) + + # Limit the amount of memory that can be consumed per GPU + if gpu_memory_limit is not None: + for gpu in gpus or range(torch.cuda.device_count()): + torch.cuda.memory.set_per_process_memory_fraction(gpu_memory_limit, gpu) + + _set_torch_init_params(param_tuple) + + +def _set_torch_init_params(params: tuple | None): + global _TORCH_INIT_PARAMS + _TORCH_INIT_PARAMS = params + + +def _get_torch_init_params() -> tuple | None: + return _TORCH_INIT_PARAMS + + +@DeveloperAPI +def model_size(model: nn.Module): + """Computes PyTorch model size in bytes.""" + size = 0 + size += sum(param.nelement() * param.element_size() for param in model.parameters()) + size += sum(buffer.nelement() * buffer.element_size() for buffer in model.buffers()) + return size diff --git a/ludwig/utils/trainer_utils.py b/ludwig/utils/trainer_utils.py new file mode 100644 index 0000000..1d19191 --- /dev/null +++ b/ludwig/utils/trainer_utils.py @@ -0,0 +1,594 @@ +import logging +import re +import time +from collections import defaultdict +from typing import TYPE_CHECKING + +try: + from typing import Literal +except ImportError: + from typing import Literal + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import AUTO, COMBINED, LOSS +from ludwig.models.base import BaseModel +from ludwig.models.ecd import ECD +from ludwig.models.llm import LLM +from ludwig.modules.metric_modules import get_best_function +from ludwig.schema.trainer import ECDTrainerConfig, FineTuneTrainerConfig +from ludwig.utils.data_utils import save_json +from ludwig.utils.metric_utils import TrainerMetric + +if TYPE_CHECKING: + from ludwig.features.base_feature import OutputFeature + from ludwig.schema.trainer import BaseTrainerConfig + + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def initialize_trainer_metric_dict(output_features) -> dict[str, dict[str, list[TrainerMetric]]]: + """Returns a dict of dict of metrics, output_feature_name -> metric_name -> List[TrainerMetric].""" + metrics = defaultdict(lambda: defaultdict(list)) + return metrics + + +def get_latest_metrics_dict( + progress_tracker_metrics: dict[str, dict[str, list[TrainerMetric]]], +) -> dict[str, dict[str, float]]: + """Returns a dict of field name -> metric name -> latest metric value.""" + latest_metrics_dict = defaultdict(dict) + for feature_name, metrics_dict in progress_tracker_metrics.items(): + for metric_name, metrics in metrics_dict.items(): + if metrics: + # Metrics may be missing if computing metrics was excepted, if the metrics are entirely empty + # due to a missing subset, or if evaluate_training_set is False. + latest_metrics_dict[feature_name][metric_name] = metrics[-1][-1] + return latest_metrics_dict + + +@DeveloperAPI +def get_new_progress_tracker( + batch_size: int, + best_eval_metric_value: float, + best_increase_batch_size_eval_metric: float, + learning_rate: float, + output_features: dict[str, "OutputFeature"], +): + """Returns a new instance of a ProgressTracker with empty metrics.""" + return ProgressTracker( + epoch=0, + batch_size=batch_size, + steps=0, + tune_checkpoint_num=0, + checkpoint_number=0, + best_eval_metric_steps=0, + best_eval_metric_epoch=0, + best_eval_metric_checkpoint_number=0, + last_learning_rate_reduction_steps=0, + last_increase_batch_size_steps=0, + last_improvement_steps=0, + best_eval_metric_value=best_eval_metric_value, + best_increase_batch_size_eval_metric=best_increase_batch_size_eval_metric, + last_increase_batch_size_eval_metric_improvement=0, + learning_rate=learning_rate, + num_reductions_learning_rate=0, + num_increases_batch_size=0, + train_metrics=initialize_trainer_metric_dict(output_features), + validation_metrics=initialize_trainer_metric_dict(output_features), + test_metrics=initialize_trainer_metric_dict(output_features), + last_learning_rate_reduction=0, + last_increase_batch_size=0, + best_eval_train_metrics={}, + best_eval_validation_metrics={}, + best_eval_test_metrics={}, + llm_eval_examples={}, + checkpoint_to_step={}, + checkpoint_to_epoch={}, + incremental_step_token_usage={}, + cumulative_step_token_usage={}, + incremental_checkpoint_token_usage={}, + cumulative_checkpoint_token_usage={}, + total_tokens_used=0, + ) + + +@DeveloperAPI +class ProgressTracker: + def __init__( + self, + epoch: int, + batch_size: int, + steps: int, + tune_checkpoint_num: int, + checkpoint_number: int, + best_eval_metric_steps: int, + best_eval_metric_epoch: int, + best_eval_metric_checkpoint_number: int, + last_improvement_steps: int, + last_learning_rate_reduction_steps: int, + last_increase_batch_size_steps: int, + best_eval_metric_value: float, + best_increase_batch_size_eval_metric: float, + last_increase_batch_size_eval_metric_improvement: int, + learning_rate: float, + num_reductions_learning_rate: int, + num_increases_batch_size: int, + train_metrics: dict[str, dict[str, list[TrainerMetric]]], + validation_metrics: dict[str, dict[str, list[TrainerMetric]]], + test_metrics: dict[str, dict[str, list[TrainerMetric]]], + last_learning_rate_reduction: int, + last_increase_batch_size: int, + best_eval_train_metrics: dict[str, dict[str, float]], + best_eval_validation_metrics: dict[str, dict[str, float]], + best_eval_test_metrics: dict[str, dict[str, float]], + llm_eval_examples: dict[str, list[str]] | None = None, + checkpoint_to_step: dict[str, int] | None = None, + checkpoint_to_epoch: dict[str, int] | None = None, + incremental_step_token_usage: dict[str, int] | None = None, + cumulative_step_token_usage: dict[str, int] | None = None, + incremental_checkpoint_token_usage: dict[str, int] | None = None, + cumulative_checkpoint_token_usage: dict[str, int] | None = None, + total_tokens_used: int = 0, + steps_per_epoch: int = 0, + total_steps: int = 0, + training_start_time: float = 0.0, + ): + """JSON-serializable holder object that stores information related to training progress. + + [train/vali/test]_metrics is a nested dictionary of TrainerMetrics: feature_name -> metric_name -> + List[TrainerMetrics], with one entry per training checkpoint. + + When the model is saved, all of the progress tracker's attributes are serialized to JSON as + `training_progress.json` under the model output directory. + + JSON serialization automatically converts all dictionary top-level keys to strings, and the string typing + is preserved when the progress tracker is deserialized from JSON when model resumes training from a checkpoint. + + For this reason, all of the dictionary attributes of the progress tracker are keyed by strings to ensure a + consistent interface before or after deserialization. For example, the `tokens` dictionaries are keyed by steps, + as strings. + + When the progress tracker is deserialized from JSON like when a model resumes training from a checkpoint, the + TrainerMetrics namedtuples are automatically converted into regular (epoch, steps, value) tuples, which is why + in trainer.py, we often use `[-1]` to index into the last element of the TrainerMetric namedtuple to get the + actual metric value instead of the named field. + + Args: + epoch: The current epoch number. + steps: The current step of training. + batch_size: The current batch size. + tune_checkpoint_num: The hyperopt checkpoint number (Ray Tune). + checkpoint_number: The current checkpoint number. + + best_eval_metric_steps: The step of training that has the best evaluation so far. + best_eval_metric_epoch: The epoch of training that has the best evaluation so far. + best_eval_metric_checkpoint_number: The checkpoint number that has the best evaluation so far. + + last_improvement_steps: The number of steps since the last improvement. + last_learning_rate_reduction_steps: The training step of the last learning rate reduction. + last_increase_batch_size_steps: The training_step of the the last batch size increase. + + best_eval_metric_value: The metric value of the best evaluation so far. + best_increase_batch_size_eval_metric: + The metric value of the best evaluation so far, for increasing the batch size. + + last_learning_rate_reduction: The number of steps since the last learning rate reduction. + last_increase_batch_size: The number of steps since the last batch size increase. + + last_increase_batch_size_eval_metric_improvement: + The number of checkpoints since the last batch size increase. + + num_reductions_learning_rate: The number of total reductions in learning rate. + num_increases_batch_size: The number of total increases in batch size. + + train_metrics: Training metrics. -> -> History of metrics. + validation_metrics: Validation metrics. -> -> History of metrics. + test_metrics: Test metrics. -> -> History of metrics. + + best_eval_train_metrics: + Best eval train metrics: -> -> . + best_eval_validation_metrics: + Best eval validation metrics: -> -> . + best_eval_test_metrics: + Best eval test metrics: -> -> . + + llm_eval_examples: + Dictionary whose keys are "inputs", "targets", and "outputs" and whose values are dicts. + The keys of each subdict are the names of the input/target/output features and the values are lists of + example tensors. This is only set for LLM fine-tuning. + + checkpoint_to_step: Map of checkpoint number to step number. + checkpoint_to_epoch: Map of checkpoint number to epoch number. + + incremental_step_token_usage: Map of step number to number of tokens used in that step. + cumulative_step_token_usage: Map of step number to cumulative number of tokens used up to that step. + incremental_checkpoint_token_usage: Map of checkpoint number to number of tokens used up to that checkpoint + since the last checkpoint. + cumulative_checkpoint_token_usage: Map of checkpoint number to cumulative number of tokens used up to that + checkpoint. + total_tokens_used: Total number of tokens used. + """ + self.batch_size = batch_size + self.epoch = epoch + self.steps = steps + self.tune_checkpoint_num = tune_checkpoint_num + self.checkpoint_number = checkpoint_number + self.best_eval_metric_steps = best_eval_metric_steps + self.best_eval_metric_epoch = best_eval_metric_epoch + self.best_eval_metric_checkpoint_number = best_eval_metric_checkpoint_number + self.last_improvement_steps = last_improvement_steps + self.last_learning_rate_reduction_steps = last_learning_rate_reduction_steps + self.last_learning_rate_reduction = last_learning_rate_reduction + self.last_increase_batch_size_steps = last_increase_batch_size_steps + self.last_increase_batch_size = last_increase_batch_size + self.learning_rate = learning_rate + self.best_eval_metric_value = best_eval_metric_value + self.best_increase_batch_size_eval_metric = best_increase_batch_size_eval_metric + self.last_increase_batch_size_eval_metric_improvement = last_increase_batch_size_eval_metric_improvement + self.num_reductions_learning_rate = num_reductions_learning_rate + self.num_increases_batch_size = num_increases_batch_size + self.train_metrics = train_metrics + self.validation_metrics = validation_metrics + self.test_metrics = test_metrics + + # This should be an dictionary whose keys are "inputs", "targets", and "outputs" and whose values are dicts. + # The keys of each subdict are the names of the input/target/output features and the values are lists of + # example tensors. This is only set for LLM fine-tuning. + self.llm_eval_examples = llm_eval_examples + + # Best metrics. + self.best_eval_train_metrics = best_eval_train_metrics + self.best_eval_validation_metrics = best_eval_validation_metrics + self.best_eval_test_metrics = best_eval_test_metrics + + # Checkpoint tracking. + self.checkpoint_to_step = checkpoint_to_step + self.checkpoint_to_epoch = checkpoint_to_epoch + + # Token usage. + self.incremental_step_token_usage = incremental_step_token_usage + self.cumulative_step_token_usage = cumulative_step_token_usage + self.incremental_checkpoint_token_usage = incremental_checkpoint_token_usage + self.cumulative_checkpoint_token_usage = cumulative_checkpoint_token_usage + self.total_tokens_used = total_tokens_used + + # Progress / ETA fields (not persisted to JSON — recomputed on resume). + self.steps_per_epoch = steps_per_epoch + self.total_steps = total_steps + self.training_start_time = training_start_time + + @property + def eta_seconds(self) -> float | None: + """Estimated seconds until training completes. None until at least one step has run.""" + if self.total_steps <= 0 or self.steps <= 0 or self.training_start_time <= 0: + return None + elapsed = time.monotonic() - self.training_start_time + rate = self.steps / elapsed # steps per second + remaining = self.total_steps - self.steps + return remaining / rate if rate > 0 else None + + @property + def progress_pct(self) -> float: + """Training progress as a fraction in [0, 1]. 0 until total_steps is set.""" + if self.total_steps <= 0: + return 0.0 + return min(self.steps / self.total_steps, 1.0) + + def save(self, filepath): + _EPHEMERAL = {"steps_per_epoch", "total_steps", "training_start_time"} + # sort_keys=False to ensure that token usage dictionaries (keyed by integers) are encodable. + save_json(filepath, {k: v for k, v in self.__dict__.items() if k not in _EPHEMERAL}) + + @staticmethod + def load(progress_tracking_dict: dict): + from ludwig.utils.backward_compatibility import upgrade_model_progress + + loaded = upgrade_model_progress(progress_tracking_dict) + return ProgressTracker(**loaded) + + def log_metrics(self): + log_metrics = { + "batch_size": self.batch_size, + "epoch": self.epoch, + "steps": self.steps, + "tune_checkpoint_num": self.tune_checkpoint_num, + "checkpoint_number": self.checkpoint_number, + "last_improvement_steps": self.last_improvement_steps, + "best_eval_metric_steps": self.best_eval_metric_steps, + "best_eval_metric_epoch": self.best_eval_metric_epoch, + "best_eval_metric_checkpoint_number": self.best_eval_metric_checkpoint_number, + "learning_rate": self.learning_rate, + "best_valid_metric": self.best_eval_metric_value, + "num_reductions_lr": self.num_reductions_learning_rate, + "num_increases_bs": self.num_increases_batch_size, + "total_tokens_used": self.total_tokens_used, + } + + # This is a non-numerical metric that is only for LLM fine-tuning + # This should be an dictionary whose keys are "inputs", "targets", and "outputs" and whose values are dicts. + # The keys of each subdict are the names of the input/target/output features and the values are lists of + # example tensors. + if self.llm_eval_examples: + log_metrics["llm_eval_examples"] = self.llm_eval_examples + + for metrics_dict_name in [ + "train_metrics", + "validation_metrics", + "test_metrics", + ]: + metrics_dict = getattr(self, metrics_dict_name) + for feature_name in metrics_dict: + for metric_name, metrics_tuples in metrics_dict[feature_name].items(): + if metrics_tuples: + # For logging, get the latest metrics. The second "-1" indexes into the TrainerMetric + # namedtuple. The last element of the TrainerMetric namedtuple is the actual metric value. + # + # TODO: when loading an existing model, this loses metric values for all but the last epoch. + log_metrics[f"{metrics_dict_name}.{feature_name}.{metric_name}"] = metrics_tuples[-1][-1] + + # Add best metrics. + for feature_name, metrics in self.best_eval_train_metrics.items(): + for metric_name, metric_value in metrics.items(): + log_metrics[f"best.train_metrics.{feature_name}.{metric_name}"] = metric_value + for feature_name, metrics in self.best_eval_validation_metrics.items(): + for metric_name, metric_value in metrics.items(): + log_metrics[f"best.validation_metrics.{feature_name}.{metric_name}"] = metric_value + for feature_name, metrics in self.best_eval_test_metrics.items(): + for metric_name, metric_value in metrics.items(): + log_metrics[f"best.test_metrics.{feature_name}.{metric_name}"] = metric_value + + return log_metrics + + def _add_checkpoint_entry_for_used_tokens(self, checkpoint_number: int): + """Adds an entry to the token usage dictionaries for the given checkpoint number. + + Assumes that the token usage dictionaries for steps are filled. + """ + self.cumulative_checkpoint_token_usage[str(checkpoint_number)] = self.total_tokens_used + + if checkpoint_number <= 0: + raise ValueError("Checkpoint number should be greater than 0.") + + if checkpoint_number == 1: + # The incremental token usage for checkpoint 0 is the same as the total tokens used so far. + self.incremental_checkpoint_token_usage[str(checkpoint_number)] = self.total_tokens_used + else: + # The incremental token usage for this checkpoint is the total tokens used minus the cumulative tokens used + # up to the previous checkpoint. + previous_checkpoint_number = checkpoint_number - 1 + + tokens_used_since_previous_checkpoint = ( + self.total_tokens_used - self.cumulative_checkpoint_token_usage[str(previous_checkpoint_number)] + ) + self.incremental_checkpoint_token_usage[str(checkpoint_number)] = tokens_used_since_previous_checkpoint + + def increment_checkpoint(self): + """Update the progress tracker for a new checkpoint.""" + self.checkpoint_number += 1 + + # Set checkpoint -> step/epoch lookup maps. + self.checkpoint_to_step[str(self.checkpoint_number)] = self.steps + self.checkpoint_to_epoch[str(self.checkpoint_number)] = self.epoch + + # Set checkpoint -> used tokens lookup maps. + self._add_checkpoint_entry_for_used_tokens(self.checkpoint_number) + + def set_token_usage_for_this_step(self, used_tokens: int): + """Update the token usage for the current step.""" + steps_str = str(self.steps) + self.incremental_step_token_usage[steps_str] = used_tokens + self.total_tokens_used += used_tokens + self.cumulative_step_token_usage[steps_str] = self.total_tokens_used + + +@DeveloperAPI +def append_metrics( + model: BaseModel, + dataset_name: Literal["train", "validation", "test"], + results: dict[str, dict[str, float]], + metrics_log: dict[str, dict[str, list[TrainerMetric]]], + progress_tracker: ProgressTracker, +) -> dict[str, dict[str, list[TrainerMetric]]]: + epoch = progress_tracker.epoch + steps = progress_tracker.steps + for output_feature in model.output_features: + scores = [dataset_name] + + # collect metric names based on output features metrics to + # ensure consistent order of reporting metrics + metric_names = sorted(results[output_feature].keys()) + + for metric in metric_names: + if metric in results[output_feature]: + # Some metrics may have been excepted and excluded from results. + score = results[output_feature][metric] + metrics_log[output_feature][metric].append(TrainerMetric(epoch=epoch, step=steps, value=score)) + scores.append(score) + + metrics_log[COMBINED][LOSS].append(TrainerMetric(epoch=epoch, step=steps, value=results[COMBINED][LOSS])) + return metrics_log + + +@DeveloperAPI +def get_total_steps(epochs: int, steps_per_epoch: int, train_steps: int): + """Returns train_steps if provided, otherwise epochs * steps_per_epoch.""" + if train_steps: + return train_steps + return epochs * steps_per_epoch + + +@DeveloperAPI +def get_final_steps_per_checkpoint( + steps_per_epoch: int, steps_per_checkpoint: int = 0, checkpoints_per_epoch: float = 0, should_log: bool = False +): + """Returns the steps per checkpoint to use for the training loop, given user+default inputs.""" + if steps_per_checkpoint != 0 and checkpoints_per_epoch != 0: + raise ValueError( + "It is invalid to specify both checkpoints_per_epoch AND steps_per_checkpoint. Please specify one or the " + "other, or specify neither to checkpoint/eval the model every epoch." + ) + + # Set steps_per_checkpoint based on the checkpoints_per_epoch, if checkpoints_per_epoch was specified. + if checkpoints_per_epoch != 0: + steps_per_checkpoint = int(steps_per_epoch / checkpoints_per_epoch) + + # Cap steps_per_checkpoint at steps_per_epoch. + if steps_per_checkpoint > steps_per_epoch: + if should_log: + logger.info( + f"Note: steps_per_checkpoint (was {steps_per_checkpoint}) is now set to the number of " + f"steps per epoch: {steps_per_epoch}.\n" + ) + return steps_per_epoch + + # steps_per_checkpoint wasn't specified. Use steps_per_epoch. + if steps_per_checkpoint == 0: + return steps_per_epoch + + return steps_per_checkpoint + + +def get_total_expected_checkpoints(total_steps: int, final_steps_per_checkpoint: int, epochs: int) -> int: + return total_steps // final_steps_per_checkpoint + epochs + + +@DeveloperAPI +def get_training_report( + validation_field: str, + validation_metric: str, + include_test_set: bool, + train_valiset_stats: dict[str, dict[str, list[float]]], + train_testset_stats: dict[str, dict[str, list[float]]], +) -> list[tuple[str, str]]: + """Returns a training report in the form of a list [(report item, value)].""" + validation_field_result = train_valiset_stats[validation_field] + best_function = get_best_function(validation_metric) + + training_report = [] + ( + best_vali_index, + ( + epoch_best_validation_metric, + step_best_validation_metric, + best_validation_metric, + ), + ) = best_function( + enumerate(validation_field_result[validation_metric]), + # -1 for the last element of the TrainerMetric namedtuple. + key=lambda index_epoch_step_value: index_epoch_step_value[1][-1], + ) + training_report.append(["Validation feature", validation_field]) + training_report.append(["Validation metric", validation_metric]) + training_report.append(["Best model step", step_best_validation_metric]) + training_report.append(["Best model epoch", epoch_best_validation_metric + 1]) + training_report.append( + [ + f"Best model's validation {validation_metric}", + best_validation_metric, + ] + ) + if include_test_set: + validation_selected_test_metric_score = train_testset_stats[validation_field][validation_metric][ + best_vali_index + ][-1] # -1 for the last element of the TrainerMetric namedtuple. + + training_report.append( + [ + f"Best model's test {validation_metric}", + validation_selected_test_metric_score, + ] + ) + return training_report + + +def get_rendered_batch_size_grad_accum(config: "BaseTrainerConfig", num_workers: int) -> tuple[int, int]: + """Returns the batch size and gradient accumulation steps to use for training. + + For batch_size==AUTO: + 1. effective_batch_size is not AUTO and gradient_accumulation_steps is not AUTO: + batch size is set to the effective batch size divided by the gradient accumulation steps, divided by the + number of workers. + 2. effective_batch_size is AUTO or gradient_accumulation_steps is AUTO: + batch size remains AUTO. + + For gradient_accumulation_steps==AUTO: + 1. batch size is AUTO: + gradient accumulation steps remains AUTO. + 2. batch_size is not AUTO and effective batch size is not AUTO: + gradient accumulation steps is set to the effective batch size divided by the batch size, divided by the number + of workers. + 3. batch size is not AUTO and effective batch size is AUTO: + gradient accumulation steps is set to 1. + """ + effective_batch_size = config.effective_batch_size + batch_size = config.batch_size + gradient_accumulation_steps = config.gradient_accumulation_steps + + if config.batch_size == AUTO: + if config.effective_batch_size != AUTO and config.gradient_accumulation_steps != AUTO: + batch_size = max(int(effective_batch_size / gradient_accumulation_steps / num_workers), 1) + + if config.gradient_accumulation_steps == AUTO: + if config.batch_size != AUTO: + if config.effective_batch_size != AUTO: + gradient_accumulation_steps = max(int(effective_batch_size / batch_size / num_workers), 1) + else: + gradient_accumulation_steps = 1 + + return batch_size, gradient_accumulation_steps + + +def freeze_layers_regex(config: ECDTrainerConfig | FineTuneTrainerConfig, model: ECD | LLM) -> None: + """Freezes layers in a model whose names match a specified regular expression pattern. + + This function iterates over all parameters of the model, checking each parameter's name against + the regular expression defined in the configuration object. + If a match is found, the parameter's `requires_grad` attribute is set to False, + effectively freezing the layer for training purposes. + If no matches are found, an error is logged indicating the issue with the regex or the model's layer names. + + Parameters: + - config (Union[ECDTrainerConfig, FineTuneTrainerConfig]): + - model (Union[ECD, LLM]): The model object containing layers and parameters. This could be an instance of either + ECD or LLM classes, which should have a method `named_parameters()` that yields the name and parameter + object of each layer. + + Raises: + - re.error: If the regular expression pattern in `config.layers_to_freeze_regex` is invalid, an error is logged + and the function exits. + + Returns: + - None: This function does not return any value but modifies the model in-place by freezing certain layers. + """ + pattern = re.compile(config.layers_to_freeze_regex) + matched_layers = set() + + for name, p in model.named_parameters(): + if re.search(pattern, str(name)): + p.requires_grad = False + matched_layers.add(name) + if matched_layers: + logger.info(f"Layers where requires_grad was set to False: {matched_layers}") + else: + logger.error(f"No regex match for {config.layers_to_freeze_regex}! Check layer names and regex syntax.") + + count_parameters(model) + + +def count_parameters(model) -> None: + """Counts number of trainable parameters post freezing. + + Returns: + - None: This function does not return any value. + """ + total_params = 0 + for _, parameter in model.named_parameters(): + if not parameter.requires_grad: + continue + params = parameter.numel() + + total_params += params + + logger.info(f"Total Trainable Parameters after freezing: {total_params}") diff --git a/ludwig/utils/training_report.py b/ludwig/utils/training_report.py new file mode 100644 index 0000000..f37ed53 --- /dev/null +++ b/ludwig/utils/training_report.py @@ -0,0 +1,172 @@ +"""Generate a structured training report JSON. + +Captures the full provenance of a training run: config, data schema, +metrics, hardware, timing, and Ludwig version. Useful for audit trails, +compliance documentation, and reproducibility. +""" + +import logging +import os +import platform +from datetime import datetime, UTC + +logger = logging.getLogger(__name__) + + +def generate_training_report( + config: dict, + training_set_metadata: dict, + train_stats=None, + output_directory: str | None = None, + model_dir: str | None = None, + dataset_statistics: list | None = None, + random_seed: int | None = None, + training_time_seconds: float | None = None, +) -> dict: + """Generate a structured training report. + + Args: + config: The full Ludwig config dict. + training_set_metadata: Feature metadata computed during preprocessing. + train_stats: Training statistics (train/validation/test metrics per epoch). + output_directory: Path to the experiment output directory. + model_dir: Path where the model is saved. + dataset_statistics: Dataset split sizes. + random_seed: Random seed used for training. + training_time_seconds: Total training time. + + Returns: + Dict with full training provenance. + """ + report = { + "report_version": "1.0", + "generated_at": datetime.now(UTC).isoformat(), + } + + # Environment + env = {"python_version": platform.python_version(), "platform": platform.platform()} + try: + import ludwig + + env["ludwig_version"] = ludwig.__version__ + except ImportError: + pass + try: + import torch + + env["pytorch_version"] = torch.__version__ + if torch.cuda.is_available(): + env["gpu"] = torch.cuda.get_device_name(0) + env["gpu_count"] = torch.cuda.device_count() + env["cuda_version"] = torch.version.cuda + except ImportError: + pass + report["environment"] = env + + # Config + report["config"] = config + report["model_type"] = config.get("model_type", "ecd") + report["random_seed"] = random_seed + + # Data schema: what features were used, their types, and key metadata + data_schema = {"input_features": [], "output_features": []} + for feat in config.get("input_features", []): + feat_info = {"name": feat["name"], "type": feat["type"]} + meta = training_set_metadata.get(feat["name"], {}) + if isinstance(meta, dict): + if "mean" in meta: + feat_info["mean"] = meta["mean"] + feat_info["std"] = meta.get("std") + if "idx2str" in meta: + feat_info["vocab_size"] = len(meta["idx2str"]) + data_schema["input_features"].append(feat_info) + + for feat in config.get("output_features", []): + feat_info = {"name": feat["name"], "type": feat["type"]} + meta = training_set_metadata.get(feat["name"], {}) + if isinstance(meta, dict): + if "idx2str" in meta: + feat_info["vocab_size"] = len(meta["idx2str"]) + feat_info["classes"] = meta["idx2str"] + data_schema["output_features"].append(feat_info) + report["data_schema"] = data_schema + + # Dataset statistics + if dataset_statistics: + ds_stats = {} + for row in dataset_statistics: + if isinstance(row, (list, tuple)) and len(row) >= 2: + ds_stats[str(row[0])] = row[1] + report["dataset_statistics"] = ds_stats + + # Training metrics: best value per metric per feature per split + if train_stats is not None: + metrics = {} + for split_name, split_attr in [("training", "training"), ("validation", "validation"), ("test", "test")]: + split_data = getattr(train_stats, split_attr, None) + if split_data: + split_metrics = {} + for feat_name, feat_metrics in split_data.items(): + if isinstance(feat_metrics, dict): + feat_best = {} + for metric_name, values in feat_metrics.items(): + if isinstance(values, list) and values: + if "loss" in metric_name or "error" in metric_name: + feat_best[metric_name] = {"best": min(values), "last": values[-1]} + else: + feat_best[metric_name] = {"best": max(values), "last": values[-1]} + if feat_best: + split_metrics[feat_name] = feat_best + if split_metrics: + metrics[split_name] = split_metrics + report["metrics"] = metrics + + # Epochs trained + combined = getattr(train_stats, "training", {}) + if isinstance(combined, dict): + combined_metrics = combined.get("combined", {}) + loss_values = combined_metrics.get("loss", []) + if loss_values: + report["epochs_trained"] = len(loss_values) + + # Timing + if training_time_seconds is not None: + report["training_time_seconds"] = round(training_time_seconds, 2) + + # Paths + if output_directory: + report["output_directory"] = output_directory + if model_dir: + report["model_directory"] = model_dir + + return report + + +def save_training_report( + output_directory: str, + config: dict, + training_set_metadata: dict, + train_stats=None, + model_dir: str | None = None, + dataset_statistics: list | None = None, + random_seed: int | None = None, + training_time_seconds: float | None = None, +): + """Generate and save a training report JSON to the output directory.""" + from ludwig.utils.data_utils import save_json + + report = generate_training_report( + config=config, + training_set_metadata=training_set_metadata, + train_stats=train_stats, + output_directory=output_directory, + model_dir=model_dir, + dataset_statistics=dataset_statistics, + random_seed=random_seed, + training_time_seconds=training_time_seconds, + ) + + report_path = os.path.join(output_directory, "training_report.json") + save_json(report_path, report) + logger.info(f"Training report saved to {report_path}") + return report_path diff --git a/ludwig/utils/types.py b/ludwig/utils/types.py new file mode 100644 index 0000000..d936ac1 --- /dev/null +++ b/ludwig/utils/types.py @@ -0,0 +1,16 @@ +import pandas as pd +import torch + +try: + import dask.dataframe as dd + + DataFrame = pd.DataFrame | dd.DataFrame + Series = pd.Series | dd.Series +except ImportError: + DataFrame = pd.DataFrame + Series = pd.Series + +# torchaudio.load returns the audio tensor and the sampling rate as a tuple. +TorchAudioTuple = tuple[torch.Tensor, int] +PreprocessingInput = list[str] | list[torch.Tensor] | list[TorchAudioTuple] | torch.Tensor +TorchDevice = str | torch.device diff --git a/ludwig/utils/upload_utils.py b/ludwig/utils/upload_utils.py new file mode 100644 index 0000000..8576c95 --- /dev/null +++ b/ludwig/utils/upload_utils.py @@ -0,0 +1,310 @@ +from __future__ import annotations + +import logging +import os +from abc import ABC, abstractmethod + +from huggingface_hub import HfApi, login +from huggingface_hub.hf_api import CommitInfo + +from ludwig.globals import ( + MODEL_FILE_NAME, + MODEL_HYPERPARAMETERS_FILE_NAME, + model_weights_exist, + MODEL_WEIGHTS_FILE_NAME, +) + +logger = logging.getLogger(__name__) + + +class BaseModelUpload(ABC): + """Abstract base class for uploading trained model artifacts to different repositories. + + This class defines the interface for uploading trained model artifacts to various repositories such as Huggingface + Hub, without specifying the concrete implementation for each repository. Subclasses of this base class must + implement the 'login' and 'upload' methods. + """ + + @abstractmethod + def login(self): + """Abstract method to handle authentication with the target repository. + + Subclasses must implement this method to provide the necessary authentication + mechanisms required by the repository where the model artifacts will be uploaded. + + Raises: + NotImplementedError: If this method is not implemented in the subclass. + """ + raise NotImplementedError() + + @abstractmethod + def upload( + self, + repo_id: str, + model_path: str, + repo_type: str | None = None, + private: bool | None = False, + commit_message: str | None = None, + commit_description: str | None = None, + dataset_file: str | None = None, + dataset_name: str | None = None, + ) -> bool: + """Abstract method to upload trained model artifacts to the target repository. + + Subclasses must implement this method to define the process of pushing model + artifacts to the respective repository. This may include creating a new model version, + uploading model files, and any other specific steps required by the model repository + service. + + Returns: + bool: True if the model artifacts were successfully uploaded, False otherwise. + + Raises: + NotImplementedError: If this method is not implemented in the subclass. + """ + raise NotImplementedError() + + @staticmethod + def _validate_upload_parameters( + repo_id: str, + model_path: str, + repo_type: str | None = None, + private: bool | None = False, + commit_message: str | None = None, + commit_description: str | None = None, + ): + """Validate parameters before uploading trained model artifacts. + + This method checks if the input parameters meet the necessary requirements before uploading + trained model artifacts to the target repository. + + Args: + repo_id (str): The ID of the target repository. Each provider will verify their specific rules. + model_path (str): The path to the directory containing the trained model artifacts. + This is the parent-folder of the folder where the 'model_weights' folder and the + 'model_hyperparameters.json' file are stored. + repo_type (str, optional): The type of the repository. Not used in the base class, but subclasses + may use it for specific repository implementations. Defaults to None. + private (bool, optional): Whether the repository should be private or not. Not used in the base class, + but subclasses may use it for specific repository implementations. Defaults to False. + commit_message (str, optional): A message to attach to the commit when uploading to version control + systems. Not used in the base class, but subclasses may use it for specific repository + implementations. Defaults to None. + commit_description (str, optional): A description of the commit when uploading to version control + systems. Not used in the base class, but subclasses may use it for specific repository + implementations. Defaults to None. + + Raises: + FileNotFoundError: If the model_path does not exist. + Exception: If the trained model artifacts are not found at the expected location within model_path, or + if the artifacts are not in the required format (i.e., 'pytorch_model.bin'; or 'adapter_model.bin' or + 'adapter_model.safetensors'). + """ + # Make sure the model's save path is actually a valid path + if not os.path.exists(model_path): + raise FileNotFoundError(f"The path '{model_path}' does not exist.") + + # Make sure the model is actually trained + model_dir = os.path.join(model_path, MODEL_FILE_NAME) + if not model_weights_exist(model_dir): + raise Exception( + f"Model artifacts not found at {model_dir}. " + f"It is possible that model at '{model_path}' hasn't been trained yet, or something went" + "wrong during training where the model's weights were not saved." + ) + + +def hf_hub_login(): + """Login to huggingface hub using the token stored in ~/.cache/huggingface/token and returns a HfApi client + object that can be used to interact with HF Hub.""" + cached_token_path = os.path.join(os.path.expanduser("~"), ".cache", "huggingface", "token") + + if not os.path.exists(cached_token_path): + login(add_to_git_credential=True) + + with open(cached_token_path) as f: + hf_token = f.read() + + hf_api = HfApi(token=hf_token) + if hf_api.token != hf_token: + raise RuntimeError( + "HuggingFace API token mismatch after initialization. " + "This is an internal error — try re-authenticating with `huggingface-cli login`." + ) + + return hf_api + + +class HuggingFaceHub(BaseModelUpload): + def __init__(self): + self.api = None + self.login() + + def login(self): + """Login to huggingface hub using the token stored in ~/.cache/huggingface/token and return a HfApi client + object that can be used to interact with HF Hub.""" + self.api = hf_hub_login() + + @staticmethod + def _validate_upload_parameters( + repo_id: str, + model_path: str, + repo_type: str | None = None, + private: bool | None = False, + commit_message: str | None = None, + commit_description: str | None = None, + ): + """Validate parameters before uploading trained model artifacts. + + This method checks if the input parameters meet the necessary requirements before uploading + trained model artifacts to the target repository. + + Args: + repo_id (str): The ID of the target repository. It must be a namespace (user or an organization) + and a repository name separated by a '/'. For example, if your HF username is 'johndoe' and you + want to create a repository called 'test', the repo_id should be 'johndoe/test'. + model_path (str): The path to the directory containing the trained model artifacts. + This is the parent-folder of the folder where the 'model_weights' folder and the + 'model_hyperparameters.json' file are stored. + repo_type (str, optional): The type of the repository. Not used in the base class, but subclasses + may use it for specific repository implementations. Defaults to None. + private (bool, optional): Whether the repository should be private or not. Not used in the base class, + but subclasses may use it for specific repository implementations. Defaults to False. + commit_message (str, optional): A message to attach to the commit when uploading to version control + systems. Not used in the base class, but subclasses may use it for specific repository + implementations. Defaults to None. + commit_description (str, optional): A description of the commit when uploading to version control + systems. Not used in the base class, but subclasses may use it for specific repository + implementations. Defaults to None. + + Raises: + ValueError: If the repo_id does not have both a namespace and a repo name separated by a '/'. + """ + # Validate repo_id has both a namespace and a repo name + if "/" not in repo_id: + raise ValueError( + "`repo_id` must be a namespace (user or an organization) and a repo name separated by a `/`." + " For example, if your HF username is `johndoe` and you want to create a repository called `test`, the" + " repo_id should be johndoe/test" + ) + BaseModelUpload._validate_upload_parameters( + repo_id, + model_path, + repo_type, + private, + commit_message, + commit_description, + ) + + trained_model_artifacts_path = os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME) + """Make sure the model's saved artifacts either contain: + + 1. pytorch_model.bin -> regular model training, such as ECD or for LLMs + 2. adapter_model.bin or adapter_model.safetensors -> LLM fine-tuning using PEFT + + As of PEFT version "0.7.0", "adapter_model" storage format was changed from ".bin" to ".safetensors". + For backward compatibility, both formats will be supported, until depracating ".bin" format formally. + + """ + files = set(os.listdir(trained_model_artifacts_path)) + acceptable_model_artifact_file_names: set[str] = { + "pytorch_model.bin", + "adapter_model.bin", # Delete per formal deprecation policy TBD (per above comment). + "adapter_model.safetensors", # New format as of PEFT version "0.7.0" (per above comment). + } + if not (files & acceptable_model_artifact_file_names): + raise ValueError( + f"Can't find model weights at {trained_model_artifacts_path}. Trained model weights should " + "either be saved as `pytorch_model.bin` for regular model training, or have `adapter_model.bin`" + "or `adapter_model.safetensors` if using parameter efficient fine-tuning methods like LoRA." + ) + model_hyperparameters_path: str = os.path.join(model_path, MODEL_FILE_NAME) + if MODEL_HYPERPARAMETERS_FILE_NAME not in os.listdir(model_hyperparameters_path): + raise ValueError(f"Can't find '{MODEL_HYPERPARAMETERS_FILE_NAME}' at {model_hyperparameters_path}.") + + def upload( + self, + repo_id: str, + model_path: str, + repo_type: str | None = None, + private: bool | None = False, + commit_message: str | None = None, + commit_description: str | None = None, + **kwargs, + ) -> bool: + """Create an empty repo on the HuggingFace Hub and upload trained model artifacts to that repo. + + Args: + repo_id (`str`): + A namespace (user or an organization) and a repo name separated + by a `/`. + model_path (`str`): + The path of the saved model. This is the parent-folder of the folder + where the 'model_weights' folder and the 'model_hyperparameters.json' file + are stored. + repo_type (`str`, *optional*): + Set to `"dataset"` or `"space"` if uploading to a dataset or + space, `None` or `"model"` if uploading to a model. Default is + `None`. + private (`bool`, *optional*, defaults to `False`): + Whether the model repo should be private. + commit_message (`str`, *optional*): + The summary / title / first line of the generated commit. Defaults to: + `f"Upload {path_in_repo} with huggingface_hub"` + commit_description (`str` *optional*): + The description of the generated commit + """ + # Validate upload parameters are in the right format + HuggingFaceHub._validate_upload_parameters( + repo_id, + model_path, + repo_type, + private, + commit_message, + commit_description, + ) + + # Create empty model repo using repo_id, but it is okay if it already exists. + self.api.create_repo( + repo_id=repo_id, + private=private, + repo_type=repo_type, + exist_ok=True, + ) + + # Upload all artifacts in model weights folder + commit_message_weights: str | None = f"{commit_message} (weights)" if commit_message else commit_message + commit_description_weights: str | None = ( + f"{commit_description} (weights)" if commit_description else commit_description + ) + folder_path = os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME) + upload_path_weights: CommitInfo = self.api.upload_folder( + folder_path=folder_path, + repo_id=repo_id, + repo_type=repo_type, + commit_message=commit_message_weights, + commit_description=commit_description_weights, + ) + + if upload_path_weights: + logger.info(f"Model weights uploaded to `{upload_path_weights}` with repository name `{repo_id}`") + # Upload the ludwig configuration file + commit_message_config: str | None = f"{commit_message} (config)" if commit_message else commit_message + commit_description_config: str | None = ( + f"{commit_description} (config)" if commit_description else commit_description + ) + path_or_fileobj = os.path.join(model_path, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME) + upload_path_config: CommitInfo = self.api.upload_file( + path_or_fileobj=path_or_fileobj, + path_in_repo="ludwig_config.json", + repo_id=repo_id, + repo_type=repo_type, + commit_message=commit_message_config, + commit_description=commit_description_config, + ) + + if upload_path_config: + logger.info(f"Model config uploaded to `{upload_path_config}` with repository name `{repo_id}`") + return True + + return False diff --git a/ludwig/utils/version_transformation.py b/ludwig/utils/version_transformation.py new file mode 100644 index 0000000..ac30866 --- /dev/null +++ b/ludwig/utils/version_transformation.py @@ -0,0 +1,171 @@ +#! /usr/bin/env python +# Copyright (c) 2022 Predibase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import copy +import logging +from collections import defaultdict +from collections.abc import Callable +from functools import total_ordering + +from packaging import version as pkg_version + +logger = logging.getLogger(__name__) + + +@total_ordering +class VersionTransformation: + """Wrapper class for transformations to config dicts.""" + + def __init__(self, transform: Callable[[dict], dict], version: str, prefixes: list[str] | None = None): + """Constructor. + + Args: + transform: A function or other callable from Dict -> Dict which returns a modified version of the config. + The callable may update the config in-place and return it, or return a new dict. + version: The Ludwig version, should be the first version which requires this transform. + prefixes: A list of config prefixes this transform should apply to, i.e. ["hyperopt"]. If not specified, + transform will be called with the entire config dictionary. + """ + self.transform = transform + self.version = version + self.pkg_version = pkg_version.parse(version) + self.prefixes = prefixes if prefixes else [] + + def transform_config(self, config: dict): + """Transforms the sepcified config, returns the transformed config.""" + prefixes = self.prefixes if self.prefixes else [""] + for prefix in prefixes: + if prefix and (prefix not in config or not config[prefix]): + # If the prefix is non-empty (transformation applies to a specific section), but the section is either + # absent or empty, then skip. + continue + config = self.transform_config_with_prefix(config, prefix) + return config + + def transform_config_with_prefix(self, config: dict, prefix: str | None = None) -> dict: + """Applied this version transformation to a specified prefix of the config, returns the updated config. If + prefix names a list, i.e. "input_features", applies the transformation to each list element (input + feature). + + Args: + config: A config dictionary. + prefix: An optional keypath prefix i.e. "input_features". If no prefix specified, transformation is applied + to config itself. + + Returns The updated config. + """ + if prefix: + components = prefix.split(".", 1) + key = components[0] + rest_of_prefix = components[1] if len(components) > 1 else "" + if key in config: + subsection = config[key] + if isinstance(subsection, list): + config[key] = [ + self.transform_config_with_prefix(v, prefix=rest_of_prefix) if isinstance(v, dict) else v + for v in subsection + ] + elif isinstance(subsection, dict): + config[key] = self.transform_config_with_prefix(subsection, prefix=rest_of_prefix) + return config + else: + # Base case: no prefix specified, pass entire dictionary to transform function. + transformed_config = self.transform(config) + if transformed_config is None: + logger.error("Error: version transformation returned None. Check for missing return statement.") + return transformed_config + + @property + def max_prefix_length(self): + """Returns the length of the longest prefix.""" + return max(len(prefix.split(".")) for prefix in self.prefixes) if self.prefixes else 0 + + @property + def longest_prefix(self): + """Returns the longest prefix, or empty string if no prefixes specified.""" + prefixes = self.prefixes + if not prefixes: + return "" + max_index = max(range(len(prefixes)), key=lambda i: prefixes[i]) + return prefixes[max_index] + + def __lt__(self, other): + """Defines sort order of version transformations. Sorted by: + + - version (ascending) + - max_prefix_length (ascending) Process outer config transformations before inner. + - longest_prefix (ascending) Order alphabetically by prefix if max_prefix_length equal. + """ + return (self.pkg_version, self.max_prefix_length, self.longest_prefix) < ( + other.pkg_version, + other.max_prefix_length, + other.longest_prefix, + ) + + def __repr__(self): + return f'VersionTransformation(, version="{self.version}", prefixes={self.prefixes!r})' + + +class VersionTransformationRegistry: + """A registry of transformations which update versioned config files.""" + + def __init__(self): + self._registry = defaultdict(list) # Maps version number to list of transformations. + + def register(self, transformation: VersionTransformation): + """Registers a version transformation.""" + self._registry[transformation.version].append(transformation) + + def get_transformations(self, from_version: str, to_version: str) -> list[VersionTransformation]: + """Filters transformations to create an ordered list of the config transformations from one version to + another. All transformations returned have version st. from_version < version <= to_version. + + Args: + from_version: The ludwig version of the input config. + to_version: The version to update the config to (usually the current LUDWIG_VERSION). + + Returns an ordered list of transformations to apply to the config to update it. + """ + from_version = pkg_version.parse(from_version) + + # Ignore pre-release, development versions. Otherwise transformations for upcoming releases will not be applied. + to_version = pkg_version.parse(to_version) + to_version = pkg_version.parse(f"{to_version.major}.{to_version.minor}") + + def in_range(v, to_version, from_version): + v = pkg_version.parse(v) + return from_version <= v <= to_version + + versions = [v for v in self._registry if in_range(v, to_version, from_version)] + + transforms = sorted(t for v in versions for t in self._registry[v]) + return transforms + + def update_config(self, config: dict, from_version: str, to_version: str) -> dict: + """Applies the transformations from an older version to a newer version. + + Args: + config: The config, created by ludwig at from_version. + from_version: The version of ludwig which wrote the older config. + to_version: The version of ludwig to update to (usually the current LUDWIG_VERSION). + + Returns The updated config after applying update transformations and updating the "ludwig_version" key. + """ + transformations = self.get_transformations(from_version, to_version) + updated_config = copy.deepcopy(config) + for t in transformations: + updated_config = t.transform_config(updated_config) + updated_config["ludwig_version"] = to_version + return updated_config diff --git a/ludwig/utils/visualization_utils.py b/ludwig/utils/visualization_utils.py new file mode 100644 index 0000000..26b9b70 --- /dev/null +++ b/ludwig/utils/visualization_utils.py @@ -0,0 +1,1568 @@ +#! /usr/bin/env python +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import copy +import logging +from collections import Counter +from sys import platform + +import numpy as np +import pandas as pd +import ptitprince as pt + +from ludwig.constants import SPACE, TRAINING, VALIDATION + +logger = logging.getLogger(__name__) + +try: + import matplotlib as mpl + + if platform == "darwin": # OS X + try: + mpl.use("TkAgg") + except ModuleNotFoundError: + logger.warning("Unable to set TkAgg backend for matplotlib. Your Python may not be configured for Tk") + import matplotlib.patches as patches + import matplotlib.path as path + import matplotlib.patheffects as PathEffects + import matplotlib.pyplot as plt + import seaborn as sns + from matplotlib import ticker + from matplotlib.lines import Line2D + from mpl_toolkits.mplot3d import Axes3D +except ImportError as e: + raise RuntimeError( + "matplotlib or seaborn are not installed. " + "In order to install all visualization dependencies run " + "pip install ludwig[viz]" + ) from e + +INT_QUANTILES = 10 +FLOAT_QUANTILES = 10 + +# mapping from RayTune search space to Ludwig types (float, int, category) for hyperopt visualizations +RAY_TUNE_FLOAT_SPACES = {"uniform", "quniform", "loguniform", "qloguniform", "randn", "qrandn"} +RAY_TUNE_INT_SPACES = {"randint", "qrandint", "lograndint", "qlograndint"} +RAY_TUNE_CATEGORY_SPACES = {"choice", "grid_search"} + + +def visualize_callbacks(callbacks, fig): + if callbacks is None: + return + for callback in callbacks: + callback.on_visualize_figure(fig) + + +def learning_curves_plot( + train_values, + vali_values, + metric, + x_label="epoch", + x_step=1, + algorithm_names=None, + title=None, + filename=None, + callbacks=None, +): + num_algorithms = len(train_values) + max_len = max(len(tv) for tv in train_values) + + fig, ax = plt.subplots() + + sns.set_style("whitegrid") + + if title is not None: + ax.set_title(title) + + if num_algorithms == 1: + colors = plt.get_cmap("tab10").colors + else: # num_algorithms > 1 + colors = plt.get_cmap("tab20").colors + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + ax.set_xlabel(x_label) + ax.set_ylabel(metric.replace("_", " ")) + + xs = np.arange(1, (max_len * x_step) + 1, x_step) + + for i in range(num_algorithms): + name_prefix = algorithm_names[i] + " " if algorithm_names is not None and i < len(algorithm_names) else "" + ax.plot( + xs[: len(train_values[i])], train_values[i], label=name_prefix + TRAINING, color=colors[i * 2], linewidth=3 + ) + if i < len(vali_values) and vali_values[i] is not None and len(vali_values[i]) > 0: + ax.plot( + xs[: len(vali_values[i])], + vali_values[i], + label=name_prefix + VALIDATION, + color=colors[i * 2 + 1], + linewidth=3, + ) + + ax.legend() + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def compare_classifiers_plot( + scores, + metrics, + algoritm_names=None, + adaptive=False, + decimals=4, + title=None, + filename=None, + callbacks=None, +): + assert len(scores) == len(metrics) + assert len(scores) > 0 + + num_metrics = len(metrics) + + sns.set_style("whitegrid") + + fig, ax = plt.subplots() + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + ax.set_xticklabels([], minor=True) + + if title is not None: + ax.set_title(title) + + width = 0.8 / num_metrics if num_metrics > 1 else 0.4 + ticks = np.arange(len(scores[0])) + + if num_metrics <= 10: + colors = plt.get_cmap("tab10").colors + else: + colors = plt.get_cmap("tab20").colors + if adaptive: + maximum = max(max(score) for score in scores) + else: + ax.set_xlim([0, 1]) + ax.set_xticks(np.linspace(0.0, 1.0, num=21), minor=True) + ax.set_xticks(np.linspace(0.0, 1.0, num=11)) + maximum = 1 + + half_total_width = 0.4 if num_metrics > 1 else 0.2 + ax.set_yticks(ticks + half_total_width - width / 2) + ax.set_yticklabels(algoritm_names if algoritm_names is not None else "") + ax.invert_yaxis() # labels read top-to-bottom + + for i, metric in enumerate(metrics): + ax.barh(ticks + (i * width), scores[i], width, label=metric, color=colors[i]) + + for j, v in enumerate(scores[i]): + if v < maximum * (0.025 * decimals + 0.1): + x = v + maximum * 0.01 + horizontal_alignment = "left" + else: + x = v - maximum * 0.01 + horizontal_alignment = "right" + txt = ax.text( + x, + ticks[j] + (i * width), + ("{:." + str(decimals) + "f}").format(v), + color="white", + fontweight="bold", + verticalalignment="center", + horizontalalignment=horizontal_alignment, + ) + txt.set_path_effects([PathEffects.withStroke(linewidth=3, foreground="black")]) + + plt.setp(ax.get_xminorticklabels(), visible=False) + + ax.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def compare_classifiers_line_plot( + xs, + scores, + metric, + algorithm_names=None, + title=None, + filename=None, + callbacks=None, +): + assert len(scores) > 0 + + sns.set_style("whitegrid") + + if len(scores) <= 10: + colors = plt.get_cmap("tab10").colors + else: + colors = plt.get_cmap("tab20").colors + + fig, ax = plt.subplots() + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + + if title is not None: + ax.set_title(title) + + ax.set_xticks(xs) + ax.set_xticklabels(xs) + ax.set_xlabel("k") + ax.set_ylabel(metric) + + for i, score in enumerate(scores): + ax.plot( + xs, + score, + label=algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else f"Algorithm {i}", + color=colors[i], + linewidth=3, + marker="o", + ) + + ax.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def compare_classifiers_multiclass_multimetric_plot( + scores, + metrics, + labels=None, + title=None, + filename=None, + callbacks=None, +): + assert len(scores) > 0 + + sns.set_style("whitegrid") + + fig, ax = plt.subplots() + + if title is not None: + ax.set_title(title) + + width = 0.9 / len(scores) + ticks = np.arange(len(scores[0])) + + if len(scores) <= 10: + colors = plt.get_cmap("tab10").colors + else: + colors = plt.get_cmap("tab20").colors + ax.set_xlabel("class") + ax.set_xticks(ticks + width) + if labels is not None: + ax.set_xticklabels(labels, rotation=90) + else: + ax.set_xticklabels(ticks, rotation=90) + + for i, score in enumerate(scores): + ax.bar(ticks + i * width, score, width, label=metrics[i], color=colors[i]) + + ax.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def radar_chart( + ground_truth, + predictions, + algorithms=None, + log_scale=False, + title=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + if title is not None: + plt.title(title) + + ground_truth = ground_truth[0:10] + predictions = [pred[0:10] for pred in predictions] + + gt_argsort = np.argsort(-ground_truth) # sort deacreasing + logger.info(gt_argsort) + ground_truth = ground_truth[gt_argsort] + predictions = [pred[gt_argsort] for pred in predictions] + + maximum = max(max(ground_truth), max(max(p) for p in predictions)) + + ax = plt.subplot(111, polar=True) + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) + ax.set_rmax(maximum) + ax.set_rlabel_position(305) + ax.set_ylabel("Probability") + # ax.set_rscale('log') + ax.grid(True) + + colors = plt.get_cmap("tab10").colors + + num_classes = len(ground_truth) + + # Set ticks to the number of properties (in radians) + t = np.arange(0, 2 * np.pi, 2 * np.pi / num_classes) + ax.set_xticks(t) + ax.set_xticklabels(np.arange(0, num_classes)) + + # Set yticks from 0 to 10 + # ax.set_yticks(np.linspace(0, 10, 11)) + # Set axes limits + # ax.set_rlim(0, 1) + # ax.set_rscale('log') + + def draw_polygon(values, label, color="grey"): + points = [(x, y) for x, y in zip(t, values)] + points.append(points[0]) + points = np.array(points) + + codes = [path.Path.MOVETO] + [path.Path.LINETO] * (len(values) - 1) + [path.Path.CLOSEPOLY] + _path = path.Path(points, codes) + _patch = patches.PathPatch(_path, fill=True, color=color, linewidth=0, alpha=0.2) + ax.add_patch(_patch) + _patch = patches.PathPatch(_path, fill=False, color=color, linewidth=3) + ax.add_patch(_patch) + + # Draw circles at value points + # line = ax.scatter(points[:, 0], points[:, 1], linewidth=3, + # s=50, color='white', edgecolor=color, zorder=10) + ax.plot( + points[:, 0], + points[:, 1], + linewidth=3, + marker="o", + fillstyle="full", + markerfacecolor="white", + markeredgecolor=color, + markeredgewidth=2, + color=color, + zorder=10, + label=label, + ) + + draw_polygon(ground_truth, "Ground Truth") + + # Draw polygon representing values + for i, alg_predictions in enumerate(predictions): + draw_polygon(alg_predictions, algorithms[i], colors[i]) + + ax.legend(frameon=True, loc="upper left") + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def pie(ax, values, **kwargs): + total = sum(values) + + def formatter(pct): + if pct > 0: + return f"{pct * total / 100:0.0f}\n({pct:0.1f}%)" + else: + return "" + + wedges, _, labels = ax.pie(values, autopct=formatter, **kwargs) + return wedges + + +def donut( + inside_values, + inside_labels, + outside_values, + outside_labels, + outside_groups, + title=None, + tight_layout=None, + filename=None, + callbacks=None, +): + fig, ax = plt.subplots(figsize=(7, 5)) + + if title is not None: + ax.set_title(title) + + ax.axis("equal") + + width = 0.35 + colors_tab20c = list(plt.get_cmap("tab20c").colors) + colors_set2 = list(plt.get_cmap("Set2").colors) + colors_set3 = list(plt.get_cmap("Set3").colors) + colors_pastel1 = list(plt.get_cmap("Pastel1").colors) + + # swap green and red + # for i in range(4): + # tmp = colors[4 + i] + # colors[4 + i] = colors[8 + i] + # colors[8 + i] = tmp + + colors = [] + colors.extend(colors_tab20c[8:12]) + colors.append(colors_set2[5]) + colors.append(colors_set3[11]) + colors.append(colors_set3[1]) + colors.append(colors_pastel1[5]) + colors.extend(colors_tab20c[4:8]) + + inside_colors = [colors[x * 4] for x in range(len(inside_values))] + + group_count = Counter(outside_groups) + outside_colors = [colors[(i * 4) + ((j % 3) + 1)] for i in list(set(outside_groups)) for j in range(group_count[i])] + + outside = pie( + ax, + outside_values, + radius=1, + pctdistance=1 - width / 2, + colors=outside_colors, + startangle=90, + counterclock=False, + textprops={ + "color": "w", + "weight": "bold", + "path_effects": [PathEffects.withStroke(linewidth=3, foreground="black")], + }, + ) + inside = pie( + ax, + inside_values, + radius=1 - width, + pctdistance=1 - (width / 2) / (1 - width), + colors=inside_colors, + startangle=90, + counterclock=False, + textprops={ + "color": "w", + "weight": "bold", + "path_effects": [PathEffects.withStroke(linewidth=3, foreground="black")], + }, + ) + plt.setp(inside + outside, width=width, edgecolor="white") + + wedges = [] + labels = [] + so_far = 0 + for i in list(set(outside_groups)): + wedges.append(inside[i]) + labels.append(inside_labels[i]) + for _j in range(group_count[i]): + wedges.append(outside[so_far]) + labels.append(outside_labels[so_far]) + so_far += 1 + + if tight_layout: + ax.legend(wedges, labels, frameon=True, loc=1, bbox_to_anchor=(1.30, 1.00)) + else: + ax.legend(wedges, labels, frameon=True, loc=1, bbox_to_anchor=(1.50, 1.00)) + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename, bbox_inches="tight") + else: + plt.show() + + +def confidence_filtering_plot( + thresholds, + accuracies, + dataset_kepts, + algorithm_names=None, + title=None, + filename=None, + callbacks=None, +): + assert len(accuracies) == len(dataset_kepts) + num_algorithms = len(accuracies) + + sns.set_style("whitegrid") + + if num_algorithms == 1: + colors = plt.get_cmap("tab10").colors + else: # num_algorithms > 1 + colors = plt.get_cmap("tab20").colors + + y_ticks_minor = np.linspace(0.0, 1.0, num=21) + y_ticks_major = np.linspace(0.0, 1.0, num=11) + y_ticks_major_labels = [f"{y * 100:3.0f}%" for y in y_ticks_major] + + fig, ax1 = plt.subplots() + + if title is not None: + ax1.set_title(title) + + ax1.grid(which="both") + ax1.grid(which="minor", alpha=0.5) + ax1.grid(which="major", alpha=0.75) + ax1.set_xticks([x for idx, x in enumerate(thresholds) if idx % 2 == 0]) + ax1.set_xticks(thresholds, minor=True) + + ax1.set_xlim(-0.05, 1.05) + ax1.set_xlabel("confidence threshold") + + ax1.set_ylim(0, 1.05) + ax1.set_yticks(y_ticks_major) + ax1.set_yticklabels(y_ticks_major_labels) + ax1.set_yticks(y_ticks_minor, minor=True) + + ax2 = ax1.twinx() + + ax2.set_ylim(0, 1.05) + ax2.set_yticks(y_ticks_major) + ax2.set_yticklabels(y_ticks_major_labels) + ax2.set_yticks(y_ticks_minor, minor=True) + + for i in range(len(accuracies)): + algorithm_name = algorithm_names[i] + " " if algorithm_names is not None and i < len(algorithm_names) else "" + ax1.plot(thresholds, accuracies[i], label=f"{algorithm_name} accuracy", color=colors[i * 2], linewidth=3) + ax1.plot( + thresholds, dataset_kepts[i], label=f"{algorithm_name} data coverage", color=colors[i * 2 + 1], linewidth=3 + ) + + ax1.legend(frameon=True, loc=3) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def confidence_filtering_data_vs_acc_plot( + accuracies, + dataset_kepts, + model_names=None, + dotted=False, + decimal_digits=0, + y_label="accuracy", + title=None, + filename=None, + callbacks=None, +): + assert len(accuracies) == len(dataset_kepts) + + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + max_dataset_kept = max(max(dataset_kept) for dataset_kept in dataset_kepts) + + x_ticks_minor = np.linspace(0.0, max_dataset_kept, num=21) + x_ticks_major = np.linspace(0.0, max_dataset_kept, num=11) + x_ticks_major_labels = [ + "{value:3.{decimal_digits}f}%".format(decimal_digits=decimal_digits, value=x * 100) for x in x_ticks_major + ] + y_ticks_minor = np.linspace(0.0, 1.0, num=21) + y_ticks_major = np.linspace(0.0, 1.0, num=11) + + fig, ax = plt.subplots() + + if title is not None: + ax.set_title(title) + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + ax.set_xticks(x_ticks_major) + ax.set_xticks(x_ticks_minor, minor=True) + ax.set_xticklabels(x_ticks_major_labels) + ax.set_xlim(0, max_dataset_kept) + ax.set_xlabel("data coverage") + + ax.set_ylim(0, 1) + ax.set_yticks(y_ticks_major) + ax.set_yticks(y_ticks_minor, minor=True) + ax.set_ylabel(y_label) + + for i in range(len(accuracies)): + curr_dotted = dotted[i] if isinstance(dotted, (list, tuple)) and i < len(dotted) else dotted + algorithm_name = model_names[i] + " " if model_names is not None and i < len(model_names) else "" + ax.plot( + dataset_kepts[i], + accuracies[i], + label=algorithm_name, + color=colors[i], + linewidth=3, + linestyle=":" if curr_dotted else "-", + ) + + ax.legend(frameon=True, loc=3) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def confidence_filtering_data_vs_acc_multiline_plot( + accuracies, + dataset_kepts, + models_names, + title=None, + filename=None, + callbacks=None, +): + assert len(accuracies) == len(dataset_kepts) + + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab20").colors + + max_dataset_kept = max(max(dataset_kept) for dataset_kept in dataset_kepts) + + x_ticks_minor = np.linspace(0.0, max_dataset_kept, num=21) + x_ticks_major = np.linspace(0.0, max_dataset_kept, num=11) + x_ticks_major_labels = [f"{x * 100:3.0f}%" for x in x_ticks_major] + y_ticks_minor = np.linspace(0.0, 1.0, num=21) + y_ticks_major = np.linspace(0.0, 1.0, num=11) + + fig, ax = plt.subplots() + + if title is not None: + ax.set_title(title) + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + ax.set_xticks(x_ticks_major) + ax.set_xticks(x_ticks_minor, minor=True) + ax.set_xticklabels(x_ticks_major_labels) + ax.set_xlim(0, max_dataset_kept) + ax.set_xlabel("data coverage") + + ax.set_ylim(0, 1) + ax.set_yticks(y_ticks_major) + ax.set_yticks(y_ticks_minor, minor=True) + ax.set_ylabel("accuracy") + + for i in range(len(accuracies)): + ax.plot(dataset_kepts[i], accuracies[i], color=colors[0], linewidth=1.0, alpha=0.35) + + legend_elements = [Line2D([0], [0], linewidth=1.0, color=colors[0])] + ax.legend(legend_elements, models_names) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def confidence_filtering_3d_plot( + thresholds_1, + thresholds_2, + accuracies, + dataset_kepts, + threshold_output_feature_names=None, + title=None, + filename=None, + callbacks=None, +): + assert len(accuracies) == len(dataset_kepts) + assert len(thresholds_1) == len(thresholds_2) + + thresholds_1, thresholds_2 = np.meshgrid(thresholds_1, thresholds_2) + + colors = plt.get_cmap("tab10").colors + sns.set_style("white") + + z_ticks_minor = np.linspace(0.0, 1.0, num=21) + z_ticks_major = np.linspace(0.0, 1.0, num=11) + z_ticks_major_labels = [f"{z * 100:3.0f}%" for z in z_ticks_major] + + fig = plt.figure() + ax = Axes3D + ax = fig.add_subplot(111, projection="3d") + + if title is not None: + ax.set_title(title) + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + + ax.set_xlabel(f"{threshold_output_feature_names[0]} probability") + ax.set_ylabel(f"{threshold_output_feature_names[1]} probability") + + ax.set_xlim(np.min(thresholds_1), np.max(thresholds_1)) + ax.set_ylim(np.min(thresholds_2), np.max(thresholds_2)) + ax.set_zlim(0, 1) + ax.set_zticks(z_ticks_major) + ax.set_zticklabels(z_ticks_major_labels) + ax.set_zticks(z_ticks_minor, minor=True) + + # HACK: Remove padding from 3D axes by adjusting coordinate info + from mpl_toolkits.mplot3d.axis3d import Axis + + if not hasattr(Axis, "_get_coord_info_old"): + + def _get_coord_info_new(self): + result = self._get_coord_info_old() + mins, maxs = result[0], result[1] + deltas = maxs - mins + mins += deltas / 4 + maxs -= deltas / 4 + return (mins, maxs) + result[2:] + + Axis._get_coord_info_old = Axis._get_coord_info + Axis._get_coord_info = _get_coord_info_new + + surf_1 = ax.plot_surface( + thresholds_1, + thresholds_2, + accuracies, + alpha=0.5, + label="accuracy", + cmap=plt.get_cmap("winter"), + edgecolor="none", + ) + surf_2 = ax.plot_surface( + thresholds_1, + thresholds_2, + dataset_kepts, + alpha=0.5, + label="data coverage", + cmap=plt.get_cmap("autumn"), + edgecolor="none", + ) + + handle_1 = copy.copy(surf_1) + handle_2 = copy.copy(surf_2) + + handle_1.set_color(colors[0]) + handle_2.set_color(colors[1]) + + # ## the next block is needed because matplotlib 3.3.3 renamed + # _edgecolors3d -> _edgecolor3d + # _facecolors3d -> _facecolor3d + # but we want to try to keep compatibility with older versions + # #### BEGIN COMPATIBILITY BLOCK ##### + if hasattr(handle_1, "_edgecolors3d"): + edgecolor3d = handle_1._edgecolors3d + else: + edgecolor3d = handle_1._edgecolor3d + handle_1._edgecolors2d = edgecolor3d + handle_1._edgecolor2d = edgecolor3d + + if hasattr(handle_2, "_edgecolors3d"): + edgecolor3d = handle_2._edgecolors3d + else: + edgecolor3d = handle_2._edgecolor3d + handle_2._edgecolors2d = edgecolor3d + handle_2._edgecolor2d = edgecolor3d + + if hasattr(handle_1, "_facecolors3d"): + facecolor3d = handle_1._facecolors3d + else: + facecolor3d = handle_1._facecolor3d + handle_1._facecolors2d = facecolor3d + handle_1._facecolor2d = facecolor3d + + if hasattr(handle_2, "_facecolors3d"): + facecolor3d = handle_2._facecolors3d + else: + facecolor3d = handle_2._facecolor3d + handle_2._facecolors2d = facecolor3d + handle_2._facecolor2d = facecolor3d + # #### END COMPATIBILITY BLOCK ##### + + ax.legend(frameon=True, loc=3, handles=[handle_1, handle_2]) + + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def threshold_vs_metric_plot( + thresholds, + scores, + algorithm_names=None, + title=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + # y_ticks_minor = np.linspace(0.0, 1.0, num=21) + # y_ticks_major = np.linspace(0.0, 1.0, num=11) + # y_ticks_major_labels = ['{:3.0f}%'.format(y * 100) for y in y_ticks_major] + + fig, ax1 = plt.subplots() + + if title is not None: + ax1.set_title(title) + + ax1.grid(which="both") + ax1.grid(which="minor", alpha=0.5) + ax1.grid(which="major", alpha=0.75) + ax1.set_xticks([x for idx, x in enumerate(thresholds) if idx % 2 == 0]) + ax1.set_xticks(thresholds, minor=True) + + # ax1.set_xlim(0, 1) + ax1.set_xlabel("confidence threshold") + + # ax1.set_ylim(0, 1) + # ax1.set_yticks(y_ticks_major) + # ax1.set_yticklabels(y_ticks_major_labels) + # ax1.set_yticks(y_ticks_minor, minor=True) + + for i in range(len(scores)): + algorithm_name = algorithm_names[i] + " " if algorithm_names is not None and i < len(algorithm_names) else "" + ax1.plot(thresholds, scores[i], label=algorithm_name, color=colors[i], linewidth=3, marker="o") + + ax1.legend(frameon=True) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def roc_curves( + fpr_tprs, + algorithm_names=None, + title=None, + graded_color=False, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + colormap = plt.get_cmap("RdYlGn") + + y_ticks_minor = np.linspace(0.0, 1.0, num=21) + y_ticks_major = np.linspace(0.0, 1.0, num=11) + + fig, ax = plt.subplots() + + if title is not None: + ax.set_title(title) + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + + ax.set_xlim(0, 1) + ax.set_xlabel("False positive rate") + + ax.set_ylim(0, 1) + ax.set_yticks(y_ticks_major) + ax.set_yticks(y_ticks_minor, minor=True) + ax.set_ylabel("True positive rate") + + plt.plot([0, 1], [0, 1], color="black", linewidth=3, linestyle="--") + + for i in range(len(fpr_tprs)): + algorithm_name = algorithm_names[i] + " " if algorithm_names is not None and i < len(algorithm_names) else "" + color = colormap(i / len(fpr_tprs)) if graded_color else colors[i] + ax.plot(fpr_tprs[i][0], fpr_tprs[i][1], label=algorithm_name, color=color, linewidth=3) + + ax.legend(frameon=True) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def precision_recall_curves_plot( + precision_recalls: dict[str, list[float]], + model_names: list[str], + title: str | None = None, + filename: str | None = None, + callbacks=None, +): + """Generates a precision recall curve for each model in the model_names list. + + Args: + precision_recalls: A list of dictionaries representing the precision and recall values for each model + in model_names. Each dictionary has two keys: "precisions" and "recalls". + """ + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + _, ax = plt.subplots() + + ax.set_xlim(0, 1) + # Create ticks for every 0.1 increment + ax.set_xticks(np.linspace(0, 1, 11)) + ax.set_xlabel("Recall") + + ax.set_ylim(0, 1) + # Create ticks for every 0.1 increment + ax.set_yticks(np.linspace(0, 1, 11)) + ax.set_ylabel("Precision") + + if title is not None: + ax.set_title(title) + + for i in range(len(precision_recalls)): + model_name = model_names[i] if model_names is not None and i < len(model_names) else "" + ax.plot( + precision_recalls[i]["recalls"], + precision_recalls[i]["precisions"], + label=model_name, + color=colors[i], + linewidth=3, + ) + + ax.legend(frameon=True) + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def calibration_plot( + fraction_positives, + mean_predicted_values, + algorithm_names=None, + class_name=None, + filename=None, + callbacks=None, +): + assert len(fraction_positives) == len(mean_predicted_values) + + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + num_algorithms = len(fraction_positives) + + plt.figure(figsize=(9, 9)) + plt.grid(which="both") + plt.grid(which="minor", alpha=0.5) + plt.grid(which="major", alpha=0.75) + + plt.plot([0, 1], [0, 1], "k:", label="Perfectly calibrated") + + for i in range(num_algorithms): + # ax1.plot(mean_predicted_values[i], fraction_positives[i], + # label=algorithms[i] if algorithm_names is not None and i < len(algorithms) else '') + + # sns.tsplot(mean_predicted_values[i], fraction_positives[i], ax=ax1, color=colors[i]) + + assert len(mean_predicted_values[i]) == len(fraction_positives[i]) + order = min(3, len(mean_predicted_values[i]) - 1) + + sns.regplot( + x=mean_predicted_values[i], + y=fraction_positives[i], + order=order, + x_estimator=np.mean, + color=colors[i], + marker="o", + scatter_kws={"s": 40}, + label=algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else f"Model {i}", + ) + + ticks = np.linspace(0.0, 1.0, num=11) + plt.xlim([-0.05, 1.05]) + plt.xticks(ticks) + plt.xlabel("Predicted probability") + plt.ylabel("Observed probability") + plt.ylim([-0.05, 1.05]) + plt.yticks(ticks) + plt.legend(loc="lower right") + if class_name is not None: + plt.title(f"{class_name}: Calibration (reliability curve)") + else: + plt.title("Calibration (reliability curve)") + + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def brier_plot( + brier_scores, + algorithm_names=None, + class_names=None, + title=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + # Dynamically set the size of the plot based on the number of labels + # Use minimum size to prevent plot from being too small + default_width, default_height = plt.rcParams.get("figure.figsize") + width = max(default_width, len(class_names) / 2) + height = max(default_height, len(class_names) / 2) + fig, ax = plt.subplots(figsize=(width, height)) + + if title is not None: + plt.title(title) + + colors = plt.get_cmap("tab10").colors + + n_algorithms = brier_scores.shape[1] + n_classes = brier_scores.shape[0] + x = np.arange(n_classes) + + max_width = 0.35 + bar_width = min(0.5 / n_algorithms, max_width) + bar_left = -bar_width * (n_algorithms // 2) + ((bar_width / 2) if (n_algorithms % 2) == 0 else 0) + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + ax.set_xlabel("class") + ax.set_ylabel("brier score") + if class_names is not None: + ax.set_xticks( + x, + class_names, + rotation=45, + ha="center", + ) + else: + ax.set_xticks( + x, + [str(i) for i in range(n_classes)], + rotation=45, + ha="center", + ) + + for i in range(n_algorithms): + # Plot bar for each class + label = algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else f"Model {i}" + ax.bar(x + bar_left + (bar_width * i), brier_scores[:, i], bar_width, color=colors[i], label=label) + + ax.legend() + fig.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def predictions_distribution_plot( + probabilities, + algorithm_names=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + num_algorithms = len(probabilities) + + plt.figure(figsize=(9, 9)) + plt.grid(which="both") + plt.grid(which="minor", alpha=0.5) + plt.grid(which="major", alpha=0.75) + + for i in range(num_algorithms): + plt.hist( + probabilities[i], + range=(0, 1), + bins=41, + color=colors[i], + label=algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else "", + histtype="stepfilled", + alpha=0.5, + lw=2, + ) + + plt.xlabel("Mean predicted value") + plt.xlim([0, 1]) + plt.xticks(np.linspace(0.0, 1.0, num=21)) + plt.ylabel("Count") + plt.legend(loc="upper center", ncol=2) + + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def confusion_matrix_plot( + confusion_matrix, + labels=None, + output_feature_name=None, + filename=None, + callbacks=None, +): + mpl.rcParams.update({"figure.autolayout": True}) + + # Dynamically set the size of the plot based on the number of labels + # Use minimum size to prevent plot from being too small + default_width, default_height = plt.rcParams.get("figure.figsize") + width = max(default_width, len(labels)) + height = max(default_height, len(labels)) + fig, ax = plt.subplots(figsize=(width, height)) + + ax.invert_yaxis() + ax.xaxis.tick_top() + ax.xaxis.set_label_position("top") + + # Set alpha value to prevent blue hues from being too dark + cax = ax.matshow(confusion_matrix, cmap="Blues", alpha=0.6) + # Annotate confusion matrix plot + for (i, j), z in np.ndenumerate(confusion_matrix): + # Format differently based on whether the value is normalized or not + if z.is_integer(): + z_format = f"{z:.0f}" + else: + z_format = f"{z:.3f}" + ax.text( + j, + i, + z_format, + ha="center", + va="center", + color="black", + fontweight="medium", + wrap=True, + ) + + ax.xaxis.set_major_locator(ticker.MultipleLocator(1)) + ax.yaxis.set_major_locator(ticker.MultipleLocator(1)) + ax.set_xticklabels([""] + labels, rotation=45, ha="left") + ax.set_yticklabels([""] + labels, rotation=45, ha="right") + ax.grid(False) + ax.tick_params(axis="both", which="both", length=0) + # https://stackoverflow.com/a/26720422/10102370 works nicely for square plots + fig.colorbar(cax, ax=ax, extend="max", fraction=0.046, pad=0.04) + ax.set_xlabel(f"Predicted {output_feature_name}") + ax.set_ylabel(f"Actual {output_feature_name}") + + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename, bbox_inches="tight") + else: + plt.show() + + +def double_axis_line_plot( + y1_sorted, + y2, + y1_name, + y2_name, + labels=None, + title=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + # Dynamically adjust figure size based on number of labels + default_width, default_height = plt.rcParams.get("figure.figsize") + width = max(default_width, len(labels) / 3) + height = max(default_height, len(labels) / 3) + fig, ax1 = plt.subplots(layout="constrained", figsize=(width, height)) + + if title is not None: + ax1.set_title(title) + + ax1.set_xlabel(f"class (sorted by {y1_name})") + ax1.set_xlim(0, len(y1_sorted) - 1) + if labels is not None: + ax1.set_xticklabels(labels, rotation=45, ha="right") + ax1.set_xticks(np.arange(len(labels))) + + ax1.set_ylabel(y1_name, color=colors[1]) + ax1.tick_params("y", colors=colors[1]) + ax1.set_ylim(min(y1_sorted), max(y1_sorted)) + + ax2 = ax1.twinx() + ax2.set_ylabel(y2_name, color=colors[0]) + ax2.tick_params("y", colors=colors[0]) + ax2.set_ylim(min(y2), max(y2)) + + ax1.plot(y1_sorted, label=y1_name, color=colors[1], linewidth=4) + ax2.plot(y2, label=y2_name, color=colors[0], linewidth=3) + + fig.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def plot_matrix( + matrix, + cmap="hot", + filename=None, + callbacks=None, +): + plt.figure() + plt.matshow(matrix, cmap=cmap) + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def plot_distributions( + distributions, + labels=None, + title=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + fig, ax1 = plt.subplots() + + if title is not None: + ax1.set_title(title) + + ax1.grid(which="both") + ax1.grid(which="minor", alpha=0.5) + ax1.grid(which="major", alpha=0.75) + + ax1.set_xlabel("class") + + ax1.set_ylabel("p") + ax1.tick_params("y") + + for i, distribution in enumerate(distributions): + ax1.plot( + distribution, + color=colors[i], + alpha=0.6, + label=labels[i] if labels is not None and i < len(labels) else f"Distribution {i}", + ) + + ax1.legend(frameon=True) + fig.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def plot_distributions_difference( + distribution, + labels=None, + title=None, + filename=None, + callbacks=None, +): + sns.set_style("whitegrid") + + colors = plt.get_cmap("tab10").colors + + fig, ax1 = plt.subplots() + + if title is not None: + ax1.set_title(title) + + ax1.grid(which="both") + ax1.grid(which="minor", alpha=0.5) + ax1.grid(which="major", alpha=0.75) + + ax1.set_xlabel("class") + + ax1.set_ylabel("p") + ax1.tick_params("y") + + ax1.plot(distribution, color=colors[0]) + + fig.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def bar_plot( + xs, + ys, + decimals=4, + labels=None, + title=None, + filename=None, + callbacks=None, +): + assert len(xs) == len(ys) + assert len(xs) > 0 + + sns.set_style("whitegrid") + + # Dynamically set the size of the plot based on the number of labels + # Use minimum size to prevent plot from being too small + default_width, default_height = plt.rcParams.get("figure.figsize") + width = max(default_width, len(labels) / 2) + _, ax = plt.subplots(figsize=(width, default_height)) + + ax.grid(which="both") + ax.grid(which="minor", alpha=0.5) + ax.grid(which="major", alpha=0.75) + + if title is not None: + ax.set_title(title) + + colors = plt.get_cmap("tab10").colors + + ax.invert_yaxis() # labels read top-to-bottom + + maximum = ys.max() + ticks = np.arange(len(xs)) + ax.set_yticks(ticks) + if labels is None: + ax.set_yticklabels(xs) + else: + ax.set_yticklabels(labels) + + ax.barh(ticks, ys, color=colors[0], align="center") + + for i, v in enumerate(ys): + if v < maximum * (0.025 * decimals + 0.1): + x = v + maximum * 0.01 + horizontal_alignment = "left" + else: + x = v - maximum * 0.01 + horizontal_alignment = "right" + txt = ax.text( + x, + ticks[i], + ("{:." + str(decimals) + "f}").format(v), + color="white", + fontweight="bold", + verticalalignment="center", + horizontalalignment=horizontal_alignment, + ) + txt.set_path_effects([PathEffects.withStroke(linewidth=3, foreground="black")]) + + plt.tight_layout() + visualize_callbacks(callbacks, plt.gcf()) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def hyperopt_report(hyperparameters, hyperopt_results_df, metric, filename_template, float_precision=3): + title = "Hyperopt Report: {}" + for hp_name, hp_params in hyperparameters.items(): + if hp_params[SPACE] in RAY_TUNE_INT_SPACES: + hyperopt_int_plot( + hyperopt_results_df, + hp_name, + metric, + title.format(hp_name), + filename_template.format(hp_name) if filename_template else None, + ) + elif hp_params[SPACE] in RAY_TUNE_FLOAT_SPACES: + hyperopt_float_plot( + hyperopt_results_df, + hp_name, + metric, + title.format(hp_name), + filename_template.format(hp_name) if filename_template else None, + log_scale_x=hp_params["scale"] == "log" if "scale" in hp_params else False, + ) + elif hp_params[SPACE] in RAY_TUNE_CATEGORY_SPACES: + hyperopt_category_plot( + hyperopt_results_df, + hp_name, + metric, + title.format(hp_name), + filename_template.format(hp_name) if filename_template else None, + ) + else: + # TODO: more research needed on how to handle RayTune "sample_from" search space + raise ValueError( + f"{hp_params[SPACE]} search space not supported in Ludwig. " + f"Supported values are {RAY_TUNE_FLOAT_SPACES | RAY_TUNE_INT_SPACES | RAY_TUNE_CATEGORY_SPACES}." + ) + + # quantize float and int columns + for hp_name, hp_params in hyperparameters.items(): + if hp_params[SPACE] in RAY_TUNE_INT_SPACES: + num_distinct_values = len(hyperopt_results_df[hp_name].unique()) + if num_distinct_values > INT_QUANTILES: + hyperopt_results_df[hp_name] = pd.qcut(hyperopt_results_df[hp_name], q=INT_QUANTILES, precision=0) + elif hp_params[SPACE] in RAY_TUNE_FLOAT_SPACES: + hyperopt_results_df[hp_name] = pd.qcut( + hyperopt_results_df[hp_name], + q=FLOAT_QUANTILES, + precision=float_precision, + duplicates="drop", + ) + + hyperopt_pair_plot( + hyperopt_results_df, + metric, + title.format("pair plot"), + filename_template.format("pair_plot") if filename_template else None, + ) + + +def hyperopt_int_plot(hyperopt_results_df, hp_name, metric, title, filename, log_scale_x=False, log_scale_y=True): + sns.set_style("whitegrid") + plt.figure() + seaborn_figure = sns.scatterplot(x=hp_name, y=metric, data=hyperopt_results_df) + seaborn_figure.set_title(title) + if log_scale_x: + seaborn_figure.set(xscale="log") + if log_scale_y: + seaborn_figure.set(yscale="log") + seaborn_figure.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) + seaborn_figure.xaxis.set_major_formatter(ticker.ScalarFormatter()) + seaborn_figure.xaxis.set_minor_formatter(ticker.NullFormatter()) + seaborn_figure.figure.tight_layout() + if filename: + seaborn_figure.figure.savefig(filename) + else: + seaborn_figure.figure.show() + + +def hyperopt_float_plot(hyperopt_results_df, hp_name, metric, title, filename, log_scale_x=False, log_scale_y=True): + sns.set_style("whitegrid") + plt.figure() + seaborn_figure = sns.scatterplot(x=hp_name, y=metric, data=hyperopt_results_df) + seaborn_figure.set_title(title) + seaborn_figure.set(ylabel=metric) + if log_scale_x: + seaborn_figure.set(xscale="log") + if log_scale_y: + seaborn_figure.set(yscale="log") + seaborn_figure.figure.tight_layout() + if filename: + seaborn_figure.figure.savefig(filename) + else: + seaborn_figure.figure.show() + + +def hyperopt_category_plot(hyperopt_results_df, hp_name, metric, title, filename, log_scale=True): + sns.set_style("whitegrid") + plt.figure() + + # Ensure that all parameter values have at least 2 trials, otherwise the Raincloud Plot will create awkward + # looking "flat clouds" in the cloud part of the plot (the "rain" part is ok with 1 trial). In this case, + # just use stripplots since they are categorical scatter plots. + parameter_to_trial_count = hyperopt_results_df[hp_name].value_counts() + parameter_to_trial_count = parameter_to_trial_count[parameter_to_trial_count < 2] + + if len(parameter_to_trial_count) != 0: + seaborn_figure = sns.stripplot(x=hp_name, y=metric, data=hyperopt_results_df, size=5) + else: + seaborn_figure = pt.RainCloud( + x=hp_name, + y=metric, + data=hyperopt_results_df, + palette="Set2", + bw=0.2, + width_viol=0.7, + point_size=6, + cut=1, + ) + + seaborn_figure.set_title(title) + seaborn_figure.set(ylabel=metric) + sns.despine() + if log_scale: + seaborn_figure.set(yscale="log") + plt.tight_layout() + if filename: + plt.savefig(filename) + else: + plt.show() + + +def hyperopt_pair_plot(hyperopt_results_df, metric, title, filename): + params = sorted(list(hyperopt_results_df.keys())) + params.remove(metric) + num_param = len(params) + + # Pair plot is empty if there's only 1 parameter, so skip creating a pair plot + if num_param == 1: + return + + sns.set_style("white") + fig = plt.figure(figsize=(20, 20)) + fig.suptitle(title) + gs = fig.add_gridspec(num_param, num_param) + + for i, param1 in enumerate(params): + for j, param2 in enumerate(params): + if i != j: + ax = fig.add_subplot(gs[i, j]) + heatmap = hyperopt_results_df.pivot_table(index=param1, columns=param2, values=metric, aggfunc="mean") + sns.heatmap( + heatmap, + linewidths=1, + cmap="viridis", + cbar_kws={"label": metric}, + ax=ax, + ) + + plt.tight_layout(pad=5) + if filename: + plt.savefig(filename) + else: + plt.show() + + +def hyperopt_hiplot( + hyperopt_df, + filename, +): + import hiplot as hip + + experiment = hip.Experiment.from_dataframe(hyperopt_df) + experiment.to_html(filename) diff --git a/ludwig/vector_index/__init__.py b/ludwig/vector_index/__init__.py new file mode 100644 index 0000000..64464b9 --- /dev/null +++ b/ludwig/vector_index/__init__.py @@ -0,0 +1,28 @@ +import logging + +from ludwig.api_annotations import DeveloperAPI +from ludwig.vector_index.base import VectorIndex + +logger = logging.getLogger(__name__) + + +FAISS = "faiss" + +ALL_INDICES = [FAISS] + + +def get_faiss_index_cls() -> type[VectorIndex]: + from ludwig.vector_index.faiss import FaissIndex + + return FaissIndex + + +# TODO(travis): add other indexing structures +vector_index_registry = { + FAISS: get_faiss_index_cls, +} + + +@DeveloperAPI +def get_vector_index_cls(type: str) -> type[VectorIndex]: + return vector_index_registry[type]() diff --git a/ludwig/vector_index/base.py b/ludwig/vector_index/base.py new file mode 100644 index 0000000..6ac26cb --- /dev/null +++ b/ludwig/vector_index/base.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod + +import numpy as np + + +class VectorIndex(ABC): + @abstractmethod + def search(self, query: np.ndarray, k: int) -> np.ndarray: + pass + + @abstractmethod + def save(self, path: str): + pass + + @classmethod + @abstractmethod + def from_path(cls, path: str) -> "VectorIndex": + pass + + @classmethod + @abstractmethod + def from_embeddings(cls, embeddings: np.ndarray) -> "VectorIndex": + pass diff --git a/ludwig/vector_index/faiss.py b/ludwig/vector_index/faiss.py new file mode 100644 index 0000000..4235424 --- /dev/null +++ b/ludwig/vector_index/faiss.py @@ -0,0 +1,27 @@ +import faiss +import numpy as np + +from ludwig.vector_index.base import VectorIndex + + +class FaissIndex(VectorIndex): + def __init__(self, index: faiss.Index): + self.index = index + + def search(self, query: np.ndarray, k: int) -> np.ndarray: + top_k = self.index.search(query.reshape(1, -1), k) + return top_k[1].tolist()[0] + + def save(self, path: str): + faiss.write_index(self.index, path) + + @classmethod + def from_path(cls, path: str) -> "VectorIndex": + index = faiss.read_index(path) + return cls(index) + + @classmethod + def from_embeddings(cls, embeddings: np.ndarray) -> "VectorIndex": + index = faiss.IndexFlatL2(embeddings.shape[1]) + index.add(embeddings) + return cls(index) diff --git a/ludwig/visualize/__init__.py b/ludwig/visualize/__init__.py new file mode 100644 index 0000000..62b76a4 --- /dev/null +++ b/ludwig/visualize/__init__.py @@ -0,0 +1,228 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""ludwig.visualize package — backward-compatible re-exports. + +All public and private symbols that were importable from ``ludwig.visualize`` +when it was a single module remain importable from this package. +""" + +# --------------------------------------------------------------------------- +# Private helpers / data-loading utilities +# --------------------------------------------------------------------------- +from ludwig.visualize._utils import ( + _convert_ground_truth, + _CSV_SUFFIX, + _encode_categorical_feature, + _extract_ground_truth_values, + _get_cols_from_predictions, + _get_ground_truth_df, + _load_training_stats, + _PARQUET_SUFFIX, + _PREDICTIONS_SUFFIX, + _PROBABILITIES_SUFFIX, + _validate_output_feature_name_from_test_stats, + _validate_output_feature_name_from_train_stats, + _vectorize_ground_truth, + convert_to_list, + generate_filename_template_path, + load_data_for_viz, + load_training_stats_for_viz, + validate_conf_thresholds_and_probabilities_2d_3d, +) + +# --------------------------------------------------------------------------- +# CLI entry point + registry +# --------------------------------------------------------------------------- +from ludwig.visualize.cli import ( + cli, + get_visualizations_registry, +) + +# --------------------------------------------------------------------------- +# Confusion matrix +# --------------------------------------------------------------------------- +from ludwig.visualize.confusion import ( + confusion_matrix, + confusion_matrix_cli, +) + +# --------------------------------------------------------------------------- +# ROC, precision-recall, calibration curves +# --------------------------------------------------------------------------- +from ludwig.visualize.curves import ( + calibration_1_vs_all, + calibration_1_vs_all_cli, + calibration_multiclass, + calibration_multiclass_cli, + precision_recall_curves, + precision_recall_curves_cli, + precision_recall_curves_from_test_statistics, + precision_recall_curves_from_test_statistics_cli, + roc_curves, + roc_curves_cli, + roc_curves_from_test_statistics, + roc_curves_from_test_statistics_cli, +) + +# --------------------------------------------------------------------------- +# Hyperopt +# --------------------------------------------------------------------------- +from ludwig.visualize.hyperopt import ( + _convert_space_to_dtype, + hyperopt_hiplot, + hyperopt_hiplot_cli, + hyperopt_report, + hyperopt_report_cli, + hyperopt_results_to_dataframe, +) + +# --------------------------------------------------------------------------- +# Performance comparisons + frequency +# --------------------------------------------------------------------------- +from ludwig.visualize.performance import ( + compare_classifiers_multiclass_multimetric, + compare_classifiers_multiclass_multimetric_cli, + compare_classifiers_performance_changing_k, + compare_classifiers_performance_changing_k_cli, + compare_classifiers_performance_from_pred, + compare_classifiers_performance_from_pred_cli, + compare_classifiers_performance_from_prob, + compare_classifiers_performance_from_prob_cli, + compare_classifiers_performance_subset, + compare_classifiers_performance_subset_cli, + compare_classifiers_predictions, + compare_classifiers_predictions_cli, + compare_classifiers_predictions_distribution, + compare_classifiers_predictions_distribution_cli, + compare_performance, + compare_performance_cli, + frequency_vs_f1, + frequency_vs_f1_cli, +) + +# --------------------------------------------------------------------------- +# Confidence thresholding + binary threshold +# --------------------------------------------------------------------------- +from ludwig.visualize.threshold import ( + binary_threshold_vs_metric, + binary_threshold_vs_metric_cli, + confidence_thresholding, + confidence_thresholding_2thresholds_2d, + confidence_thresholding_2thresholds_2d_cli, + confidence_thresholding_2thresholds_3d, + confidence_thresholding_2thresholds_3d_cli, + confidence_thresholding_cli, + confidence_thresholding_data_vs_acc, + confidence_thresholding_data_vs_acc_cli, + confidence_thresholding_data_vs_acc_subset, + confidence_thresholding_data_vs_acc_subset_cli, + confidence_thresholding_data_vs_acc_subset_per_class, + confidence_thresholding_data_vs_acc_subset_per_class_cli, +) + +# --------------------------------------------------------------------------- +# Training / learning curves +# --------------------------------------------------------------------------- +from ludwig.visualize.training import ( + learning_curves, + learning_curves_cli, +) + +__all__ = [ + # constants + "_PREDICTIONS_SUFFIX", + "_PROBABILITIES_SUFFIX", + "_CSV_SUFFIX", + "_PARQUET_SUFFIX", + # private helpers + "_convert_ground_truth", + "_vectorize_ground_truth", + "_encode_categorical_feature", + "_get_ground_truth_df", + "_extract_ground_truth_values", + "_get_cols_from_predictions", + "_load_training_stats", + "_validate_output_feature_name_from_train_stats", + "_validate_output_feature_name_from_test_stats", + # public utils + "validate_conf_thresholds_and_probabilities_2d_3d", + "load_data_for_viz", + "load_training_stats_for_viz", + "convert_to_list", + "generate_filename_template_path", + # training + "learning_curves_cli", + "learning_curves", + # performance + "compare_performance_cli", + "compare_performance", + "compare_classifiers_performance_from_prob_cli", + "compare_classifiers_performance_from_prob", + "compare_classifiers_performance_from_pred_cli", + "compare_classifiers_performance_from_pred", + "compare_classifiers_performance_subset_cli", + "compare_classifiers_performance_subset", + "compare_classifiers_performance_changing_k_cli", + "compare_classifiers_performance_changing_k", + "compare_classifiers_multiclass_multimetric_cli", + "compare_classifiers_multiclass_multimetric", + "compare_classifiers_predictions_cli", + "compare_classifiers_predictions", + "compare_classifiers_predictions_distribution_cli", + "compare_classifiers_predictions_distribution", + "frequency_vs_f1_cli", + "frequency_vs_f1", + # threshold + "confidence_thresholding_cli", + "confidence_thresholding", + "confidence_thresholding_data_vs_acc_cli", + "confidence_thresholding_data_vs_acc", + "confidence_thresholding_data_vs_acc_subset_cli", + "confidence_thresholding_data_vs_acc_subset", + "confidence_thresholding_data_vs_acc_subset_per_class_cli", + "confidence_thresholding_data_vs_acc_subset_per_class", + "confidence_thresholding_2thresholds_2d_cli", + "confidence_thresholding_2thresholds_2d", + "confidence_thresholding_2thresholds_3d_cli", + "confidence_thresholding_2thresholds_3d", + "binary_threshold_vs_metric_cli", + "binary_threshold_vs_metric", + # curves + "precision_recall_curves_cli", + "precision_recall_curves", + "precision_recall_curves_from_test_statistics_cli", + "precision_recall_curves_from_test_statistics", + "roc_curves_cli", + "roc_curves", + "roc_curves_from_test_statistics_cli", + "roc_curves_from_test_statistics", + "calibration_1_vs_all_cli", + "calibration_1_vs_all", + "calibration_multiclass_cli", + "calibration_multiclass", + # confusion + "confusion_matrix_cli", + "confusion_matrix", + # hyperopt + "hyperopt_report_cli", + "hyperopt_report", + "hyperopt_hiplot_cli", + "hyperopt_hiplot", + "_convert_space_to_dtype", + "hyperopt_results_to_dataframe", + # cli + "get_visualizations_registry", + "cli", +] diff --git a/ludwig/visualize/__main__.py b/ludwig/visualize/__main__.py new file mode 100644 index 0000000..27df33c --- /dev/null +++ b/ludwig/visualize/__main__.py @@ -0,0 +1,5 @@ +import sys + +from ludwig.visualize.cli import cli + +cli(sys.argv[1:]) diff --git a/ludwig/visualize/_utils.py b/ludwig/visualize/_utils.py new file mode 100644 index 0000000..d391476 --- /dev/null +++ b/ludwig/visualize/_utils.py @@ -0,0 +1,369 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Shared private helpers and data-loading utilities for visualizations.""" + +import itertools +import logging +import os +from functools import partial +from typing import Any + +import numpy as np +import pandas as pd +from yaml import warnings + +from ludwig.api import EvaluationFrequency, TrainingStats +from ludwig.api_annotations import DeveloperAPI +from ludwig.backend import LOCAL_BACKEND +from ludwig.constants import SPLIT +from ludwig.utils import visualization_utils # noqa: F401 (re-exported for submodules) +from ludwig.utils.data_utils import ( + CACHEABLE_FORMATS, + data_reader_registry, + figure_data_format_dataset, + load_array, + load_from_file, + load_json, + replace_file_extension, +) +from ludwig.utils.dataframe_utils import to_numpy_dataset, unflatten_df +from ludwig.utils.fs_utils import path_exists +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.types import DataFrame + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Module-level constants +# --------------------------------------------------------------------------- +_PREDICTIONS_SUFFIX = "_predictions" +_PROBABILITIES_SUFFIX = "_probabilities" +_CSV_SUFFIX = "csv" +_PARQUET_SUFFIX = "parquet" + + +# --------------------------------------------------------------------------- +# Ground-truth helpers +# --------------------------------------------------------------------------- + + +def _convert_ground_truth(ground_truth, feature_metadata, ground_truth_apply_idx, positive_label): + """Converts non-np.array representation to be np.array.""" + if "str2idx" in feature_metadata: + # categorical output feature as binary + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + # convert category index to binary representation + ground_truth = ground_truth == positive_label + else: + # binary output feature + if "str2bool" in feature_metadata: + # non-standard boolean representation + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2bool"], ground_truth_apply_idx) + else: + # standard boolean representation + ground_truth = ground_truth.values + + # ensure positive_label is 1 for binary feature + positive_label = 1 + + # convert to 0/1 representation and return + return ground_truth.astype(int), positive_label + + +def _vectorize_ground_truth( + ground_truth: pd.Series, str2idx: np.array, ground_truth_apply_idx: bool = True +) -> np.array: + # raw hdf5 files generated during preprocessing don't need to be converted with str2idx + if not ground_truth_apply_idx: + return np.vectorize(lambda x, y: x)(ground_truth, str2idx) + + try: + return np.vectorize(_encode_categorical_feature)(ground_truth, str2idx) + except KeyError as e: + logger.info(f"Unable to vectorize using str2idx with exception {e}. Falling back to ignoring str2idx") + return np.vectorize(lambda x, y: x)(ground_truth, str2idx) + + +def _encode_categorical_feature(raw: np.array, str2idx: dict) -> np.array: + """Encodes raw categorical string value to encoded numeric value. + + Args: + raw: String categorical representation. + str2idx: Dictionary that maps string representation to encoded value. + + Returns: + Encoded numeric value. + """ + return str2idx[raw] + + +def _get_ground_truth_df(ground_truth: str) -> DataFrame: + # determine ground truth data format and get appropriate reader + data_format = figure_data_format_dataset(ground_truth) + if data_format not in CACHEABLE_FORMATS: + raise ValueError(f"{data_format} is not supported for ground truth file, valid types are {CACHEABLE_FORMATS}") + reader = get_from_registry(data_format, data_reader_registry) + + # retrieve ground truth from source data set + if data_format in {"csv", "tsv"}: + return reader(ground_truth, dtype=None, df_lib=pd) # allow type inference + return reader(ground_truth, df_lib=pd) + + +def _extract_ground_truth_values( + ground_truth: "str | DataFrame", + output_feature_name: str, + ground_truth_split: int, + split_file: "str | None" = None, +) -> pd.Series: + """Helper function to extract ground truth values. + + Args: + ground_truth: Path to source data containing ground truth or ground truth dataframe. + output_feature_name: Output feature name for ground truth values. + ground_truth_split: Dataset split to use for ground truth, defaults to 2. + split_file: Optional file path to split values. + + Returns: + Ground truth values from source data set. + """ + ground_truth_df = _get_ground_truth_df(ground_truth) if isinstance(ground_truth, str) else ground_truth + + # extract ground truth for visualization + if SPLIT in ground_truth_df: + # get split value from source data set + split = ground_truth_df[SPLIT] + gt = ground_truth_df[output_feature_name][split == ground_truth_split] + elif split_file is not None: + # retrieve from split file + if split_file.endswith(".csv"): + # Legacy code path for previous split file format + warnings.warn( + "Using a CSV split file is deprecated and will be removed in a future version. " + "Please retrain or convert to Parquet", + DeprecationWarning, + ) + split = load_array(split_file) + mask = split == ground_truth_split + else: + split = pd.read_parquet(split_file) + + # Realign index from the split file with the ground truth to account for + # dropped rows during preprocessing. + # https://stackoverflow.com/a/65731168 + mask = split.iloc[:, 0] == ground_truth_split + mask = mask.reindex(ground_truth_df.index, fill_value=False) + + gt = ground_truth_df[output_feature_name][mask] + else: + # use all the data in ground_truth + gt = ground_truth_df[output_feature_name] + + return gt + + +def _get_cols_from_predictions(predictions_paths, cols, metadata): + results_per_model = [] + for predictions_path in predictions_paths: + pred_df = pd.read_parquet(predictions_path) + + shapes_fname = replace_file_extension(predictions_path, "shapes.json") + if path_exists(shapes_fname): + column_shapes = load_json(shapes_fname) + pred_df = unflatten_df(pred_df, column_shapes, LOCAL_BACKEND.df_engine) + + for col in cols: + # Convert categorical features back to indices + if col.endswith(_PREDICTIONS_SUFFIX): + feature_name = col[: -len(_PREDICTIONS_SUFFIX)] + feature_metadata = metadata[feature_name] + if "str2idx" in feature_metadata: + pred_df[col] = pred_df[col].map(lambda x: feature_metadata["str2idx"][x]) + + pred_df = to_numpy_dataset(pred_df, LOCAL_BACKEND) + results_per_model += [pred_df[col] for col in cols] + + return results_per_model + + +# --------------------------------------------------------------------------- +# Data loading +# --------------------------------------------------------------------------- + + +@DeveloperAPI +def validate_conf_thresholds_and_probabilities_2d_3d(probabilities, threshold_output_feature_names): + """Ensure probabilities and threshold output_feature_names arrays have two members each. + + Args: + probabilities: List of probabilities per model. + threshold_output_feature_names: List of threshold output_feature_names per model. + + Raises: + RuntimeError: If either list does not contain exactly two members. + """ + validation_mapping = { + "probabilities": probabilities, + "threshold_output_feature_names": threshold_output_feature_names, + } + for item, value in validation_mapping.items(): + item_len = len(value) + if item_len != 2: + exception_message = f"Two {item} should be provided - {item_len} was given." + logger.error(exception_message) + raise RuntimeError(exception_message) + + +@DeveloperAPI +def load_data_for_viz(load_type, model_file_statistics, dtype=int, ground_truth_split=2) -> "dict[str, Any]": + """Load JSON files (training stats, evaluation stats...) for a list of models. + + Args: + load_type: Type of the data loader to be used. + model_file_statistics: JSON file or list of json files containing any model experiment stats. + + Returns: + List of training statistics loaded as json objects. + """ + supported_load_types = { + "load_json": load_json, + "load_from_file": partial(load_from_file, dtype=dtype, ground_truth_split=ground_truth_split), + } + loader = supported_load_types[load_type] + # Loads training stats from JSON file(s). + try: + stats_per_model = [loader(stats_f) for stats_f in model_file_statistics] + except (TypeError, AttributeError): + logger.exception(f"Unable to open model statistics file {model_file_statistics}!") + raise + return stats_per_model + + +def _load_training_stats(data: dict) -> TrainingStats: + """Construct a TrainingStats from a dict loaded from JSON.""" + eval_freq = data.get("evaluation_frequency") + if isinstance(eval_freq, dict): + eval_freq = EvaluationFrequency(**eval_freq) + elif eval_freq is None: + eval_freq = EvaluationFrequency() + return TrainingStats( + training=data.get("training", {}), + validation=data.get("validation", {}), + test=data.get("test", {}), + evaluation_frequency=eval_freq, + ) + + +@DeveloperAPI +def load_training_stats_for_viz(load_type, model_file_statistics, dtype=int, ground_truth_split=2) -> TrainingStats: + """Load model file data (specifically training stats) for a list of models. + + Args: + load_type: Type of the data loader to be used. + model_file_statistics: JSON file or list of json files containing any model experiment stats. + + Returns: + List of model statistics loaded as TrainingStats objects. + """ + stats_per_model = load_data_for_viz( + load_type, model_file_statistics, dtype=dtype, ground_truth_split=ground_truth_split + ) + try: + stats_per_model = [_load_training_stats(j) for j in stats_per_model] + except Exception: + logger.exception(f"Failed to load model statistics {model_file_statistics}!") + raise + return stats_per_model + + +@DeveloperAPI +def convert_to_list(item): + """If item is not list class instance or None put inside a list. + + Args: + item: Object to be checked and converted. + + Returns: + Original item if it is a list instance or list containing the item. + """ + return item if item is None or isinstance(item, list) else [item] + + +def _validate_output_feature_name_from_train_stats(output_feature_name, train_stats_per_model): + """Validate prediction output_feature_name from model train stats and return it as list. + + Args: + output_feature_name: Output feature name containing ground truth. + train_stats_per_model: List of per model train stats. + + Returns: + List of output_feature_name(s) containing ground truth. + """ + output_feature_names_set = set() + for train_stats in train_stats_per_model: + for key in itertools.chain(train_stats.training.keys(), train_stats.validation.keys(), train_stats.test.keys()): + output_feature_names_set.add(key) + try: + if output_feature_name in output_feature_names_set: + return [output_feature_name] + else: + return output_feature_names_set + # raised if output_feature_name is empty iterable (e.g. [] in set()) + except TypeError: + return output_feature_names_set + + +def _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model): + """Validate prediction output_feature_name from model test stats and return it as list. + + Args: + output_feature_name: Output feature name containing ground truth. + test_stats_per_model: List of per model test stats. + + Returns: + List of output_feature_name(s) containing ground truth. + """ + output_feature_names_set = set() + for ls in test_stats_per_model: + for key in ls: + output_feature_names_set.add(key) + try: + if output_feature_name in output_feature_names_set: + return [output_feature_name] + else: + return output_feature_names_set + # raised if output_feature_name is empty iterable (e.g. [] in set()) + except TypeError: + return output_feature_names_set + + +@DeveloperAPI +def generate_filename_template_path(output_dir, filename_template): + """Ensure path to template file can be constructed given an output dir. + + Create output directory if it does not yet exist. + + Args: + output_dir: Directory that will contain the filename_template file. + filename_template: Name of the file template to be appended to the filename template path. + + Returns: + Path to filename template inside the output dir or None if the output dir is None. + """ + if output_dir: + os.makedirs(output_dir, exist_ok=True) + return os.path.join(output_dir, filename_template) + return None diff --git a/ludwig/visualize/cli.py b/ludwig/visualize/cli.py new file mode 100644 index 0000000..137e78f --- /dev/null +++ b/ludwig/visualize/cli.py @@ -0,0 +1,201 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""CLI entry point and visualizations registry.""" + +import argparse +import logging +import sys +from collections.abc import Callable + +from ludwig.api_annotations import PublicAPI +from ludwig.constants import PREDICTIONS +from ludwig.contrib import add_contrib_callback_args +from ludwig.utils.print_utils import get_logging_level_registry +from ludwig.visualize.confusion import confusion_matrix_cli +from ludwig.visualize.curves import ( + calibration_1_vs_all_cli, + calibration_multiclass_cli, + precision_recall_curves_cli, + precision_recall_curves_from_test_statistics_cli, + roc_curves_cli, + roc_curves_from_test_statistics_cli, +) +from ludwig.visualize.hyperopt import hyperopt_hiplot_cli, hyperopt_report_cli +from ludwig.visualize.performance import ( + compare_classifiers_multiclass_multimetric_cli, + compare_classifiers_performance_changing_k_cli, + compare_classifiers_performance_from_pred_cli, + compare_classifiers_performance_from_prob_cli, + compare_classifiers_performance_subset_cli, + compare_classifiers_predictions_cli, + compare_classifiers_predictions_distribution_cli, + compare_performance_cli, + frequency_vs_f1_cli, +) +from ludwig.visualize.threshold import ( + binary_threshold_vs_metric_cli, + confidence_thresholding_2thresholds_2d_cli, + confidence_thresholding_2thresholds_3d_cli, + confidence_thresholding_cli, + confidence_thresholding_data_vs_acc_cli, + confidence_thresholding_data_vs_acc_subset_cli, + confidence_thresholding_data_vs_acc_subset_per_class_cli, +) +from ludwig.visualize.training import learning_curves_cli + +logger = logging.getLogger(__name__) + + +@PublicAPI +def get_visualizations_registry() -> "dict[str, Callable]": + return { + "compare_performance": compare_performance_cli, + "compare_classifiers_performance_from_prob": compare_classifiers_performance_from_prob_cli, + "compare_classifiers_performance_from_pred": compare_classifiers_performance_from_pred_cli, + "compare_classifiers_performance_subset": compare_classifiers_performance_subset_cli, + "compare_classifiers_performance_changing_k": compare_classifiers_performance_changing_k_cli, + "compare_classifiers_multiclass_multimetric": compare_classifiers_multiclass_multimetric_cli, + "compare_classifiers_predictions": compare_classifiers_predictions_cli, + "compare_classifiers_predictions_distribution": compare_classifiers_predictions_distribution_cli, + "confidence_thresholding": confidence_thresholding_cli, + "confidence_thresholding_data_vs_acc": confidence_thresholding_data_vs_acc_cli, + "confidence_thresholding_data_vs_acc_subset": confidence_thresholding_data_vs_acc_subset_cli, + "confidence_thresholding_data_vs_acc_subset_per_class": confidence_thresholding_data_vs_acc_subset_per_class_cli, + "confidence_thresholding_2thresholds_2d": confidence_thresholding_2thresholds_2d_cli, + "confidence_thresholding_2thresholds_3d": confidence_thresholding_2thresholds_3d_cli, + "binary_threshold_vs_metric": binary_threshold_vs_metric_cli, + "roc_curves": roc_curves_cli, + "roc_curves_from_test_statistics": roc_curves_from_test_statistics_cli, + "precision_recall_curves": precision_recall_curves_cli, + "precision_recall_curves_from_test_statistics": precision_recall_curves_from_test_statistics_cli, + "calibration_1_vs_all": calibration_1_vs_all_cli, + "calibration_multiclass": calibration_multiclass_cli, + "confusion_matrix": confusion_matrix_cli, + "frequency_vs_f1": frequency_vs_f1_cli, + "learning_curves": learning_curves_cli, + "hyperopt_report": hyperopt_report_cli, + "hyperopt_hiplot": hyperopt_hiplot_cli, + } + + +@PublicAPI +def cli(sys_argv): + parser = argparse.ArgumentParser( + description="This script analyzes results and shows some nice plots.", + prog="ludwig visualize", + usage="%(prog)s [options]", + ) + + parser.add_argument("-g", "--ground_truth", help="ground truth file") + parser.add_argument("-gm", "--ground_truth_metadata", help="input metadata JSON file") + parser.add_argument( + "-sf", + "--split_file", + default=None, + help="file containing split values used in conjunction with ground truth file.", + ) + + parser.add_argument( + "-od", + "--output_directory", + help="directory where to save plots.If not specified, plots will be displayed in a window", + ) + parser.add_argument( + "-ff", "--file_format", help="file format of output plots", default="pdf", choices=["pdf", "png"] + ) + + parser.add_argument( + "-v", + "--visualization", + choices=sorted(list(get_visualizations_registry().keys())), + help="type of visualization to generate", + required=True, + ) + + parser.add_argument("-ofn", "--output_feature_name", default=[], help="name of the output feature to visualize") + parser.add_argument( + "-gts", "--ground_truth_split", default=2, help="ground truth split - 0:train, 1:validation, 2:test split" + ) + parser.add_argument( + "-tf", + "--threshold_output_feature_names", + default=[], + nargs="+", + help="names of output features for 2d threshold", + ) + parser.add_argument("-pred", "--predictions", default=[], nargs="+", type=str, help="predictions files") + parser.add_argument("-prob", "--probabilities", default=[], nargs="+", type=str, help="probabilities files") + parser.add_argument("-trs", "--training_statistics", default=[], nargs="+", type=str, help="training stats files") + parser.add_argument("-tes", "--test_statistics", default=[], nargs="+", type=str, help="test stats files") + parser.add_argument("-hs", "--hyperopt_stats_path", default=None, type=str, help="hyperopt stats file") + parser.add_argument( + "-mn", "--model_names", default=[], nargs="+", type=str, help="names of the models to use as labels" + ) + parser.add_argument("-tn", "--top_n_classes", default=[0], nargs="+", type=int, help="number of classes to plot") + parser.add_argument("-k", "--top_k", default=3, type=int, help="number of elements in the ranklist to consider") + parser.add_argument( + "-ll", + "--labels_limit", + default=0, + type=int, + help="maximum numbers of labels. Encoded numeric label values in dataset that are higher than " + 'labels_limit are considered to be "rare" labels', + ) + parser.add_argument( + "-ss", + "--subset", + default="ground_truth", + choices=["ground_truth", PREDICTIONS], + help="type of subset filtering", + ) + parser.add_argument( + "-n", "--normalize", action="store_true", default=False, help="normalize rows in confusion matrix" + ) + parser.add_argument( + "-m", "--metrics", default=["f1"], nargs="+", type=str, help="metrics to display in threshold_vs_metric" + ) + parser.add_argument( + "-pl", "--positive_label", type=int, default=1, help="label of the positive class for the roc curve" + ) + parser.add_argument( + "-l", + "--logging_level", + default="info", + help="the level of logging to use", + choices=["critical", "error", "warning", "info", "debug", "notset"], + ) + + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + + args.callbacks = args.callbacks or [] + for callback in args.callbacks: + callback.on_cmdline("visualize", *sys_argv) + + args.logging_level = get_logging_level_registry()[args.logging_level] + logging.getLogger("ludwig").setLevel(args.logging_level) + global logger + logger = logging.getLogger("ludwig.visualize") + + try: + vis_func = get_visualizations_registry()[args.visualization] + except KeyError: + logger.info("Visualization argument not recognized") + raise + vis_func(**vars(args)) + + +if __name__ == "__main__": + cli(sys.argv[1:]) diff --git a/ludwig/visualize/confusion.py b/ludwig/visualize/confusion.py new file mode 100644 index 0000000..391b9d0 --- /dev/null +++ b/ludwig/visualize/confusion.py @@ -0,0 +1,155 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Confusion matrix visualization.""" + +import logging +import os + +import numpy as np +from scipy.stats import entropy + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils import visualization_utils +from ludwig.utils.data_utils import load_json +from ludwig.visualize._utils import ( + _validate_output_feature_name_from_test_stats, + convert_to_list, + generate_filename_template_path, + load_data_for_viz, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def confusion_matrix_cli(test_statistics: "str | list[str]", ground_truth_metadata: str, **kwargs: dict) -> None: + """Load model data from files to be shown by confusion_matrix. + + Args: + test_statistics: Path to experiment test statistics file. + ground_truth_metadata: Path to ground truth metadata file. + **kwargs: Parameters for the requested visualizations. + """ + test_stats_per_model = load_data_for_viz("load_json", test_statistics) + metadata = load_json(ground_truth_metadata) + confusion_matrix(test_stats_per_model, metadata, **kwargs) + + +@DeveloperAPI +def confusion_matrix( + test_stats_per_model: "list[dict]", + metadata: dict, + output_feature_name: "str | None", + top_n_classes: "list[int]", + normalize: bool, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Show confusion matrix in the models predictions for each `output_feature_name`. + + For each model (in the aligned lists of test_statistics and model_names) + it produces a heatmap of the confusion matrix in the predictions for + each output_feature_name that has a confusion matrix in test_statistics. + The value of `top_n_classes` limits the heatmap to the n most frequent + classes. + + Args: + test_stats_per_model: Dictionary containing evaluation performance statistics. + metadata: Intermediate preprocess structure created during training containing + the mappings of the input dataset. + output_feature_name: Name of the output feature to use for the visualization. + If None, use all output features. + top_n_classes: Number of top classes or list containing the number of top + classes to plot. + normalize: Flag to normalize rows in confusion matrix. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots will + be displayed in a window. + file_format: File format of output plots — 'pdf' or 'png'. + """ + test_stats_per_model_list = test_stats_per_model + model_names_list = convert_to_list(model_names) + filename_template = "confusion_matrix_{}_{}_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list) + + confusion_matrix_found = False + for i, test_statistics in enumerate(test_stats_per_model_list): + for output_feature_name in output_feature_names: + if "confusion_matrix" in test_statistics[output_feature_name]: + confusion_matrix_found = True + _confusion_matrix = np.array(test_statistics[output_feature_name]["confusion_matrix"]) + model_name_name = ( + model_names_list[i] if (model_names_list is not None and i < len(model_names_list)) else "" + ) + if ( + metadata is not None + and output_feature_name in metadata + and ("idx2str" in metadata[output_feature_name] or "bool2str" in metadata[output_feature_name]) + ): + if "bool2str" in metadata[output_feature_name]: # Handles the binary output case + labels = metadata[output_feature_name]["bool2str"] + else: + labels = metadata[output_feature_name]["idx2str"] + else: + labels = list(range(len(_confusion_matrix))) + + for k in top_n_classes: + k = min(k, _confusion_matrix.shape[0]) if k > 0 else _confusion_matrix.shape[0] + cm = _confusion_matrix[:k, :k] + if normalize: + with np.errstate(divide="ignore", invalid="ignore"): + cm_norm = np.true_divide(cm, cm.sum(1)[:, np.newaxis]) + cm_norm[cm_norm == np.inf] = 0 + cm_norm = np.nan_to_num(cm_norm) + cm = cm_norm + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(model_name_name, output_feature_name, "top" + str(k)) + + visualization_utils.confusion_matrix_plot( + cm, labels[:k], output_feature_name=output_feature_name, filename=filename + ) + + entropies = [] + for row in cm: + if np.count_nonzero(row) > 0: + entropies.append(entropy(row)) + else: + entropies.append(0) + class_entropy = np.array(entropies) + class_desc_entropy = np.argsort(class_entropy)[::-1] + desc_entropy = class_entropy[class_desc_entropy] + + filename = None + if output_directory: + filename = filename_template_path.format( + "entropy_" + model_name_name, output_feature_name, "top" + str(k) + ) + + visualization_utils.bar_plot( + class_desc_entropy, + desc_entropy, + labels=[labels[i] for i in class_desc_entropy], + title="Classes ranked by entropy of Confusion Matrix row", + filename=filename, + ) + if not confusion_matrix_found: + logger.error("Cannot find confusion_matrix in evaluation data") + raise FileNotFoundError("Cannot find confusion_matrix in evaluation data") diff --git a/ludwig/visualize/curves.py b/ludwig/visualize/curves.py new file mode 100644 index 0000000..e55442d --- /dev/null +++ b/ludwig/visualize/curves.py @@ -0,0 +1,618 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""ROC, precision-recall, and calibration curve visualizations.""" + +import logging +import os + +import numpy as np +import pandas as pd +import sklearn +import sklearn.metrics +from scipy.stats import entropy # noqa: F401 - keep import available +from sklearn.calibration import calibration_curve +from sklearn.metrics import brier_score_loss + +from ludwig.api_annotations import DeveloperAPI +from ludwig.utils import visualization_utils +from ludwig.utils.data_utils import load_json +from ludwig.visualize._utils import ( + _convert_ground_truth, + _extract_ground_truth_values, + _get_cols_from_predictions, + _PROBABILITIES_SUFFIX, + _vectorize_ground_truth, + convert_to_list, + generate_filename_template_path, + load_data_for_viz, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def precision_recall_curves_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by precision_recall_curves_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + precision_recall_curves( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def precision_recall_curves( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + positive_label: int = 1, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show the precision recall curves for output features in the specified models. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + positive_label: Numeric encoded value for the positive class. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth, positive_label = _convert_ground_truth( + ground_truth, feature_metadata, ground_truth_apply_idx, positive_label + ) + + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + precision_recalls = [] + + for _, prob in enumerate(probs): + if len(prob.shape) > 1: + prob = prob[:, positive_label] + precision, recall, _ = sklearn.metrics.precision_recall_curve(ground_truth, prob, pos_label=positive_label) + precision_recalls.append({"precisions": precision, "recalls": recall}) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "precision_recall_curve." + file_format) + + visualization_utils.precision_recall_curves_plot( + precision_recalls, model_names_list, title="Precision Recall Curves", filename=filename + ) + + +@DeveloperAPI +def precision_recall_curves_from_test_statistics_cli(test_statistics: "str | list[str]", **kwargs: dict) -> None: + """Load model data from files to be shown by precision_recall_curves_from_test_statistics_cli. + + Args: + test_statistics: Path to experiment test statistics file. + kwargs: Parameters for the requested visualizations. + """ + test_stats_per_model = load_data_for_viz("load_json", test_statistics) + precision_recall_curves_from_test_statistics(test_stats_per_model, **kwargs) + + +@DeveloperAPI +def precision_recall_curves_from_test_statistics( + test_stats_per_model: "list[dict]", + output_feature_name: str, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Show the PR curves for the specified models output binary `output_feature_name`. + + Args: + test_stats_per_model: Dictionary containing evaluation performance statistics. + output_feature_name: Name of the output feature to use for the visualization. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + """ + model_names_list = convert_to_list(model_names) + filename_template = "precision_recall_curves_from_prediction_statistics." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + precision_recalls = [] + for curr_test_statistics in test_stats_per_model: + precisions = curr_test_statistics[output_feature_name]["precision_recall_curve"]["precisions"] + recalls = curr_test_statistics[output_feature_name]["precision_recall_curve"]["recalls"] + precision_recalls.append({"precisions": precisions, "recalls": recalls}) + + visualization_utils.precision_recall_curves_plot( + precision_recalls, model_names_list, title="Precision Recall Curves", filename=filename_template_path + ) + + +@DeveloperAPI +def roc_curves_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by roc_curves_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + roc_curves( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def roc_curves( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + positive_label: int = 1, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show the roc curves for output features in the specified models. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + positive_label: Numeric encoded value for the positive class. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth, positive_label = _convert_ground_truth( + ground_truth, feature_metadata, ground_truth_apply_idx, positive_label + ) + + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + fpr_tprs = [] + + for _i, prob in enumerate(probs): + if len(prob.shape) > 1: + prob = prob[:, positive_label] + fpr, tpr, _ = sklearn.metrics.roc_curve(ground_truth, prob, pos_label=positive_label) + fpr_tprs.append((fpr, tpr)) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "roc_curves." + file_format) + + visualization_utils.roc_curves(fpr_tprs, model_names_list, title="ROC curves", filename=filename) + + +@DeveloperAPI +def roc_curves_from_test_statistics_cli(test_statistics: "str | list[str]", **kwargs: dict) -> None: + """Load model data from files to be shown by roc_curves_from_test_statistics_cli. + + Args: + test_statistics: Path to experiment test statistics file. + kwargs: Parameters for the requested visualizations. + """ + test_stats_per_model = load_data_for_viz("load_json", test_statistics) + roc_curves_from_test_statistics(test_stats_per_model, **kwargs) + + +@DeveloperAPI +def roc_curves_from_test_statistics( + test_stats_per_model: "list[dict]", + output_feature_name: str, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Show the roc curves for the specified models output binary `output_feature_name`. + + Args: + test_stats_per_model: Dictionary containing evaluation performance statistics. + output_feature_name: Name of the output feature to use for the visualization. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + """ + model_names_list = convert_to_list(model_names) + filename_template = "roc_curves_from_prediction_statistics." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + fpr_tprs = [] + for curr_test_statistics in test_stats_per_model: + fpr = curr_test_statistics[output_feature_name]["roc_curve"]["false_positive_rate"] + tpr = curr_test_statistics[output_feature_name]["roc_curve"]["true_positive_rate"] + fpr_tprs.append((fpr, tpr)) + + visualization_utils.roc_curves(fpr_tprs, model_names_list, title="ROC curves", filename=filename_template_path) + + +@DeveloperAPI +def calibration_1_vs_all_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + output_feature_proc_name: "str | None" = None, + ground_truth_apply_idx: bool = True, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by calibration_1_vs_all_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + output_feature_proc_name: Name of the output feature column in ground_truth. If ground_truth + is a preprocessed parquet or hdf5 file, the column name will be _. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + kwargs: Parameters for the requested visualizations. + """ + + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values( + ground_truth, output_feature_proc_name or output_feature_name, ground_truth_split, split_file + ) + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + calibration_1_vs_all( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def calibration_1_vs_all( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + top_n_classes: "list[int]", + labels_limit: int, + model_names: "list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show models probability of predictions for the specified output_feature_name. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + top_n_classes: List containing the number of classes to plot. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: List of the names of the models to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + feature_metadata = metadata[output_feature_name] + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + filename_template = "calibration_1_vs_all_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + for i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + probs[i] = prob_limit + + num_classes = len(metadata[output_feature_name]["str2idx"]) + + brier_scores = [] + + classes = min(num_classes, top_n_classes[0]) if top_n_classes[0] > 0 else num_classes + class_names = [feature_metadata["idx2str"][i] for i in range(classes)] + + for class_idx in range(classes): + fraction_positives_class = [] + mean_predicted_vals_class = [] + probs_class = [] + brier_scores_class = [] + for prob in probs: + gt_class = (ground_truth == class_idx).astype(int) + prob_class = prob[:, class_idx] + + curr_fraction_positives, curr_mean_predicted_vals = calibration_curve(gt_class, prob_class, n_bins=21) + + if len(curr_fraction_positives) < 2: + curr_fraction_positives = np.concatenate((np.array([0.0]), curr_fraction_positives)) + if len(curr_mean_predicted_vals) < 2: + curr_mean_predicted_vals = np.concatenate((np.array([0.0]), curr_mean_predicted_vals)) + + fraction_positives_class.append(curr_fraction_positives) + mean_predicted_vals_class.append(curr_mean_predicted_vals) + probs_class.append(prob[:, class_idx]) + brier_scores_class.append(brier_score_loss(gt_class, prob_class, pos_label=1)) + + brier_scores.append(brier_scores_class) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(class_idx) + + visualization_utils.calibration_plot( + fraction_positives_class, + mean_predicted_vals_class, + model_names_list, + class_name=class_names[class_idx], + filename=filename, + ) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format("prediction_distribution_" + str(class_idx)) + + visualization_utils.predictions_distribution_plot(probs_class, model_names_list, filename=filename) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format("brier") + + visualization_utils.brier_plot( + np.array(brier_scores), + algorithm_names=model_names_list, + class_names=class_names, + title="Brier scores for each class", + filename=filename, + ) + + +@DeveloperAPI +def calibration_multiclass_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by calibration_multiclass_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + calibration_multiclass( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def calibration_multiclass( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show models probability of predictions for each class of the specified output_feature_name. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: List of the names of the models to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + filename_template = "calibration_multiclass{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + + prob_classes = 0 + for i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + probs[i] = prob_limit + if probs[i].shape[1] > prob_classes: + prob_classes = probs[i].shape[1] + + gt_one_hot_dim_2 = max(prob_classes, max(ground_truth) + 1) + gt_one_hot = np.zeros((len(ground_truth), gt_one_hot_dim_2)) + gt_one_hot[np.arange(len(ground_truth)), ground_truth] = 1 + gt_one_hot_flat = gt_one_hot.flatten() + + fraction_positives = [] + mean_predicted_vals = [] + brier_scores = [] + for prob in probs: + # flatten probabilities to be compared to flatten ground truth + prob_flat = prob.flatten() + curr_fraction_positives, curr_mean_predicted_vals = calibration_curve(gt_one_hot_flat, prob_flat, n_bins=21) + fraction_positives.append(curr_fraction_positives) + mean_predicted_vals.append(curr_mean_predicted_vals) + brier_scores.append(brier_score_loss(gt_one_hot_flat, prob_flat, pos_label=1)) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format("") + + visualization_utils.calibration_plot(fraction_positives, mean_predicted_vals, model_names_list, filename=filename) + + filename = None + if output_directory: + filename = filename_template_path.format("_brier") + + visualization_utils.compare_classifiers_plot( + [brier_scores], ["brier"], model_names_list, adaptive=True, decimals=8, filename=filename + ) + + for i, brier_score in enumerate(brier_scores): + if i < len(model_names_list): + tokenizer_name = f"{model_names_list[i]}: " + tokenizer_name += "{}" + else: + tokenizer_name = "{}" + logger.info(tokenizer_name.format(brier_score)) diff --git a/ludwig/visualize/hyperopt.py b/ludwig/visualize/hyperopt.py new file mode 100644 index 0000000..2107903 --- /dev/null +++ b/ludwig/visualize/hyperopt.py @@ -0,0 +1,125 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Hyperparameter optimization visualizations.""" + +import logging + +import pandas as pd + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import SPACE +from ludwig.utils import visualization_utils +from ludwig.utils.data_utils import load_json +from ludwig.visualize._utils import generate_filename_template_path + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def hyperopt_report_cli(hyperopt_stats_path, output_directory=None, file_format="pdf", **kwargs) -> None: + """Produces a report about hyperparameter optimization creating one graph per hyperparameter to show the + distribution of results and one additional graph of pairwise hyperparameters interactions. + + Args: + hyperopt_stats_path: Path to the hyperopt results JSON file. + output_directory: Path where to save the output plots. + file_format: Format of the output plot, pdf or png. + """ + + hyperopt_report(hyperopt_stats_path, output_directory=output_directory, file_format=file_format) + + +@DeveloperAPI +def hyperopt_report( + hyperopt_stats_path: str, output_directory: "str | None" = None, file_format: str = "pdf", **kwargs +) -> None: + """Produces a report about hyperparameter optimization creating one graph per hyperparameter to show the + distribution of results and one additional graph of pairwise hyperparameters interactions. + + Args: + hyperopt_stats_path: Path to the hyperopt results JSON file. + output_directory: Directory where to save plots. If not specified, plots will be displayed in a window. + file_format: File format of output plots — 'pdf' or 'png'. + """ + filename_template = "hyperopt_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + + hyperopt_stats = load_json(hyperopt_stats_path) + + visualization_utils.hyperopt_report( + hyperopt_stats["hyperopt_config"]["parameters"], + hyperopt_results_to_dataframe( + hyperopt_stats["hyperopt_results"], + hyperopt_stats["hyperopt_config"]["parameters"], + hyperopt_stats["hyperopt_config"]["metric"], + ), + metric=hyperopt_stats["hyperopt_config"]["metric"], + filename_template=filename_template_path, + ) + + +@DeveloperAPI +def hyperopt_hiplot_cli(hyperopt_stats_path: str, output_directory: str | None = None, **kwargs) -> None: + """Produces a parallel coordinate plot about hyperparameter optimization creating one HTML file and optionally + a CSV file to be read by hiplot. + + Args: + hyperopt_stats_path: Path to the hyperopt results JSON file. + output_directory: Path where to save the output plots. + """ + + hyperopt_hiplot(hyperopt_stats_path, output_directory=output_directory) + + +@DeveloperAPI +def hyperopt_hiplot(hyperopt_stats_path: str, output_directory: str | None = None, **kwargs) -> None: + """Produces a parallel coordinate plot about hyperparameter optimization creating one HTML file and optionally + a CSV file to be read by hiplot. + + Args: + hyperopt_stats_path: Path to the hyperopt results JSON file. + output_directory: Directory where to save plots. If not specified, plots will be displayed in a window. + """ + filename = "hyperopt_hiplot.html" + filename_path = generate_filename_template_path(output_directory, filename) + + hyperopt_stats = load_json(hyperopt_stats_path) + hyperopt_df = hyperopt_results_to_dataframe( + hyperopt_stats["hyperopt_results"], + hyperopt_stats["hyperopt_config"]["parameters"], + hyperopt_stats["hyperopt_config"]["metric"], + ) + visualization_utils.hyperopt_hiplot( + hyperopt_df, + filename=filename_path, + ) + + +def _convert_space_to_dtype(space: str) -> str: + if space in visualization_utils.RAY_TUNE_FLOAT_SPACES: + return "float" + elif space in visualization_utils.RAY_TUNE_INT_SPACES: + return "int" + else: + return "object" + + +@DeveloperAPI +def hyperopt_results_to_dataframe(hyperopt_results, hyperopt_parameters, metric): + df = pd.DataFrame([{metric: res["metric_score"], **res["parameters"]} for res in hyperopt_results]) + df = df.astype( + {hp_name: _convert_space_to_dtype(hp_params[SPACE]) for hp_name, hp_params in hyperopt_parameters.items()} + ) + return df diff --git a/ludwig/visualize/performance.py b/ludwig/visualize/performance.py new file mode 100644 index 0000000..b1c126c --- /dev/null +++ b/ludwig/visualize/performance.py @@ -0,0 +1,1227 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Performance comparison and frequency visualizations.""" + +import logging +import os + +import numpy as np +import pandas as pd +import sklearn +import sklearn.metrics + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ACCURACY, HITS_AT_K, LOSS, PREDICTIONS +from ludwig.utils import visualization_utils +from ludwig.utils.data_utils import load_json +from ludwig.visualize._utils import ( + _extract_ground_truth_values, + _get_cols_from_predictions, + _PREDICTIONS_SUFFIX, + _PROBABILITIES_SUFFIX, + _validate_output_feature_name_from_test_stats, + _vectorize_ground_truth, + convert_to_list, + generate_filename_template_path, + load_data_for_viz, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def compare_performance_cli(test_statistics: "str | list[str]", **kwargs: dict) -> None: + """Load model data from files to be shown by compare_performance. + + Args: + test_statistics: Path to experiment test statistics file. + kwargs: Parameters for the requested visualizations. + """ + test_stats_per_model = load_data_for_viz("load_json", test_statistics) + compare_performance(test_stats_per_model, **kwargs) + + +@DeveloperAPI +def compare_performance( + test_stats_per_model: "list[dict]", + output_feature_name: "str | None" = None, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Produces model comparison barplot visualization for each overall metric. + + For each model (in the aligned lists of test_statistics and model_names) + it produces bars in a bar plot, one for each overall metric available + in the test_statistics file for the specified output_feature_name. + + Args: + test_stats_per_model: Dictionary containing evaluation performance statistics. + output_feature_name: Name of the output feature to use for the visualization. + If `None`, use all output features. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + + Example usage: + + ```python + model_a = LudwigModel(config) + model_a.train(dataset) + a_evaluation_stats, _, _ = model_a.evaluate(eval_set) + model_b = LudwigModel.load("path/to/model/") + b_evaluation_stats, _, _ = model_b.evaluate(eval_set) + compare_performance([a_evaluation_stats, b_evaluation_stats], model_names=["A", "B"]) + ``` + """ + ignore_names = { + "overall_stats", + "confusion_matrix", + "per_class_stats", + "predictions", + "probabilities", + "roc_curve", + "precision_recall_curve", + LOSS, + } + + filename_template = "compare_performance_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + + test_stats_per_model_list = convert_to_list(test_stats_per_model) + model_names_list = convert_to_list(model_names) + output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list) + + for output_feature_name in output_feature_names: + metric_names_sets = [set(tspr[output_feature_name].keys()) for tspr in test_stats_per_model_list] + metric_names = metric_names_sets[0] + for metric_names_set in metric_names_sets: + metric_names = metric_names.intersection(metric_names_set) + metric_names = metric_names - ignore_names + metrics_dict = {name: [] for name in metric_names} + + for test_stats_per_model in test_stats_per_model_list: + for metric_name in metric_names: + metrics_dict[metric_name].append(test_stats_per_model[output_feature_name][metric_name]) + + # are there any metrics to compare? + if metrics_dict: + metrics = [] + metrics_names = [] + min_val = float("inf") + max_val = float("-inf") + for metric_name, metric_vals in metrics_dict.items(): + if len(metric_vals) > 0: + metrics.append(metric_vals) + metrics_names.append(metric_name) + curr_min = min(metric_vals) + if curr_min < min_val: + min_val = curr_min + curr_max = max(metric_vals) + if curr_max > max_val: + max_val = curr_max + + filename = None + + if filename_template_path: + filename = filename_template_path.format(output_feature_name) + os.makedirs(output_directory, exist_ok=True) + + visualization_utils.compare_classifiers_plot( + metrics, + metrics_names, + model_names_list, + adaptive=min_val < 0 or max_val > 1, + title=f"Performance comparison on {output_feature_name}", + filename=filename, + ) + + +@DeveloperAPI +def compare_classifiers_performance_from_prob_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_classifiers_from_prob. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # translate string to encoded numeric value + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values( + ground_truth, output_feature_name, ground_truth_split, split_file=split_file + ) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + + compare_classifiers_performance_from_prob( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def compare_classifiers_performance_from_prob( + probabilities_per_model: "list[np.ndarray]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int = 0, + top_n_classes: "list[int] | int" = 3, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Produces model comparison barplot visualization from probabilities. + + For each model it produces bars in a bar plot, one for each overall metric + computed on the fly from the probabilities of predictions for the specified + `model_names`. + + Args: + probabilities_per_model: Path to experiment probabilities file. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + top_n_classes: List containing the number of classes to plot. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + top_n_classes_list = convert_to_list(top_n_classes) + k = top_n_classes_list[0] + model_names_list = convert_to_list(model_names) + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + + probs = probabilities_per_model + accuracies = [] + hits_at_ks = [] + mrrs = [] + + for _i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + prob = np.argsort(prob, axis=1) + top1 = prob[:, -1] + topk = prob[:, -k:] + + accuracies.append((ground_truth == top1).sum() / len(ground_truth)) + + hits_at_k = 0 + for j in range(len(ground_truth)): + hits_at_k += np.isin(ground_truth[j], topk[j]) + hits_at_ks.append(hits_at_k.item() / len(ground_truth)) + + mrr = 0 + for j in range(len(ground_truth)): + ground_truth_pos_in_probs = prob[j] == ground_truth[j] + if np.any(ground_truth_pos_in_probs): + mrr += 1 / -(np.argwhere(ground_truth_pos_in_probs).item() - prob.shape[1]) + mrrs.append(mrr / len(ground_truth)) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "compare_classifiers_performance_from_prob." + file_format) + + visualization_utils.compare_classifiers_plot( + [accuracies, hits_at_ks, mrrs], [ACCURACY, HITS_AT_K, "mrr"], model_names_list, filename=filename + ) + + +@DeveloperAPI +def compare_classifiers_performance_from_pred_cli( + predictions: "list[str]", + ground_truth: str, + ground_truth_metadata: str, + ground_truth_split: int, + split_file: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_classifiers_from_pred. + + Args: + predictions: List of prediction results file names to extract predictions from. + ground_truth: Path to ground truth file. + ground_truth_metadata: Path to ground truth metadata file / file path to + feature metadata json file created during training. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PREDICTIONS_SUFFIX}" + predictions_per_model = _get_cols_from_predictions(predictions, [col], metadata) + + compare_classifiers_performance_from_pred( + predictions_per_model, ground_truth, metadata, output_feature_name, output_directory=output_directory, **kwargs + ) + + +@DeveloperAPI +def compare_classifiers_performance_from_pred( + predictions_per_model: "list[np.ndarray]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Produces model comparison barplot visualization from predictions. + + For each model it produces bars in a bar plot, one for each overall metric + computed on the fly from the predictions for the specified + `model_names`. + + Args: + predictions_per_model: Path to experiment predictions file. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Name of the output feature to visualize. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + predictions_per_model = [np.ndarray.flatten(np.array(pred)) for pred in predictions_per_model] + + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + + preds = predictions_per_model + model_names_list = convert_to_list(model_names) + mapped_preds = [] + try: + for pred in preds: + mapped_preds.append([metadata[output_feature_name]["str2idx"][val] for val in pred]) + preds = mapped_preds + # If predictions are coming from npy file there is no need to convert to + # numeric labels using metadata + except (TypeError, KeyError): + pass + accuracies = [] + precisions = [] + recalls = [] + f1s = [] + + for _i, pred in enumerate(preds): + accuracies.append(sklearn.metrics.accuracy_score(ground_truth, pred)) + precisions.append(sklearn.metrics.precision_score(ground_truth, pred, average="macro")) + recalls.append(sklearn.metrics.recall_score(ground_truth, pred, average="macro")) + f1s.append(sklearn.metrics.f1_score(ground_truth, pred, average="macro")) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "compare_classifiers_performance_from_pred." + file_format) + + visualization_utils.compare_classifiers_plot( + [accuracies, precisions, recalls, f1s], + [ACCURACY, "precision", "recall", "f1"], + model_names_list, + filename=filename, + ) + + +@DeveloperAPI +def compare_classifiers_performance_subset_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_classifiers_subset. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + + compare_classifiers_performance_subset( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def compare_classifiers_performance_subset( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + top_n_classes: "list[int]", + labels_limit: int, + subset: str, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Produces model comparison barplot visualization from train subset. + + For each model it produces bars in a bar plot, one for each overall metric + computed on the fly from the probabilities predictions for the + specified `model_names`, considering only a subset of the full training set. + The way the subset is obtained is using the `top_n_classes` and + `subset` parameters. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + top_n_classes: List containing the number of classes to plot. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + subset: String specifying type of subset filtering. Valid values are + `ground_truth` or `predictions`. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + top_n_classes_list = convert_to_list(top_n_classes) + k = top_n_classes_list[0] + model_names_list = convert_to_list(model_names) + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + + subset_indices = ground_truth > 0 + gt_subset = ground_truth + if subset == "ground_truth": + subset_indices = ground_truth < k + gt_subset = ground_truth[subset_indices] + logger.info(f"Subset is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data") + + probs = probabilities_per_model + accuracies = [] + hits_at_ks = [] + + for i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + if subset == PREDICTIONS: + subset_indices = np.argmax(prob, axis=1) < k + gt_subset = ground_truth[subset_indices] + logger.info( + f"Subset for model_name {model_names[i] if model_names and i < len(model_names) else i} is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data" + ) + model_names[i] = ( + f"{model_names[i] if model_names and i < len(model_names) else i} ({len(gt_subset) / len(ground_truth) * 100:.2f}%)" + ) + + prob_subset = prob[subset_indices] + + prob_subset = np.argsort(prob_subset, axis=1) + top1_subset = prob_subset[:, -1] + top3_subset = prob_subset[:, -3:] + + accuracies.append(np.sum(gt_subset == top1_subset) / len(gt_subset)) + + hits_at_k = 0 + for j in range(len(gt_subset)): + hits_at_k += np.isin(gt_subset[j], top3_subset[i, :]) + hits_at_ks.append(hits_at_k.item() / len(gt_subset)) + + title = None + if subset == "ground_truth": + title = "Classifier performance on first {} class{} ({:.2f}%)".format( + k, "es" if k > 1 else "", len(gt_subset) / len(ground_truth) * 100 + ) + elif subset == PREDICTIONS: + title = "Classifier performance on first {} class{}".format(k, "es" if k > 1 else "") + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "compare_classifiers_performance_subset." + file_format) + + visualization_utils.compare_classifiers_plot( + [accuracies, hits_at_ks], [ACCURACY, HITS_AT_K], model_names_list, title=title, filename=filename + ) + + +@DeveloperAPI +def compare_classifiers_performance_changing_k_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_classifiers_changing_k. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + compare_classifiers_performance_changing_k( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def compare_classifiers_performance_changing_k( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + top_k: int, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Produce lineplot that show Hits@K metric while k goes from 1 to `top_k`. + + For each model it produces a line plot that shows the Hits@K metric + (that counts a prediction as correct if the model produces it among the + first k) while changing k from 1 to top_k for the specified + `output_feature_name`. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + top_k: Number of elements in the ranklist to consider. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + k = top_k + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + probs = probabilities_per_model + + hits_at_ks = [] + model_names_list = convert_to_list(model_names) + for _i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + prob = np.argsort(prob, axis=1) + + hits_at_k = [0.0] * k + for g in range(len(ground_truth)): + for j in range(k): + hits_at_k[j] += np.isin(ground_truth[g], prob[g, -j - 1 :]) + hits_at_ks.append(np.array(hits_at_k) / len(ground_truth)) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "compare_classifiers_performance_changing_k." + file_format) + + visualization_utils.compare_classifiers_line_plot( + np.arange(1, k + 1), + hits_at_ks, + "hits@k", + model_names_list, + title="Classifier comparison (hits@k)", + filename=filename, + ) + + +@DeveloperAPI +def compare_classifiers_multiclass_multimetric_cli( + test_statistics: "str | list[str]", ground_truth_metadata: str, **kwargs: dict +) -> None: + """Load model data from files to be shown by compare_classifiers_multiclass. + + Args: + test_statistics: Path to experiment test statistics file. + ground_truth_metadata: Path to ground truth metadata file. + kwargs: Parameters for the requested visualizations. + """ + test_stats_per_model = load_data_for_viz("load_json", test_statistics) + metadata = load_json(ground_truth_metadata) + compare_classifiers_multiclass_multimetric(test_stats_per_model, metadata=metadata, **kwargs) + + +@DeveloperAPI +def compare_classifiers_multiclass_multimetric( + test_stats_per_model: "list[dict]", + metadata: dict, + output_feature_name: str, + top_n_classes: "list[int]", + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Show the precision, recall and F1 of the model for the specified output_feature_name. + + For each model it produces four plots that show the precision, + recall and F1 of the model on several classes for the specified output_feature_name. + + Args: + test_stats_per_model: List containing dictionary of evaluation performance statistics. + metadata: Intermediate preprocess structure created during training containing + the mappings of the input dataset. + output_feature_name: Name of the output feature to use for the visualization. + If `None`, use all output features. + top_n_classes: List containing the number of classes to plot. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + """ + filename_template = "compare_classifiers_multiclass_multimetric_{}_{}_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + + test_stats_per_model_list = convert_to_list(test_stats_per_model) + model_names_list = convert_to_list(model_names) + output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list) + + for i, test_statistics in enumerate(test_stats_per_model_list): + for output_feature_name in output_feature_names: + model_name_name = model_names_list[i] if model_names_list is not None and i < len(model_names_list) else "" + if "per_class_stats" not in test_statistics[output_feature_name]: + logger.warning( + f"The output_feature_name {output_feature_name} in test statistics does not contain " + + "per_class_stats, skipping it." + ) + break + per_class_stats = test_statistics[output_feature_name]["per_class_stats"] + precisions = [] + recalls = [] + f1_scores = [] + labels = [] + for _, class_name in sorted( + ((metadata[output_feature_name]["str2idx"][key], key) for key in per_class_stats), + key=lambda tup: tup[0], + ): + class_stats = per_class_stats[class_name] + precisions.append(class_stats["precision"]) + recalls.append(class_stats["recall"]) + f1_scores.append(class_stats["f1_score"]) + labels.append(class_name) + for k in top_n_classes: + k = min(k, len(precisions)) if k > 0 else len(precisions) + ps = precisions[0:k] + rs = recalls[0:k] + fs = f1_scores[0:k] + ls = labels[0:k] + + filename = None + if filename_template_path: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(model_name_name, output_feature_name, f"top{k}") + + visualization_utils.compare_classifiers_multiclass_multimetric_plot( + [ps, rs, fs], + ["precision", "recall", "f1 score"], + labels=ls, + title=f"{model_name_name} Multiclass Precision / Recall / F1 Score top {k} {output_feature_name}", + filename=filename, + ) + + p_np = np.nan_to_num(np.array(precisions, dtype=np.float32)) + r_np = np.nan_to_num(np.array(recalls, dtype=np.float32)) + f1_np = np.nan_to_num(np.array(f1_scores, dtype=np.float32)) + labels_np = np.nan_to_num(np.array(labels)) + + sorted_indices = f1_np.argsort() + higher_f1s = sorted_indices[-k:][::-1] + filename = None + if filename_template_path: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(model_name_name, output_feature_name, f"best{k}") + visualization_utils.compare_classifiers_multiclass_multimetric_plot( + [p_np[higher_f1s], r_np[higher_f1s], f1_np[higher_f1s]], + ["precision", "recall", "f1 score"], + labels=labels_np[higher_f1s].tolist(), + title=f"{model_name_name} Multiclass Precision / Recall / " + f"F1 Score best {k} classes {output_feature_name}", + filename=filename, + ) + lower_f1s = sorted_indices[:k] + filename = None + if filename_template_path: + filename = filename_template_path.format(model_name_name, output_feature_name, f"worst{k}") + visualization_utils.compare_classifiers_multiclass_multimetric_plot( + [p_np[lower_f1s], r_np[lower_f1s], f1_np[lower_f1s]], + ["precision", "recall", "f1 score"], + labels=labels_np[lower_f1s].tolist(), + title=( + f"{model_name_name} Multiclass Precision / Recall / F1 Score worst " + + f"{k} classes {output_feature_name}" + ), + filename=filename, + ) + + filename = None + if filename_template_path: + filename = filename_template_path.format(model_name_name, output_feature_name, "sorted") + visualization_utils.compare_classifiers_multiclass_multimetric_plot( + [p_np[sorted_indices[::-1]], r_np[sorted_indices[::-1]], f1_np[sorted_indices[::-1]]], + ["precision", "recall", "f1 score"], + labels=labels_np[sorted_indices[::-1]].tolist(), + title=f"{model_name_name} Multiclass Precision / Recall / F1 Score {output_feature_name} sorted", + filename=filename, + ) + + logger.info("\n") + logger.info(model_name_name) + tmp_str = f"{output_feature_name} best 5 classes: " + tmp_str += "{}" + logger.info(tmp_str.format(higher_f1s)) + logger.info(f1_np[higher_f1s]) + tmp_str = f"{output_feature_name} worst 5 classes: " + tmp_str += "{}" + logger.info(tmp_str.format(lower_f1s)) + logger.info(f1_np[lower_f1s]) + tmp_str = f"{output_feature_name} number of classes with f1 score > 0: " + tmp_str += "{}" + logger.info(tmp_str.format(np.sum(f1_np > 0))) + tmp_str = f"{output_feature_name} number of classes with f1 score = 0: " + tmp_str += "{}" + logger.info(tmp_str.format(np.sum(f1_np == 0))) + + +@DeveloperAPI +def compare_classifiers_predictions_cli( + predictions: "list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_classifiers_predictions. + + Args: + predictions: List of prediction results file names to extract predictions from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PREDICTIONS_SUFFIX}" + predictions_per_model = _get_cols_from_predictions(predictions, [col], metadata) + + compare_classifiers_predictions( + predictions_per_model, ground_truth, metadata, output_feature_name, output_directory=output_directory, **kwargs + ) + + +@DeveloperAPI +def compare_classifiers_predictions( + predictions_per_model: "list[list]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show two models comparison of their output_feature_name predictions. + + Args: + predictions_per_model: List containing the model predictions for the + specified output_feature_name. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + model_names_list = convert_to_list(model_names) + name_c1 = model_names_list[0] if model_names is not None and len(model_names) > 0 else "c1" + name_c2 = model_names_list[1] if model_names is not None and len(model_names) > 1 else "c2" + + pred_c1 = predictions_per_model[0] + pred_c2 = predictions_per_model[1] + + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + pred_c1[pred_c1 > labels_limit] = labels_limit + pred_c2[pred_c2 > labels_limit] = labels_limit + + # TODO all shadows built in name - come up with a more descriptive name + all = len(ground_truth) + if all == 0: + logger.error("No labels in the ground truth") + return + + both_right = 0 + both_wrong_same = 0 + both_wrong_different = 0 + c1_right_c2_wrong = 0 + c1_wrong_c2_right = 0 + + for i in range(all): + if ground_truth[i] == pred_c1[i] and ground_truth[i] == pred_c2[i]: + both_right += 1 + elif ground_truth[i] != pred_c1[i] and ground_truth[i] != pred_c2[i]: + if pred_c1[i] == pred_c2[i]: + both_wrong_same += 1 + else: + both_wrong_different += 1 + elif ground_truth[i] == pred_c1[i] and ground_truth[i] != pred_c2[i]: + c1_right_c2_wrong += 1 + elif ground_truth[i] != pred_c1[i] and ground_truth[i] == pred_c2[i]: + c1_wrong_c2_right += 1 + + one_right = c1_right_c2_wrong + c1_wrong_c2_right + both_wrong = both_wrong_same + both_wrong_different + + logger.info(f"Test datapoints: {all}") + logger.info(f"Both right: {both_right} {100 * both_right / all:.2f}%") + logger.info(f"One right: {one_right} {100 * one_right / all:.2f}%") + logger.info( + f" {name_c1} right / {name_c2} wrong: {c1_right_c2_wrong} {100 * c1_right_c2_wrong / all:.2f}% {100 * c1_right_c2_wrong / one_right if one_right > 0 else 0:.2f}%" + ) + logger.info( + f" {name_c1} wrong / {name_c2} right: {c1_wrong_c2_right} {100 * c1_wrong_c2_right / all:.2f}% {100 * c1_wrong_c2_right / one_right if one_right > 0 else 0:.2f}%" + ) + logger.info(f"Both wrong: {both_wrong} {100 * both_wrong / all:.2f}%") + logger.info( + f" same prediction: {both_wrong_same} {100 * both_wrong_same / all:.2f}% {100 * both_wrong_same / both_wrong if both_wrong > 0 else 0:.2f}%" + ) + logger.info( + f" different prediction: {both_wrong_different} {100 * both_wrong_different / all:.2f}% {100 * both_wrong_different / both_wrong if both_wrong > 0 else 0:.2f}%" + ) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, f"compare_classifiers_predictions_{name_c1}_{name_c2}.{file_format}") + + visualization_utils.donut( + [both_right, one_right, both_wrong], + ["both right", "one right", "both wrong"], + [both_right, c1_right_c2_wrong, c1_wrong_c2_right, both_wrong_same, both_wrong_different], + [ + "both right", + f"{name_c1} right / {name_c2} wrong", + f"{name_c1} wrong / {name_c2} right", + "same prediction", + "different prediction", + ], + [0, 1, 1, 2, 2], + title=f"{name_c1} vs {name_c2}", + tight_layout=kwargs.pop("tight_layout", True), + filename=filename, + ) + + +@DeveloperAPI +def compare_classifiers_predictions_distribution_cli( + predictions: "list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_predictions_distribution. + + Args: + predictions: List of prediction results file names to extract predictions from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PREDICTIONS_SUFFIX}" + predictions_per_model = _get_cols_from_predictions(predictions, [col], metadata) + compare_classifiers_predictions_distribution( + predictions_per_model, ground_truth, metadata, output_feature_name, output_directory=output_directory, **kwargs + ) + + +@DeveloperAPI +def compare_classifiers_predictions_distribution( + predictions_per_model: "list[list]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show comparison of models predictions distribution for 10 output_feature_name classes. + + This visualization produces a radar plot comparing the distributions of + predictions of the models for the first 10 classes of the specified + output_feature_name. + + Args: + predictions_per_model: List containing the model predictions for the + specified output_feature_name. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + model_names_list = convert_to_list(model_names) + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + for i in range(len(predictions_per_model)): + predictions_per_model[i][predictions_per_model[i] > labels_limit] = labels_limit + + max_gt = max(ground_truth) + max_pred = max(max(alg_predictions) for alg_predictions in predictions_per_model) + max_val = max(max_gt, max_pred) + 1 + + counts_gt = np.bincount(ground_truth, minlength=max_val) + prob_gt = counts_gt / counts_gt.sum() + + counts_predictions = [np.bincount(alg_predictions, minlength=max_val) for alg_predictions in predictions_per_model] + + prob_predictions = [ + alg_count_prediction / alg_count_prediction.sum() for alg_count_prediction in counts_predictions + ] + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "compare_classifiers_predictions_distribution." + file_format) + + visualization_utils.radar_chart(prob_gt, prob_predictions, model_names_list, filename=filename) + + +@DeveloperAPI +def frequency_vs_f1_cli(test_statistics: "str | list[str]", ground_truth_metadata: str, **kwargs: dict) -> None: + """Load model data from files to be shown by frequency_vs_f1. + + Args: + test_statistics: Path to experiment test statistics file. + ground_truth_metadata: Path to ground truth metadata file. + kwargs: Parameters for the requested visualizations. + """ + test_stats_per_model = load_data_for_viz("load_json", test_statistics) + metadata = load_json(ground_truth_metadata) + frequency_vs_f1(test_stats_per_model, metadata, **kwargs) + + +@DeveloperAPI +def frequency_vs_f1( + test_stats_per_model: "list[dict]", + metadata: dict, + output_feature_name: "str | None", + top_n_classes: "list[int]", + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +): + """Show prediction statistics for the specified `output_feature_name` for each model. + + For each model (in the aligned lists of `test_stats_per_model` and + `model_names`), produces two plots statistics of predictions for the + specified `output_feature_name`. + + The first plot is a line plot with one x axis representing the different + classes and two vertical axes colored in orange and blue respectively. + The orange one is the frequency of the class and an orange line is plotted + to show the trend. The blue one is the F1 score for that class and a blue + line is plotted to show the trend. The classes on the x axis are sorted by + f1 score. + + The second plot has the same structure of the first one, + but the axes are flipped and the classes on the x axis are sorted by + frequency. + + Args: + test_stats_per_model: Dictionary containing evaluation performance statistics. + metadata: Intermediate preprocess structure created during training containing + the mappings of the input dataset. + output_feature_name: Name of the output feature to use for the visualization. + If `None`, use all output features. + top_n_classes: Number of top classes or list containing the number of top + classes to plot. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + """ + test_stats_per_model_list = test_stats_per_model + model_names_list = convert_to_list(model_names) + filename_template = "frequency_vs_f1_{}_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list) + k = top_n_classes[0] + + for i, test_stats in enumerate(test_stats_per_model_list): + for of_name in output_feature_names: + # Figure out model name + model_name = model_names_list[i] if model_names_list is not None and i < len(model_names_list) else "" + + # setup directory and filename + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(model_name, of_name) + + # setup local variables + per_class_stats = test_stats[of_name]["per_class_stats"] + class_names = metadata[of_name]["idx2str"] + + # get np arrays of frequencies, f1s and labels + idx2freq = {metadata[of_name]["str2idx"][key]: val for key, val in metadata[of_name]["str2freq"].items()} + freq_np = np.array([idx2freq[class_id] for class_id in sorted(idx2freq)], dtype=np.int32) + + if k > 0: + class_names = class_names[:k] + freq_np = freq_np[:k] + + f1_scores = [] + labels = [] + + for class_name in class_names: + class_stats = per_class_stats[class_name] + f1_scores.append(class_stats["f1_score"]) + labels.append(class_name) + + f1_np = np.nan_to_num(np.array(f1_scores, dtype=np.float32)) + labels_np = np.array(labels) + + # sort by f1 + f1_sort_idcs = f1_np.argsort()[::-1] + len_f1_sort_idcs = len(f1_sort_idcs) + + freq_sorted_by_f1 = freq_np[f1_sort_idcs] + freq_sorted_by_f1 = freq_sorted_by_f1[:len_f1_sort_idcs] + f1_sorted_by_f1 = f1_np[f1_sort_idcs] + f1_sorted_by_f1 = f1_sorted_by_f1[:len_f1_sort_idcs] + labels_sorted_by_f1 = labels_np[f1_sort_idcs] + labels_sorted_by_f1 = labels_sorted_by_f1[:len_f1_sort_idcs] + + # create viz sorted by f1 + visualization_utils.double_axis_line_plot( + f1_sorted_by_f1, + freq_sorted_by_f1, + "F1 score", + "frequency", + labels=labels_sorted_by_f1, + title=f"{model_name} F1 Score vs Frequency {of_name}", + filename=filename, + ) + + # sort by freq + freq_sort_idcs = freq_np.argsort()[::-1] + len_freq_sort_idcs = len(freq_sort_idcs) + + freq_sorted_by_freq = freq_np[freq_sort_idcs] + freq_sorted_by_freq = freq_sorted_by_freq[:len_freq_sort_idcs] + f1_sorted_by_freq = f1_np[freq_sort_idcs] + f1_sorted_by_freq = f1_sorted_by_freq[:len_freq_sort_idcs] + labels_sorted_by_freq = labels_np[freq_sort_idcs] + labels_sorted_by_freq = labels_sorted_by_freq[:len_freq_sort_idcs] + + # create viz sorted by freq + visualization_utils.double_axis_line_plot( + freq_sorted_by_freq, + f1_sorted_by_freq, + "frequency", + "F1 score", + labels=labels_sorted_by_freq, + title=f"{model_name} F1 Score vs Frequency {of_name}", + filename=filename, + ) diff --git a/ludwig/visualize/threshold.py b/ludwig/visualize/threshold.py new file mode 100644 index 0000000..7ceba12 --- /dev/null +++ b/ludwig/visualize/threshold.py @@ -0,0 +1,1123 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Confidence thresholding and binary threshold visualizations.""" + +import logging +import os + +import numpy as np +import pandas as pd +import sklearn +import sklearn.metrics + +from ludwig.api_annotations import DeveloperAPI +from ludwig.constants import ACCURACY, PREDICTIONS +from ludwig.utils import visualization_utils +from ludwig.utils.data_utils import load_json +from ludwig.visualize._utils import ( + _convert_ground_truth, + _encode_categorical_feature, + _extract_ground_truth_values, + _get_cols_from_predictions, + _PROBABILITIES_SUFFIX, + _vectorize_ground_truth, + convert_to_list, + generate_filename_template_path, + validate_conf_thresholds_and_probabilities_2d_3d, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def confidence_thresholding_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by confidence_thresholding. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + confidence_thresholding( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def confidence_thresholding( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show models accuracy and data coverage while increasing treshold. + + For each model it produces a pair of lines indicating the accuracy of + the model and the data coverage while increasing a threshold (x axis) on + the probabilities of predictions for the specified output_feature_name. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + thresholds = [t / 100 for t in range(0, 101, 5)] + + accuracies = [] + dataset_kept = [] + + for _i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + max_prob = np.max(prob, axis=1) + predictions = np.argmax(prob, axis=1) + + accuracies_alg = [] + dataset_kept_alg = [] + + for threshold in thresholds: + threshold = threshold if threshold < 1 else 0.999 + filtered_indices = max_prob >= threshold + filtered_gt = ground_truth[filtered_indices] + filtered_predictions = predictions[filtered_indices] + accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt) + + accuracies_alg.append(accuracy) + dataset_kept_alg.append(len(filtered_gt) / len(ground_truth)) + + accuracies.append(accuracies_alg) + dataset_kept.append(dataset_kept_alg) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "confidence_thresholding." + file_format) + + visualization_utils.confidence_filtering_plot( + thresholds, accuracies, dataset_kept, model_names_list, title="Confidence_Thresholding", filename=filename + ) + + +@DeveloperAPI +def confidence_thresholding_data_vs_acc_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by confidence_thresholding_data_vs_acc_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + confidence_thresholding_data_vs_acc( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def confidence_thresholding_data_vs_acc( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show models comparison of confidence threshold data vs accuracy. + + For each model it produces a line indicating the accuracy of the model + and the data coverage while increasing a threshold on the probabilities + of predictions for the specified output_feature_name. The difference with + confidence_thresholding is that it uses two axes instead of three, + not visualizing the threshold and having coverage as x axis instead of + the threshold. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + thresholds = [t / 100 for t in range(0, 101, 5)] + + accuracies = [] + dataset_kept = [] + + for _i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + max_prob = np.max(prob, axis=1) + predictions = np.argmax(prob, axis=1) + + accuracies_alg = [] + dataset_kept_alg = [] + + for threshold in thresholds: + threshold = threshold if threshold < 1 else 0.999 + filtered_indices = max_prob >= threshold + filtered_gt = ground_truth[filtered_indices] + filtered_predictions = predictions[filtered_indices] + accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt) + + accuracies_alg.append(accuracy) + dataset_kept_alg.append(len(filtered_gt) / len(ground_truth)) + + accuracies.append(accuracies_alg) + dataset_kept.append(dataset_kept_alg) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "confidence_thresholding_data_vs_acc." + file_format) + + visualization_utils.confidence_filtering_data_vs_acc_plot( + accuracies, + dataset_kept, + model_names_list, + title="Confidence_Thresholding (Data vs Accuracy)", + filename=filename, + ) + + +@DeveloperAPI +def confidence_thresholding_data_vs_acc_subset_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by confidence_thresholding_data_vs_acc_subset. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + confidence_thresholding_data_vs_acc_subset( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def confidence_thresholding_data_vs_acc_subset( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + top_n_classes: "list[int]", + labels_limit: int, + subset: str, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show models comparison of confidence threshold data vs accuracy on a subset of data. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + top_n_classes: List containing the number of classes to plot. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + subset: String specifying type of subset filtering. Valid values are + `ground_truth` or `predictions`. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + top_n_classes_list = convert_to_list(top_n_classes) + k = top_n_classes_list[0] + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + thresholds = [t / 100 for t in range(0, 101, 5)] + + accuracies = [] + dataset_kept = [] + + subset_indices = ground_truth > 0 + gt_subset = ground_truth + if subset == "ground_truth": + subset_indices = ground_truth < k + gt_subset = ground_truth[subset_indices] + logger.info(f"Subset is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data") + + for i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + if subset == PREDICTIONS: + subset_indices = np.argmax(prob, axis=1) < k + gt_subset = ground_truth[subset_indices] + logger.info( + f"Subset for model_name {model_names[i] if model_names and i < len(model_names) else i} is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data" + ) + + prob_subset = prob[subset_indices] + + max_prob = np.max(prob_subset, axis=1) + predictions = np.argmax(prob_subset, axis=1) + + accuracies_alg = [] + dataset_kept_alg = [] + + for threshold in thresholds: + threshold = threshold if threshold < 1 else 0.999 + filtered_indices = max_prob >= threshold + filtered_gt = gt_subset[filtered_indices] + filtered_predictions = predictions[filtered_indices] + accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt) + + accuracies_alg.append(accuracy) + dataset_kept_alg.append(len(filtered_gt) / len(ground_truth)) + + accuracies.append(accuracies_alg) + dataset_kept.append(dataset_kept_alg) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "confidence_thresholding_data_vs_acc_subset." + file_format) + + visualization_utils.confidence_filtering_data_vs_acc_plot( + accuracies, + dataset_kept, + model_names_list, + title="Confidence_Thresholding (Data vs Accuracy)", + filename=filename, + ) + + +@DeveloperAPI +def confidence_thresholding_data_vs_acc_subset_per_class_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_metadata: str, + ground_truth_split: int, + split_file: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by compare_classifiers_multiclass. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_metadata: Path to ground truth metadata file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + confidence_thresholding_data_vs_acc_subset_per_class( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def confidence_thresholding_data_vs_acc_subset_per_class( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + top_n_classes: "int | list[int]", + labels_limit: int, + subset: str, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show models comparison of confidence threshold data vs accuracy on a subset of data per class in top n classes. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Intermediate preprocess structure created during training containing + the mappings of the input dataset. + output_feature_name: Name of the output feature to use for the visualization. + top_n_classes: Number of top classes or list containing the number of top + classes to plot. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + subset: String specifying type of subset filtering. Valid values are + `ground_truth` or `predictions`. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata["str2idx"], ground_truth_apply_idx) + + filename_template = "confidence_thresholding_data_vs_acc_subset_per_class_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + top_n_classes_list = convert_to_list(top_n_classes) + k = top_n_classes_list[0] + # If top_n_classes is greater than the maximum number of tokens, truncate to use max token size + if k > len(metadata[output_feature_name]["idx2str"]): + k = len(metadata[output_feature_name]["idx2str"]) + if labels_limit > 0: + ground_truth[ground_truth > labels_limit] = labels_limit + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + + thresholds = [t / 100 for t in range(0, 101, 5)] + + for curr_k in range(k): + accuracies = [] + dataset_kept = [] + + subset_indices = ground_truth > 0 + gt_subset = ground_truth + if subset == "ground_truth": + subset_indices = ground_truth == curr_k + gt_subset = ground_truth[subset_indices] + logger.info(f"Subset is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data") + + for i, prob in enumerate(probs): + if labels_limit > 0 and prob.shape[1] > labels_limit + 1: + prob_limit = prob[:, : labels_limit + 1] + prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1) + prob = prob_limit + + if subset == PREDICTIONS: + subset_indices = np.argmax(prob, axis=1) == curr_k + gt_subset = ground_truth[subset_indices] + logger.info( + f"Subset for model_name {model_names_list[i] if model_names_list and i < len(model_names_list) else i} is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data" + ) + + prob_subset = prob[subset_indices] + + max_prob = np.max(prob_subset, axis=1) + predictions = np.argmax(prob_subset, axis=1) + + accuracies_alg = [] + dataset_kept_alg = [] + + for threshold in thresholds: + threshold = threshold if threshold < 1 else 0.999 + filtered_indices = max_prob >= threshold + filtered_gt = gt_subset[filtered_indices] + filtered_predictions = predictions[filtered_indices] + accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt) if len(filtered_gt) > 0 else 0 + + accuracies_alg.append(accuracy) + dataset_kept_alg.append(len(filtered_gt) / len(ground_truth)) + + accuracies.append(accuracies_alg) + dataset_kept.append(dataset_kept_alg) + + output_feature_name_name = metadata[output_feature_name]["idx2str"][curr_k] + + filename = None + if filename_template_path: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(output_feature_name_name) + + visualization_utils.confidence_filtering_data_vs_acc_plot( + accuracies, + dataset_kept, + model_names_list, + decimal_digits=2, + title=f"Confidence_Thresholding (Data vs Accuracy) for class {output_feature_name_name}", + filename=filename, + ) + + +@DeveloperAPI +def confidence_thresholding_2thresholds_2d_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + threshold_output_feature_names: "list[str]", + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by confidence_thresholding_2thresholds_2d_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + threshold_output_feature_names: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth0 = _extract_ground_truth_values( + ground_truth, threshold_output_feature_names[0], ground_truth_split, split_file + ) + + ground_truth1 = _extract_ground_truth_values( + ground_truth, threshold_output_feature_names[1], ground_truth_split, split_file + ) + + cols = [f"{feature_name}{_PROBABILITIES_SUFFIX}" for feature_name in threshold_output_feature_names] + probabilities_per_model = _get_cols_from_predictions(probabilities, cols, metadata) + + confidence_thresholding_2thresholds_2d( + probabilities_per_model, + [ground_truth0, ground_truth1], + metadata, + threshold_output_feature_names, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def confidence_thresholding_2thresholds_2d( + probabilities_per_model: "list[np.array]", + ground_truths: "list[np.array] | list[pd.Series]", + metadata: dict, + threshold_output_feature_names: "list[str]", + labels_limit: int, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Show confidence threshold data vs accuracy for two output feature names. + + Args: + probabilities_per_model: List of model probabilities. + ground_truths: Containing ground truth data. + metadata: Feature metadata dictionary. + threshold_output_feature_names: List containing two output feature names + for visualization. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + """ + try: + validate_conf_thresholds_and_probabilities_2d_3d(probabilities_per_model, threshold_output_feature_names) + except RuntimeError: + return + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + filename_template = "confidence_thresholding_2thresholds_2d_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + + if not isinstance(ground_truths[0], np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[threshold_output_feature_names[0]] + vfunc = np.vectorize(_encode_categorical_feature) + gt_1 = vfunc(ground_truths[0], feature_metadata["str2idx"]) + feature_metadata = metadata[threshold_output_feature_names[1]] + gt_2 = vfunc(ground_truths[1], feature_metadata["str2idx"]) + else: + gt_1 = ground_truths[0] + gt_2 = ground_truths[1] + + if labels_limit > 0: + gt_1[gt_1 > labels_limit] = labels_limit + gt_2[gt_2 > labels_limit] = labels_limit + + thresholds = [t / 100 for t in range(0, 101, 5)] + fixed_step_coverage = thresholds + name_t1 = f"{threshold_output_feature_names[0]} threshold" + name_t2 = f"{threshold_output_feature_names[1]} threshold" + + accuracies = [] + dataset_kept = [] + interps = [] + table = [[name_t1, name_t2, "coverage", ACCURACY]] + + if labels_limit > 0 and probs[0].shape[1] > labels_limit + 1: + prob_limit = probs[0][:, : labels_limit + 1] + prob_limit[:, labels_limit] = probs[0][:, labels_limit:].sum(1) + probs[0] = prob_limit + + if labels_limit > 0 and probs[1].shape[1] > labels_limit + 1: + prob_limit = probs[1][:, : labels_limit + 1] + prob_limit[:, labels_limit] = probs[1][:, labels_limit:].sum(1) + probs[1] = prob_limit + + max_prob_1 = np.max(probs[0], axis=1) + predictions_1 = np.argmax(probs[0], axis=1) + + max_prob_2 = np.max(probs[1], axis=1) + predictions_2 = np.argmax(probs[1], axis=1) + + for threshold_1 in thresholds: + threshold_1 = threshold_1 if threshold_1 < 1 else 0.999 + curr_accuracies = [] + curr_dataset_kept = [] + + for threshold_2 in thresholds: + threshold_2 = threshold_2 if threshold_2 < 1 else 0.999 + + filtered_indices = np.logical_and(max_prob_1 >= threshold_1, max_prob_2 >= threshold_2) + + filtered_gt_1 = gt_1[filtered_indices] + filtered_predictions_1 = predictions_1[filtered_indices] + filtered_gt_2 = gt_2[filtered_indices] + filtered_predictions_2 = predictions_2[filtered_indices] + + coverage = len(filtered_gt_1) / len(gt_1) + accuracy = ( + np.logical_and(filtered_gt_1 == filtered_predictions_1, filtered_gt_2 == filtered_predictions_2) + ).sum() / len(filtered_gt_1) + + curr_accuracies.append(accuracy) + curr_dataset_kept.append(coverage) + table.append([threshold_1, threshold_2, coverage, accuracy]) + + accuracies.append(curr_accuracies) + dataset_kept.append(curr_dataset_kept) + interps.append( + np.interp( + fixed_step_coverage, list(reversed(curr_dataset_kept)), list(reversed(curr_accuracies)), left=1, right=0 + ) + ) + + logger.info("CSV table") + for row in table: + logger.info(",".join([str(e) for e in row])) + + # ===========# + # Multiline # + # ===========# + filename = None + if filename_template_path: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format("multiline") + visualization_utils.confidence_filtering_data_vs_acc_multiline_plot( + accuracies, dataset_kept, model_names_list, title="Coverage vs Accuracy, two thresholds", filename=filename + ) + + # ==========# + # Max line # + # ==========# + filename = None + if filename_template_path: + filename = filename_template_path.format("maxline") + max_accuracies = np.amax(np.array(interps), 0) + visualization_utils.confidence_filtering_data_vs_acc_plot( + [max_accuracies], + [thresholds], + model_names_list, + title="Coverage vs Accuracy, two thresholds", + filename=filename, + ) + + # ==========================# + # Max line with thresholds # + # ==========================# + acc_matrix = np.array(accuracies) + cov_matrix = np.array(dataset_kept) + t1_maxes = [1] + t2_maxes = [1] + for i in range(len(fixed_step_coverage) - 1): + lower = fixed_step_coverage[i] + upper = fixed_step_coverage[i + 1] + indices = np.logical_and(cov_matrix >= lower, cov_matrix < upper) + selected_acc = acc_matrix.copy() + selected_acc[np.logical_not(indices)] = -1 + threshold_indices = np.unravel_index(np.argmax(selected_acc, axis=None), selected_acc.shape) + t1_maxes.append(thresholds[threshold_indices[0]]) + t2_maxes.append(thresholds[threshold_indices[1]]) + model_name = model_names_list[0] if model_names_list is not None and len(model_names_list) > 0 else "" + + filename = None + if filename_template_path: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format("maxline_with_thresholds") + + visualization_utils.confidence_filtering_data_vs_acc_plot( + [max_accuracies, t1_maxes, t2_maxes], + [fixed_step_coverage, fixed_step_coverage, fixed_step_coverage], + model_names=[model_name + " accuracy", name_t1, name_t2], + dotted=[False, True, True], + y_label="", + title="Coverage vs Accuracy & Threshold", + filename=filename, + ) + + +@DeveloperAPI +def confidence_thresholding_2thresholds_3d_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + threshold_output_feature_names: "list[str]", + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by confidence_thresholding_2thresholds_3d_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + threshold_output_feature_names: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth0 = _extract_ground_truth_values( + ground_truth, threshold_output_feature_names[0], ground_truth_split, split_file + ) + + ground_truth1 = _extract_ground_truth_values( + ground_truth, threshold_output_feature_names[1], ground_truth_split, split_file + ) + + cols = [f"{feature_name}{_PROBABILITIES_SUFFIX}" for feature_name in threshold_output_feature_names] + probabilities_per_model = _get_cols_from_predictions(probabilities, cols, metadata) + confidence_thresholding_2thresholds_3d( + probabilities_per_model, + [ground_truth0, ground_truth1], + metadata, + threshold_output_feature_names, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def confidence_thresholding_2thresholds_3d( + probabilities_per_model: "list[np.array]", + ground_truths: "list[np.array] | list[pd.Series]", + metadata: dict, + threshold_output_feature_names: "list[str]", + labels_limit: int, + output_directory: "str | None" = None, + file_format: str = "pdf", + **kwargs, +) -> None: + """Show 3d confidence threshold data vs accuracy for two output feature names. + + Args: + probabilities_per_model: List of model probabilities. + ground_truths: Containing ground truth data. + metadata: Feature metadata dictionary. + threshold_output_feature_names: List containing two output feature names + for visualization. + labels_limit: Upper limit on the numeric encoded label value. Encoded + numeric label values in dataset that are higher than `labels_limit` + are considered to be "rare" labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + """ + try: + validate_conf_thresholds_and_probabilities_2d_3d(probabilities_per_model, threshold_output_feature_names) + except RuntimeError: + return + probs = probabilities_per_model + + if not isinstance(ground_truths[0], np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[threshold_output_feature_names[0]] + vfunc = np.vectorize(_encode_categorical_feature) + gt_1 = vfunc(ground_truths[0], feature_metadata["str2idx"]) + feature_metadata = metadata[threshold_output_feature_names[1]] + gt_2 = vfunc(ground_truths[1], feature_metadata["str2idx"]) + else: + gt_1 = ground_truths[0] + gt_2 = ground_truths[1] + + if labels_limit > 0: + gt_1[gt_1 > labels_limit] = labels_limit + gt_2[gt_2 > labels_limit] = labels_limit + + thresholds = [t / 100 for t in range(0, 101, 5)] + + accuracies = [] + dataset_kept = [] + + if labels_limit > 0 and probs[0].shape[1] > labels_limit + 1: + prob_limit = probs[0][:, : labels_limit + 1] + prob_limit[:, labels_limit] = probs[0][:, labels_limit:].sum(1) + probs[0] = prob_limit + + if labels_limit > 0 and probs[1].shape[1] > labels_limit + 1: + prob_limit = probs[1][:, : labels_limit + 1] + prob_limit[:, labels_limit] = probs[1][:, labels_limit:].sum(1) + probs[1] = prob_limit + + max_prob_1 = np.max(probs[0], axis=1) + predictions_1 = np.argmax(probs[0], axis=1) + + max_prob_2 = np.max(probs[1], axis=1) + predictions_2 = np.argmax(probs[1], axis=1) + + for threshold_1 in thresholds: + threshold_1 = threshold_1 if threshold_1 < 1 else 0.999 + curr_accuracies = [] + curr_dataset_kept = [] + + for threshold_2 in thresholds: + threshold_2 = threshold_2 if threshold_2 < 1 else 0.999 + + filtered_indices = np.logical_and(max_prob_1 >= threshold_1, max_prob_2 >= threshold_2) + + filtered_gt_1 = gt_1[filtered_indices] + filtered_predictions_1 = predictions_1[filtered_indices] + filtered_gt_2 = gt_2[filtered_indices] + filtered_predictions_2 = predictions_2[filtered_indices] + + accuracy = ( + np.logical_and(filtered_gt_1 == filtered_predictions_1, filtered_gt_2 == filtered_predictions_2) + ).sum() / len(filtered_gt_1) + + curr_accuracies.append(accuracy) + curr_dataset_kept.append(len(filtered_gt_1) / len(gt_1)) + + accuracies.append(curr_accuracies) + dataset_kept.append(curr_dataset_kept) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = os.path.join(output_directory, "confidence_thresholding_2thresholds_3d." + file_format) + + visualization_utils.confidence_filtering_3d_plot( + np.array(thresholds), + np.array(thresholds), + np.array(accuracies), + np.array(dataset_kept), + threshold_output_feature_names, + title="Confidence_Thresholding, two thresholds", + filename=filename, + ) + + +@DeveloperAPI +def binary_threshold_vs_metric_cli( + probabilities: "str | list[str]", + ground_truth: str, + ground_truth_split: int, + split_file: str, + ground_truth_metadata: str, + output_feature_name: str, + output_directory: str, + **kwargs: dict, +) -> None: + """Load model data from files to be shown by binary_threshold_vs_metric_cli. + + Args: + probabilities: List of prediction results file names to extract probabilities from. + ground_truth: Path to ground truth file. + ground_truth_split: Type of ground truth split - `0` for training split, + `1` for validation split or `2` for test split. + split_file: File path to csv file containing split values. + ground_truth_metadata: File path to feature metadata json file created during training. + output_feature_name: Name of the output feature to visualize. + output_directory: Name of output directory containing training results. + kwargs: Parameters for the requested visualizations. + """ + + # retrieve feature metadata to convert raw predictions to encoded value + metadata = load_json(ground_truth_metadata) + + # retrieve ground truth from source data set + ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file) + + col = f"{output_feature_name}{_PROBABILITIES_SUFFIX}" + probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata) + binary_threshold_vs_metric( + probabilities_per_model, + ground_truth, + metadata, + output_feature_name, + output_directory=output_directory, + **kwargs, + ) + + +@DeveloperAPI +def binary_threshold_vs_metric( + probabilities_per_model: "list[np.array]", + ground_truth: "pd.Series | np.ndarray", + metadata: dict, + output_feature_name: str, + metrics: "list[str]", + positive_label: int = 1, + model_names: "list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + ground_truth_apply_idx: bool = True, + **kwargs, +) -> None: + """Show confidence of the model against metric for the specified output_feature_name. + + For each metric specified in metrics (options are `f1`, `precision`, `recall`, + `accuracy`), this visualization produces a line chart plotting a threshold + on the confidence of the model against the metric for the specified + output_feature_name. + + Args: + probabilities_per_model: List of model probabilities. + ground_truth: Ground truth values. + metadata: Feature metadata dictionary. + output_feature_name: Output feature name. + metrics: Metrics to display (`'f1'`, `'precision'`, `'recall'`, `'accuracy'`). + positive_label: Numeric encoded value for the positive class. + model_names: List of the names of the models to use as labels. + output_directory: Directory where to save plots. If not specified, plots + will be displayed in a window. + file_format: File format of output plots - `'pdf'` or `'png'`. + ground_truth_apply_idx: Whether to use metadata['str2idx'] in np.vectorize. + """ + + if not isinstance(ground_truth, np.ndarray): + # not np array, assume we need to translate raw value to encoded value + feature_metadata = metadata[output_feature_name] + ground_truth, positive_label = _convert_ground_truth( + ground_truth, feature_metadata, ground_truth_apply_idx, positive_label + ) + + probs = probabilities_per_model + model_names_list = convert_to_list(model_names) + metrics_list = convert_to_list(metrics) + filename_template = "binary_threshold_vs_metric_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + + thresholds = [t / 100 for t in range(0, 101, 5)] + + supported_metrics = {"f1", "precision", "recall", "accuracy"} + + for metric in metrics_list: + if metric not in supported_metrics: + logger.error(f"Metric {metric} not supported") + continue + + scores = [] + + for _i, prob in enumerate(probs): + scores_alg = [] + + if len(prob.shape) == 2: + if prob.shape[1] > positive_label: + prob = prob[:, positive_label] + else: + raise Exception( + f"the specified positive label {positive_label} is not present in the probabilities" + ) + + for threshold in thresholds: + threshold = threshold if threshold < 1 else 0.99 + + predictions = prob >= threshold + + if metric == "f1": + metric_score = sklearn.metrics.f1_score(ground_truth, predictions) + elif metric == "precision": + metric_score = sklearn.metrics.precision_score(ground_truth, predictions) + elif metric == "recall": + metric_score = sklearn.metrics.recall_score(ground_truth, predictions) + elif metric == ACCURACY: + metric_score = sklearn.metrics.accuracy_score(ground_truth, predictions) + + scores_alg.append(metric_score) + + scores.append(scores_alg) + + filename = None + if output_directory: + os.makedirs(output_directory, exist_ok=True) + filename = filename_template_path.format(metric) + + visualization_utils.threshold_vs_metric_plot( + thresholds, scores, model_names_list, title=f"Binary threshold vs {metric}", filename=filename + ) diff --git a/ludwig/visualize/training.py b/ludwig/visualize/training.py new file mode 100644 index 0000000..bd26f66 --- /dev/null +++ b/ludwig/visualize/training.py @@ -0,0 +1,112 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Learning-curve visualizations.""" + +import logging + +from ludwig.api_annotations import DeveloperAPI +from ludwig.callbacks import Callback +from ludwig.constants import ACCURACY, EDIT_DISTANCE, HITS_AT_K, LOSS +from ludwig.utils import visualization_utils +from ludwig.visualize._utils import ( + _validate_output_feature_name_from_train_stats, + convert_to_list, + generate_filename_template_path, + load_training_stats_for_viz, +) + +logger = logging.getLogger(__name__) + + +@DeveloperAPI +def learning_curves_cli(training_statistics: "str | list[str]", **kwargs: dict) -> None: + """Load model data from files to be shown by learning_curves. + + Args: + training_statistics: Path to experiment training statistics file. + **kwargs: Parameters for the requested visualizations. + """ + train_stats_per_model = load_training_stats_for_viz("load_json", training_statistics) + learning_curves(train_stats_per_model, **kwargs) + + +@DeveloperAPI +def learning_curves( + train_stats_per_model: "list[dict]", + output_feature_name: "str | None" = None, + model_names: "str | list[str] | None" = None, + output_directory: "str | None" = None, + file_format: str = "pdf", + callbacks: "list[Callback] | None" = None, + **kwargs, +) -> None: + """Show how model metrics change over training and validation data epochs. + + For each model and for each output feature and metric of the model, + it produces a line plot showing how that metric changed over the course + of the epochs of training on the training and validation sets. + + Args: + train_stats_per_model: List containing dictionary of training statistics per model. + output_feature_name: Name of the output feature to use for the visualization. + If None, use all output features. + model_names: Model name or list of the model names to use as labels. + output_directory: Directory where to save plots. If not specified, plots will + be displayed in a window. + file_format: File format of output plots — 'pdf' or 'png'. + callbacks: A list of `ludwig.callbacks.Callback` objects that provide hooks + into the Ludwig pipeline. + """ + filename_template = "learning_curves_{}_{}." + file_format + filename_template_path = generate_filename_template_path(output_directory, filename_template) + train_stats_per_model_list = convert_to_list(train_stats_per_model) + model_names_list = convert_to_list(model_names) + output_feature_names = _validate_output_feature_name_from_train_stats( + output_feature_name, train_stats_per_model_list + ) + + metrics = [LOSS, ACCURACY, HITS_AT_K, EDIT_DISTANCE] + for output_feature_name in output_feature_names: + for metric in metrics: + if metric in train_stats_per_model_list[0].training[output_feature_name]: + filename = None + if filename_template_path: + filename = filename_template_path.format(output_feature_name, metric) + + training_stats = [ + learning_stats.training[output_feature_name][metric] + for learning_stats in train_stats_per_model_list + ] + + validation_stats = [] + for learning_stats in train_stats_per_model_list: + if learning_stats.validation and output_feature_name in learning_stats.validation: + validation_stats.append(learning_stats.validation[output_feature_name][metric]) + else: + validation_stats.append(None) + + evaluation_frequency = train_stats_per_model_list[0].evaluation_frequency + + visualization_utils.learning_curves_plot( + training_stats, + validation_stats, + metric, + x_label=evaluation_frequency.period, + x_step=evaluation_frequency.frequency, + algorithm_names=model_names_list, + title=f"Learning Curves {output_feature_name}", + filename=filename, + callbacks=callbacks, + ) diff --git a/notebooks/advanced_peft_adapters.ipynb b/notebooks/advanced_peft_adapters.ipynb new file mode 100644 index 0000000..3dce27c --- /dev/null +++ b/notebooks/advanced_peft_adapters.ipynb @@ -0,0 +1,471 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "# Advanced PEFT Adapters in Ludwig\n", + "\n", + "This notebook demonstrates Ludwig's extended parameter-efficient fine-tuning (PEFT) support:\n", + "\n", + "- **Advanced LoRA initializers**: PiSSA, EVA, CorDA, LoftQ, OLoRA, orthogonal\n", + "- **LoRA extras**: rsLoRA, DoRA, LoRA+, per-layer rank/alpha patterns, layer replication\n", + "- **TinyLoRA** — LoRA-XS variant: SVD + fixed projections, as few as 13 parameters\n", + "- **C3A** — contextual/conditional/compositional adapter\n", + "- **OFT** — orthogonal fine-tuning (preserves hyperspherical energy)\n", + "- **HRA** — Householder Reflection Adaptation\n", + "- **WaveFT** — wavelet-domain fine-tuning\n", + "- **LN-Tuning** — fine-tune only layer normalization parameters\n", + "- **VBLoRA** — vector bank LoRA (shared vector reuse across layers)\n", + "\n", + "**References**: Issue [#4129](https://github.com/ludwig-ai/ludwig/issues/4129)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acae54e37e7d407bbb7b55eff062a284", + "metadata": {}, + "outputs": [], + "source": [ + "# Install dependencies\n", + "# !pip install ludwig peft>=0.19.0 transformers torch" + ] + }, + { + "cell_type": "markdown", + "id": "9a63283cbaf04dbcab1f6479b197f3a8", + "metadata": {}, + "source": [ + "## 1. Advanced LoRA Initializers\n", + "\n", + "The `init_lora_weights` field selects how the LoRA A and B matrices are initialized.\n", + "The default (`True`) uses Kaiming uniform (A) and zeros (B).\n", + "Advanced initializers often converge faster or reach higher final performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd0d8092fe74a7c96281538738b07e2", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import adapter_registry, LoraConfig\n", + "\n", + "# Standard LoRA (baseline)\n", + "standard = LoraConfig(r=8, alpha=16)\n", + "print(\"Standard LoRA init_lora_weights:\", standard.to_config(\"CAUSAL_LM\").init_lora_weights)\n", + "\n", + "# PiSSA: Principal Singular values and Singular vectors Adaptation\n", + "# Initializes A, B from SVD of the pretrained weight. Faster convergence.\n", + "pissa = LoraConfig(r=8, alpha=16, init_lora_weights=\"pissa\")\n", + "print(\"PiSSA init_lora_weights:\", pissa.to_config(\"CAUSAL_LM\").init_lora_weights)\n", + "\n", + "# CorDA: Context-Oriented Decomposition Adaptation\n", + "corda = LoraConfig(r=8, alpha=16, init_lora_weights=\"corda\")\n", + "print(\"CorDA init_lora_weights:\", corda.to_config(\"CAUSAL_LM\").init_lora_weights)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72eea5119410473aa328ad9291626812", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import LoraConfig\n", + "\n", + "# EVA: Explained Variance Adaptation — data-driven initialization from activation SVD\n", + "eva_cfg = LoraConfig.model_validate(\n", + " {\n", + " \"type\": \"lora\",\n", + " \"r\": 16,\n", + " \"init_lora_weights\": \"eva\",\n", + " \"eva_config\": {\"rho\": 2.0, \"tau\": 0.99, \"adjust_scaling_factors\": True},\n", + " }\n", + ")\n", + "peft_cfg = eva_cfg.to_config(\"CAUSAL_LM\")\n", + "print(\"EVA init:\", peft_cfg.init_lora_weights)\n", + "print(\"EVA config rho:\", peft_cfg.eva_config.rho)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8edb47106e1a46a883d545849b8ab81b", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import LoraConfig\n", + "\n", + "# LoftQ: jointly quantizes base weights and initializes LoRA to minimize error\n", + "loftq_cfg = LoraConfig.model_validate(\n", + " {\"type\": \"lora\", \"r\": 16, \"init_lora_weights\": \"loftq\", \"loftq_config\": {\"loftq_bits\": 4, \"loftq_iter\": 1}}\n", + ")\n", + "peft_cfg = loftq_cfg.to_config(\"CAUSAL_LM\")\n", + "print(\"LoftQ init:\", peft_cfg.init_lora_weights)\n", + "print(\"LoftQ bits:\", peft_cfg.loftq_config[\"loftq_bits\"])" + ] + }, + { + "cell_type": "markdown", + "id": "10185d26023b46108eb7d9f57d49d2b3", + "metadata": {}, + "source": [ + "## 2. Per-Layer Rank Patterns\n", + "\n", + "Different layers may benefit from different ranks. Use `rank_pattern` and `alpha_pattern`\n", + "to override the global `r` and `alpha` for specific layers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8763a12b2bbd4a93a75aff182afb95dc", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import LoraConfig\n", + "\n", + "cfg = LoraConfig.model_validate(\n", + " {\n", + " \"type\": \"lora\",\n", + " \"r\": 8, # global default rank\n", + " \"alpha\": 16,\n", + " \"init_lora_weights\": \"pissa\",\n", + " # Override specific layers: attention layers get higher rank\n", + " \"rank_pattern\": {\n", + " \"model.layers.0.self_attn.q_proj\": 16,\n", + " \"model.layers.0.self_attn.v_proj\": 16,\n", + " },\n", + " \"alpha_pattern\": {\n", + " \"model.layers.0.self_attn.q_proj\": 32.0,\n", + " },\n", + " }\n", + ")\n", + "peft_cfg = cfg.to_config(\"CAUSAL_LM\")\n", + "print(\"rank_pattern:\", peft_cfg.rank_pattern)\n", + "print(\"alpha_pattern:\", peft_cfg.alpha_pattern)" + ] + }, + { + "cell_type": "markdown", + "id": "7623eae2785240b9bd12b16a66d81610", + "metadata": {}, + "source": [ + "## 3. TinyLoRA — Extreme Parameter Efficiency\n", + "\n", + "TinyLoRA ([arXiv:2602.04118](https://arxiv.org/abs/2602.04118)) achieves fine-tuning with\n", + "as few as 13 parameters. It uses SVD decomposition of frozen weights and projects a tiny\n", + "trainable vector through fixed random tensors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cdc8c89c7104fffa095e18ddfef8986", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import TinyLoraAdapterConfig\n", + "\n", + "# Standard TinyLoRA: r=2, u=64 (like LoRA-XS)\n", + "tinylora = TinyLoraAdapterConfig.model_validate({\"type\": \"tinylora\", \"r\": 2, \"u\": 64})\n", + "print(\"TinyLoRA config:\", tinylora)\n", + "\n", + "# Ultra-minimal: r=2, u=13 (13 parameters per layer group!)\n", + "micro = TinyLoraAdapterConfig.model_validate({\"type\": \"tinylora\", \"r\": 2, \"u\": 13})\n", + "peft_cfg = micro.to_config(\"CAUSAL_LM\")\n", + "print(\"\\nUltra-minimal u:\", peft_cfg.u)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b118ea5561624da68c537baed56e602f", + "metadata": {}, + "outputs": [], + "source": [ + "# Ludwig YAML config equivalent:\n", + "tinylora_yaml = \"\"\"\n", + "model_type: llm\n", + "base_model: meta-llama/Llama-3.2-1B\n", + "\n", + "input_features:\n", + " - name: instruction\n", + " type: text\n", + "\n", + "output_features:\n", + " - name: output\n", + " type: text\n", + "\n", + "adapter:\n", + " type: tinylora\n", + " r: 2\n", + " u: 64\n", + " weight_tying: 0.0 # 0.5 shares vectors across 50% of modules\n", + "\n", + "trainer:\n", + " type: finetune\n", + " learning_rate: 0.001\n", + " epochs: 3\n", + "\"\"\"\n", + "print(tinylora_yaml)" + ] + }, + { + "cell_type": "markdown", + "id": "938c804e27f84196a10c8828c723f798", + "metadata": {}, + "source": [ + "## 4. C3A — Contextual/Conditional/Compositional Adapter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "504fb2a444614c0babb325280ed9130a", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import C3AAdapterConfig\n", + "\n", + "c3a = C3AAdapterConfig.model_validate({\"type\": \"c3a\", \"block_size\": 256})\n", + "peft_cfg = c3a.to_config(\"CAUSAL_LM\")\n", + "print(\"C3A block_size:\", peft_cfg.block_size)" + ] + }, + { + "cell_type": "markdown", + "id": "59bbdb311c014d738909a11f9e486628", + "metadata": {}, + "source": [ + "## 5. Orthogonal Methods: OFT and HRA\n", + "\n", + "OFT and HRA apply orthogonal transformations to weight matrices, preserving the\n", + "hyperspherical energy of the pre-trained model. This prevents catastrophic forgetting\n", + "and is particularly effective for subject-driven generation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b43b363d81ae4b689946ece5c682cd59", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import HRAAdapterConfig, OFTAdapterConfig\n", + "\n", + "# OFT: Orthogonal Fine-Tuning\n", + "oft = OFTAdapterConfig.model_validate({\"type\": \"oft\", \"oft_block_size\": 32})\n", + "print(\"OFT:\", oft.to_config(\"CAUSAL_LM\"))\n", + "\n", + "# COFT variant (constrained OFT):\n", + "coft = OFTAdapterConfig.model_validate({\"type\": \"oft\", \"coft\": True, \"eps\": 6e-5})\n", + "peft_cfg = coft.to_config(\"CAUSAL_LM\")\n", + "print(\"\\nCOFT enabled:\", peft_cfg.coft)\n", + "\n", + "# HRA: Householder Reflection Adaptation\n", + "hra = HRAAdapterConfig.model_validate({\"type\": \"hra\", \"r\": 8, \"apply_GS\": True})\n", + "print(\"\\nHRA r:\", hra.r, \"apply_GS:\", hra.apply_GS)" + ] + }, + { + "cell_type": "markdown", + "id": "8a65eabff63a45729fe45fb5ade58bdc", + "metadata": {}, + "source": [ + "## 6. WaveFT — Wavelet-Domain Fine-Tuning" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3933fab20d04ec698c2621248eb3be0", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import WaveFTAdapterConfig\n", + "\n", + "# db1 (Haar) wavelet — simplest, fastest\n", + "waveft = WaveFTAdapterConfig.model_validate({\"type\": \"waveft\", \"wavelet_family\": \"db1\", \"n_frequency\": 2592})\n", + "print(\"WaveFT wavelet:\", waveft.wavelet_family, \"n_frequency:\", waveft.n_frequency)" + ] + }, + { + "cell_type": "markdown", + "id": "4dd4641cc4064e0191573fe9c69df29b", + "metadata": {}, + "source": [ + "## 7. LN-Tuning — Ultra-Lightweight Adaptation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8309879909854d7188b41380fd92a7c3", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import LNTuningAdapterConfig\n", + "\n", + "# Fine-tune only LayerNorm/RMSNorm params — typically <0.1% of total params\n", + "ln = LNTuningAdapterConfig.model_validate({\"type\": \"ln_tuning\"})\n", + "print(\"LN-Tuning target_modules:\", ln.target_modules, \"(None = all LayerNorms)\")" + ] + }, + { + "cell_type": "markdown", + "id": "3ed186c9a28b402fb0bc4494df01f08d", + "metadata": {}, + "source": [ + "## 8. VBLoRA — Vector Bank LoRA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb1e1581032b452c9409d6c6813c49d1", + "metadata": {}, + "outputs": [], + "source": [ + "from ludwig.schema.llms.peft import VBLoRAAdapterConfig\n", + "\n", + "# VBLoRA: shared vector bank across all layers\n", + "vblora = VBLoRAAdapterConfig.model_validate(\n", + " {\n", + " \"type\": \"vblora\",\n", + " \"r\": 4,\n", + " \"num_vectors\": 256,\n", + " \"vector_length\": 4096, # set to model hidden size\n", + " \"topk\": 2,\n", + " }\n", + ")\n", + "peft_cfg = vblora.to_config(\"CAUSAL_LM\")\n", + "print(\"VBLoRA r:\", peft_cfg.r, \"num_vectors:\", peft_cfg.num_vectors, \"topk:\", peft_cfg.topk)" + ] + }, + { + "cell_type": "markdown", + "id": "379cbbc1e968416e875cc15c1202d7eb", + "metadata": {}, + "source": [ + "## 9. Full End-to-End Training Example\n", + "\n", + "Here's how to use these adapters in a complete Ludwig training run:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "277c27b1587741f2af2001be3712ef0d", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "# Choose your adapter:\n", + "adapter_configs = {\n", + " \"pissa\": {\"type\": \"lora\", \"r\": 16, \"alpha\": 32, \"init_lora_weights\": \"pissa\"},\n", + " \"tinylora\": {\"type\": \"tinylora\", \"r\": 2, \"u\": 64},\n", + " \"ln_tuning\": {\"type\": \"ln_tuning\"},\n", + " \"oft\": {\"type\": \"oft\", \"oft_block_size\": 32},\n", + "}\n", + "\n", + "selected = \"pissa\"\n", + "\n", + "config = {\n", + " \"model_type\": \"llm\",\n", + " \"base_model\": \"meta-llama/Llama-3.2-1B\",\n", + " \"input_features\": [{\"name\": \"instruction\", \"type\": \"text\"}],\n", + " \"output_features\": [{\"name\": \"output\", \"type\": \"text\"}],\n", + " \"adapter\": adapter_configs[selected],\n", + " \"trainer\": {\n", + " \"type\": \"finetune\",\n", + " \"learning_rate\": 1e-4,\n", + " \"epochs\": 3,\n", + " \"batch_size\": 4,\n", + " },\n", + " \"preprocessing\": {\"global_max_sequence_length\": 512},\n", + "}\n", + "\n", + "print(f\"Training with {selected} adapter:\")\n", + "print(yaml.dump(config, default_flow_style=False))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db7b79bc585a40fcaf58bf750017e135", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment to run training:\n", + "# from ludwig.api import LudwigModel\n", + "# model = LudwigModel(config=config)\n", + "# train_stats, _, output_dir = model.train(dataset=\"path/to/dataset.csv\")\n", + "# print(\"Results saved to:\", output_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "916684f9a58a4a2aa5f864670399430d", + "metadata": {}, + "source": [ + "## 10. All Available Adapters\n", + "\n", + "Check which adapter types are registered:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1671c31a24314836a5b85d7ef7fbf015", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Registered PEFT adapters:\")\n", + "for name, cls in adapter_registry.items():\n", + " print(f\" {name:<20} -> {cls.__name__}\")" + ] + }, + { + "cell_type": "markdown", + "id": "33b0902fd34d4ace834912fa1002cf8e", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Adapter | Key benefit | Params vs. LoRA r=8 |\n", + "|---------|-------------|---------------------|\n", + "| `lora` + PiSSA | Faster convergence, same params | Same |\n", + "| `lora` + EVA | SOTA via data-driven init | Same |\n", + "| `lora` + CorDA | Fast convergence + knowledge preservation | Same |\n", + "| `lora` + LoftQ | Better QLoRA starting point | Same |\n", + "| `tinylora` r=2, u=64 | ~10x fewer params than LoRA | ~10x fewer |\n", + "| `tinylora` r=2, u=13 | 13 params per layer group | ~100x fewer |\n", + "| `c3a` | Multi-task modular composition | Varies |\n", + "| `oft` | Preserves pretrained knowledge | Comparable |\n", + "| `hra` | Orthogonal, fewer hyperparams | Comparable |\n", + "| `waveft` | Frequency-domain inductive bias | Varies |\n", + "| `ln_tuning` | Ultra-lightweight domain shift | <0.1% of total |\n", + "| `vblora` | Layer-shared vectors, extreme compression | 10-100x fewer |\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d0de82b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,277 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ludwig" +dynamic = ["version"] +description = "Declarative machine learning: End-to-end machine learning pipelines using data-driven configurations." +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.12" +authors = [ + { name = "Piero Molino", email = "piero.molino@gmail.com" }, +] +keywords = [ + "ludwig", + "deep learning", + "machine learning", + "natural language processing", + "computer vision", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] +dependencies = [ + "numpy>=1.24", + "pandas>=2.0", + "scipy>=1.10", + "tabulate>=0.9", + "scikit-learn>=1.3", + "tqdm>=4.60", + "torch>=2.11", + "torchaudio>=2.11", + "torchcodec>=0.1", + "torchvision>=0.26", + "transformers>=5.0", + "sentencepiece>=0.2", + "spacy>=2.3", + "PyYAML>=6.0", + "absl-py", + "kaggle", + "requests>=2.28", + "py-cpuinfo", + "fsspec[http]", + "dataclasses-json", + "jsonschema>=4.17", + "tensorboard", + "torchmetrics>=1.0", + "torchinfo", + "filelock", + "psutil", + "protobuf>=4.0", + "gpustat", + "rich>=12.4.4", + "packaging", + "retry", + "sacremoses", + "bitsandbytes>=0.44.0", + "xlwt", + "xlrd", + "openpyxl", + "pyarrow>=14.0", + "lxml", + "datasets", + "safetensors>=0.4", +] + +[project.optional-dependencies] +serve = [ + "uvicorn", + "httpx", + "fastapi", + "python-multipart", + "prometheus_client", +] +viz = [ + "matplotlib>=3.4", + "seaborn", + "hiplot", + "ptitprince", +] +distributed = [ + "dask[dataframe]>=2026.1.2", + "pyarrow>=14.0", + "ray[default,data,serve,tune]>=2.9", + "GPUtil", + "tblib", + "awscli", +] +hyperopt = [ + "ray[default,tune]>=2.9", + "hyperopt", + "future<1.0", +] +llm = [ + "sentence-transformers", + "faiss-cpu", + "accelerate", + "loralib", + "peft>=0.10.0", + "torchao>=0.17.0", +] +explain = [ + "captum", +] +benchmarking = [ + "s3fs", +] +extra = [ + "modin[ray]", + "predibase>=2023.10.2", +] +full = [ + "ludwig[serve,viz,distributed,hyperopt,llm,explain,benchmarking]", +] +test = [ + "ludwig[full]", + "pytest", + "pytest-timeout", + "pytest-rerunfailures", + "tifffile", + "wget", + "aim", + "wandb", + "comet_ml", + "mlflow", + "hpbandster", + "ConfigSpace>=1.0", + "ax-platform", + "sqlalchemy", + "bayesian-optimization", + "flaml[blendsearch]", + "nevergrad", + "optuna", + "scikit-optimize", + "zoopt", + "hyperopt", + "s3fs>=2022.8.2", +] + +[dependency-groups] +dev = ["pre-commit", "ruff>=0.8"] +docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike"] + +[project.scripts] +ludwig = "ludwig.cli:main" + +[project.urls] +Homepage = "https://github.com/ludwig-ai/ludwig" +Documentation = "https://ludwig.ai" +Repository = "https://github.com/ludwig-ai/ludwig" +"Bug Tracker" = "https://github.com/ludwig-ai/ludwig/issues" + +[tool.hatch.version] +source = "regex" +path = "ludwig/globals.py" +pattern = 'LUDWIG_VERSION = "(?P[^"]+)"' + +[tool.hatch.build.targets.wheel] +packages = ["ludwig"] +artifacts = ["ludwig/py.typed"] + +[tool.hatch.build.targets.wheel.shared-data] + +[tool.hatch.build.targets.sdist] +include = [ + "ludwig/", + "README.md", + "LICENSE", + "pyproject.toml", +] + +[tool.ruff] +target-version = "py312" +line-length = 120 +exclude = ["python/"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade (includes UP007: Optional→X|None, Union→X|Y) + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "RUF", # ruff-specific rules +] +ignore = [ + "E501", # line length — handled by formatter + "B006", # mutable default args in function — handled separately + "B008", # function calls in default args + "SIM108", # ternary operator — too aggressive + "SIM102", # nested if — too aggressive + "SIM117", # nested with — too aggressive + "UP035", # deprecated imports — too broad + "RUF012", # mutable class attrs — too aggressive for schema classes + "B028", # no-explicit-stacklevel in warnings.warn — deferred cleanup + "B904", # raise-without-from-inside-except — deferred cleanup + "B905", # zip-without-explicit-strict — deferred cleanup + "B027", # empty-method-without-abstract-decorator — deferred cleanup + "B007", # unused-loop-control-variable — deferred cleanup + "RUF059", # unused-unpacked-variable — deferred cleanup + "RUF005", # collection-literal-concatenation — style preference + "RUF001", # ambiguous-unicode-character-string — intentional unicode in some files + "RUF002", # ambiguous-unicode-character-docstring — intentional + "RUF003", # ambiguous-unicode-character-comment — intentional + "SIM105", # suppressible-exception — try/except/pass has legitimate uses + "B023", # function-uses-loop-variable — too many false positives + "SIM115", # open-file-with-context-handler — deferred cleanup + "RUF015", # unnecessary-iterable-allocation — deferred cleanup + "C414", # unnecessary-double-cast-or-process — deferred cleanup + "SIM201", # negate-equal-op — deferred cleanup + "B024", # abstract-base-class-without-abstract-method — deferred cleanup + "E731", # lambda-assignment — some lambdas are intentionally concise + "RUF046", # unnecessary-cast-to-int — deferred cleanup + "RUF022", # unsorted-dunder-all — thematic grouping with comments is preferred + "SIM110", # reimplemented-builtin — deferred cleanup + "B019", # cached-instance-method — deferred cleanup (requires careful review) + "SIM101", # duplicate-isinstance-call — deferred cleanup + "SIM103", # needless-bool — deferred cleanup + "SIM401", # if-else-block-instead-of-dict-get — deferred cleanup + "C419", # unnecessary-comprehension-in-call — deferred cleanup + "UP046", # non-pep695-generic-class — Python 3.12+ syntax, deferred + "UP042", # replace-str-enum — deferred cleanup + "B003", # assignment-to-os-environ — reviewed, safe in context manager + "SIM113", # enumerate-for-loop — deferred cleanup + "RUF017", # quadratic-list-summation — deferred cleanup + "B020", # loop-variable-overrides-iterator — fixed in automl.py +] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "S101", # allow assert in tests + "B011", # allow assertFalse in tests + "B017", # allow pytest.raises(Exception) in tests + "C408", # dict()/list() literals fine in tests + "C416", # unnecessary list comprehension — tests are explicit + "C417", # map() usage fine in tests + "RUF013", # implicit Optional fine in test signatures + "SIM118", # key in dict.keys() fine in tests + "SIM109", # multiple equality comparisons fine in tests + "SIM212", # ternary ordering fine in tests + "F821", # undefined name (sometimes from notebook stubs) + "F811", # redefinition fine in tests + "E721", # type comparison fine in tests + "UP031", # %-format fine in tests + "RUF043", # regex metacharacter warning — backticks are not special in Python regex +] +"examples/**" = [ + "E402", # imports not at top — notebooks interleave imports with code + "F821", # undefined names in notebooks (seaborn loaded earlier) + "C408", # dict() literals fine in examples + "C405", # list literal for set fine in examples + "RUF007", # itertools.pairwise — style preference, not required + "RUF043", # regex metacharacters in match= — intentional in examples + "B015", # pointless comparison fine in examples + "SIM118", # key in dict.keys() fine in examples + "SIM212", # ternary ordering fine in examples + "UP031", # %-format fine in examples +] + +[tool.ruff.lint.isort] +known-first-party = ["ludwig"] +force-sort-within-sections = false +order-by-type = false + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a7b94a8 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,29 @@ +[pytest] +markers = + benchmark: mark a test as a benchmarking test. + distributed: mark a test as a distributed test. + filesystem: mark to test operating system systems. + slow: mark test as slow. + combinatorial: mark a test as combinatorial. + llm: mark a test as an LLM test. + integration_tests_a: mark a test to be run as part of integration tests, group A. + integration_tests_b: mark a test to be run as part of integration tests, group B. + integration_tests_c: mark a test to be run as part of integration tests, group C. + integration_tests_d: mark a test to be run as part of integration tests, group D. + integration_tests_e: mark a test to be run as part of integration tests, group E. + integration_tests_f: mark a test to be run as part of integration tests, group F. + integration_tests_g: mark a test to be run as part of integration tests, group G. + integration_tests_h: mark a test to be run as part of integration tests, group H. + integration_tests_i: mark a test to be run as part of integration tests, group I. + integration_tests_j: mark a test to be run as part of integration tests, group J. + distributed_a: mark a distributed test to be run in distributed group A. + distributed_b: mark a distributed test to be run in distributed group B. + distributed_c: mark a distributed test to be run in distributed group C. + distributed_d: mark a distributed test to be run in distributed group D. + distributed_e: mark a distributed test to be run in distributed group E. + distributed_f: mark a distributed test to be run in distributed group F. +filterwarnings = + ignore::marshmallow.warnings.RemovedInMarshmallow4Warning + ignore::DeprecationWarning:importlib._bootstrap + ignore:builtin type Swig:DeprecationWarning + ignore:.*torch.jit.script.*is deprecated:DeprecationWarning diff --git a/schemastore/README.md b/schemastore/README.md new file mode 100644 index 0000000..e5f07dc --- /dev/null +++ b/schemastore/README.md @@ -0,0 +1,21 @@ +# SchemaStore Submission Materials + +This directory contains materials for submitting Ludwig's JSON Schema to +the [JSON Schema Store](https://www.schemastore.org/json/). + +## Catalog Entry + +The file `catalog-entry.json` contains the entry to add to SchemaStore's +`src/api/json/catalog.json`. + +## Test Configs + +The `test/` directory contains example Ludwig config files that validate +against the schema. These are used as positive test cases in the SchemaStore PR. + +## How to Submit + +1. Fork [SchemaStore/schemastore](https://github.com/SchemaStore/schemastore) +1. Add the catalog entry from `catalog-entry.json` to `src/api/json/catalog.json` +1. Copy test configs from `test/` to `src/test/ludwig/` +1. Submit a PR referencing Ludwig issue #1343 diff --git a/schemastore/catalog-entry.json b/schemastore/catalog-entry.json new file mode 100644 index 0000000..36bab48 --- /dev/null +++ b/schemastore/catalog-entry.json @@ -0,0 +1,15 @@ +{ + "name": "Ludwig", + "description": "Ludwig declarative deep learning framework configuration", + "fileMatch": [ + "ludwig.yaml", + "ludwig.yml", + "ludwig.json", + "ludwig_config.yaml", + "ludwig_config.yml", + "ludwig_config.json", + "**/ludwig/**/config.yaml", + "**/ludwig/**/config.yml" + ], + "url": "https://ludwig-ai.github.io/schema/ludwig-config.json" +} diff --git a/schemastore/test/ludwig.yaml b/schemastore/test/ludwig.yaml new file mode 100644 index 0000000..13e64f4 --- /dev/null +++ b/schemastore/test/ludwig.yaml @@ -0,0 +1,23 @@ +# Rotten Tomatoes review classification - multimodal Ludwig config +input_features: + - name: genres + type: set + preprocessing: + tokenizer: comma + - name: content_rating + type: category + - name: top_critic + type: binary + - name: runtime + type: number + - name: review_content + type: text + encoder: + type: embed + +output_features: + - name: recommended + type: binary + +trainer: + epochs: 3 diff --git a/schemastore/test/ludwig_config.yaml b/schemastore/test/ludwig_config.yaml new file mode 100644 index 0000000..0ec967a --- /dev/null +++ b/schemastore/test/ludwig_config.yaml @@ -0,0 +1,24 @@ +# Titanic survival prediction - basic Ludwig config +input_features: + - name: Pclass + type: category + - name: Sex + type: category + - name: Age + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: SibSp + type: number + - name: Parch + type: number + - name: Fare + type: number + preprocessing: + missing_value_strategy: fill_with_mean + - name: Embarked + type: category + +output_features: + - name: Survived + type: binary diff --git a/scripts/benchmark_lazy_decode.py b/scripts/benchmark_lazy_decode.py new file mode 100644 index 0000000..ea7dbdc --- /dev/null +++ b/scripts/benchmark_lazy_decode.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Benchmark lazy media decode throughput: local (LazyColumn) vs Ray (map_batches). + +Measures batch iteration speed for audio features with lazy=True across three paths: + 1. eager_local ��� all files decoded upfront into numpy arrays, then iterated (best-case baseline) + 2. lazy_local — PandasDataset + LazyColumn, decode per-batch via ThreadPoolExecutor + 3. lazy_ray — RayDataset + _with_lazy_decode, decode per-batch via Ray map_batches + +Run: + python scripts/benchmark_lazy_decode.py [--n-samples N] [--batch-size B] [--epochs E] + +Requirements: soundfile, ray[data], torchaudio (already installed in the Ludwig environment). +""" + +import argparse +import os +import sys +import tempfile +import time + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + + +def _write_wav_files(dest_dir: str, n: int, duration_s: float = 0.5, sample_rate: int = 16_000) -> list[str]: + """Write N silent WAV files and return their paths.""" + import torch + import torchaudio + + os.makedirs(dest_dir, exist_ok=True) + paths = [] + n_samples = int(duration_s * sample_rate) + silence = torch.zeros(1, n_samples) # (channels, samples) + for i in range(n): + p = os.path.join(dest_dir, f"audio_{i:05d}.wav") + torchaudio.save(p, silence, sample_rate) + paths.append(p) + return paths + + +def _make_lazy_audio_metadata(feature_dim: int = 8, max_length: int = 23) -> dict: + return { + "lazy": True, + "reshape": None, + "lazy_audio_params": { + "audio_feature_dict": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": feature_dim, + }, + "feature_dim": feature_dim, + "max_length": max_length, + "padding_value": 0.0, + "normalization_type": None, + }, + } + + +def _make_features(proc_col: str, feature_name: str) -> dict: + return {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + + +# --------------------------------------------------------------------------- +# Benchmark helpers +# --------------------------------------------------------------------------- + + +def bench_eager_local(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float: + """Pre-decode all files, then iterate batches from a plain numpy array.""" + from ludwig.features.audio_feature import AudioFeatureMixin + + # Build decode function and decode everything upfront + meta = _make_lazy_audio_metadata(feature_dim, max_length)["lazy_audio_params"] + decode_fn = AudioFeatureMixin._make_lazy_decode_fn( + audio_feature_dict=meta["audio_feature_dict"], + feature_dim=meta["feature_dim"], + max_length=meta["max_length"], + padding_value=meta["padding_value"], + normalization_type=meta["normalization_type"], + ) + from concurrent.futures import ThreadPoolExecutor + + with ThreadPoolExecutor(max_workers=min(16, len(paths))) as ex: + decoded = np.stack(list(ex.map(decode_fn, paths))) # (N, f, t) + + total_batches = 0 + t0 = time.perf_counter() + for _ in range(epochs): + for start in range(0, len(decoded), batch_size): + batch = decoded[start : start + batch_size] + _ = batch.sum() # simulate minimal usage + total_batches += 1 + elapsed = time.perf_counter() - t0 + return len(paths) * epochs / elapsed # samples/sec + + +def bench_lazy_local(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float: + """PandasDataset + LazyColumn decode-per-batch.""" + from ludwig.data.dataset.pandas import PandasDataset + + proc_col = "audio_proc" + feature_name = "audio_0" + features = _make_features(proc_col, feature_name) + training_set_metadata = {feature_name: _make_lazy_audio_metadata(feature_dim, max_length)} + + dataset_dict = {proc_col: np.array(paths, dtype=object)} + ds = PandasDataset(dataset_dict, features, data_cache_fp=None, training_set_metadata=training_set_metadata) + + total_batches = 0 + t0 = time.perf_counter() + for _ in range(epochs): + with ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher: + while not batcher.last_batch(): + batch = batcher.next_batch() + _ = batch[proc_col].sum() + total_batches += 1 + elapsed = time.perf_counter() - t0 + return len(paths) * epochs / elapsed + + +def bench_eager_ray(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float: + """Ray dataset with pre-decoded tensors (lazy=False baseline) — no decode overhead at batch time.""" + from concurrent.futures import ThreadPoolExecutor + + import pandas as pd + import ray + + from ludwig.data.dataset.ray import RayDataset + from ludwig.features.audio_feature import AudioFeatureMixin + + proc_col = "audio_proc" + feature_name = "audio_0" + # Decode everything upfront (simulates lazy=False behaviour) + meta = _make_lazy_audio_metadata(feature_dim, max_length)["lazy_audio_params"] + decode_fn = AudioFeatureMixin._make_lazy_decode_fn( + audio_feature_dict=meta["audio_feature_dict"], + feature_dim=meta["feature_dim"], + max_length=meta["max_length"], + padding_value=meta["padding_value"], + normalization_type=meta["normalization_type"], + ) + with ThreadPoolExecutor(max_workers=min(16, len(paths))) as ex: + decoded = list(ex.map(decode_fn, paths)) + + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + # No lazy in metadata → _with_lazy_decode is a no-op + training_set_metadata = {feature_name: {"lazy": False, "reshape": (feature_dim, max_length)}} + + df = pd.DataFrame({proc_col: decoded}) + ray_ds = RayDataset.__new__(RayDataset) + ray_ds.ds = ray.data.from_pandas(df) + ray_ds.features = features + ray_ds.training_set_metadata = training_set_metadata + ray_ds.data_cache_fp = None + ray_ds.data_parquet_fp = None + + total_batches = 0 + t0 = time.perf_counter() + for _ in range(epochs): + with ray_ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher: + while not batcher.last_batch(): + batch = batcher.next_batch() + _ = batch[proc_col].sum() + total_batches += 1 + elapsed = time.perf_counter() - t0 + return len(paths) * epochs / elapsed + + +def bench_lazy_ray(paths: list[str], batch_size: int, epochs: int, feature_dim: int, max_length: int) -> float: + """RayDataset + _with_lazy_decode decode-per-batch.""" + import pandas as pd + import ray + + from ludwig.data.dataset.ray import RayDataset + + proc_col = "audio_proc" + feature_name = "audio_0" + features = _make_features(proc_col, feature_name) + training_set_metadata = {feature_name: _make_lazy_audio_metadata(feature_dim, max_length)} + + df = pd.DataFrame({proc_col: paths}) + ray_ds = RayDataset.__new__(RayDataset) + ray_ds.ds = ray.data.from_pandas(df) + ray_ds.features = features + ray_ds.training_set_metadata = training_set_metadata + ray_ds.data_cache_fp = None + ray_ds.data_parquet_fp = None + + total_batches = 0 + t0 = time.perf_counter() + for _ in range(epochs): + with ray_ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher: + while not batcher.last_batch(): + batch = batcher.next_batch() + _ = batch[proc_col].sum() + total_batches += 1 + elapsed = time.perf_counter() - t0 + return len(paths) * epochs / elapsed + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--n-samples", type=int, default=200, help="Number of audio files to generate") + parser.add_argument("--batch-size", type=int, default=32, help="Batch size for iteration") + parser.add_argument("--epochs", type=int, default=3, help="Number of full passes over the dataset") + parser.add_argument("--feature-dim", type=int, default=8, help="Audio feature dim (num_filter_bands)") + parser.add_argument("--max-length", type=int, default=23, help="Audio max length (frames)") + args = parser.parse_args() + + with tempfile.TemporaryDirectory() as tmpdir: + print(f"Generating {args.n_samples} WAV files ...", flush=True) + paths = _write_wav_files(tmpdir, args.n_samples) + print(" done.\n") + + print("Initialising Ray ...", flush=True) + import ray + + if not ray.is_initialized(): + ray.init(ignore_reinit_error=True, num_cpus=4, include_dashboard=False) + print(" done.\n") + + configs = [ + ("eager_local", bench_eager_local), + ("lazy_local ", bench_lazy_local), + ("eager_ray ", bench_eager_ray), + ("lazy_ray ", bench_lazy_ray), + ] + + results = {} + for name, fn in configs: + print(f"Running {name.strip()} ...", flush=True) + try: + sps = fn(paths, args.batch_size, args.epochs, args.feature_dim, args.max_length) + results[name] = sps + print(f" {sps:,.0f} samples/sec") + except Exception as e: + results[name] = None + print(f" FAILED: {e}") + + print("\n--- Summary ---") + eager_ray_sps = results.get("eager_ray ") + for name, sps in results.items(): + if sps is None: + print(f" {name}: FAILED") + else: + print(f" {name}: {sps:>10,.0f} sps") + + print() + if eager_ray_sps and results.get("lazy_ray "): + overhead_pct = (eager_ray_sps - results["lazy_ray "]) / eager_ray_sps * 100 + print(f" lazy_ray overhead vs eager_ray: {overhead_pct:.0f}%") + print(" (this is the decode-per-batch cost; scales linearly with file I/O speed)") + print() + print("Notes:") + print(" eager_local: pre-decoded in-memory arrays — pure numpy, no I/O, not a fair comparison") + print(" lazy_local: PandasDataset + LazyColumn — decode per batch via ThreadPoolExecutor") + print(" eager_ray: pre-decoded tensors in Ray object store — Ray overhead without decode") + print(" lazy_ray: RayDataset + _with_lazy_decode — decode per batch via map_batches") + print() + print(" eager_ray vs lazy_ray shows the cost of decode-per-batch in the Ray pipeline.") + print(" lazy_local vs lazy_ray shows the extra overhead of Ray vs direct numpy access.") + print(" In distributed training, each worker decodes in parallel, so effective") + print(" throughput scales with number of workers.") + + ray.shutdown() + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_lazy_preprocessing.py b/scripts/benchmark_lazy_preprocessing.py new file mode 100644 index 0000000..d1fb03a --- /dev/null +++ b/scripts/benchmark_lazy_preprocessing.py @@ -0,0 +1,330 @@ +"""Benchmark: lazy vs eager preprocessing for image and audio features. + +Each mode (eager, lazy) runs in full isolation — separate CSV copies so +Ludwig cannot reuse any preprocessed cache between modes. + +Measures per mode: + 1. Preprocessing-only time + peak heap (model.preprocess on copy A) + 2. Full-pipeline time + peak heap (model.train on copy B — independent) + 3. Training throughput derived from (2) - (1) + +Run: + python scripts/benchmark_lazy_preprocessing.py [--n-samples N] +Requires torch, torchaudio (via Ludwig venv) +""" + +from __future__ import annotations + +import argparse +import csv +import json +import os +import shutil +import struct +import tempfile +import time +import tracemalloc +import wave +from pathlib import Path + +import numpy as np + +# --------------------------------------------------------------------------- +# Synthetic data generators +# --------------------------------------------------------------------------- + + +def _make_png_rgb(path: Path, width: int = 64, height: int = 64, seed: int = 0) -> None: + import zlib + + rng = np.random.default_rng(seed) + pixels = rng.integers(0, 256, (height, width, 3), dtype=np.uint8) + + def _chunk(t: bytes, d: bytes) -> bytes: + crc = zlib.crc32(t + d) & 0xFFFFFFFF + return struct.pack(">I", len(d)) + t + d + struct.pack(">I", crc) + + sig = b"\x89PNG\r\n\x1a\n" + ihdr = _chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + raw = b"".join(b"\x00" + row.tobytes() for row in pixels) + idat = _chunk(b"IDAT", zlib.compress(raw)) + iend = _chunk(b"IEND", b"") + path.write_bytes(sig + ihdr + idat + iend) + + +def _make_wav_mono(path: Path, duration_s: float = 2.0, sample_rate: int = 16000, seed: int = 0) -> None: + rng = np.random.default_rng(seed) + n = int(duration_s * sample_rate) + freq = 200.0 + rng.integers(0, 600) + t = np.linspace(0, duration_s, n, endpoint=False) + samples = (np.sin(2 * np.pi * freq * t) * 32767).astype(np.int16) + with wave.open(str(path), "w") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(samples.tobytes()) + + +def _write_csv(path: str, rows: list[dict], fieldnames: list[str]) -> None: + with open(path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def create_image_dataset(root: Path, n: int, tag: str = "") -> str: + """Return CSV path; creates N 64×64 PNG images under root/images_{tag}/.""" + img_dir = root / f"images_{tag}" + img_dir.mkdir(parents=True, exist_ok=True) + labels = ["cat", "dog", "bird", "fish"] + rows = [] + for i in range(n): + p = img_dir / f"img_{i:05d}.png" + _make_png_rgb(p, seed=i) + rows.append({"image_path": str(p), "label": labels[i % len(labels)]}) + csv_path = str(root / f"images_{tag}.csv") + _write_csv(csv_path, rows, ["image_path", "label"]) + return csv_path + + +def create_audio_dataset(root: Path, n: int, tag: str = "", duration_s: float = 2.0) -> str: + """Return CSV path; creates N WAV files under root/audio_{tag}/.""" + audio_dir = root / f"audio_{tag}" + audio_dir.mkdir(parents=True, exist_ok=True) + labels = ["yes", "no", "up", "down"] + rows = [] + for i in range(n): + p = audio_dir / f"clip_{i:05d}.wav" + _make_wav_mono(p, duration_s=duration_s, seed=i) + rows.append({"audio_path": str(p), "label": labels[i % len(labels)]}) + csv_path = str(root / f"audio_{tag}.csv") + _write_csv(csv_path, rows, ["audio_path", "label"]) + return csv_path + + +# --------------------------------------------------------------------------- +# Memory + timing helpers +# --------------------------------------------------------------------------- + + +def _tracked(fn): + """Run fn(); return (result, peak_heap_mb, elapsed_s).""" + tracemalloc.start() + t0 = time.perf_counter() + result = fn() + elapsed = time.perf_counter() - t0 + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + tracemalloc.clear_traces() + return result, peak / 1024 / 1024, elapsed + + +# --------------------------------------------------------------------------- +# Ludwig wrappers +# --------------------------------------------------------------------------- + + +def _image_config(lazy: bool, batch_size: int) -> dict: + return { + "input_features": [ + { + "name": "image_path", + "type": "image", + "preprocessing": {"lazy": lazy, "num_channels": 3, "height": 64, "width": 64}, + "encoder": { + "type": "stacked_cnn", + "conv_layers": [{"num_filters": 8, "filter_size": 3}], + "fc_layers": [{"output_size": 16}], + }, + } + ], + "output_features": [{"name": "label", "type": "category"}], + "trainer": {"epochs": 1, "batch_size": batch_size}, + "combiner": {"type": "concat", "fc_layers": [{"output_size": 16}]}, + } + + +def _audio_config(lazy: bool, batch_size: int) -> dict: + return { + "input_features": [ + { + "name": "audio_path", + "type": "audio", + "preprocessing": { + "lazy": lazy, + "audio_feature": {"type": "fbank", "num_filter_bands": 80}, + "audio_file_length_limit_in_s": 2.0, + "norm": None, + }, + "encoder": { + "type": "stacked_cnn", + "conv_layers": [{"num_filters": 8, "filter_size": 3}], + "fc_layers": [{"output_size": 16}], + }, + } + ], + "output_features": [{"name": "label", "type": "category"}], + "trainer": {"epochs": 1, "batch_size": batch_size}, + "combiner": {"type": "concat", "fc_layers": [{"output_size": 16}]}, + } + + +def _run_preprocess(csv_path: str, config: dict, out_dir: str) -> None: + import ludwig.api + + model = ludwig.api.LudwigModel(config=config, logging_level=40) + model.preprocess(dataset=csv_path, output_directory=out_dir, skip_save_processed_input=False) + + +def _run_train(csv_path: str, config: dict, out_dir: str) -> None: + import ludwig.api + + model = ludwig.api.LudwigModel(config=config, logging_level=40) + model.train( + dataset=csv_path, + output_directory=out_dir, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=False, + ) + + +# --------------------------------------------------------------------------- +# Per-modality benchmark +# --------------------------------------------------------------------------- + + +def benchmark_modality(modality: str, n_samples: int, batch_size: int, tmpdir: Path) -> dict: + print(f"\n{'=' * 64}") + print(f" {modality.upper()} · {n_samples} samples · batch_size={batch_size}") + print("=" * 64) + + cfg_fn = _image_config if modality == "image" else _audio_config + + t0 = time.perf_counter() + # Create THREE independent copies so there is zero shared cache between runs + if modality == "image": + csv_prep_eager = create_image_dataset(tmpdir / modality, n_samples, tag="prep_eager") + csv_prep_lazy = create_image_dataset(tmpdir / modality, n_samples, tag="prep_lazy") + csv_full_eager = create_image_dataset(tmpdir / modality, n_samples, tag="full_eager") + csv_full_lazy = create_image_dataset(tmpdir / modality, n_samples, tag="full_lazy") + else: + csv_prep_eager = create_audio_dataset(tmpdir / modality, n_samples, tag="prep_eager") + csv_prep_lazy = create_audio_dataset(tmpdir / modality, n_samples, tag="prep_lazy") + csv_full_eager = create_audio_dataset(tmpdir / modality, n_samples, tag="full_eager") + csv_full_lazy = create_audio_dataset(tmpdir / modality, n_samples, tag="full_lazy") + print(f" Dataset copies generated in {time.perf_counter() - t0:.1f}s") + + results: dict = {} + + for lazy in [False, True]: + mode = "lazy" if lazy else "eager" + config = cfg_fn(lazy=lazy, batch_size=batch_size) + csv_prep = csv_prep_lazy if lazy else csv_prep_eager + csv_full = csv_full_lazy if lazy else csv_full_eager + + # ── Preprocessing only ──────────────────────────────────────── + prep_out = str(tmpdir / f"{modality}_{mode}_prep") + _, prep_heap_mb, prep_s = _tracked(lambda _c=csv_prep, _cfg=config, _o=prep_out: _run_preprocess(_c, _cfg, _o)) + if os.path.exists(prep_out): + shutil.rmtree(prep_out) + + # ── Full pipeline (preprocess + train) — completely isolated ── + full_out = str(tmpdir / f"{modality}_{mode}_full") + _, full_heap_mb, full_s = _tracked(lambda _c=csv_full, _cfg=config, _o=full_out: _run_train(_c, _cfg, _o)) + if os.path.exists(full_out): + shutil.rmtree(full_out) + + # Training-only time = full pipeline − preprocessing-only + train_s = max(0.0, full_s - prep_s) + throughput = n_samples / train_s if train_s > 0 else float("inf") + + print(f"\n [{mode.upper()}]") + print(f" Preprocessing : {prep_s:7.2f}s peak heap: {prep_heap_mb:7.1f} MB") + print(f" Full pipeline : {full_s:7.2f}s peak heap: {full_heap_mb:7.1f} MB") + print(f" Training est. : {train_s:7.2f}s throughput: {throughput:.1f} samples/s") + + results[mode] = { + "preprocessing_s": round(prep_s, 3), + "preprocessing_peak_heap_mb": round(prep_heap_mb, 1), + "full_pipeline_s": round(full_s, 3), + "full_pipeline_peak_heap_mb": round(full_heap_mb, 1), + "training_s_est": round(train_s, 3), + "training_throughput_samples_per_s": round(throughput, 1) if throughput != float("inf") else None, + } + + # ── Summary ─────────────────────────────────────────────────────── + e, la = results["eager"], results["lazy"] + + def _ratio(a, b, key): + return round(a[key] / b[key], 2) if b.get(key) and b[key] > 0 else None + + prep_speedup = _ratio(e, la, "preprocessing_s") + prep_mem_ratio = _ratio(e, la, "preprocessing_peak_heap_mb") + full_speedup = _ratio(e, la, "full_pipeline_s") + + lazy_tp = la.get("training_throughput_samples_per_s") or 0 + eager_tp = e.get("training_throughput_samples_per_s") or 0 + tp_ratio = round(lazy_tp / eager_tp, 2) if eager_tp > 0 else None + + print(f"\n SUMMARY ({modality}):") + print(f" Preprocessing speedup (lazy vs eager): {prep_speedup}x faster") + print(f" Preprocessing heap ratio (eager/lazy): {prep_mem_ratio}x less heap with lazy") + print(f" Full-pipeline speedup (eager/lazy): {full_speedup}x") + print(f" Training throughput ratio (lazy/eager): {tp_ratio}x") + + results["_summary"] = { + "preprocessing_speedup_lazy_over_eager": prep_speedup, + "preprocessing_heap_reduction_eager_over_lazy": prep_mem_ratio, + "full_pipeline_speedup_eager_over_lazy": full_speedup, + "training_throughput_ratio_lazy_over_eager": tp_ratio, + } + return results + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--n-samples", type=int, default=1000) + parser.add_argument("--batch-size", type=int, default=32) + parser.add_argument("--output", default="scripts/benchmark_results.json") + parser.add_argument("--modalities", nargs="+", default=["image", "audio"], choices=["image", "audio"]) + args = parser.parse_args() + + try: + import torch # noqa: F401 + except ImportError: + print("ERROR: torch not available — run from the Ludwig venv") + return + + print("Ludwig lazy-preprocessing benchmark") + print(f" n_samples={args.n_samples} batch_size={args.batch_size} modalities={args.modalities}") + + results: dict = {"config": vars(args)} + with tempfile.TemporaryDirectory(prefix="ludwig_lazy_bench_") as td: + tmpdir = Path(td) + for modality in args.modalities: + try: + results[modality] = benchmark_modality(modality, args.n_samples, args.batch_size, tmpdir) + except Exception as exc: + import traceback + + print(f"\nERROR in {modality}: {exc}") + traceback.print_exc() + results[modality] = {"error": str(exc)} + + print(f"\n{'=' * 64}") + print("Full results:") + print(json.dumps(results, indent=2)) + with open(args.output, "w") as f: + json.dump(results, f, indent=2) + print(f"\nSaved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_results.json b/scripts/benchmark_results.json new file mode 100644 index 0000000..f442bb6 --- /dev/null +++ b/scripts/benchmark_results.json @@ -0,0 +1,59 @@ +{ + "config": { + "n_samples": 1000, + "batch_size": 32, + "output": "scripts/benchmark_results.json", + "modalities": [ + "image", + "audio" + ] + }, + "image": { + "eager": { + "preprocessing_s": 57.196, + "preprocessing_peak_heap_mb": 365.4, + "full_pipeline_s": 8.115, + "full_pipeline_peak_heap_mb": 18.7, + "training_s_est": 0.0, + "training_throughput_samples_per_s": null + }, + "lazy": { + "preprocessing_s": 3.247, + "preprocessing_peak_heap_mb": 1.3, + "full_pipeline_s": 7.359, + "full_pipeline_peak_heap_mb": 14.5, + "training_s_est": 4.112, + "training_throughput_samples_per_s": 243.2 + }, + "_summary": { + "preprocessing_speedup_lazy_over_eager": 17.62, + "preprocessing_heap_reduction_eager_over_lazy": 281.08, + "full_pipeline_speedup_eager_over_lazy": 1.1, + "training_throughput_ratio_lazy_over_eager": null + } + }, + "audio": { + "eager": { + "preprocessing_s": 3.857, + "preprocessing_peak_heap_mb": 1.8, + "full_pipeline_s": 115.629, + "full_pipeline_peak_heap_mb": 9.4, + "training_s_est": 111.772, + "training_throughput_samples_per_s": 8.9 + }, + "lazy": { + "preprocessing_s": 3.404, + "preprocessing_peak_heap_mb": 0.9, + "full_pipeline_s": 115.653, + "full_pipeline_peak_heap_mb": 9.3, + "training_s_est": 112.25, + "training_throughput_samples_per_s": 8.9 + }, + "_summary": { + "preprocessing_speedup_lazy_over_eager": 1.13, + "preprocessing_heap_reduction_eager_over_lazy": 2.0, + "full_pipeline_speedup_eager_over_lazy": 1.0, + "training_throughput_ratio_lazy_over_eager": 1.0 + } + } +} diff --git a/scripts/benchmark_training_pipeline.py b/scripts/benchmark_training_pipeline.py new file mode 100644 index 0000000..9b63902 --- /dev/null +++ b/scripts/benchmark_training_pipeline.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +"""Benchmark the training data pipeline to measure GPU utilization and bottlenecks. + +Measures per-step timing for 7 pipeline configurations: + + eager_local — pre-decoded numpy arrays in memory (zero fetch overhead) + lazy_local_sync — LazyColumn decode per batch, synchronous on main thread + lazy_local_pre2 — LazyColumn with prefetch_size=2 background thread + lazy_local_pre4 — LazyColumn with prefetch_size=4 background thread + lazy_cached_ep1 — CachedLazyColumn: epoch 1 (decode + write to memmap) + lazy_cached_ep2plus — CachedLazyColumn: epoch 2+ (read from memmap, no decode) + lazy_ray — RayDataset + _with_lazy_decode (distributed backend) + +Per step, records: + t_fetch — time next_batch() blocks (= GPU idle time without prefetch) + t_gpu — time simulated GPU work takes + util_pct — GPU utilization = t_gpu / (t_fetch + t_gpu) × 100 + +GPU work is simulated by time.sleep(--gpu-work-ms / 1000), which gives precise +control over the fetch/GPU ratio independent of hardware. Vary --gpu-work-ms +to understand the regime you care about: + + --gpu-work-ms 5 small model / fast GPU → decode usually dominates + --gpu-work-ms 30 mid-size model → decode can dominate + --gpu-work-ms 100 large model / slow GPU → GPU dominates, decode hidden + +Run: + python scripts/benchmark_training_pipeline.py [options] + +Requirements: torchaudio (for WAV generation), ray[data] (for lazy_ray mode). +""" + +import argparse +import os +import sys +import tempfile +import time +from statistics import mean, median, quantiles + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + +# ───────────────────────────────────────────────────────────────────────────── +# Helpers: synthetic WAV files + metadata +# ───────────────────────────────────────────────────────────────────────────── + +_SAMPLE_RATE = 16_000 +_DURATION_S = 0.5 + + +def _write_wav_files(dest_dir: str, n: int) -> list[str]: + import torch + import torchaudio + + os.makedirs(dest_dir, exist_ok=True) + n_samples = int(_DURATION_S * _SAMPLE_RATE) + silence = torch.zeros(1, n_samples) + paths = [] + for i in range(n): + p = os.path.join(dest_dir, f"audio_{i:05d}.wav") + torchaudio.save(p, silence, _SAMPLE_RATE) + paths.append(p) + return paths + + +def _lazy_audio_metadata(feature_dim: int = 8, max_length: int = 23, mode: str = "lazy") -> dict: + return { + "lazy": True, + "mode": mode, + "reshape": None, + "lazy_audio_params": { + "audio_feature_dict": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": feature_dim, + }, + "feature_dim": feature_dim, + "max_length": max_length, + "padding_value": 0.0, + "normalization_type": None, + }, + } + + +def _make_decode_fn(feature_dim: int, max_length: int): + from ludwig.features.audio_feature import AudioFeatureMixin + + p = _lazy_audio_metadata(feature_dim, max_length)["lazy_audio_params"] + return AudioFeatureMixin._make_lazy_decode_fn( + audio_feature_dict=p["audio_feature_dict"], + feature_dim=p["feature_dim"], + max_length=p["max_length"], + padding_value=p["padding_value"], + normalization_type=p["normalization_type"], + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# Timing loop +# ───────────────────────────────────────────────────────────────────────────── + +StepTiming = dict # {'t_fetch': float, 't_gpu': float, 'epoch': int, 'step': int} + + +def _run_timing_loop(batcher, n_epochs: int, gpu_work_s: float, n_warmup: int) -> list[StepTiming]: + """Run the batcher for n_epochs and return per-step timings after warmup.""" + timings = [] + global_step = 0 + + for epoch in range(n_epochs): + batcher.set_epoch(epoch, batcher.batch_size) + while not batcher.last_batch(): + t0 = time.perf_counter() + _batch = batcher.next_batch() + t1 = time.perf_counter() + + # Simulate GPU forward + backward pass + time.sleep(gpu_work_s) + + t2 = time.perf_counter() + + if global_step >= n_warmup: + timings.append( + { + "t_fetch": t1 - t0, + "t_gpu": t2 - t1, + "epoch": epoch, + "step": global_step, + } + ) + global_step += 1 + + return timings + + +def _stats(values: list[float]) -> dict: + if not values: + return {"mean": 0, "p50": 0, "p95": 0, "p99": 0, "min": 0, "max": 0} + qs = quantiles(values, n=100) if len(values) >= 2 else [values[0]] * 99 + return { + "mean": mean(values), + "p50": median(values), + "p95": qs[94] if len(qs) > 94 else max(values), + "p99": qs[98] if len(qs) > 98 else max(values), + "min": min(values), + "max": max(values), + } + + +def _analyze(timings: list[StepTiming], n_samples: int, n_epochs: int) -> dict: + if not timings: + return {} + fetch_ms = [t["t_fetch"] * 1000 for t in timings] + gpu_ms = [t["t_gpu"] * 1000 for t in timings] + util = [g / (f + g) * 100 for f, g in zip(fetch_ms, gpu_ms) if (f + g) > 0] + total_s = sum(t["t_fetch"] + t["t_gpu"] for t in timings) + sps = (n_samples * n_epochs) / total_s if total_s > 0 else 0 + return { + "fetch_ms": _stats(fetch_ms), + "gpu_ms": _stats(gpu_ms), + "util_pct": _stats(util), + "sps": sps, + "n_steps": len(timings), + } + + +# ───────────────────────────────────────────────────────────────────────────── +# Mode: eager_local (pre-decoded arrays) +# ───────────────────────────────────────────────────────────────────────────── + + +def bench_eager_local(paths, batch_size, epochs, feature_dim, max_length, gpu_work_s, n_warmup): + from concurrent.futures import ThreadPoolExecutor + + from ludwig.data.dataset.pandas import PandasDataset + + decode_fn = _make_decode_fn(feature_dim, max_length) + with ThreadPoolExecutor(max_workers=min(16, len(paths))) as ex: + decoded = np.stack(list(ex.map(decode_fn, paths))) + + proc_col = "audio_proc" + feature_name = "audio_0" + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + # Non-lazy metadata: decoded array stored directly + training_set_metadata = {feature_name: {"lazy": False, "reshape": (feature_dim, max_length)}} + ds = PandasDataset( + {proc_col: decoded.reshape(len(paths), -1)}, + features, + data_cache_fp=None, + training_set_metadata=training_set_metadata, + ) + with ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher: + return _run_timing_loop(batcher, epochs, gpu_work_s, n_warmup) + + +# ───────────────────────────────────────────────────────────────────────────── +# Mode: lazy_local (sync or prefetch) +# ───────────────────────────────────────────────────────────────────────────── + + +def bench_lazy_local(paths, batch_size, epochs, feature_dim, max_length, gpu_work_s, n_warmup, prefetch_size=0): + + from ludwig.data.dataset.pandas import PandasDataset + + proc_col = "audio_proc" + feature_name = "audio_0" + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + training_set_metadata = {feature_name: _lazy_audio_metadata(feature_dim, max_length)} + ds = PandasDataset( + {proc_col: np.array(paths, dtype=object)}, + features, + data_cache_fp=None, + training_set_metadata=training_set_metadata, + ) + with ds.initialize_batcher(batch_size=batch_size, should_shuffle=False, prefetch_size=prefetch_size) as batcher: + return _run_timing_loop(batcher, epochs, gpu_work_s, n_warmup) + + +# ───────────────────────────────────────────────────────────────────────────── +# Mode: lazy_cached (CachedLazyColumn — decode+cache on ep1, memmap on ep2+) +# ───────────────────────────────────────────────────────────────────────────── + + +def bench_lazy_cached( + paths, batch_size, epochs, feature_dim, max_length, gpu_work_s, n_warmup +) -> tuple[list[StepTiming], list[StepTiming]]: + """Run ``lazy_cached`` mode and return (ep1_timings, ep2plus_timings). + + Epoch 0 is a mandatory cache-fill pass (not counted in either result set). + ``ep1_timings`` covers the first measured epoch (decode + memmap write). + ``ep2plus_timings`` covers all subsequent epochs (pure memmap reads). + """ + import tempfile + + from ludwig.data.dataset.pandas import PandasDataset + + proc_col = "audio_proc" + feature_name = "audio_0" + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + training_set_metadata = {feature_name: _lazy_audio_metadata(feature_dim, max_length, mode="lazy_cached")} + + with tempfile.TemporaryDirectory() as cache_dir: + # Place a fake data_cache_fp so _decoded_cache_path uses this directory. + fake_cache_fp = os.path.join(cache_dir, "train.parquet") + + ds = PandasDataset( + {proc_col: np.array(paths, dtype=object)}, + features, + data_cache_fp=fake_cache_fp, + training_set_metadata=training_set_metadata, + ) + + # Run (1 + epochs) epochs total; epoch 0 fills the cache, remaining epochs + # are measured. n_warmup applies within the measured epochs only. + total_epochs = 1 + epochs + all_timings: list[StepTiming] = [] + global_step = 0 + + with ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher: + for epoch in range(total_epochs): + batcher.set_epoch(epoch, batcher.batch_size) + step_in_epoch = 0 + while not batcher.last_batch(): + t0 = time.perf_counter() + _batch = batcher.next_batch() + t1 = time.perf_counter() + time.sleep(gpu_work_s) + t2 = time.perf_counter() + + measured_epoch = epoch - 1 # epoch 0 is cache-fill, not measured + if epoch >= 1: + warmup_step = global_step - (batcher.steps_per_epoch) # steps since cache-fill ended + if warmup_step >= n_warmup: + all_timings.append( + { + "t_fetch": t1 - t0, + "t_gpu": t2 - t1, + "epoch": measured_epoch, + "step": global_step, + } + ) + global_step += 1 + step_in_epoch += 1 + + ep1_timings = [t for t in all_timings if t["epoch"] == 0] + ep2plus_timings = [t for t in all_timings if t["epoch"] >= 1] + return ep1_timings, ep2plus_timings + + +# ───────────────────────────────────────────────────────────────────────────── +# Mode: lazy_ray +# ───────────────────────────────────────────────────────────────────────────── + + +def bench_lazy_ray(paths, batch_size, epochs, feature_dim, max_length, gpu_work_s, n_warmup): + import pandas as pd + import ray + + from ludwig.data.dataset.ray import RayDataset + + proc_col = "audio_proc" + feature_name = "audio_0" + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + training_set_metadata = {feature_name: _lazy_audio_metadata(feature_dim, max_length)} + df = pd.DataFrame({proc_col: paths}) + ray_ds = RayDataset.__new__(RayDataset) + ray_ds.ds = ray.data.from_pandas(df) + ray_ds.features = features + ray_ds.training_set_metadata = training_set_metadata + ray_ds.data_cache_fp = None + ray_ds.data_parquet_fp = None + + all_timings = [] + for epoch in range(epochs): + with ray_ds.initialize_batcher(batch_size=batch_size, should_shuffle=False) as batcher: + step = 0 + while not batcher.last_batch(): + t0 = time.perf_counter() + _batch = batcher.next_batch() + t1 = time.perf_counter() + time.sleep(gpu_work_s) + t2 = time.perf_counter() + global_step = epoch * batcher.steps_per_epoch + step + if global_step >= n_warmup: + all_timings.append({"t_fetch": t1 - t0, "t_gpu": t2 - t1, "epoch": epoch, "step": global_step}) + step += 1 + + return all_timings + + +# ───────────────────────────────────────────────────────────────────────────── +# Reporting +# ───────────────────────────────────────────────────────────────────────────── + +_COL = 22 + + +def _fmt_stat(s: dict, unit: str = "ms") -> str: + return f"{s['mean']:6.1f} (p95={s['p95']:5.1f}) {unit}" + + +def _print_report(results: dict, n_samples: int, n_epochs: int, gpu_work_ms: float): + print() + print(f"{'Mode':<26} {'t_fetch mean(p95)':>22} {'t_gpu mean(p95)':>22} {'util%':>7} {'sps':>8}") + print("─" * 90) + for mode, r in results.items(): + if not r: + print(f" {mode:<24} FAILED") + continue + fetch = _fmt_stat(r["fetch_ms"]) + gpu = _fmt_stat(r["gpu_ms"]) + util = f"{r['util_pct']['mean']:5.1f}%" + sps = f"{r['sps']:>8,.0f}" + print(f" {mode:<24} {fetch:>22} {gpu:>22} {util} {sps}") + print() + + # Show GPU-idle time breakdown + print("GPU idle analysis (t_fetch / (t_fetch + t_gpu) × 100 = idle %):") + baseline_sps = None + for mode, r in results.items(): + if not r: + continue + idle = 100 - r["util_pct"]["mean"] + overhead = "" + if baseline_sps is None: + baseline_sps = r["sps"] + elif baseline_sps and r["sps"]: + ratio = baseline_sps / r["sps"] + overhead = f" ({ratio:.1f}× slower than {list(results.keys())[0]})" + print(f" {mode:<26} GPU idle {idle:5.1f}%{overhead}") + print() + print(f" GPU work per step: {gpu_work_ms:.0f} ms (simulated)") + print(f" Decode budget to match eager: ≤ {gpu_work_ms:.0f} ms per batch") + print() + + +# ───────────────────────────────────────────────────────────────────────────── +# Main +# ───────────────────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--n-samples", type=int, default=200) + parser.add_argument("--batch-size", type=int, default=32) + parser.add_argument("--epochs", type=int, default=3) + parser.add_argument("--feature-dim", type=int, default=8) + parser.add_argument("--max-length", type=int, default=23) + parser.add_argument( + "--gpu-work-ms", + type=float, + default=30.0, + help="Simulated GPU step time per batch in ms. Try 5 (small model) / 30 / 100 (large model).", + ) + parser.add_argument("--n-warmup", type=int, default=3, help="Steps to discard for warm-up") + parser.add_argument("--skip-ray", action="store_true", help="Skip the Ray backend benchmarks") + args = parser.parse_args() + + gpu_work_s = args.gpu_work_ms / 1000.0 + + with tempfile.TemporaryDirectory() as tmpdir: + print(f"\nGenerating {args.n_samples} WAV files ...", flush=True) + paths = _write_wav_files(tmpdir, args.n_samples) + print(" done.") + + ray_ready = False + if not args.skip_ray: + try: + import ray + + if not ray.is_initialized(): + ray.init(ignore_reinit_error=True, num_cpus=4, include_dashboard=False) + ray_ready = True + print("Ray initialised.") + except Exception as e: + print(f"Ray not available ({e}), skipping lazy_ray.") + + kw = { + "paths": paths, + "batch_size": args.batch_size, + "epochs": args.epochs, + "feature_dim": args.feature_dim, + "max_length": args.max_length, + "gpu_work_s": gpu_work_s, + "n_warmup": args.n_warmup, + } + + configs = [ + ("eager_local", lambda: bench_eager_local(**kw)), + ("lazy_local_sync", lambda: bench_lazy_local(**kw, prefetch_size=0)), + ("lazy_local_pre2", lambda: bench_lazy_local(**kw, prefetch_size=2)), + ("lazy_local_pre4", lambda: bench_lazy_local(**kw, prefetch_size=4)), + ] + if ray_ready: + configs.append(("lazy_ray", lambda: bench_lazy_ray(**kw))) + + print( + f"\n{'─' * 70}\n" + f" n_samples={args.n_samples} batch_size={args.batch_size} " + f"epochs={args.epochs} gpu_work={args.gpu_work_ms:.0f}ms\n" + f"{'─' * 70}\n" + ) + + results = {} + for name, fn in configs: + print(f"Running {name} ...", flush=True) + try: + timings = fn() + results[name] = _analyze(timings, args.n_samples, args.epochs) + r = results[name] + print( + f" fetch {r['fetch_ms']['mean']:.1f}ms gpu {r['gpu_ms']['mean']:.1f}ms util {r['util_pct']['mean']:.1f}% {r['sps']:,.0f} sps" + ) + except Exception as e: + import traceback + + results[name] = None + print(f" FAILED: {e}") + traceback.print_exc() + + # lazy_cached is run once; ep1 and ep2+ timings are split and reported separately. + print("Running lazy_cached (ep1 + ep2+) ...", flush=True) + try: + ep1_timings, ep2plus_timings = bench_lazy_cached(**kw) + results["lazy_cached_ep1"] = _analyze(ep1_timings, args.n_samples, 1) if ep1_timings else {} + results["lazy_cached_ep2plus"] = ( + _analyze(ep2plus_timings, args.n_samples, max(1, args.epochs - 1)) if ep2plus_timings else {} + ) + for suffix in ("ep1", "ep2plus"): + r = results[f"lazy_cached_{suffix}"] + if r: + print( + f" lazy_cached_{suffix}: fetch {r['fetch_ms']['mean']:.1f}ms " + f"gpu {r['gpu_ms']['mean']:.1f}ms util {r['util_pct']['mean']:.1f}% {r['sps']:,.0f} sps" + ) + except Exception as e: + import traceback + + results["lazy_cached_ep1"] = None + results["lazy_cached_ep2plus"] = None + print(f" FAILED: {e}") + traceback.print_exc() + + _print_report(results, args.n_samples, args.epochs, args.gpu_work_ms) + + if ray_ready: + ray.shutdown() + + +if __name__ == "__main__": + main() diff --git a/scripts/dataset_smoke_test.py b/scripts/dataset_smoke_test.py new file mode 100644 index 0000000..79c93e7 --- /dev/null +++ b/scripts/dataset_smoke_test.py @@ -0,0 +1,610 @@ +#!/usr/bin/env python3 +"""1-epoch Ludwig smoke test for every dataset config. + +Streams 1000 rows directly from HF (no full download), builds a minimal +Ludwig config from the YAML, runs 1 epoch, then wipes the HF cache entry +before moving to the next dataset. + +Usage: + python scripts/dataset_smoke_test.py [--names ds1 ds2 ...] [--resume] +""" + +from __future__ import annotations + +import argparse +import json +import logging +import os +import shutil +import tempfile +import traceback +from collections import Counter +from typing import Any + +import datasets as hf_datasets +import pandas as pd +import yaml + +logging.basicConfig(level=logging.WARNING) +os.environ.setdefault("LUDWIG_DISABLE_PROGRESS_BAR", "1") + +CONFIGS_DIR = os.path.join(os.path.dirname(__file__), "..", "ludwig", "datasets", "configs") +RESULTS_PATH = os.path.join(os.path.dirname(__file__), "smoke_results.json") +HF_HUB_CACHE = os.path.expanduser("~/.cache/huggingface/hub") +HF_DS_CACHE = os.path.expanduser("~/.cache/huggingface/datasets") + +SAMPLE_ROWS = 1000 +MIN_ROWS = 32 + +# Skip: gated/no supervised task/already covered +SKIP = {"imagenet1k", "gigaspeech", "hugging_face"} + +ENCODER_OVERRIDES: dict[str, dict] = { + "text": {"encoder": {"type": "embed", "embedding_size": 16, "trainable": True}}, + "image": {"encoder": {"type": "stacked_cnn", "num_conv_layers": 1, "num_filters": 8, "output_size": 16}}, + "audio": {"encoder": {"type": "stacked_cnn", "num_conv_layers": 1, "num_filters": 8, "output_size": 16}}, + "sequence": {"encoder": {"type": "embed", "embedding_size": 16}}, + "set": {"encoder": {"type": "embed", "embedding_size": 16}}, +} + + +def load_dataset_config(name: str) -> dict: + path = os.path.join(CONFIGS_DIR, f"{name}.yaml") + with open(path) as f: + return yaml.safe_load(f) + + +_MAX_AUDIO_ARRAY_SAMPLES = 2 * 16000 # 2 s at 16 kHz; scaled for other rates + + +def _truncate_audio_in_row(row: dict) -> dict: + """Truncate decoded audio arrays in a streaming row to bound per-row RAM use. + + HF datasets decode audio columns to {'array': ndarray, 'sampling_rate': int} + before we can filter. Long clips (e.g. multi-minute Quran recitations) can be + 100+ MB each; 1000 such rows exhaust RAM before any other processing occurs. + We truncate to 2 s worth of samples right here in the streaming loop. + """ + truncated = {} + for key, val in row.items(): + if isinstance(val, dict) and "array" in val and "sampling_rate" in val: + sr = int(val.get("sampling_rate") or 16000) + max_samp = int(_MAX_AUDIO_ARRAY_SAMPLES * sr / 16000) + arr = val["array"] + if hasattr(arr, "__len__") and len(arr) > max_samp: + val = {**val, "array": arr[:max_samp]} + truncated[key] = val + return truncated + + +def stream_sample( + hf_id: str, + hf_sub: str | None, + n: int = SAMPLE_ROWS, + shuffle_buffer: int = 100000, + skip: int = 0, + revision: str | None = None, + data_files: dict | None = None, + truncate_audio: bool = False, + select_columns: list[str] | None = None, +) -> pd.DataFrame | None: + """Stream up to n rows from HF without downloading the full dataset. + + select_columns: if provided, drop all other columns from each row before + building the DataFrame, keeping memory bounded for datasets with large + unneeded columns (e.g. image bytes in a tabular dataset). + """ + try: + kwargs: dict = { + "path": hf_id, + "name": hf_sub, + "trust_remote_code": False, + "streaming": True, + } + if revision: + kwargs["revision"] = revision + if data_files: + kwargs["data_files"] = data_files + ds_stream = hf_datasets.load_dataset(**kwargs) + split_name = "train" if "train" in ds_stream else list(ds_stream.keys())[0] + ds = ds_stream[split_name] + if skip: + ds = ds.skip(skip) + # Shuffle to ensure label diversity in sorted datasets (e.g. dbpedia, imdb). + # Use a smaller buffer for media datasets to avoid streaming 100k large files. + ds = ds.shuffle(seed=42, buffer_size=min(n * 100, shuffle_buffer)) + _keep = set(select_columns) if select_columns else None + if truncate_audio: + rows = [_truncate_audio_in_row(r) for r in ds.take(n)] + else: + rows = list(ds.take(n)) + if _keep: + rows = [{k: v for k, v in row.items() if k in _keep} for row in rows] + if len(rows) < MIN_ROWS: + return None + return pd.DataFrame(rows) + except Exception as e: + raise RuntimeError(f"Stream failed: {e}") from e + + +def wipe_hf_cache_for(hf_id: str, hf_sub: str | None) -> None: + """Delete hub and dataset cache entries for a specific dataset.""" + # Hub cache dirs are named datasets--org--repo + normalized = hf_id.replace("/", "--") + for cache_root in [HF_HUB_CACHE, HF_DS_CACHE]: + if not os.path.isdir(cache_root): + continue + for entry in os.listdir(cache_root): + if normalized in entry or (hf_sub and hf_sub in entry): + path = os.path.join(cache_root, entry) + try: + shutil.rmtree(path) + except Exception: + pass + + +def build_ludwig_config(cfg: dict) -> dict: + out_names = {f["name"] for f in cfg.get("output_features", [])} + input_features = [] + for col in cfg.get("columns", []): + if col["name"] in out_names: + continue + feat = {"name": col["name"], "type": col["type"]} + feat.update(ENCODER_OVERRIDES.get(col["type"], {})) + input_features.append(feat) + output_features = [{"name": f["name"], "type": f["type"]} for f in cfg.get("output_features", [])] + return { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "fc_size": 32}, + "trainer": {"epochs": 1, "batch_size": 32, "learning_rate": 0.001, "eval_batch_size": 32}, + "preprocessing": {"split": {"type": "random", "probabilities": [0.7, 0.1, 0.2]}}, + } + + +def apply_custom_loader(name: str, df: pd.DataFrame) -> pd.DataFrame: + """Run the custom loader's _transform on the raw dataframe.""" + cfg = load_dataset_config(name) + loader_spec = cfg.get("loader", "") + if not loader_spec or loader_spec == "hugging_face.HFLoader": + return df + + module_name, cls_name = loader_spec.rsplit(".", 1) + full_module = f"ludwig.datasets.loaders.{module_name}" + try: + import importlib + + mod = importlib.import_module(full_module) + cls = getattr(mod, cls_name) + + # processed_dataset_dir is a read-only @property — override via subclass + _tmpdir = tempfile.mkdtemp(prefix=f"ludwig_smoke_{name}_") + PatchedCls = type( + f"_{cls_name}", + (cls,), + { + "processed_dataset_dir": property(lambda self, d=_tmpdir: d), + }, + ) + instance = object.__new__(PatchedCls) + return instance._transform(df) + except Exception as e: + raise RuntimeError(f"Loader {loader_spec} failed: {e}") from e + + +def _materialize_media_columns(df: pd.DataFrame, tmpdir: str, col_types: dict[str, str] | None = None) -> pd.DataFrame: + """Replace PIL images and HF audio objects with file paths. + + col_types maps column name → declared type ('audio' or 'image') so that + raw-bytes audio dicts {'bytes': ..., 'path': ...} are not misidentified as images. + """ + import io + + try: + from PIL import Image as PILImage + + _pil_available = True + except ImportError: + _pil_available = False + + df = df.copy() + for col in df.columns: + sample = df[col].dropna().iloc[:1] + if sample.empty: + continue + val = sample.iloc[0] + + declared = (col_types or {}).get(col) # 'audio', 'image', or None + + # HF audio dict {'array': ..., 'sampling_rate': ...} or TorchCodec object + # or raw bytes dict {'bytes': ..., 'path': ...} for a declared audio column + _is_raw_bytes_dict = isinstance(val, dict) and ("bytes" in val or "path" in val) and "array" not in val + is_audio = ( + (isinstance(val, dict) and "array" in val and "sampling_rate" in val) + or (hasattr(val, "__class__") and "AudioDecoder" in type(val).__name__) + or (declared == "audio" and _is_raw_bytes_dict) + ) + + # PIL image or HF image dict — but NOT if the column is declared as audio + is_image = ( + (not is_audio) + and _pil_available + and (isinstance(val, PILImage.Image) or (_is_raw_bytes_dict and declared != "audio")) + ) + + if is_image: + img_dir = os.path.join(tmpdir, col) + os.makedirs(img_dir, exist_ok=True) + MAX_IMG_SIZE = (128, 128) + paths = [] + for idx, v in df[col].items(): + path = os.path.join(img_dir, f"{idx}.jpg") + try: + if isinstance(v, PILImage.Image): + img = v.convert("RGB") + elif isinstance(v, dict) and "bytes" in v and v["bytes"]: + img = PILImage.open(io.BytesIO(v["bytes"])).convert("RGB") + elif isinstance(v, dict) and "path" in v and v["path"]: + img = PILImage.open(v["path"]).convert("RGB") + else: + path = "" + paths.append(path) + continue + # Cap resolution to avoid OOM during Ludwig preprocessing + img.thumbnail(MAX_IMG_SIZE, PILImage.LANCZOS) + img.save(path, format="JPEG") + except Exception: + path = "" + paths.append(path) + df[col] = paths + + elif is_audio: + import numpy as np + + try: + import soundfile as sf + + _sf_available = True + except ImportError: + _sf_available = False + aud_dir = os.path.join(tmpdir, col) + os.makedirs(aud_dir, exist_ok=True) + MAX_AUDIO_SAMPLES = 5 * 16000 # 5 seconds at 16 kHz + paths = [] + for idx, v in df[col].items(): + path = os.path.join(aud_dir, f"{idx}.wav") + try: + if isinstance(v, dict) and "array" in v: + arr = np.array(v["array"], dtype=np.float32) + sr = int(v.get("sampling_rate", 16000)) + max_samples = int(MAX_AUDIO_SAMPLES * sr / 16000) + arr = arr[:max_samples] + if _sf_available: + sf.write(path, arr, sr) + else: + from scipy.io import wavfile + + wavfile.write(path, sr, (arr * 32767).astype(np.int16)) + elif isinstance(v, dict) and "bytes" in v and v["bytes"]: + # Raw audio bytes {'bytes': b'RIFF...', 'path': '...'} — read with soundfile + import io as _io + + bio = _io.BytesIO(v["bytes"]) + if _sf_available: + arr, sr = sf.read(bio, dtype="float32", always_2d=False) + else: + from scipy.io import wavfile + + sr, arr = wavfile.read(bio) + arr = arr.astype(np.float32) / 32768.0 + if arr.ndim > 1: + arr = arr[:, 0] # first channel + max_samples = int(MAX_AUDIO_SAMPLES * sr / 16000) + arr = arr[:max_samples] + if _sf_available: + sf.write(path, arr, sr) + else: + from scipy.io import wavfile + + wavfile.write(path, sr, (arr * 32767).astype(np.int16)) + elif hasattr(v, "get_all_samples"): + samples = v.get_all_samples() + arr = samples.data.numpy() + sr = int(samples.sample_rate) + if arr.ndim > 1: + arr = arr[0] # first channel + arr = arr.astype(np.float32) + max_samples = int(MAX_AUDIO_SAMPLES * sr / 16000) + arr = arr[:max_samples] + if _sf_available: + sf.write(path, arr, sr) + else: + from scipy.io import wavfile + + wavfile.write(path, sr, (arr * 32767).astype(np.int16)) + else: + path = "" + except Exception: + path = "" + paths.append(path) + df[col] = paths + + return df + + +def run_smoke_test(name: str) -> dict[str, Any]: + result: dict[str, Any] = {"name": name, "status": "unknown", "error": None, "rows": 0} + + if name in SKIP: + result["status"] = "skipped" + return result + + try: + cfg = load_dataset_config(name) + except Exception as e: + result["status"] = "error" + result["error"] = f"YAML load failed: {e}" + return result + + hf_id = cfg.get("huggingface_dataset_id") + hf_sub = cfg.get("huggingface_subsample") + hf_revision = cfg.get("huggingface_revision") + hf_data_files = cfg.get("huggingface_data_files") + + if not hf_id: + result["status"] = "skipped" + result["error"] = "Not an HF dataset (pre-existing Ludwig dataset)" + return result + + # 1. Stream 1000 rows — use small shuffle buffer for media datasets to avoid + # streaming 100k large files; text datasets can afford the large buffer + # to ensure label diversity in sorted datasets (e.g. dbpedia_14, imdb). + # Only category/binary outputs need large buffers (text/number outputs have no + # "at least 2 distinct values" requirement). Media datasets use small buffers + # to avoid streaming hundreds of thousands of large files into RAM. + loader_spec = cfg.get("loader", "") + has_custom_loader = bool(loader_spec and loader_spec != "hugging_face.HFLoader") + col_types = {col["type"] for col in cfg.get("columns", [])} + has_media = bool(col_types & {"audio", "image"}) + # Only stream the columns we actually need — avoids loading large unneeded columns + # (e.g. image bytes in a dataset where we only use tabular metadata columns). + # Custom loaders may read different column names than declared (e.g. TranslationLoader + # reads 'translation' and splits it into 'de'/'en'), so skip column filtering for them. + cfg_col_names = [col["name"] for col in cfg.get("columns", [])] + if has_media: + shuffle_buf = 5000 + else: + # Start small for all non-media datasets to avoid streaming huge amounts + # of data. We'll escalate to 100k only if the diversity check fails below. + shuffle_buf = 2000 + try: + df = stream_sample( + hf_id, + hf_sub, + SAMPLE_ROWS, + shuffle_buffer=shuffle_buf, + revision=hf_revision, + data_files=hf_data_files, + truncate_audio=has_media, + select_columns=cfg_col_names if (cfg_col_names and not has_custom_loader) else None, + ) + except Exception as e: + result["status"] = "error" + result["error"] = str(e)[:200] + return result + + if df is None: + result["status"] = "error" + result["error"] = f"Too few rows (< {MIN_ROWS})" + return result + + result["rows"] = len(df) + + # 2. Apply custom loader transform if needed + try: + df = apply_custom_loader(name, df) + except Exception as e: + result["status"] = "error" + result["error"] = f"Custom loader failed: {e}" + return result + + # 3. Materialize any PIL images/audio to disk before passing to Ludwig. + # Images are capped at 256×256 and audio at 5 s to avoid OOM during + # Ludwig's upfront preprocessing of the full sample. + _img_tmp = tempfile.mkdtemp(prefix=f"ludwig_imgs_{name}_") + try: + col_type_map = {col["name"]: col["type"] for col in cfg.get("columns", [])} + try: + df = _materialize_media_columns(df, _img_tmp, col_types=col_type_map) + except ImportError: + pass # PIL not available — skip + + # 4. Keep only columns declared in the config; drop extras. + # Also drop rows where media columns failed to materialize (empty path). + cfg_cols = {f["name"] for f in cfg.get("columns", [])} + df = df[[c for c in df.columns if c in cfg_cols or c == "split"]] + media_col_types = {f["name"]: f["type"] for f in cfg.get("columns", []) if f["type"] in ("audio", "image")} + for mc in media_col_types: + if mc in df.columns: + # Keep only rows where the column is a non-empty string path + # (materialization replaces dicts with paths; dicts/None/empty → drop row) + df = df[df[mc].apply(lambda x: isinstance(x, str) and x.strip() != "")] + df = df.reset_index(drop=True) + + # 5. Verify output columns exist and have values; retry with skip for sorted datasets + out_names = [f["name"] for f in cfg.get("output_features", [])] + out_types = {f["name"]: f["type"] for f in cfg.get("output_features", [])} + for oc in out_names: + if oc not in df.columns: + result["status"] = "error" + result["error"] = f"Output column '{oc}' missing after transform. Cols: {list(df.columns)}" + return result + if df[oc].isna().all(): + result["status"] = "error" + result["error"] = f"Output column '{oc}' is all-null" + return result + # For category/binary outputs: if only 1 distinct non-null value, the dataset + # is likely sorted. Retry by skipping 40k rows to sample from a different region. + try: + _nunique = df[oc].dropna().nunique() + except TypeError: + _nunique = 2 # unhashable (list labels) — skip diversity retry + if out_types.get(oc) in ("category", "binary") and _nunique < 2 and not has_media: + # Try a larger shuffle buffer first (catches balanced datasets that appeared + # uniform at buffer=2000 due to unlucky sampling, before paying the cost + # of a full skip-to-40k retry). + try: + df_retry = stream_sample( + hf_id, + hf_sub, + SAMPLE_ROWS, + shuffle_buffer=50000, + revision=hf_revision, + data_files=hf_data_files, + truncate_audio=False, + select_columns=cfg_col_names if (cfg_col_names and not has_custom_loader) else None, + ) + if df_retry is not None: + df_retry = apply_custom_loader(name, df_retry) + df_retry = df_retry[[c for c in df_retry.columns if c in cfg_cols or c == "split"]] + _r2 = df_retry[oc].dropna().nunique() if oc in df_retry.columns else 0 + if _r2 >= 2: + df = df_retry + _nunique = _r2 + except Exception: + pass + if out_types.get(oc) in ("category", "binary") and _nunique < 2: + try: + # Use buffer=1 for skip-sampled data to avoid OOM on large image datasets + df2 = stream_sample( + hf_id, + hf_sub, + SAMPLE_ROWS // 2, + shuffle_buffer=1, + skip=40000, + revision=hf_revision, + data_files=hf_data_files, + truncate_audio=has_media, + select_columns=cfg_col_names if (cfg_col_names and not has_custom_loader) else None, + ) + if df2 is not None: + df2 = apply_custom_loader(name, df2) + df2 = _materialize_media_columns(df2, _img_tmp, col_types=col_type_map) + df2 = df2[[c for c in df2.columns if c in cfg_cols or c == "split"]] + for mc in media_col_types: + if mc in df2.columns: + df2 = df2[df2[mc].apply(lambda x: isinstance(x, str) and x.strip() != "")] + df = pd.concat([df[: SAMPLE_ROWS // 2], df2], ignore_index=True) + except Exception: + pass # keep original df + + # 6. Build Ludwig config and train + ludwig_cfg = build_ludwig_config(cfg) + if not ludwig_cfg["input_features"]: + result["status"] = "error" + result["error"] = "No input features" + return result + + try: + from ludwig.api import LudwigModel + + with tempfile.TemporaryDirectory() as tmpdir: + model = LudwigModel(ludwig_cfg, logging_level=logging.ERROR) + model.train( + dataset=df, + output_directory=tmpdir, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + result["status"] = "pass" + except Exception as e: + result["status"] = "fail" + result["error"] = f"{type(e).__name__}: {str(e)[:200]}" + result["traceback"] = traceback.format_exc()[-600:] + finally: + shutil.rmtree(_img_tmp, ignore_errors=True) + + return result + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--names", nargs="*", default=None) + parser.add_argument("--resume", action="store_true") + args = parser.parse_args() + + all_names = sorted( + f.replace(".yaml", "") for f in os.listdir(CONFIGS_DIR) if f.endswith(".yaml") and not f.startswith("__") + ) + names = [n for n in args.names if n in set(all_names)] if args.names else all_names + + existing: dict[str, dict] = {} + if os.path.exists(RESULTS_PATH): + try: + for r in json.load(open(RESULTS_PATH)): + existing[r["name"]] = r + except Exception: + pass + + results: list[dict] = [] + total = len(names) + for i, name in enumerate(names): + if args.resume and name in existing and existing[name]["status"] == "pass": + results.append(existing[name]) + print(f"[{i + 1}/{total}] SKIP {name} (already passed)") + continue + + cfg = {} + try: + cfg = load_dataset_config(name) + except Exception: + pass + hf_id = cfg.get("huggingface_dataset_id", "") + hf_sub = cfg.get("huggingface_subsample") + + print(f"[{i + 1}/{total}] {name}...", end=" ", flush=True) + r = run_smoke_test(name) + results.append(r) + + sym = {"pass": "✓", "fail": "✗", "error": "E", "skipped": "—"}.get(r["status"], "?") + print(f"{sym} [{r['status']}]") + if r.get("error"): + print(f" {r['error'][:120]}") + + # Wipe HF cache for this dataset immediately + if hf_id: + wipe_hf_cache_for(hf_id, hf_sub) + + # Encourage Python to release memory from the completed test + import gc + + gc.collect() + + # Save results after every dataset + all_results = {**existing, **{rr["name"]: rr for rr in results}} + with open(RESULTS_PATH, "w") as f: + json.dump(list(all_results.values()), f, indent=2) + + status_counts = Counter(r["status"] for r in results) + print("\n=== Smoke Test Summary ===") + for s, n in status_counts.most_common(): + print(f" {s}: {n}") + print(f" Total: {len(results)}") + + print("\nFAILED:") + for r in results: + if r["status"] == "fail": + print(f" {r['name']}: {(r.get('error') or '')[:100]}") + + print("\nERRORS:") + for r in results: + if r["status"] == "error": + print(f" {r['name']}: {(r.get('error') or '')[:100]}") + + +if __name__ == "__main__": + main() diff --git a/scripts/hf_dataset_candidates.json b/scripts/hf_dataset_candidates.json new file mode 100644 index 0000000..c9bb17e --- /dev/null +++ b/scripts/hf_dataset_candidates.json @@ -0,0 +1,3759 @@ +[ + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "mnli", + "name": "mnli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Multi-Genre Natural Language Inference; premise + hypothesis -> entailment/neutral/contradiction" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "qqp", + "name": "qqp", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Quora Question Pairs; whether two questions are semantically equivalent" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "qnli", + "name": "qnli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Question-answering NLI; whether context sentence contains answer to question" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "cola", + "name": "cola", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Corpus of Linguistic Acceptability; grammatically acceptable or not" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "rte", + "name": "rte", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Recognizing Textual Entailment; entailment vs not-entailment" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "mrpc", + "name": "mrpc", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Microsoft Research Paraphrase Corpus; paraphrase detection" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "stsb", + "name": "stsb", + "task": "regression", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Semantic Textual Similarity Benchmark; similarity score 0-5" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "wnli", + "name": "wnli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Winograd NLI; pronoun reference coreference classification" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "boolq", + "name": "boolq", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Boolean Questions; reading comprehension yes/no questions from Google" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "cb", + "name": "commitment_bank", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "CommitmentBank; textual entailment with 3-way classification" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "copa", + "name": "copa", + "task": "text_classification", + "input_features": [ + "text", + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Choice Of Plausible Alternatives; causal commonsense reasoning" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "multirc", + "name": "multirc", + "task": "text_classification", + "input_features": [ + "text", + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Multi-Sentence Reading Comprehension; answer validation" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "rte", + "name": "superglue_rte", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "SuperGLUE version of Recognizing Textual Entailment" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "wic", + "name": "wic", + "task": "text_classification", + "input_features": [ + "text", + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Word-in-Context; word sense disambiguation as binary classification" + }, + { + "hf_id": "aps/super_glue", + "hf_subsample": "wsc.fixed", + "name": "winograd_schema", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Winograd Schema Challenge; pronoun coreference resolution" + }, + { + "hf_id": "stanfordnlp/imdb", + "hf_subsample": null, + "name": "imdb_sentiment", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "IMDB movie review sentiment; positive/negative binary classification" + }, + { + "hf_id": "dair-ai/emotion", + "hf_subsample": null, + "name": "emotion", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Twitter emotion classification; 6 classes: joy, sadness, anger, fear, surprise, love" + }, + { + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "sentiment", + "name": "tweeteval_sentiment", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "TweetEval sentiment; positive/negative/neutral tweet classification" + }, + { + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "hate", + "name": "tweeteval_hate", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "TweetEval hate speech detection" + }, + { + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "offensive", + "name": "tweeteval_offensive", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "TweetEval offensive language detection" + }, + { + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "irony", + "name": "tweeteval_irony", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "TweetEval irony detection" + }, + { + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "emotion", + "name": "tweeteval_emotion", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "TweetEval emotion classification; 4 classes" + }, + { + "hf_id": "takala/financial_phrasebank", + "hf_subsample": "sentences_allagree", + "name": "financial_phrasebank", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Financial news sentiment; positive/negative/neutral (all-annotator-agreement subset)" + }, + { + "hf_id": "google-research-datasets/go_emotions", + "hf_subsample": "simplified", + "name": "go_emotions_multiclass", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "GoEmotions multi-label 27 emotion categories from Reddit comments" + }, + { + "hf_id": "fancyzhx/ag_news", + "hf_subsample": null, + "name": "ag_news_hf", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "AG News 4-class topic classification (HF version)" + }, + { + "hf_id": "fancyzhx/dbpedia_14", + "hf_subsample": null, + "name": "dbpedia_14", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "DBpedia 14 ontology text classification; 14 categories" + }, + { + "hf_id": "Yelp/yelp_review_full", + "hf_subsample": null, + "name": "yelp_review_full", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Yelp review 5-star rating classification" + }, + { + "hf_id": "fancyzhx/amazon_polarity", + "hf_subsample": null, + "name": "amazon_polarity", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Amazon product review polarity; positive/negative" + }, + { + "hf_id": "cornell-movie-review-data/rotten_tomatoes", + "hf_subsample": null, + "name": "rotten_tomatoes", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Rotten Tomatoes movie review sentiment; positive/negative" + }, + { + "hf_id": "stanfordnlp/snli", + "hf_subsample": null, + "name": "snli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Stanford NLI; premise/hypothesis pairs -> entailment/neutral/contradiction" + }, + { + "hf_id": "nyu-mll/multi_nli", + "hf_subsample": null, + "name": "multi_nli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Multi-Genre NLI; 10 diverse genres, 3-way NLI" + }, + { + "hf_id": "google-research-datasets/paws", + "hf_subsample": "labeled_final", + "name": "paws", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Paraphrase Adversaries from Word Scrambling; challenging paraphrase detection" + }, + { + "hf_id": "facebook/anli", + "hf_subsample": null, + "name": "anli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Adversarial NLI; iteratively adversarial NLI benchmark" + }, + { + "hf_id": "CogComp/trec", + "hf_subsample": null, + "name": "trec", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "TREC question type classification; 6 coarse classes" + }, + { + "hf_id": "stanfordnlp/sst2", + "hf_subsample": null, + "name": "sst2_hf", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Stanford Sentiment Treebank 2-class (HF canonical version)" + }, + { + "hf_id": "google/boolq", + "hf_subsample": null, + "name": "boolq_standalone", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "BoolQ standalone; naturally occurring yes/no questions with passage" + }, + { + "hf_id": "facebook/belebele", + "hf_subsample": "eng_Latn", + "name": "belebele", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Belebele multilingual reading comprehension; multiple-choice" + }, + { + "hf_id": "PKU-Alignment/BeaverTails", + "hf_subsample": null, + "name": "beavertails", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Safety classification of model responses; harmful vs safe" + }, + { + "hf_id": "Rowan/hellaswag", + "hf_subsample": null, + "name": "hellaswag", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "HellaSwag; commonsense NLI, pick correct continuation from 4 choices" + }, + { + "hf_id": "allenai/winogrande", + "hf_subsample": "winogrande_xl", + "name": "winogrande", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "WinoGrande; large-scale Winograd schema challenge" + }, + { + "hf_id": "ybisk/piqa", + "hf_subsample": null, + "name": "piqa", + "task": "text_classification", + "input_features": [ + "text", + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Physical Intuition QA; goal + two solutions -> pick correct one" + }, + { + "hf_id": "tau/commonsense_qa", + "hf_subsample": null, + "name": "commonsense_qa", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "CommonsenseQA; 5-choice commonsense reasoning questions" + }, + { + "hf_id": "allenai/ai2_arc", + "hf_subsample": "ARC-Easy", + "name": "arc_easy", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "AI2 Reasoning Challenge Easy subset; multiple-choice science questions" + }, + { + "hf_id": "allenai/ai2_arc", + "hf_subsample": "ARC-Challenge", + "name": "arc_challenge", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "AI2 Reasoning Challenge hard subset; grade-school science questions" + }, + { + "hf_id": "allenai/openbookqa", + "hf_subsample": "main", + "name": "openbookqa", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "OpenBookQA; elementary science questions with open-book style" + }, + { + "hf_id": "allenai/sciq", + "hf_subsample": null, + "name": "sciq", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "SciQ; crowdsourced science exam questions with 4 choices" + }, + { + "hf_id": "rajpurkar/squad", + "hf_subsample": null, + "name": "squad", + "task": "extractive_qa", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Stanford QA Dataset v1.1; extractive reading comprehension" + }, + { + "hf_id": "rajpurkar/squad_v2", + "hf_subsample": null, + "name": "squad_v2", + "task": "extractive_qa", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "SQuAD v2; includes unanswerable questions" + }, + { + "hf_id": "mandarjoshi/trivia_qa", + "hf_subsample": "rc", + "name": "trivia_qa", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "TriviaQA; reading comprehension with trivia question-answer pairs" + }, + { + "hf_id": "google-research-datasets/natural_questions", + "hf_subsample": null, + "name": "natural_questions", + "task": "extractive_qa", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Google Natural Questions; real user queries from Google Search" + }, + { + "hf_id": "hotpotqa/hotpot_qa", + "hf_subsample": "distractor", + "name": "hotpot_qa", + "task": "extractive_qa", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "HotpotQA; multi-hop reasoning QA with supporting facts" + }, + { + "hf_id": "qiaojin/PubMedQA", + "hf_subsample": "pqa_labeled", + "name": "pubmed_qa", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "PubMedQA; biomedical research QA; yes/no/maybe" + }, + { + "hf_id": "openlifescienceai/medmcqa", + "hf_subsample": null, + "name": "medmcqa", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Medical entrance exam QA; 4-choice medical questions" + }, + { + "hf_id": "truthfulqa/truthful_qa", + "hf_subsample": "multiple_choice", + "name": "truthful_qa", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "TruthfulQA multiple choice; tests for truthful model answers" + }, + { + "hf_id": "cais/mmlu", + "hf_subsample": "all", + "name": "mmlu", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "MMLU; 57-subject multiple choice; question -> A/B/C/D" + }, + { + "hf_id": "TIGER-Lab/MMLU-Pro", + "hf_subsample": null, + "name": "mmlu_pro", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "MMLU-Pro; harder 10-choice version of MMLU" + }, + { + "hf_id": "eriktks/conll2003", + "hf_subsample": null, + "name": "conll2003", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "CoNLL-2003 NER; English newspaper NER with PER/ORG/LOC/MISC" + }, + { + "hf_id": "unimelb-nlp/wikiann", + "hf_subsample": "en", + "name": "wikiann_en", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "WikiANN English NER; cross-lingual NER from Wikipedia" + }, + { + "hf_id": "leondz/wnut_17", + "hf_subsample": null, + "name": "wnut17", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "WNUT-17 emerging entities NER; Twitter/social media domain" + }, + { + "hf_id": "ncbi/ncbi_disease", + "hf_subsample": null, + "name": "ncbi_disease", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "NCBI Disease NER; biomedical disease entity recognition" + }, + { + "hf_id": "Babelscape/wikineural", + "hf_subsample": "en", + "name": "wikineural", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "WikiNEuRal multilingual NER; automatically annotated Wikipedia" + }, + { + "hf_id": "Babelscape/multinerd", + "hf_subsample": null, + "name": "multinerd", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "MultiNERD; 10 languages, 15 NE types including fine-grained" + }, + { + "hf_id": "DFKI-SLT/few-nerd", + "hf_subsample": "supervised", + "name": "few_nerd", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "FewNERD; 66 fine-grained entity types NER dataset" + }, + { + "hf_id": "masakhane/masakhaner2", + "hf_subsample": "eng", + "name": "masakhaner2", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "MasakhaNER 2.0; African languages NER" + }, + { + "hf_id": "universal-dependencies/universal_dependencies", + "hf_subsample": "en_ewt", + "name": "universal_dependencies", + "task": "sequence_labeling", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "Universal Dependencies POS tagging; English Web Treebank" + }, + { + "hf_id": "ai4privacy/pii-masking-300k", + "hf_subsample": null, + "name": "pii_masking", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "PII Masking; 300k examples for personally identifiable information NER" + }, + { + "hf_id": "abisee/cnn_dailymail", + "hf_subsample": "3.0.0", + "name": "cnn_dailymail", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "CNN/DailyMail news summarization; article -> highlights" + }, + { + "hf_id": "EdinburghNLP/xsum", + "hf_subsample": null, + "name": "xsum", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "XSum; extreme summarization from BBC articles; single-sentence summaries" + }, + { + "hf_id": "knkarthick/dialogsum", + "hf_subsample": null, + "name": "dialogsum", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "DialogSum; dialogue summarization dataset" + }, + { + "hf_id": "knkarthick/samsum", + "hf_subsample": null, + "name": "samsum", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "SAMSum; messenger-style conversation summarization" + }, + { + "hf_id": "FiscalNote/billsum", + "hf_subsample": null, + "name": "billsum", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "BillSum; US congressional and California bill summarization" + }, + { + "hf_id": "ccdv/arxiv-summarization", + "hf_subsample": null, + "name": "arxiv_summarization", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "ArXiv paper abstract summarization; long scientific documents" + }, + { + "hf_id": "ccdv/pubmed-summarization", + "hf_subsample": null, + "name": "pubmed_summarization", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "PubMed biomedical article summarization" + }, + { + "hf_id": "alexfabbri/multi_news", + "hf_subsample": null, + "name": "multi_news", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Multi-News; multi-document news summarization" + }, + { + "hf_id": "csebuetnlp/xlsum", + "hf_subsample": "english", + "name": "xlsum", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "XL-Sum; BBC multilingual summarization, 44 languages" + }, + { + "hf_id": "NortheasternUniversity/big_patent", + "hf_subsample": "a", + "name": "big_patent", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "BigPatent; patent document summarization" + }, + { + "hf_id": "ccdv/govreport-summarization", + "hf_subsample": null, + "name": "govreport_summarization", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "GovReport; long US government report summarization" + }, + { + "hf_id": "Helsinki-NLP/opus-100", + "hf_subsample": "en-de", + "name": "opus100_en_de", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "OPUS-100 English-German translation" + }, + { + "hf_id": "Helsinki-NLP/opus-100", + "hf_subsample": "en-fr", + "name": "opus100_en_fr", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "OPUS-100 English-French translation" + }, + { + "hf_id": "Helsinki-NLP/opus-100", + "hf_subsample": "en-es", + "name": "opus100_en_es", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "OPUS-100 English-Spanish translation" + }, + { + "hf_id": "wmt/wmt14", + "hf_subsample": "de-en", + "name": "wmt14_de_en", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "WMT14 German-English translation benchmark" + }, + { + "hf_id": "wmt/wmt16", + "hf_subsample": "de-en", + "name": "wmt16_de_en", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "WMT16 German-English translation" + }, + { + "hf_id": "wmt/wmt19", + "hf_subsample": "de-en", + "name": "wmt19_de_en", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "WMT19 German-English translation" + }, + { + "hf_id": "Helsinki-NLP/opus_books", + "hf_subsample": "en-fr", + "name": "opus_books_en_fr", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "OPUS Books English-French literary translation" + }, + { + "hf_id": "facebook/flores", + "hf_subsample": "eng_Latn-deu_Latn", + "name": "flores_en_de", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "FLORES-200 English-German; low-resource translation benchmark" + }, + { + "hf_id": "IWSLT/iwslt2017", + "hf_subsample": "iwslt2017-en-de", + "name": "iwslt2017_en_de", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "IWSLT 2017 English-German spoken language translation" + }, + { + "hf_id": "Helsinki-NLP/europarl", + "hf_subsample": "en-de", + "name": "europarl_en_de", + "task": "translation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Europarl parallel corpus English-German (European Parliament proceedings)" + }, + { + "hf_id": "uoft-cs/cifar10", + "hf_subsample": null, + "name": "cifar10", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "CIFAR-10; 10-class 32x32 image classification" + }, + { + "hf_id": "uoft-cs/cifar100", + "hf_subsample": null, + "name": "cifar100", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "CIFAR-100; 100-class 32x32 image classification" + }, + { + "hf_id": "ylecun/mnist", + "hf_subsample": null, + "name": "mnist", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "MNIST handwritten digit recognition; 10 classes" + }, + { + "hf_id": "zalando-datasets/fashion_mnist", + "hf_subsample": null, + "name": "fashion_mnist", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Fashion MNIST; 10-class clothing item classification" + }, + { + "hf_id": "ethz/food101", + "hf_subsample": null, + "name": "food101", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Food-101; 101-class food image classification" + }, + { + "hf_id": "tanganke/stanford_cars", + "hf_subsample": null, + "name": "stanford_cars", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Stanford Cars; 196-class fine-grained car make/model/year classification" + }, + { + "hf_id": "timm/oxford-iiit-pet", + "hf_subsample": null, + "name": "oxford_pets", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Oxford-IIIT Pet; 37 pet breed classification" + }, + { + "hf_id": "tanganke/eurosat", + "hf_subsample": null, + "name": "eurosat", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "EuroSAT; land use/cover classification from satellite imagery; 10 classes" + }, + { + "hf_id": "tanganke/gtsrb", + "hf_subsample": null, + "name": "gtsrb", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "GTSRB; German Traffic Sign Recognition Benchmark; 43 classes" + }, + { + "hf_id": "ufldl-stanford/svhn", + "hf_subsample": "cropped_digits", + "name": "svhn", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "SVHN; Street View House Numbers digit classification" + }, + { + "hf_id": "zh-plus/tiny-imagenet", + "hf_subsample": null, + "name": "tiny_imagenet", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Tiny ImageNet; 200-class 64x64 subset of ImageNet" + }, + { + "hf_id": "tanganke/sun397", + "hf_subsample": null, + "name": "sun397", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "SUN397; scene understanding; 397 scene categories" + }, + { + "hf_id": "AI-Lab-Makerere/beans", + "hf_subsample": null, + "name": "beans", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Beans leaf disease classification; 3 classes: angular leaf spot, bean rust, healthy" + }, + { + "hf_id": "huggan/wikiart", + "hf_subsample": null, + "name": "wikiart", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "WikiArt; artwork style classification across 27 art styles" + }, + { + "hf_id": "ILSVRC/imagenet-1k", + "hf_subsample": null, + "name": "imagenet1k", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "ImageNet-1K; 1000-class image classification (requires HF agreement)" + }, + { + "hf_id": "Voxel51/mvtec-ad", + "hf_subsample": null, + "name": "mvtec_ad", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "MVTec Anomaly Detection; industrial surface defect detection" + }, + { + "hf_id": "openslr/librispeech_asr", + "hf_subsample": "clean", + "name": "librispeech", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "LibriSpeech; English speech recognition from audiobooks; clean 100h split" + }, + { + "hf_id": "google/fleurs", + "hf_subsample": "en_us", + "name": "fleurs_en", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "FLEURS; Few-shot Learning Evaluation of Universal Representations of Speech" + }, + { + "hf_id": "facebook/multilingual_librispeech", + "hf_subsample": "english", + "name": "multilingual_librispeech", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Multilingual LibriSpeech; 8-language ASR dataset" + }, + { + "hf_id": "facebook/voxpopuli", + "hf_subsample": "en", + "name": "voxpopuli", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "VoxPopuli; European Parliament speech ASR; 23 languages" + }, + { + "hf_id": "speechcolab/gigaspeech", + "hf_subsample": "xs", + "name": "gigaspeech", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "GigaSpeech; 10K-hour English ASR; xs=10h subset" + }, + { + "hf_id": "agkphysics/AudioSet", + "hf_subsample": "unbalanced_train", + "name": "audioset", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "AudioSet; 527-class multi-label audio event classification" + }, + { + "hf_id": "Fhrozen/FSD50k", + "hf_subsample": null, + "name": "fsd50k", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "FSD50K; Freesound Dataset 50k; 200-class audio event classification" + }, + { + "hf_id": "MLCommons/peoples_speech", + "hf_subsample": "clean", + "name": "peoples_speech", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "People's Speech; 30K-hour diverse English ASR" + }, + { + "hf_id": "ProgramComputer/voxceleb", + "hf_subsample": null, + "name": "voxceleb", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "VoxCeleb; speaker identification from celebrity interviews" + }, + { + "hf_id": "DBD-research-group/BirdSet", + "hf_subsample": "HSN", + "name": "birdset", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "BirdSet; bird species classification from audio recordings" + }, + { + "hf_id": "Loie/VGGSound", + "hf_subsample": null, + "name": "vggsound", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "VGGSound; 309-class audio-visual sound classification from YouTube" + }, + { + "hf_id": "MMMU/MMMU", + "hf_subsample": "Accounting", + "name": "mmmu", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": true, + "notes": "MMMU; Massive Multi-discipline Multimodal Understanding; college-level VQA" + }, + { + "hf_id": "derek-thomas/ScienceQA", + "hf_subsample": null, + "name": "scienceqa", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "ScienceQA; multimodal science questions with images and context" + }, + { + "hf_id": "flaviagiammarino/vqa-rad", + "hf_subsample": null, + "name": "vqa_rad", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "VQA-RAD; radiology visual question answering" + }, + { + "hf_id": "AI4Math/MathVista", + "hf_subsample": null, + "name": "mathvista", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": true, + "notes": "MathVista; mathematical reasoning in visual context" + }, + { + "hf_id": "keremberke/satellite-building-segmentation", + "hf_subsample": null, + "name": "satellite_building_segmentation", + "task": "image_segmentation", + "input_features": [ + "image" + ], + "output_features": [ + "image" + ], + "needs_custom_loader": true, + "notes": "Satellite building semantic segmentation" + }, + { + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "clf_cat", + "name": "tabular_benchmark_clf", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Tabular benchmark categorical classification tasks" + }, + { + "hf_id": "mstz/adult", + "hf_subsample": null, + "name": "adult_income_hf", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Adult/Census Income dataset for income >50K classification" + }, + { + "hf_id": "scikit-learn/diabetes", + "hf_subsample": null, + "name": "diabetes_regression", + "task": "regression", + "input_features": [ + "number" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Diabetes progression regression (scikit-learn version)" + }, + { + "hf_id": "mstz/heart_failure", + "hf_subsample": null, + "name": "heart_failure", + "task": "tabular_classification", + "input_features": [ + "number", + "binary" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Heart failure clinical records; death event prediction" + }, + { + "hf_id": "mstz/breast_cancer", + "hf_subsample": null, + "name": "breast_cancer", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Breast Cancer Wisconsin; malignant vs benign tumor classification" + }, + { + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "reg_num", + "name": "tabular_benchmark_reg", + "task": "regression", + "input_features": [ + "number" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Tabular benchmark numerical regression tasks" + }, + { + "hf_id": "scikit-learn/wine-quality", + "hf_subsample": null, + "name": "wine_quality", + "task": "regression", + "input_features": [ + "number" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Wine quality prediction; physicochemical properties -> quality score" + }, + { + "hf_id": "scikit-learn/boston", + "hf_subsample": null, + "name": "boston_housing", + "task": "regression", + "input_features": [ + "number" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Boston Housing regression; predict median home value" + }, + { + "hf_id": "poloclub/diffusiondb", + "hf_subsample": "2m_random_1k", + "name": "diffusiondb", + "task": "text_classification", + "input_features": [ + "text", + "image" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "DiffusionDB; Stable Diffusion prompts and generated images with metadata" + }, + { + "hf_id": "mstz/bank_marketing", + "hf_subsample": null, + "name": "bank_marketing", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Bank marketing campaign subscription prediction" + }, + { + "hf_id": "mstz/covertype", + "hf_subsample": null, + "name": "covertype", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Forest cover type prediction from cartographic features; 7 classes" + }, + { + "hf_id": "mstz/magic_telescope", + "hf_subsample": null, + "name": "magic_telescope", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "MAGIC telescope; gamma particle vs background classification" + }, + { + "hf_id": "mstz/electricity", + "hf_subsample": null, + "name": "electricity_tabular", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Electricity market price direction classification" + }, + { + "hf_id": "mstz/jannis", + "hf_subsample": null, + "name": "jannis", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Jannis tabular benchmark dataset; 4-class classification" + }, + { + "hf_id": "mstz/higgs", + "hf_subsample": null, + "name": "higgs_tabular", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Higgs boson detection; physics particle classification" + }, + { + "hf_id": "mteb/amazon_reviews_multi", + "hf_subsample": "en", + "name": "amazon_reviews_multi", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Amazon Reviews Multilingual; 1-5 star rating prediction" + }, + { + "hf_id": "mteb/tweet_sentiment_extraction", + "hf_subsample": null, + "name": "tweet_sentiment_extraction", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Tweet sentiment extraction; positive/negative/neutral" + }, + { + "hf_id": "climatebert/climate_sentiment", + "hf_subsample": null, + "name": "climate_sentiment", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "ClimateBERT climate-related text sentiment analysis" + }, + { + "hf_id": "sst/sst", + "hf_subsample": null, + "name": "sst_spans", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "SST original with fine-grained phrase-level sentiment" + }, + { + "hf_id": "mteb/stsbenchmark-sts", + "hf_subsample": null, + "name": "sts_benchmark", + "task": "regression", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "STS Benchmark; sentence pair semantic similarity scoring" + }, + { + "hf_id": "bigbio/bc5cdr", + "hf_subsample": "bc5cdr_bigbio_kb", + "name": "bc5cdr", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "BC5CDR; chemical and disease NER from PubMed abstracts" + }, + { + "hf_id": "coastalcph/lex_glue", + "hf_subsample": "ecthr_a", + "name": "lex_glue_ecthr", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "LexGLUE ECtHR; European Court of Human Rights multi-label article prediction" + }, + { + "hf_id": "coastalcph/lex_glue", + "hf_subsample": "eurlex", + "name": "lex_glue_eurlex", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "LexGLUE EurLex; EU legislation multi-label classification" + }, + { + "hf_id": "nguha/legalbench", + "hf_subsample": "contract_nli_confidentiality_of_agreement", + "name": "legalbench", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "LegalBench; legal reasoning tasks including NLI on contracts" + }, + { + "hf_id": "EMBO/SourceData", + "hf_subsample": "NER", + "name": "sourcedata_ner", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "SourceData NER; biomedical figure captions entity recognition" + }, + { + "hf_id": "google-research-datasets/mbpp", + "hf_subsample": null, + "name": "mbpp", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "MBPP; Python programming problems and solutions" + }, + { + "hf_id": "openai/openai_humaneval", + "hf_subsample": null, + "name": "humaneval", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "HumanEval; Python function synthesis from docstrings" + }, + { + "hf_id": "google/code_x_glue_ct_code_to_text", + "hf_subsample": "python", + "name": "codexglue_code_to_text", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "CodeXGLUE code documentation generation; code -> docstring" + }, + { + "hf_id": "code-search-net/code_search_net", + "hf_subsample": "python", + "name": "code_search_net", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "CodeSearchNet; code-natural language relevance matching" + }, + { + "hf_id": "liar/liar", + "hf_subsample": null, + "name": "liar", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "LIAR; political statement veracity; 6-class fake news detection" + }, + { + "hf_id": "GonzaloA/fake_news", + "hf_subsample": null, + "name": "fake_news_detection", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Fake news detection; real vs fake news articles" + }, + { + "hf_id": "climate_fever/climate_fever", + "hf_subsample": null, + "name": "climate_fever", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "ClimateFEVER; climate claim fact-checking with evidence" + }, + { + "hf_id": "HateSpeechMLResearch/hate_speech_18", + "hf_subsample": null, + "name": "hate_speech18", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Hate Speech 18; social media hate speech classification" + }, + { + "hf_id": "ucberkeley-dlab/measuring-hate-speech", + "hf_subsample": null, + "name": "measuring_hate_speech", + "task": "regression", + "input_features": [ + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Measuring Hate Speech; continuous hate speech score regression" + }, + { + "hf_id": "google/civil_comments", + "hf_subsample": null, + "name": "civil_comments", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Civil Comments toxicity classification; multi-attribute toxicity labels" + }, + { + "hf_id": "Babelscape/SREDFM", + "hf_subsample": "en", + "name": "sredfm", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "SREDFM; relation classification from sentences" + }, + { + "hf_id": "docred/docred", + "hf_subsample": null, + "name": "docred", + "task": "relation_extraction", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "DocRED; document-level relation extraction from Wikipedia" + }, + { + "hf_id": "conll2012_ontonotesv5/english_v4", + "hf_subsample": null, + "name": "ontonotes5", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "OntoNotes 5.0; NER, POS, coreference, SRL annotations" + }, + { + "hf_id": "papluca/language-identification", + "hf_subsample": null, + "name": "language_identification", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Language identification; 20 languages from Twitter data" + }, + { + "hf_id": "cis-lmu/glotlid", + "hf_subsample": null, + "name": "glotlid", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "GlotLID; language identification for 1600+ languages" + }, + { + "hf_id": "blog-authorship-corpus/blog-authorship-corpus", + "hf_subsample": null, + "name": "blog_authorship", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Blog Authorship Corpus; age/gender/topic classification from blogs" + }, + { + "hf_id": "openai/gsm8k", + "hf_subsample": "main", + "name": "gsm8k", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "GSM8K; grade school math word problems with step-by-step solutions" + }, + { + "hf_id": "EleutherAI/hendrycks_math", + "hf_subsample": "algebra", + "name": "hendrycks_math", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "MATH dataset; competition math problems across 7 subjects" + }, + { + "hf_id": "daily_dialog/daily_dialog", + "hf_subsample": null, + "name": "daily_dialog", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "DailyDialog; dialog act and emotion classification in conversations" + }, + { + "hf_id": "pfb30/multi_woz_v22", + "hf_subsample": null, + "name": "multiwoz", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "MultiWOZ 2.2; task-oriented dialogue state tracking" + }, + { + "hf_id": "mteb/msmarco", + "hf_subsample": null, + "name": "msmarco_passage", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "MS MARCO passage retrieval; query-passage relevance scoring" + }, + { + "hf_id": "sentence-transformers/natural-questions", + "hf_subsample": null, + "name": "natural_questions_hard_negatives", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Natural Questions hard negatives for retrieval/ranking" + }, + { + "hf_id": "uclanlp/wino_bias", + "hf_subsample": "type1_anti", + "name": "winobias", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "WinoBias; gender coreference bias in occupation contexts" + }, + { + "hf_id": "klue/klue", + "hf_subsample": "ynat", + "name": "klue_topic", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "KLUE YNAT; Korean news topic classification; 7 categories" + }, + { + "hf_id": "klue/klue", + "hf_subsample": "sts", + "name": "klue_sts", + "task": "regression", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "KLUE STS; Korean sentence semantic similarity" + }, + { + "hf_id": "ai4bharat/indic_glue", + "hf_subsample": "wnli-en", + "name": "indic_glue", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "IndicGLUE; Indian language NLP benchmark" + }, + { + "hf_id": "google/xtreme", + "hf_subsample": "XNLI", + "name": "xnli", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "XNLI; Cross-lingual NLI in 15 languages" + }, + { + "hf_id": "Muennighoff/multi_eurlex", + "hf_subsample": "en", + "name": "multi_eurlex", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "MultiEURLEX; EU legislation multi-label classification in 23 languages" + }, + { + "hf_id": "nyu-mll/blimp", + "hf_subsample": "anaphor_agreement", + "name": "blimp", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "BLiMP; linguistic minimal pairs for grammaticality judgment" + }, + { + "hf_id": "ceval/ceval-exam", + "hf_subsample": "computer_science", + "name": "ceval", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "C-EVAL; Chinese multiple choice exam benchmark" + }, + { + "hf_id": "openai/MMMLU", + "hf_subsample": "EN_US", + "name": "mmmlu", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "Multilingual MMLU; MMLU translated into 14 languages" + }, + { + "hf_id": "Idavidrein/gpqa", + "hf_subsample": "gpqa_main", + "name": "gpqa", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "GPQA; Graduate-level QA; PhD-level science questions" + }, + { + "hf_id": "coastalcph/lex_glue", + "hf_subsample": "scotus", + "name": "scotus_classification", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "LexGLUE SCOTUS; US Supreme Court opinion issue area classification" + }, + { + "hf_id": "allenai/scitail", + "hf_subsample": "dgem_format", + "name": "scitail", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "SciTail; science textual entailment from multiple-choice questions" + }, + { + "hf_id": "allenai/qasper", + "hf_subsample": null, + "name": "qasper", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "QASPER; question answering on scientific research papers" + }, + { + "hf_id": "HumynLabs/e-commerce-customersupport-english-audio", + "hf_subsample": null, + "name": "customer_support_audio", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "E-commerce customer support audio classification; intent detection" + }, + { + "hf_id": "Salesforce/GiftEvalPretrain", + "hf_subsample": null, + "name": "gift_eval_pretrain", + "task": "time_series_forecasting", + "input_features": [ + "sequence" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": true, + "notes": "GIFT-Eval pretraining data for time-series forecasting" + }, + { + "hf_id": "autogluon/chronos_datasets", + "hf_subsample": "electricity_hourly", + "name": "chronos_electricity", + "task": "time_series_forecasting", + "input_features": [ + "sequence" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": true, + "notes": "Chronos electricity hourly time-series forecasting" + }, + { + "hf_id": "DeepChem/tox21", + "hf_subsample": null, + "name": "tox21", + "task": "tabular_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "Tox21; toxicity prediction from SMILES molecular strings; 12 targets" + }, + { + "hf_id": "DeepChem/bbbp", + "hf_subsample": null, + "name": "bbbp", + "task": "tabular_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "BBBP; blood-brain barrier permeability from SMILES" + }, + { + "hf_id": "DeepChem/esol", + "hf_subsample": null, + "name": "esol", + "task": "regression", + "input_features": [ + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "ESOL; aqueous solubility regression from molecular SMILES" + }, + { + "hf_id": "DeepChem/lipo", + "hf_subsample": null, + "name": "lipophilicity", + "task": "regression", + "input_features": [ + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Lipophilicity; octanol/water distribution coefficient from SMILES" + }, + { + "hf_id": "jigsaw-toxic-comment-classification-challenge/jigsaw_toxic_comments", + "hf_subsample": null, + "name": "jigsaw_toxic_multi", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "Jigsaw Toxic Comment Classification; 6 toxicity attribute multi-label" + }, + { + "hf_id": "sem_eval_2018_task_1/sem_eval_2018_task_1", + "hf_subsample": "subtask5.english", + "name": "semeval2018_emotion", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "SemEval-2018 Task 1 multi-label emotion classification from tweets" + }, + { + "hf_id": "rceborg/reuters-21578", + "hf_subsample": null, + "name": "reuters21578", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "set" + ], + "needs_custom_loader": false, + "notes": "Reuters-21578; newswire multi-label topic classification" + }, + { + "hf_id": "rvl-cdip/rvl_cdip", + "hf_subsample": null, + "name": "rvl_cdip", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "RVL-CDIP; document image classification; 16 classes" + }, + { + "hf_id": "nateraw/rendered-sst2", + "hf_subsample": null, + "name": "rendered_sst2", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Rendered SST2; sentiment classification from rendered text images" + }, + { + "hf_id": "katanaml-org/invoices-donut-data-v1", + "hf_subsample": null, + "name": "invoice_data", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": true, + "notes": "Invoice document understanding; key-value extraction from scanned invoices" + }, + { + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "clf_num", + "name": "tabular_benchmark_num_clf", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Tabular benchmark numerical binary classification tasks" + }, + { + "hf_id": "scikit-learn/california-housing", + "hf_subsample": null, + "name": "california_housing_sklearn", + "task": "regression", + "input_features": [ + "number" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "California housing regression (scikit-learn version)" + }, + { + "hf_id": "imodels/diabetes-readmission", + "hf_subsample": null, + "name": "diabetes_readmission", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Diabetes hospital readmission prediction" + }, + { + "hf_id": "imodels/compas-recidivism", + "hf_subsample": null, + "name": "compas_recidivism", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "COMPAS recidivism risk prediction; criminal justice fairness benchmark" + }, + { + "hf_id": "imodels/credit-card", + "hf_subsample": null, + "name": "credit_card_default", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Credit card default prediction from payment history" + }, + { + "hf_id": "lukaemon/bbh", + "hf_subsample": "boolean_expressions", + "name": "bbh", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "BIG-Bench Hard; challenging reasoning tasks for LLMs" + }, + { + "hf_id": "lighteval/winogender_schemas", + "hf_subsample": null, + "name": "winogender", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "WinoGender; gender bias in coreference resolution" + }, + { + "hf_id": "keirp/social_i_qa", + "hf_subsample": null, + "name": "social_iqa", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": true, + "notes": "Social IQa; social interaction commonsense reasoning; 3-choice" + }, + { + "hf_id": "HuggingFaceH4/MATH-500", + "hf_subsample": null, + "name": "math500", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "MATH-500 test subset; competition math with step-by-step solutions" + }, + { + "hf_id": "di-mi/conll2012_ontonotesv5", + "hf_subsample": "english_v12", + "name": "conll2012", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "CoNLL-2012 OntoNotes NER; 18 entity types" + }, + { + "hf_id": "HuggingFaceM4/Docmatix", + "hf_subsample": null, + "name": "docmatix", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Docmatix; document visual QA; 1.3M image-QA pairs from PDF documents" + }, + { + "hf_id": "McAuley-Lab/Amazon-Reviews-2023", + "hf_subsample": "raw_review_All_Beauty", + "name": "amazon_reviews_2023", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Amazon Reviews 2023; product review rating prediction; 5 stars" + }, + { + "hf_id": "mteb/amazon_counterfactual_classification", + "hf_subsample": "en", + "name": "amazon_counterfactual", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Amazon counterfactual claim detection" + }, + { + "hf_id": "mteb/banking77", + "hf_subsample": null, + "name": "banking77", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Banking77; 77-class banking customer intent classification" + }, + { + "hf_id": "mteb/emotion", + "hf_subsample": null, + "name": "mteb_emotion", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "MTEB emotion task; 6-class emotion from tweets" + }, + { + "hf_id": "SetFit/sst5", + "hf_subsample": null, + "name": "sst5_setfit", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "SST-5 fine-grained sentiment (SetFit version); 5 classes" + }, + { + "hf_id": "SetFit/enron_spam", + "hf_subsample": null, + "name": "enron_spam", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Enron Spam; email spam/ham classification" + }, + { + "hf_id": "SetFit/CR", + "hf_subsample": null, + "name": "customer_reviews", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Customer Reviews; product review sentiment binary classification" + }, + { + "hf_id": "mteb/clinc150", + "hf_subsample": "small", + "name": "clinc150", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "CLINC150; 150-class out-of-scope intent detection benchmark" + }, + { + "hf_id": "naver-clova-ix/cord-v2", + "hf_subsample": null, + "name": "cord_v2", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": true, + "notes": "CORD v2; receipt document understanding; entity extraction" + }, + { + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "reg_cat", + "name": "tabular_benchmark_cat_reg", + "task": "regression", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Tabular benchmark mixed (categorical+numerical) regression" + }, + { + "hf_id": "openml/miceprotein", + "hf_subsample": null, + "name": "mice_protein", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Mice Protein Expression; protein levels for 8 behavioral classes" + }, + { + "hf_id": "openml/anneal", + "hf_subsample": null, + "name": "anneal", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Steel Annealing; predict steel type from process parameters" + }, + { + "hf_id": "openml/vehicle", + "hf_subsample": null, + "name": "vehicle_silhouettes", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Vehicle Silhouettes; 4-class vehicle type from geometric features" + }, + { + "hf_id": "keremberke/pokemon-classification", + "hf_subsample": null, + "name": "pokemon_classification", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Pokemon species image classification" + }, + { + "hf_id": "dcdmllm/Flowers102", + "hf_subsample": null, + "name": "flowers102", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Oxford Flowers 102; 102-class flower image classification" + }, + { + "hf_id": "Matthijs/snacks", + "hf_subsample": null, + "name": "snacks", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Snacks; 20-class snack food image classification" + }, + { + "hf_id": "mrm8488/chest-xrays-indiana-university", + "hf_subsample": null, + "name": "chest_xray", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Chest X-rays Indiana University; normal vs abnormal classification" + }, + { + "hf_id": "fastai/imagenette", + "hf_subsample": null, + "name": "imagenette", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "ImageNette; 10 easily classified classes from ImageNet" + }, + { + "hf_id": "fcakyon/tomato-disease", + "hf_subsample": null, + "name": "tomato_disease", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Tomato plant disease image classification" + }, + { + "hf_id": "andrewmvd/isic-2019", + "hf_subsample": null, + "name": "isic2019", + "task": "image_classification", + "input_features": [ + "image" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "ISIC 2019; skin lesion dermoscopy image classification; 9 classes" + }, + { + "hf_id": "speech_commands/speech_commands", + "hf_subsample": "v0.02", + "name": "speech_commands", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Google Speech Commands; 35-class keyword spotting from audio" + }, + { + "hf_id": "UrbanSound8K/UrbanSound8K", + "hf_subsample": null, + "name": "urbansound8k", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "UrbanSound8K; 10-class urban sound event classification" + }, + { + "hf_id": "narad/RAVDESS", + "hf_subsample": null, + "name": "ravdess", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "RAVDESS; speech emotion recognition; 8 emotion classes" + }, + { + "hf_id": "renumics/emodb", + "hf_subsample": null, + "name": "emodb", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "EMo-DB; Berlin Database of Emotional Speech; 7 emotion classes" + }, + { + "hf_id": "facebook/multilingual_librispeech", + "hf_subsample": "german", + "name": "mls_german", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Multilingual LibriSpeech German ASR" + }, + { + "hf_id": "mozilla-foundation/common_voice_11_0", + "hf_subsample": "en", + "name": "common_voice_11", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Mozilla Common Voice 11.0 English; crowdsourced speech ASR" + }, + { + "hf_id": "FBK-MT/Speech-MASSIVE", + "hf_subsample": "en-US", + "name": "speech_massive", + "task": "audio_classification", + "input_features": [ + "audio" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Speech MASSIVE; spoken language understanding; intent classification" + }, + { + "hf_id": "CanCLID/zoengjyutgaai", + "hf_subsample": null, + "name": "cantonese_asr", + "task": "automatic_speech_recognition", + "input_features": [ + "audio" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Cantonese speech recognition dataset" + }, + { + "hf_id": "daekeun-ml/naver-news-summarization-ko", + "hf_subsample": null, + "name": "naver_news_summary", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "Naver News Korean summarization dataset" + }, + { + "hf_id": "ccdv/cnn_dailymail", + "hf_subsample": "3.0.0", + "name": "cnn_dailymail_ccdv", + "task": "summarization", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "CNN/DailyMail (ccdv version); article to highlights summarization" + }, + { + "hf_id": "nyu-mll/glue", + "hf_subsample": "ax", + "name": "glue_diagnostic", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "GLUE Diagnostic; adversarial NLI examples testing linguistic phenomena" + }, + { + "hf_id": "copenlu/fever_gold_evidence", + "hf_subsample": null, + "name": "fever_gold", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "FEVER with gold evidence; fact claim + evidence -> SUPPORTS/REFUTES/NEI" + }, + { + "hf_id": "silicone/silicone", + "hf_subsample": "dyda_da", + "name": "dyda_dialog_acts", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "DyDa dialog act classification from DailyDialog" + }, + { + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "ai2d", + "name": "ai2d_diagrams", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "AI2D; science diagram visual QA from The Cauldron" + }, + { + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "textvqa", + "name": "textvqa", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "TextVQA; read text in images to answer questions" + }, + { + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "vqav2", + "name": "vqav2", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "VQA v2; open-ended visual question answering on natural images" + }, + { + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "docvqa", + "name": "docvqa", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "DocVQA; document image visual question answering" + }, + { + "hf_id": "facebook/winoground", + "hf_subsample": null, + "name": "winoground", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Winoground; visual-linguistic compositionality; image-text matching" + }, + { + "hf_id": "amirveyseh/acronym_identification", + "hf_subsample": null, + "name": "acronym_identification", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "Acronym Identification; scientific abbreviation detection and expansion" + }, + { + "hf_id": "unimelb-nlp/wikiann", + "hf_subsample": "de", + "name": "wikiann_de", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "WikiANN German NER; cross-lingual NER" + }, + { + "hf_id": "unimelb-nlp/wikiann", + "hf_subsample": "zh", + "name": "wikiann_zh", + "task": "ner", + "input_features": [ + "sequence" + ], + "output_features": [ + "sequence" + ], + "needs_custom_loader": false, + "notes": "WikiANN Chinese NER; cross-lingual NER" + }, + { + "hf_id": "datamol-io/polaris-admet-benchmark", + "hf_subsample": null, + "name": "admet_benchmark", + "task": "regression", + "input_features": [ + "text" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "ADMET molecular property regression benchmark from SMILES" + }, + { + "hf_id": "mstz/obesity", + "hf_subsample": null, + "name": "obesity_prediction", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Obesity level prediction from lifestyle and biometric features" + }, + { + "hf_id": "mstz/dry_bean", + "hf_subsample": null, + "name": "dry_bean", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Dry Bean Dataset; 7-class legume variety classification from shape features" + }, + { + "hf_id": "mstz/rice", + "hf_subsample": null, + "name": "rice_classification", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Rice image classification (morphological features); 2 varieties" + }, + { + "hf_id": "mstz/water_quality", + "hf_subsample": null, + "name": "water_quality", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Water Quality; potability prediction from chemical properties" + }, + { + "hf_id": "mstz/room_occupancy", + "hf_subsample": null, + "name": "room_occupancy", + "task": "tabular_classification", + "input_features": [ + "number" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Room Occupancy; sensor data -> occupancy binary classification" + }, + { + "hf_id": "mstz/online_shoppers", + "hf_subsample": null, + "name": "online_shoppers", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Online Shoppers Intention; predict purchase intent from clickstream" + }, + { + "hf_id": "mstz/airline_passenger_satisfaction", + "hf_subsample": null, + "name": "airline_satisfaction", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Airline passenger satisfaction binary classification" + }, + { + "hf_id": "mstz/spaceship_titanic", + "hf_subsample": null, + "name": "spaceship_titanic", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Spaceship Titanic; Kaggle competition tabular binary classification" + }, + { + "hf_id": "mstz/stroke_prediction", + "hf_subsample": null, + "name": "stroke_prediction", + "task": "tabular_classification", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "binary" + ], + "needs_custom_loader": false, + "notes": "Stroke prediction from health and demographic features" + }, + { + "hf_id": "mstz/student_performance", + "hf_subsample": null, + "name": "student_performance", + "task": "regression", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Student performance regression; predict final grade from demographics" + }, + { + "hf_id": "mstz/used_cars", + "hf_subsample": null, + "name": "used_cars_price", + "task": "regression", + "input_features": [ + "number", + "category" + ], + "output_features": [ + "number" + ], + "needs_custom_loader": false, + "notes": "Used car price regression from vehicle features" + }, + { + "hf_id": "facebook/belebele", + "hf_subsample": "fra_Latn", + "name": "belebele_fr", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "Belebele French; multilingual reading comprehension" + }, + { + "hf_id": "ibm/duorc", + "hf_subsample": "SelfRC", + "name": "duorc", + "task": "extractive_qa", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "DuoRC; reading comprehension from Wikipedia and IMDb movie plots" + }, + { + "hf_id": "nq_open/nq_open", + "hf_subsample": null, + "name": "nq_open", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "NQ-Open; open-domain version of Natural Questions" + }, + { + "hf_id": "web_questions/web_questions", + "hf_subsample": null, + "name": "web_questions", + "task": "text_generation", + "input_features": [ + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "WebQuestions; Freebase-based open-domain QA" + }, + { + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "stance_abortion", + "name": "tweeteval_stance", + "task": "text_classification", + "input_features": [ + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "TweetEval stance detection; against/in favor/neutral" + }, + { + "hf_id": "ibm/claim_stance", + "hf_subsample": null, + "name": "claim_stance", + "task": "text_classification", + "input_features": [ + "text", + "text" + ], + "output_features": [ + "category" + ], + "needs_custom_loader": false, + "notes": "IBM Claim Stance Dataset; claim stance detection for debate topics" + }, + { + "hf_id": "ibm-granite/ChartNet", + "hf_subsample": null, + "name": "chartnet", + "task": "visual_question_answering", + "input_features": [ + "image", + "text" + ], + "output_features": [ + "text" + ], + "needs_custom_loader": false, + "notes": "ChartNet; chart and figure visual QA" + } +] diff --git a/scripts/probe_hf_datasets.py b/scripts/probe_hf_datasets.py new file mode 100644 index 0000000..fc1307d --- /dev/null +++ b/scripts/probe_hf_datasets.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python3 +"""Probe HuggingFace datasets, auto-generate YAML configs, and produce a smoke-test report. + +Usage: + python scripts/probe_hf_datasets.py [--start N] [--end N] [--resume] + +The script: +1. Streams 20 rows from each candidate dataset (no full download) +2. Inspects column types +3. Auto-generates a YAML config for simple datasets +4. Writes results to scripts/probe_results.json +""" + +from __future__ import annotations + +import argparse +import json +import logging +import os +import traceback +from collections import Counter +from typing import Any + +import datasets as hf_datasets +import pandas as pd + +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger(__name__) + +CONFIGS_DIR = os.path.join(os.path.dirname(__file__), "..", "ludwig", "datasets", "configs") +RESULTS_PATH = os.path.join(os.path.dirname(__file__), "probe_results.json") +CANDIDATES_PATH = os.path.join(os.path.dirname(__file__), "hf_dataset_candidates.json") + +EXISTING = {p.replace(".yaml", "") for p in os.listdir(CONFIGS_DIR) if p.endswith(".yaml")} + +LUDWIG_TYPES = {"text", "category", "binary", "number", "image", "audio", "sequence", "set", "vector", "bag"} + +_OUTPUT_COL_CANDIDATES = [ + "label", + "labels", + "target", + "output", + "class", + "category", + "sentiment", + "answer", + "score", + "rating", + "tag", + "tags", + "intent", + "emotion", + "polarity", + "stance", + "result", + "verdict", + "quality", +] + + +# ── Column-type inference ──────────────────────────────────────────────────── + + +def infer_ludwig_type(col: pd.Series, feature_info=None) -> str: + dtype = col.dtype + + if feature_info is not None: + fname = str(type(feature_info).__name__) + if "ClassLabel" in fname: + return "category" + if "Sequence" in fname: + return "_list" + if "Image" in fname: + return "image" + if "Audio" in fname: + return "audio" + if "Translation" in fname: + return "_translation" + + if dtype is object: + sample = col.dropna().head(20) + if len(sample) == 0: + return "text" + first = sample.iloc[0] + if isinstance(first, (list, tuple)): + return "_list" + if isinstance(first, dict): + return "_dict" + if isinstance(first, (bytes, bytearray)): + return "_bytes" + unique_ratio = col.nunique() / max(len(col), 1) + avg_len = sample.apply(lambda x: len(str(x))).mean() + if unique_ratio < 0.05 and avg_len < 50: + return "category" + return "text" + + if pd.api.types.is_bool_dtype(dtype): + return "binary" + if pd.api.types.is_integer_dtype(dtype): + return "category" if col.nunique() <= 20 else "number" + if pd.api.types.is_float_dtype(dtype): + return "number" + return "text" + + +def infer_columns(df: pd.DataFrame, hf_features) -> dict[str, str]: + result = {} + for col in df.columns: + if col == "split": + continue + fi = hf_features.get(col) if hf_features else None + result[col] = infer_ludwig_type(df[col], fi) + return result + + +# ── Output column resolution ───────────────────────────────────────────────── + + +def _resolve_output_cols(entry: dict, columns: dict[str, str]) -> list[str] | None: + """Handle both column-name and type-name forms for output_features.""" + raw = entry.get("output_features", []) + if not raw: + return None + + col_names = set(columns.keys()) + # If all values are actual column names, use them directly + if all(oc in col_names for oc in raw): + return raw + + # Agent returned types ("category", "binary") not column names — auto-detect + for cand in _OUTPUT_COL_CANDIDATES: + if cand in col_names and not columns[cand].startswith("_"): + return [cand] + + # Fallback: first column with category/binary/number type that isn't an id + for col, typ in columns.items(): + if col in ("idx", "id", "index") or typ.startswith("_"): + continue + if typ in ("category", "binary", "number"): + return [col] + return None + + +# ── YAML generation ────────────────────────────────────────────────────────── + +YAML_TMPL = """\ +version: 1.0 +name: {name} +huggingface_dataset_id: {hf_id} +{subsample_line}loader: hugging_face.HFLoader +description: | + {description} +columns: +{columns_block} +output_features: +{output_block} +""" + + +def make_yaml(entry: dict, columns: dict[str, str]) -> tuple[str | None, list[str] | None]: + """Return (yaml_string, output_cols) or (None, None).""" + for t in columns.values(): + if t.startswith("_"): + return None, None + + output_cols = _resolve_output_cols(entry, columns) + if not output_cols: + return None, None + for oc in output_cols: + if oc not in columns: + return None, None + + subsample = entry.get("hf_subsample") + subsample_line = f"huggingface_subsample: {subsample}\n" if subsample else "" + columns_block = "\n".join( + f" - name: {col}\n type: {typ}" for col, typ in columns.items() if not col.startswith("_") + ) + output_block = "\n".join(f" - name: {oc}\n type: {columns[oc]}" for oc in output_cols if oc in columns) + notes = entry.get("notes", "") or entry.get("name", "") + yaml_str = YAML_TMPL.format( + name=entry["name"], + hf_id=entry["hf_id"], + subsample_line=subsample_line, + description=notes, + columns_block=columns_block, + output_block=output_block, + ) + return yaml_str, output_cols + + +# ── Dataset probing ────────────────────────────────────────────────────────── + + +def probe_one(entry: dict) -> dict[str, Any]: + hf_id = entry["hf_id"] + hf_sub = entry.get("hf_subsample") + name = entry["name"] + + result: dict[str, Any] = { + "name": name, + "hf_id": hf_id, + "hf_subsample": hf_sub, + "task": entry.get("task", ""), + "status": "unknown", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": False, + "output_cols": [], + "error": None, + "needs_custom_loader": entry.get("needs_custom_loader", False), + } + + if name in EXISTING: + result["status"] = "already_exists" + return result + + try: + ds_stream = hf_datasets.load_dataset( + path=hf_id, + name=hf_sub, + trust_remote_code=False, + streaming=True, + ) + split_name = "train" if "train" in ds_stream else list(ds_stream.keys())[0] + ds = ds_stream[split_name] + rows = list(ds.take(20)) + if not rows: + result["status"] = "error" + result["error"] = "No rows returned" + return result + + df = pd.DataFrame(rows) + hf_features = ds.features if hasattr(ds, "features") else None + columns = infer_columns(df, hf_features) + result["columns"] = columns + result["splits"] = list(ds_stream.keys()) + + has_complex = any(t.startswith("_") for t in columns.values()) + result["needs_custom_loader"] = has_complex or entry.get("needs_custom_loader", False) + + yaml_str, output_cols = make_yaml(entry, columns) + if yaml_str and not result["needs_custom_loader"]: + out_path = os.path.join(CONFIGS_DIR, f"{name}.yaml") + with open(out_path, "w") as f: + f.write(yaml_str) + result["yaml_written"] = True + result["output_cols"] = output_cols + result["status"] = "auto_generated" + elif result["needs_custom_loader"]: + result["status"] = "needs_custom_loader" + else: + result["status"] = "skipped_no_yaml" + # Store why: which output cols were tried + result["debug_output_cols"] = _resolve_output_cols(entry, columns) + + except Exception as e: + result["status"] = "error" + result["error"] = f"{type(e).__name__}: {e}" + logger.debug(traceback.format_exc()) + + return result + + +# ── Main ───────────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--start", type=int, default=0) + parser.add_argument("--end", type=int, default=None) + parser.add_argument("--resume", action="store_true") + args = parser.parse_args() + + with open(CANDIDATES_PATH) as f: + candidates = json.load(f) + candidates = candidates[args.start : args.end] + + existing_results: dict[str, dict] = {} + if args.resume and os.path.exists(RESULTS_PATH): + try: + with open(RESULTS_PATH) as f: + for r in json.load(f): + existing_results[r["name"]] = r + except Exception: + pass + + results = [] + total = len(candidates) + for i, entry in enumerate(candidates): + name = entry["name"] + if args.resume and name in existing_results: + results.append(existing_results[name]) + print(f"[{i + 1}/{total}] SKIP (resumed) {name}") + continue + + print(f"[{i + 1}/{total}] Probing {name} ({entry['hf_id']})...", end=" ", flush=True) + r = probe_one(entry) + results.append(r) + + sym = { + "auto_generated": "✓", + "already_exists": "=", + "needs_custom_loader": "~", + "error": "✗", + "skipped_no_yaml": "?", + }.get(r["status"], "?") + print(f"{sym} [{r['status']}]") + if r["error"]: + print(f" ERROR: {r['error'][:100]}") + + # Merge with any resumed results and save + all_results = {**existing_results, **{rr["name"]: rr for rr in results}} + with open(RESULTS_PATH, "w") as f: + json.dump(list(all_results.values()), f, indent=2) + + status_counts = Counter(r["status"] for r in results) + print("\n=== Summary ===") + for status, count in status_counts.most_common(): + print(f" {status}: {count}") + print(f" Total: {len(results)}") + + print("\nAuto-generated:") + for r in results: + if r["status"] == "auto_generated": + print(f" {r['name']} → output: {r.get('output_cols')}") + + print("\nNeeds custom loader:") + for r in results: + if r["status"] == "needs_custom_loader": + complex_cols = [c for c, t in r["columns"].items() if t.startswith("_")] + print(f" {r['name']} ({r['task']}): complex={complex_cols}") + + print("\nErrors:") + for r in results: + if r["status"] == "error": + print(f" {r['name']}: {r['error'][:80]}") + + +if __name__ == "__main__": + main() diff --git a/scripts/probe_results.json b/scripts/probe_results.json new file mode 100644 index 0000000..c6da9c5 --- /dev/null +++ b/scripts/probe_results.json @@ -0,0 +1,5592 @@ +[ + { + "name": "mnli", + "hf_id": "nyu-mll/glue", + "hf_subsample": "mnli", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "hypothesis": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation_matched", + "validation_mismatched", + "test_matched", + "test_mismatched" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "qqp", + "hf_id": "nyu-mll/glue", + "hf_subsample": "qqp", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "question1": "text", + "question2": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "qnli", + "hf_id": "nyu-mll/glue", + "hf_subsample": "qnli", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "question": "text", + "sentence": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "cola", + "hf_id": "nyu-mll/glue", + "hf_subsample": "cola", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "sentence": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "rte", + "hf_id": "nyu-mll/glue", + "hf_subsample": "rte", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "sentence1": "text", + "sentence2": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "mrpc", + "hf_id": "nyu-mll/glue", + "hf_subsample": "mrpc", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "sentence1": "text", + "sentence2": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "stsb", + "hf_id": "nyu-mll/glue", + "hf_subsample": "stsb", + "task": "regression", + "status": "auto_generated", + "columns": { + "sentence1": "text", + "sentence2": "text", + "label": "number", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "wnli", + "hf_id": "nyu-mll/glue", + "hf_subsample": "wnli", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "sentence1": "text", + "sentence2": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "boolq", + "hf_id": "aps/super_glue", + "hf_subsample": "boolq", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "question": "text", + "passage": "text", + "idx": "category", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "commitment_bank", + "hf_id": "aps/super_glue", + "hf_subsample": "cb", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "hypothesis": "text", + "idx": "category", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "copa", + "hf_id": "aps/super_glue", + "hf_subsample": "copa", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "choice1": "text", + "choice2": "text", + "question": "text", + "idx": "category", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "multirc", + "hf_id": "aps/super_glue", + "hf_subsample": "multirc", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "paragraph": "text", + "question": "text", + "answer": "text", + "idx": "_dict", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "superglue_rte", + "hf_id": "aps/super_glue", + "hf_subsample": "rte", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "hypothesis": "text", + "idx": "category", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "wic", + "hf_id": "aps/super_glue", + "hf_subsample": "wic", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "word": "text", + "sentence1": "text", + "sentence2": "text", + "start1": "category", + "start2": "category", + "end1": "category", + "end2": "category", + "idx": "category", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "winograd_schema", + "hf_id": "aps/super_glue", + "hf_subsample": "wsc.fixed", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "span1_index": "category", + "span2_index": "category", + "span1_text": "text", + "span2_text": "text", + "idx": "category", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "imdb_sentiment", + "hf_id": "stanfordnlp/imdb", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "unsupervised" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "emotion", + "hf_id": "dair-ai/emotion", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "tweeteval_sentiment", + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "sentiment", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "tweeteval_hate", + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "hate", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "tweeteval_offensive", + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "offensive", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "tweeteval_irony", + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "irony", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "tweeteval_emotion", + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "emotion", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "financial_phrasebank", + "hf_id": "takala/financial_phrasebank", + "hf_subsample": "sentences_allagree", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found financial_phrasebank.py", + "needs_custom_loader": false + }, + { + "name": "go_emotions_multiclass", + "hf_id": "google-research-datasets/go_emotions", + "hf_subsample": "simplified", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "text": "text", + "labels": "_list", + "id": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "ag_news_hf", + "hf_id": "fancyzhx/ag_news", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "dbpedia_14", + "hf_id": "fancyzhx/dbpedia_14", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "label": "category", + "title": "text", + "content": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "yelp_review_full", + "hf_id": "Yelp/yelp_review_full", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "label": "category", + "text": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "amazon_polarity", + "hf_id": "fancyzhx/amazon_polarity", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "label": "category", + "title": "text", + "content": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "rotten_tomatoes", + "hf_id": "cornell-movie-review-data/rotten_tomatoes", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "snli", + "hf_id": "stanfordnlp/snli", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "hypothesis": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "test", + "validation", + "train" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "multi_nli", + "hf_id": "nyu-mll/multi_nli", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "promptID": "category", + "pairID": "text", + "premise": "text", + "premise_binary_parse": "text", + "premise_parse": "text", + "hypothesis": "text", + "hypothesis_binary_parse": "text", + "hypothesis_parse": "text", + "genre": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation_matched", + "validation_mismatched" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "paws", + "hf_id": "google-research-datasets/paws", + "hf_subsample": "labeled_final", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "id": "category", + "sentence1": "text", + "sentence2": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "anli", + "hf_id": "facebook/anli", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "uid": "text", + "premise": "text", + "hypothesis": "text", + "label": "category", + "reason": "text" + }, + "rows": -1, + "splits": [ + "train_r1", + "dev_r1", + "test_r1", + "train_r2", + "dev_r2", + "test_r2", + "train_r3", + "dev_r3", + "test_r3" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "trec", + "hf_id": "CogComp/trec", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found trec.py", + "needs_custom_loader": false + }, + { + "name": "sst2_hf", + "hf_id": "stanfordnlp/sst2", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "idx": "category", + "sentence": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "boolq_standalone", + "hf_id": "google/boolq", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "question": "text", + "answer": "binary", + "passage": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "belebele", + "hf_id": "facebook/belebele", + "hf_subsample": "eng_Latn", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "link": "text", + "question_number": "category", + "flores_passage": "text", + "question": "text", + "mc_answer1": "text", + "mc_answer2": "text", + "mc_answer3": "text", + "mc_answer4": "text", + "correct_answer_num": "text", + "dialect": "text", + "ds": "text" + }, + "rows": -1, + "splits": [ + "test" + ], + "yaml_written": true, + "output_cols": [ + "question_number" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "beavertails", + "hf_id": "PKU-Alignment/BeaverTails", + "hf_subsample": null, + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "prompt": "text", + "response": "text", + "category": "_dict", + "is_safe": "binary" + }, + "rows": -1, + "splits": [ + "330k_train", + "330k_test", + "30k_train", + "30k_test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "hellaswag", + "hf_id": "Rowan/hellaswag", + "hf_subsample": null, + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "ind": "category", + "activity_label": "text", + "ctx_a": "text", + "ctx_b": "text", + "ctx": "text", + "endings": "_list", + "source_id": "text", + "split_type": "text", + "label": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "winogrande", + "hf_id": "allenai/winogrande", + "hf_subsample": "winogrande_xl", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "sentence": "text", + "option1": "text", + "option2": "text", + "answer": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "piqa", + "hf_id": "ybisk/piqa", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found piqa.py", + "needs_custom_loader": false + }, + { + "name": "commonsense_qa", + "hf_id": "tau/commonsense_qa", + "hf_subsample": null, + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "question": "text", + "question_concept": "text", + "choices": "_dict", + "answerKey": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "arc_easy", + "hf_id": "allenai/ai2_arc", + "hf_subsample": "ARC-Easy", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "question": "text", + "choices": "_dict", + "answerKey": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "arc_challenge", + "hf_id": "allenai/ai2_arc", + "hf_subsample": "ARC-Challenge", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "question": "text", + "choices": "_dict", + "answerKey": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "openbookqa", + "hf_id": "allenai/openbookqa", + "hf_subsample": "main", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "question_stem": "text", + "choices": "_dict", + "answerKey": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "sciq", + "hf_id": "allenai/sciq", + "hf_subsample": null, + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "question": "text", + "distractor3": "text", + "distractor1": "text", + "distractor2": "text", + "correct_answer": "text", + "support": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "squad", + "hf_id": "rajpurkar/squad", + "hf_subsample": null, + "task": "extractive_qa", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "title": "text", + "context": "text", + "question": "text", + "answers": "_dict" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "squad_v2", + "hf_id": "rajpurkar/squad_v2", + "hf_subsample": null, + "task": "extractive_qa", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "title": "text", + "context": "text", + "question": "text", + "answers": "_dict" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "trivia_qa", + "hf_id": "mandarjoshi/trivia_qa", + "hf_subsample": "rc", + "task": "text_generation", + "status": "needs_custom_loader", + "columns": { + "question": "text", + "question_id": "text", + "question_source": "text", + "entity_pages": "_dict", + "search_results": "_dict", + "answer": "_dict" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "natural_questions", + "hf_id": "google-research-datasets/natural_questions", + "hf_subsample": null, + "task": "extractive_qa", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "document": "_dict", + "question": "_dict", + "long_answer_candidates": "_dict", + "annotations": "_dict" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "hotpot_qa", + "hf_id": "hotpotqa/hotpot_qa", + "hf_subsample": "distractor", + "task": "extractive_qa", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "question": "text", + "answer": "text", + "type": "text", + "level": "text", + "supporting_facts": "_dict", + "context": "_dict" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "pubmed_qa", + "hf_id": "qiaojin/PubMedQA", + "hf_subsample": "pqa_labeled", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "pubid": "category", + "question": "text", + "context": "_dict", + "long_answer": "text", + "final_decision": "text" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "medmcqa", + "hf_id": "openlifescienceai/medmcqa", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "id": "text", + "question": "text", + "opa": "text", + "opb": "text", + "opc": "text", + "opd": "text", + "cop": "category", + "choice_type": "text", + "exp": "text", + "subject_name": "text", + "topic_name": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "cop" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "truthful_qa", + "hf_id": "truthfulqa/truthful_qa", + "hf_subsample": "multiple_choice", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "question": "text", + "mc1_targets": "_dict", + "mc2_targets": "_dict" + }, + "rows": -1, + "splits": [ + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "mmlu", + "hf_id": "cais/mmlu", + "hf_subsample": "all", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "question": "text", + "subject": "text", + "choices": "_list", + "answer": "category" + }, + "rows": -1, + "splits": [ + "test", + "validation", + "dev", + "auxiliary_train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "mmlu_pro", + "hf_id": "TIGER-Lab/MMLU-Pro", + "hf_subsample": null, + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "question_id": "category", + "question": "text", + "options": "_list", + "answer": "text", + "answer_index": "category", + "cot_content": "text", + "category": "text", + "src": "text" + }, + "rows": -1, + "splits": [ + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "conll2003", + "hf_id": "eriktks/conll2003", + "hf_subsample": null, + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found conll2003.py", + "needs_custom_loader": false + }, + { + "name": "wikiann_en", + "hf_id": "unimelb-nlp/wikiann", + "hf_subsample": "en", + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "tokens": "_list", + "ner_tags": "_list", + "langs": "_list", + "spans": "_list" + }, + "rows": -1, + "splits": [ + "validation", + "test", + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "wnut17", + "hf_id": "leondz/wnut_17", + "hf_subsample": null, + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found wnut_17.py", + "needs_custom_loader": false + }, + { + "name": "ncbi_disease", + "hf_id": "ncbi/ncbi_disease", + "hf_subsample": null, + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found ncbi_disease.py", + "needs_custom_loader": false + }, + { + "name": "wikineural", + "hf_id": "Babelscape/wikineural", + "hf_subsample": "en", + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'en' not found. Available: ['default']", + "needs_custom_loader": false + }, + { + "name": "multinerd", + "hf_id": "Babelscape/multinerd", + "hf_subsample": null, + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "tokens": "_list", + "ner_tags": "_list", + "lang": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "few_nerd", + "hf_id": "DFKI-SLT/few-nerd", + "hf_subsample": "supervised", + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "tokens": "_list", + "ner_tags": "_list", + "fine_ner_tags": "_list" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "masakhaner2", + "hf_id": "masakhane/masakhaner2", + "hf_subsample": "eng", + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found masakhaner2.py", + "needs_custom_loader": false + }, + { + "name": "universal_dependencies", + "hf_id": "universal-dependencies/universal_dependencies", + "hf_subsample": "en_ewt", + "task": "sequence_labeling", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found universal_dependencies.py", + "needs_custom_loader": false + }, + { + "name": "pii_masking", + "hf_id": "ai4privacy/pii-masking-300k", + "hf_subsample": null, + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "source_text": "text", + "target_text": "text", + "privacy_mask": "_list", + "span_labels": "text", + "mbert_text_tokens": "_list", + "mbert_bio_labels": "_list", + "id": "text", + "language": "text", + "set": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "cnn_dailymail", + "hf_id": "abisee/cnn_dailymail", + "hf_subsample": "3.0.0", + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "article": "text", + "highlights": "text", + "id": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "xsum", + "hf_id": "EdinburghNLP/xsum", + "hf_subsample": null, + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "document": "text", + "summary": "text", + "id": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "dialogsum", + "hf_id": "knkarthick/dialogsum", + "hf_subsample": null, + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "id": "text", + "dialogue": "text", + "summary": "text", + "topic": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "samsum", + "hf_id": "knkarthick/samsum", + "hf_subsample": null, + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "id": "text", + "dialogue": "text", + "summary": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "billsum", + "hf_id": "FiscalNote/billsum", + "hf_subsample": null, + "task": "summarization", + "status": "already_exists", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false + }, + { + "name": "arxiv_summarization", + "hf_id": "ccdv/arxiv-summarization", + "hf_subsample": null, + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "article": "text", + "abstract": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "pubmed_summarization", + "hf_id": "ccdv/pubmed-summarization", + "hf_subsample": null, + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "article": "text", + "abstract": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "multi_news", + "hf_id": "alexfabbri/multi_news", + "hf_subsample": null, + "task": "summarization", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found multi_news.py", + "needs_custom_loader": false + }, + { + "name": "xlsum", + "hf_id": "csebuetnlp/xlsum", + "hf_subsample": "english", + "task": "summarization", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found xlsum.py", + "needs_custom_loader": false + }, + { + "name": "big_patent", + "hf_id": "NortheasternUniversity/big_patent", + "hf_subsample": "a", + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "description": "text", + "abstract": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "govreport_summarization", + "hf_id": "ccdv/govreport-summarization", + "hf_subsample": null, + "task": "summarization", + "status": "skipped_no_yaml", + "columns": { + "report": "text", + "summary": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "opus100_en_de", + "hf_id": "Helsinki-NLP/opus-100", + "hf_subsample": "en-de", + "task": "translation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'en-de' not found. Available: ['af-en', 'am-en', 'an-en', 'ar-de', 'ar-en', 'ar-fr', 'ar-nl', 'ar-ru', 'ar-zh', 'as-en', 'az-en', 'be-en', 'bg-en', 'bn-en', 'br-en', 'bs-en', 'ca-en', 'cs-en', 'cy-en', 'da-en', 'de-en', 'de-fr', 'de-nl', 'de-ru', 'de-zh', 'dz-en', 'el-en', 'en-eo', 'en-es', 'en-et', 'en-eu', 'en-fa', 'en-fi', 'en-fr', 'en-fy', 'en-ga', 'en-gd', 'en-gl', 'en-gu', 'en-ha', 'en-he', 'en-hi', 'en-hr', 'en-hu', 'en-hy', 'en-id', 'en-ig', 'en-is', 'en-it', 'en-ja', 'en-ka', 'en-kk', 'en-km', 'en-kn', 'en-ko', 'en-ku', 'en-ky', 'en-li', 'en-lt', 'en-lv', 'en-mg', 'en-mk', 'en-ml', 'en-mn', 'en-mr', 'en-ms', 'en-mt', 'en-my', 'en-nb', 'en-ne', 'en-nl', 'en-nn', 'en-no', 'en-oc', 'en-or', 'en-pa', 'en-pl', 'en-ps', 'en-pt', 'en-ro', 'en-ru', 'en-rw', 'en-se', 'en-sh', 'en-si', 'en-sk', 'en-sl', 'en-sq', 'en-sr', 'en-sv', 'en-ta', 'en-te', 'en-tg', 'en-th', 'en-tk', 'en-tr', 'en-tt', 'en-ug', 'en-uk', 'en-ur', 'en-uz', 'en-vi', 'en-wa', 'en-xh', 'en-yi', 'en-yo', 'en-zh', 'en-zu', 'fr-nl', 'fr-ru', 'fr-zh', 'nl-ru', 'nl-zh', 'ru-zh']", + "needs_custom_loader": false + }, + { + "name": "opus100_en_fr", + "hf_id": "Helsinki-NLP/opus-100", + "hf_subsample": "en-fr", + "task": "translation", + "status": "needs_custom_loader", + "columns": { + "translation": "_translation" + }, + "rows": -1, + "splits": [ + "test", + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "opus100_en_es", + "hf_id": "Helsinki-NLP/opus-100", + "hf_subsample": "en-es", + "task": "translation", + "status": "needs_custom_loader", + "columns": { + "translation": "_translation" + }, + "rows": -1, + "splits": [ + "test", + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "wmt14_de_en", + "hf_id": "wmt/wmt14", + "hf_subsample": "de-en", + "task": "translation", + "status": "needs_custom_loader", + "columns": { + "translation": "_translation" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "wmt16_de_en", + "hf_id": "wmt/wmt16", + "hf_subsample": "de-en", + "task": "translation", + "status": "needs_custom_loader", + "columns": { + "translation": "_translation" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "wmt19_de_en", + "hf_id": "wmt/wmt19", + "hf_subsample": "de-en", + "task": "translation", + "status": "needs_custom_loader", + "columns": { + "translation": "_translation" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "opus_books_en_fr", + "hf_id": "Helsinki-NLP/opus_books", + "hf_subsample": "en-fr", + "task": "translation", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "translation": "_translation" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "flores_en_de", + "hf_id": "facebook/flores", + "hf_subsample": "eng_Latn-deu_Latn", + "task": "translation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found flores.py", + "needs_custom_loader": false + }, + { + "name": "iwslt2017_en_de", + "hf_id": "IWSLT/iwslt2017", + "hf_subsample": "iwslt2017-en-de", + "task": "translation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found iwslt2017.py", + "needs_custom_loader": false + }, + { + "name": "europarl_en_de", + "hf_id": "Helsinki-NLP/europarl", + "hf_subsample": "en-de", + "task": "translation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'en-de' not found. Available: ['bg-cs', 'bg-da', 'bg-de', 'bg-el', 'bg-en', 'bg-es', 'bg-et', 'bg-fi', 'bg-fr', 'bg-hu', 'bg-it', 'bg-lt', 'bg-lv', 'bg-nl', 'bg-pl', 'bg-pt', 'bg-ro', 'bg-sk', 'bg-sl', 'bg-sv', 'cs-da', 'cs-de', 'cs-el', 'cs-en', 'cs-es', 'cs-et', 'cs-fi', 'cs-fr', 'cs-hu', 'cs-it', 'cs-lt', 'cs-lv', 'cs-nl', 'cs-pl', 'cs-pt', 'cs-ro', 'cs-sk', 'cs-sl', 'cs-sv', 'da-de', 'da-el', 'da-en', 'da-es', 'da-et', 'da-fi', 'da-fr', 'da-hu', 'da-it', 'da-lt', 'da-lv', 'da-nl', 'da-pl', 'da-pt', 'da-ro', 'da-sk', 'da-sl', 'da-sv', 'de-el', 'de-en', 'de-es', 'de-et', 'de-fi', 'de-fr', 'de-hu', 'de-it', 'de-lt', 'de-lv', 'de-nl', 'de-pl', 'de-pt', 'de-ro', 'de-sk', 'de-sl', 'de-sv', 'el-en', 'el-es', 'el-et', 'el-fi', 'el-fr', 'el-hu', 'el-it', 'el-lt', 'el-lv', 'el-nl', 'el-pl', 'el-pt', 'el-ro', 'el-sk', 'el-sl', 'el-sv', 'en-es', 'en-et', 'en-fi', 'en-fr', 'en-hu', 'en-it', 'en-lt', 'en-lv', 'en-nl', 'en-pl', 'en-pt', 'en-ro', 'en-sk', 'en-sl', 'en-sv', 'es-et', 'es-fi', 'es-fr', 'es-hu', 'es-it', 'es-lt', 'es-lv', 'es-nl', 'es-pl', 'es-pt', 'es-ro', 'es-sk', 'es-sl', 'es-sv', 'et-fi', 'et-fr', 'et-hu', 'et-it', 'et-lt', 'et-lv', 'et-nl', 'et-pl', 'et-pt', 'et-ro', 'et-sk', 'et-sl', 'et-sv', 'fi-fr', 'fi-hu', 'fi-it', 'fi-lt', 'fi-lv', 'fi-nl', 'fi-pl', 'fi-pt', 'fi-ro', 'fi-sk', 'fi-sl', 'fi-sv', 'fr-hu', 'fr-it', 'fr-lt', 'fr-lv', 'fr-nl', 'fr-pl', 'fr-pt', 'fr-ro', 'fr-sk', 'fr-sl', 'fr-sv', 'hu-it', 'hu-lt', 'hu-lv', 'hu-nl', 'hu-pl', 'hu-pt', 'hu-ro', 'hu-sk', 'hu-sl', 'hu-sv', 'it-lt', 'it-lv', 'it-nl', 'it-pl', 'it-pt', 'it-ro', 'it-sk', 'it-sl', 'it-sv', 'lt-lv', 'lt-nl', 'lt-pl', 'lt-pt', 'lt-ro', 'lt-sk', 'lt-sl', 'lt-sv', 'lv-nl', 'lv-pl', 'lv-pt', 'lv-ro', 'lv-sk', 'lv-sl', 'lv-sv', 'nl-pl', 'nl-pt', 'nl-ro', 'nl-sk', 'nl-sl', 'nl-sv', 'pl-pt', 'pl-ro', 'pl-sk', 'pl-sl', 'pl-sv', 'pt-ro', 'pt-sk', 'pt-sl', 'pt-sv', 'ro-sk', 'ro-sl', 'ro-sv', 'sk-sl', 'sk-sv', 'sl-sv']", + "needs_custom_loader": false + }, + { + "name": "cifar10", + "hf_id": "uoft-cs/cifar10", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "img": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "cifar100", + "hf_id": "uoft-cs/cifar100", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "img": "image", + "fine_label": "category", + "coarse_label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "fine_label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "mnist", + "hf_id": "ylecun/mnist", + "hf_subsample": null, + "task": "image_classification", + "status": "already_exists", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false + }, + { + "name": "fashion_mnist", + "hf_id": "zalando-datasets/fashion_mnist", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "food101", + "hf_id": "ethz/food101", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "stanford_cars", + "hf_id": "tanganke/stanford_cars", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "contrast", + "gaussian_noise", + "impulse_noise", + "jpeg_compression", + "motion_blur", + "pixelate", + "spatter" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "oxford_pets", + "hf_id": "timm/oxford-iiit-pet", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category", + "image_id": "text", + "label_cat_dog": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "eurosat", + "hf_id": "tanganke/eurosat", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "contrast", + "gaussian_noise", + "impulse_noise", + "jpeg_compression", + "motion_blur", + "pixelate", + "spatter" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "gtsrb", + "hf_id": "tanganke/gtsrb", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "contrast", + "gaussian_noise", + "impulse_noise", + "jpeg_compression", + "motion_blur", + "pixelate", + "spatter" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "svhn", + "hf_id": "ufldl-stanford/svhn", + "hf_subsample": "cropped_digits", + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "extra" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "tiny_imagenet", + "hf_id": "zh-plus/tiny-imagenet", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "valid" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "sun397", + "hf_id": "tanganke/sun397", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "beans", + "hf_id": "AI-Lab-Makerere/beans", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image_file_path": "text", + "image": "image", + "labels": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "labels" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "wikiart", + "hf_id": "huggan/wikiart", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "artist": "category", + "genre": "category", + "style": "category" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "artist" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "imagenet1k", + "hf_id": "ILSVRC/imagenet-1k", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'ILSVRC/imagenet-1k' is a gated dataset on the Hub. You must be authenticated to access it.", + "needs_custom_loader": false + }, + { + "name": "mvtec_ad", + "hf_id": "Voxel51/mvtec-ad", + "hf_subsample": null, + "task": "image_classification", + "status": "skipped_no_yaml", + "columns": { + "image": "image" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "librispeech", + "hf_id": "openslr/librispeech_asr", + "hf_subsample": "clean", + "task": "automatic_speech_recognition", + "status": "already_exists", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false + }, + { + "name": "fleurs_en", + "hf_id": "google/fleurs", + "hf_subsample": "en_us", + "task": "automatic_speech_recognition", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found fleurs.py", + "needs_custom_loader": false + }, + { + "name": "multilingual_librispeech", + "hf_id": "facebook/multilingual_librispeech", + "hf_subsample": "english", + "task": "automatic_speech_recognition", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'english' not found. Available: ['dutch', 'french', 'german', 'italian', 'polish', 'portuguese', 'spanish']", + "needs_custom_loader": false + }, + { + "name": "voxpopuli", + "hf_id": "facebook/voxpopuli", + "hf_subsample": "en", + "task": "automatic_speech_recognition", + "status": "auto_generated", + "columns": { + "audio_id": "text", + "language": "category", + "audio": "audio", + "raw_text": "text", + "normalized_text": "text", + "gender": "text", + "speaker_id": "text", + "is_gold_transcript": "binary", + "accent": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "language" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "gigaspeech", + "hf_id": "speechcolab/gigaspeech", + "hf_subsample": "xs", + "task": "automatic_speech_recognition", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'speechcolab/gigaspeech' is a gated dataset on the Hub. You must be authenticated to access it.", + "needs_custom_loader": false + }, + { + "name": "audioset", + "hf_id": "agkphysics/AudioSet", + "hf_subsample": "unbalanced_train", + "task": "audio_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'unbalanced_train' not found. Available: ['balanced', 'unbalanced', 'full']", + "needs_custom_loader": false + }, + { + "name": "fsd50k", + "hf_id": "Fhrozen/FSD50k", + "hf_subsample": null, + "task": "audio_classification", + "status": "auto_generated", + "columns": { + "audio": "audio", + "label": "category" + }, + "rows": -1, + "splits": [ + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "peoples_speech", + "hf_id": "MLCommons/peoples_speech", + "hf_subsample": "clean", + "task": "automatic_speech_recognition", + "status": "auto_generated", + "columns": { + "id": "text", + "audio": "audio", + "duration_ms": "category", + "text": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "text" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "voxceleb", + "hf_id": "ProgramComputer/voxceleb", + "hf_subsample": null, + "task": "audio_classification", + "status": "skipped_no_yaml", + "columns": { + "text": "text" + }, + "rows": -1, + "splits": [ + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "birdset", + "hf_id": "DBD-research-group/BirdSet", + "hf_subsample": "HSN", + "task": "audio_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found BirdSet.py", + "needs_custom_loader": false + }, + { + "name": "vggsound", + "hf_id": "Loie/VGGSound", + "hf_subsample": null, + "task": "audio_classification", + "status": "skipped_no_yaml", + "columns": { + "image": "image" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "mmmu", + "hf_id": "MMMU/MMMU", + "hf_subsample": "Accounting", + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "question": "text", + "options": "text", + "explanation": "text", + "image_1": "image", + "image_2": "image", + "image_3": "image", + "image_4": "image", + "image_5": "image", + "image_6": "image", + "image_7": "image", + "img_type": "text", + "answer": "text", + "topic_difficulty": "text", + "question_type": "text", + "subfield": "text" + }, + "rows": -1, + "splits": [ + "dev", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "scienceqa", + "hf_id": "derek-thomas/ScienceQA", + "hf_subsample": null, + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "image": "image", + "question": "text", + "choices": "_list", + "answer": "category", + "hint": "text", + "task": "text", + "grade": "text", + "subject": "text", + "topic": "text", + "category": "text", + "skill": "text", + "lecture": "text", + "solution": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "vqa_rad", + "hf_id": "flaviagiammarino/vqa-rad", + "hf_subsample": null, + "task": "visual_question_answering", + "status": "auto_generated", + "columns": { + "image": "image", + "question": "text", + "answer": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "mathvista", + "hf_id": "AI4Math/MathVista", + "hf_subsample": null, + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "pid": "text", + "question": "text", + "image": "text", + "decoded_image": "image", + "choices": "_list", + "unit": "text", + "precision": "number", + "answer": "text", + "question_type": "text", + "answer_type": "text", + "metadata": "_dict", + "query": "text" + }, + "rows": -1, + "splits": [ + "testmini", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "satellite_building_segmentation", + "hf_id": "keremberke/satellite-building-segmentation", + "hf_subsample": null, + "task": "image_segmentation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found satellite-building-segmentation.py", + "needs_custom_loader": true + }, + { + "name": "tabular_benchmark_clf", + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "clf_cat", + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'clf_cat' not found. Available: ['clf_cat_albert', 'clf_cat_compas-two-years', 'clf_cat_covertype', 'clf_cat_default-of-credit-card-clients', 'clf_cat_electricity', 'clf_cat_eye_movements', 'clf_cat_road-safety', 'clf_num_Bioresponse', 'clf_num_Diabetes130US', 'clf_num_Higgs', 'clf_num_MagicTelescope', 'clf_num_MiniBooNE', 'clf_num_bank-marketing', 'clf_num_california', 'clf_num_covertype', 'clf_num_credit', 'clf_num_default-of-credit-card-clients', 'clf_num_electricity', 'clf_num_eye_movements', 'clf_num_heloc', 'clf_num_house_16H', 'clf_num_jannis', 'clf_num_pol', 'reg_cat_Airlines_DepDelay_1M', 'reg_cat_Allstate_Claims_Severity', 'reg_cat_Bike_Sharing_Demand', 'reg_cat_Brazilian_houses', 'reg_cat_Mercedes_Benz_Greener_Manufacturing', 'reg_cat_SGEMM_GPU_kernel_performance', 'reg_cat_abalone', 'reg_cat_analcatdata_supreme', 'reg_cat_delays_zurich_transport', 'reg_cat_diamonds', 'reg_cat_house_sales', 'reg_cat_medical_charges', 'reg_cat_nyc-taxi-green-dec-2016', 'reg_cat_particulate-matter-ukair-2017', 'reg_cat_seattlecrime6', 'reg_cat_topo_2_1', 'reg_cat_visualizing_soil', 'reg_num_Ailerons', 'reg_num_Bike_Sharing_Demand', 'reg_num_Brazilian_houses', 'reg_num_MiamiHousing2016', 'reg_num_abalone', 'reg_num_cpu_act', 'reg_num_delays_zurich_transport', 'reg_num_diamonds', 'reg_num_elevators', 'reg_num_house_16H', 'reg_num_house_sales', 'reg_num_houses', 'reg_num_medical_charges', 'reg_num_nyc-taxi-green-dec-2016', 'reg_num_pol', 'reg_num_sulfur', 'reg_num_superconduct', 'reg_num_wine_quality', 'reg_num_yprop_4_1']", + "needs_custom_loader": false + }, + { + "name": "adult_income_hf", + "hf_id": "mstz/adult", + "hf_subsample": null, + "task": "tabular_classification", + "status": "auto_generated", + "columns": { + "age": "category", + "capital_gain": "number", + "capital_loss": "number", + "education": "category", + "final_weight": "category", + "hours_worked_per_week": "category", + "marital_status": "text", + "native_country": "text", + "occupation": "text", + "race": "text", + "relationship": "text", + "is_male": "binary", + "workclass": "text", + "over_threshold": "category" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "age" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "diabetes_regression", + "hf_id": "scikit-learn/diabetes", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'scikit-learn/diabetes' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "heart_failure", + "hf_id": "mstz/heart_failure", + "hf_subsample": null, + "task": "tabular_classification", + "status": "auto_generated", + "columns": { + "age": "category", + "has_anaemia": "binary", + "creatinine_phosphokinase_concentration_in_blood": "number", + "has_diabetes": "binary", + "heart_ejection_fraction": "number", + "has_high_blood_pressure": "binary", + "platelets_concentration_in_blood": "number", + "serum_creatinine_concentration_in_blood": "number", + "serum_sodium_concentration_in_blood": "number", + "is_male": "binary", + "is_smoker": "binary", + "days_in_study": "category", + "is_dead": "category" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "age" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "breast_cancer", + "hf_id": "mstz/breast_cancer", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/breast_cancer' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "tabular_benchmark_reg", + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "reg_num", + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'reg_num' not found. Available: ['clf_cat_albert', 'clf_cat_compas-two-years', 'clf_cat_covertype', 'clf_cat_default-of-credit-card-clients', 'clf_cat_electricity', 'clf_cat_eye_movements', 'clf_cat_road-safety', 'clf_num_Bioresponse', 'clf_num_Diabetes130US', 'clf_num_Higgs', 'clf_num_MagicTelescope', 'clf_num_MiniBooNE', 'clf_num_bank-marketing', 'clf_num_california', 'clf_num_covertype', 'clf_num_credit', 'clf_num_default-of-credit-card-clients', 'clf_num_electricity', 'clf_num_eye_movements', 'clf_num_heloc', 'clf_num_house_16H', 'clf_num_jannis', 'clf_num_pol', 'reg_cat_Airlines_DepDelay_1M', 'reg_cat_Allstate_Claims_Severity', 'reg_cat_Bike_Sharing_Demand', 'reg_cat_Brazilian_houses', 'reg_cat_Mercedes_Benz_Greener_Manufacturing', 'reg_cat_SGEMM_GPU_kernel_performance', 'reg_cat_abalone', 'reg_cat_analcatdata_supreme', 'reg_cat_delays_zurich_transport', 'reg_cat_diamonds', 'reg_cat_house_sales', 'reg_cat_medical_charges', 'reg_cat_nyc-taxi-green-dec-2016', 'reg_cat_particulate-matter-ukair-2017', 'reg_cat_seattlecrime6', 'reg_cat_topo_2_1', 'reg_cat_visualizing_soil', 'reg_num_Ailerons', 'reg_num_Bike_Sharing_Demand', 'reg_num_Brazilian_houses', 'reg_num_MiamiHousing2016', 'reg_num_abalone', 'reg_num_cpu_act', 'reg_num_delays_zurich_transport', 'reg_num_diamonds', 'reg_num_elevators', 'reg_num_house_16H', 'reg_num_house_sales', 'reg_num_houses', 'reg_num_medical_charges', 'reg_num_nyc-taxi-green-dec-2016', 'reg_num_pol', 'reg_num_sulfur', 'reg_num_superconduct', 'reg_num_wine_quality', 'reg_num_yprop_4_1']", + "needs_custom_loader": false + }, + { + "name": "wine_quality", + "hf_id": "scikit-learn/wine-quality", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'scikit-learn/wine-quality' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "boston_housing", + "hf_id": "scikit-learn/boston", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'scikit-learn/boston' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "diffusiondb", + "hf_id": "poloclub/diffusiondb", + "hf_subsample": "2m_random_1k", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found diffusiondb.py", + "needs_custom_loader": false + }, + { + "name": "bank_marketing", + "hf_id": "mstz/bank_marketing", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/bank_marketing' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "covertype", + "hf_id": "mstz/covertype", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found covertype.py", + "needs_custom_loader": false + }, + { + "name": "magic_telescope", + "hf_id": "mstz/magic_telescope", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/magic_telescope' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "electricity_tabular", + "hf_id": "mstz/electricity", + "hf_subsample": null, + "task": "tabular_classification", + "status": "auto_generated", + "columns": { + "date": "number", + "day": "category", + "period": "number", + "nswprice": "number", + "nswdemand": "number", + "vicprice": "number", + "vicdemand": "number", + "transfer": "number", + "is_up": "category" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "date" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "jannis", + "hf_id": "mstz/jannis", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/jannis' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "higgs_tabular", + "hf_id": "mstz/higgs", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found higgs.py", + "needs_custom_loader": false + }, + { + "name": "amazon_reviews_multi", + "hf_id": "mteb/amazon_reviews_multi", + "hf_subsample": "en", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found amazon_reviews_multi.py", + "needs_custom_loader": false + }, + { + "name": "tweet_sentiment_extraction", + "hf_id": "mteb/tweet_sentiment_extraction", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "id": "text", + "text": "text", + "label": "category", + "label_text": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "climate_sentiment", + "hf_id": "climatebert/climate_sentiment", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "sst_spans", + "hf_id": "sst/sst", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'sst/sst' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "sts_benchmark", + "hf_id": "mteb/stsbenchmark-sts", + "hf_subsample": null, + "task": "regression", + "status": "auto_generated", + "columns": { + "genre": "text", + "dataset": "text", + "year": "text", + "sid": "text", + "score": "number", + "sentence1": "text", + "sentence2": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "score" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "bc5cdr", + "hf_id": "bigbio/bc5cdr", + "hf_subsample": "bc5cdr_bigbio_kb", + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found bc5cdr.py", + "needs_custom_loader": false + }, + { + "name": "lex_glue_ecthr", + "hf_id": "coastalcph/lex_glue", + "hf_subsample": "ecthr_a", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "text": "_list", + "labels": "_list" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "lex_glue_eurlex", + "hf_id": "coastalcph/lex_glue", + "hf_subsample": "eurlex", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "text": "text", + "labels": "_list" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "legalbench", + "hf_id": "nguha/legalbench", + "hf_subsample": "contract_nli_confidentiality_of_agreement", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "index": "category", + "answer": "text", + "text": "text", + "document_name": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "sourcedata_ner", + "hf_id": "EMBO/SourceData", + "hf_subsample": "NER", + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found SourceData.py", + "needs_custom_loader": false + }, + { + "name": "mbpp", + "hf_id": "google-research-datasets/mbpp", + "hf_subsample": null, + "task": "text_generation", + "status": "needs_custom_loader", + "columns": { + "task_id": "category", + "text": "text", + "code": "text", + "test_list": "_list", + "test_setup_code": "text", + "challenge_test_list": "_list" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation", + "prompt" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "humaneval", + "hf_id": "openai/openai_humaneval", + "hf_subsample": null, + "task": "text_generation", + "status": "skipped_no_yaml", + "columns": { + "task_id": "text", + "prompt": "text", + "canonical_solution": "text", + "test": "text", + "entry_point": "text" + }, + "rows": -1, + "splits": [ + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "codexglue_code_to_text", + "hf_id": "google/code_x_glue_ct_code_to_text", + "hf_subsample": "python", + "task": "text_generation", + "status": "needs_custom_loader", + "columns": { + "id": "category", + "repo": "text", + "path": "text", + "func_name": "text", + "original_string": "text", + "language": "text", + "code": "text", + "code_tokens": "_list", + "docstring": "text", + "docstring_tokens": "_list", + "sha": "text", + "url": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "code_search_net", + "hf_id": "code-search-net/code_search_net", + "hf_subsample": "python", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "repository_name": "text", + "func_path_in_repository": "text", + "func_name": "text", + "whole_func_string": "text", + "language": "text", + "func_code_string": "text", + "func_code_tokens": "_list", + "func_documentation_string": "text", + "func_documentation_tokens": "_list", + "split_name": "text", + "func_code_url": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "liar", + "hf_id": "liar/liar", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'liar/liar' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "fake_news_detection", + "hf_id": "GonzaloA/fake_news", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "Unnamed: 0": "category", + "title": "text", + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "climate_fever", + "hf_id": "climate_fever/climate_fever", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'climate_fever/climate_fever' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "hate_speech18", + "hf_id": "HateSpeechMLResearch/hate_speech_18", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'HateSpeechMLResearch/hate_speech_18' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "measuring_hate_speech", + "hf_id": "ucberkeley-dlab/measuring-hate-speech", + "hf_subsample": null, + "task": "regression", + "status": "auto_generated", + "columns": { + "comment_id": "category", + "annotator_id": "category", + "platform": "category", + "sentiment": "number", + "respect": "number", + "insult": "number", + "humiliate": "number", + "status": "number", + "dehumanize": "number", + "violence": "number", + "genocide": "number", + "attack_defend": "number", + "hatespeech": "number", + "hate_speech_score": "number", + "text": "text", + "infitms": "number", + "outfitms": "number", + "annotator_severity": "number", + "std_err": "number", + "annotator_infitms": "number", + "annotator_outfitms": "number", + "hypothesis": "number", + "target_race_asian": "binary", + "target_race_black": "binary", + "target_race_latinx": "binary", + "target_race_middle_eastern": "binary", + "target_race_native_american": "binary", + "target_race_pacific_islander": "binary", + "target_race_white": "binary", + "target_race_other": "binary", + "target_race": "binary", + "target_religion_atheist": "binary", + "target_religion_buddhist": "binary", + "target_religion_christian": "binary", + "target_religion_hindu": "binary", + "target_religion_jewish": "binary", + "target_religion_mormon": "binary", + "target_religion_muslim": "binary", + "target_religion_other": "binary", + "target_religion": "binary", + "target_origin_immigrant": "binary", + "target_origin_migrant_worker": "binary", + "target_origin_specific_country": "binary", + "target_origin_undocumented": "binary", + "target_origin_other": "binary", + "target_origin": "binary", + "target_gender_men": "binary", + "target_gender_non_binary": "binary", + "target_gender_transgender_men": "binary", + "target_gender_transgender_unspecified": "binary", + "target_gender_transgender_women": "binary", + "target_gender_women": "binary", + "target_gender_other": "binary", + "target_gender": "binary", + "target_sexuality_bisexual": "binary", + "target_sexuality_gay": "binary", + "target_sexuality_lesbian": "binary", + "target_sexuality_straight": "binary", + "target_sexuality_other": "binary", + "target_sexuality": "binary", + "target_age_children": "binary", + "target_age_teenagers": "binary", + "target_age_young_adults": "binary", + "target_age_middle_aged": "binary", + "target_age_seniors": "binary", + "target_age_other": "binary", + "target_age": "binary", + "target_disability_physical": "binary", + "target_disability_cognitive": "binary", + "target_disability_neurological": "binary", + "target_disability_visually_impaired": "binary", + "target_disability_hearing_impaired": "binary", + "target_disability_unspecific": "binary", + "target_disability_other": "binary", + "target_disability": "binary", + "target_politics_alt_right": "binary", + "target_politics_communist": "binary", + "target_politics_conservative": "binary", + "target_politics_democrat": "binary", + "target_politics_green_party": "binary", + "target_politics_leftist": "binary", + "target_politics_liberal": "binary", + "target_politics_libertarian": "binary", + "target_politics_republican": "binary", + "target_politics_socialist": "binary", + "target_politics_other": "binary", + "target_politics": "binary", + "annotator_gender": "text", + "annotator_trans": "text", + "annotator_educ": "text", + "annotator_income": "text", + "annotator_ideology": "text", + "annotator_gender_men": "binary", + "annotator_gender_women": "binary", + "annotator_gender_non_binary": "binary", + "annotator_gender_prefer_not_to_say": "binary", + "annotator_gender_self_describe": "binary", + "annotator_transgender": "binary", + "annotator_cisgender": "binary", + "annotator_transgender_prefer_not_to_say": "binary", + "annotator_education_some_high_school": "binary", + "annotator_education_high_school_grad": "binary", + "annotator_education_some_college": "binary", + "annotator_education_college_grad_aa": "binary", + "annotator_education_college_grad_ba": "binary", + "annotator_education_professional_degree": "binary", + "annotator_education_masters": "binary", + "annotator_education_phd": "binary", + "annotator_income_<10k": "binary", + "annotator_income_10k-50k": "binary", + "annotator_income_50k-100k": "binary", + "annotator_income_100k-200k": "binary", + "annotator_income_>200k": "binary", + "annotator_ideology_extremeley_conservative": "binary", + "annotator_ideology_conservative": "binary", + "annotator_ideology_slightly_conservative": "binary", + "annotator_ideology_neutral": "binary", + "annotator_ideology_slightly_liberal": "binary", + "annotator_ideology_liberal": "binary", + "annotator_ideology_extremeley_liberal": "binary", + "annotator_ideology_no_opinion": "binary", + "annotator_race_asian": "binary", + "annotator_race_black": "binary", + "annotator_race_latinx": "binary", + "annotator_race_middle_eastern": "binary", + "annotator_race_native_american": "binary", + "annotator_race_pacific_islander": "binary", + "annotator_race_white": "binary", + "annotator_race_other": "binary", + "annotator_age": "number", + "annotator_religion_atheist": "binary", + "annotator_religion_buddhist": "binary", + "annotator_religion_christian": "binary", + "annotator_religion_hindu": "binary", + "annotator_religion_jewish": "binary", + "annotator_religion_mormon": "binary", + "annotator_religion_muslim": "binary", + "annotator_religion_nothing": "binary", + "annotator_religion_other": "binary", + "annotator_sexuality_bisexual": "binary", + "annotator_sexuality_gay": "binary", + "annotator_sexuality_straight": "binary", + "annotator_sexuality_other": "binary" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "sentiment" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "civil_comments", + "hf_id": "google/civil_comments", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "toxicity": "number", + "severe_toxicity": "number", + "obscene": "number", + "threat": "number", + "insult": "number", + "identity_attack": "number", + "sexual_explicit": "number" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "toxicity" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "sredfm", + "hf_id": "Babelscape/SREDFM", + "hf_subsample": "en", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found SREDFM.py", + "needs_custom_loader": false + }, + { + "name": "docred", + "hf_id": "docred/docred", + "hf_subsample": null, + "task": "relation_extraction", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'docred/docred' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": true + }, + { + "name": "ontonotes5", + "hf_id": "conll2012_ontonotesv5/english_v4", + "hf_subsample": null, + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'conll2012_ontonotesv5/english_v4' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "language_identification", + "hf_id": "papluca/language-identification", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "labels": "text", + "text": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "labels" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "glotlid", + "hf_id": "cis-lmu/glotlid", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'cis-lmu/glotlid' is a gated dataset on the Hub. You must be authenticated to access it.", + "needs_custom_loader": false + }, + { + "name": "blog_authorship", + "hf_id": "blog-authorship-corpus/blog-authorship-corpus", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'blog-authorship-corpus/blog-authorship-corpus' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "gsm8k", + "hf_id": "openai/gsm8k", + "hf_subsample": "main", + "task": "text_generation", + "status": "auto_generated", + "columns": { + "question": "text", + "answer": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "hendrycks_math", + "hf_id": "EleutherAI/hendrycks_math", + "hf_subsample": "algebra", + "task": "text_generation", + "status": "skipped_no_yaml", + "columns": { + "problem": "text", + "level": "text", + "type": "text", + "solution": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "daily_dialog", + "hf_id": "daily_dialog/daily_dialog", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'daily_dialog/daily_dialog' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "multiwoz", + "hf_id": "pfb30/multi_woz_v22", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found multi_woz_v22.py", + "needs_custom_loader": false + }, + { + "name": "msmarco_passage", + "hf_id": "mteb/msmarco", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "query-id": "text", + "corpus-id": "text", + "score": "number" + }, + "rows": -1, + "splits": [ + "train", + "dev", + "test" + ], + "yaml_written": true, + "output_cols": [ + "score" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "natural_questions_hard_negatives", + "hf_id": "sentence-transformers/natural-questions", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "query": "text", + "answer": "text" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "winobias", + "hf_id": "uclanlp/wino_bias", + "hf_subsample": "type1_anti", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "document_id": "text", + "part_number": "text", + "word_number": "_list", + "tokens": "_list", + "pos_tags": "_list", + "parse_bit": "_list", + "predicate_lemma": "_list", + "predicate_framenet_id": "_list", + "word_sense": "_list", + "speaker": "_list", + "ner_tags": "_list", + "verbal_predicates": "_list", + "coreference_clusters": "_list" + }, + "rows": -1, + "splits": [ + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "klue_topic", + "hf_id": "klue/klue", + "hf_subsample": "ynat", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "guid": "text", + "title": "text", + "label": "category", + "url": "text", + "date": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "klue_sts", + "hf_id": "klue/klue", + "hf_subsample": "sts", + "task": "regression", + "status": "needs_custom_loader", + "columns": { + "guid": "text", + "source": "text", + "sentence1": "text", + "sentence2": "text", + "labels": "_dict" + }, + "rows": -1, + "splits": [ + "train", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "indic_glue", + "hf_id": "ai4bharat/indic_glue", + "hf_subsample": "wnli-en", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'wnli-en' not found. Available: ['actsa-sc.te', 'bbca.hi', 'copa.en', 'copa.gu', 'copa.hi', 'copa.mr', 'csqa.as', 'csqa.bn', 'csqa.gu', 'csqa.hi', 'csqa.kn', 'csqa.ml', 'csqa.mr', 'csqa.or', 'csqa.pa', 'csqa.ta', 'csqa.te', 'cvit-mkb-clsr.en-bn', 'cvit-mkb-clsr.en-gu', 'cvit-mkb-clsr.en-hi', 'cvit-mkb-clsr.en-ml', 'cvit-mkb-clsr.en-mr', 'cvit-mkb-clsr.en-or', 'cvit-mkb-clsr.en-ta', 'cvit-mkb-clsr.en-te', 'cvit-mkb-clsr.en-ur', 'iitp-mr.hi', 'iitp-pr.hi', 'inltkh.gu', 'inltkh.ml', 'inltkh.mr', 'inltkh.ta', 'inltkh.te', 'md.hi', 'sna.bn', 'wiki-ner.as', 'wiki-ner.bn', 'wiki-ner.gu', 'wiki-ner.hi', 'wiki-ner.kn', 'wiki-ner.ml', 'wiki-ner.mr', 'wiki-ner.or', 'wiki-ner.pa', 'wiki-ner.ta', 'wiki-ner.te', 'wnli.en', 'wnli.gu', 'wnli.hi', 'wnli.mr', 'wstp.as', 'wstp.bn', 'wstp.gu', 'wstp.hi', 'wstp.kn', 'wstp.ml', 'wstp.mr', 'wstp.or', 'wstp.pa', 'wstp.ta', 'wstp.te']", + "needs_custom_loader": false + }, + { + "name": "xnli", + "hf_id": "google/xtreme", + "hf_subsample": "XNLI", + "task": "text_classification", + "status": "skipped_no_yaml", + "columns": { + "language": "text", + "sentence1": "text", + "sentence2": "text", + "gold_label": "text" + }, + "rows": -1, + "splits": [ + "test", + "validation" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "multi_eurlex", + "hf_id": "Muennighoff/multi_eurlex", + "hf_subsample": "en", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found multi_eurlex.py", + "needs_custom_loader": false + }, + { + "name": "blimp", + "hf_id": "nyu-mll/blimp", + "hf_subsample": "anaphor_agreement", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'anaphor_agreement' not found. Available: ['adjunct_island', 'anaphor_gender_agreement', 'anaphor_number_agreement', 'animate_subject_passive', 'animate_subject_trans', 'causative', 'complex_NP_island', 'coordinate_structure_constraint_complex_left_branch', 'coordinate_structure_constraint_object_extraction', 'determiner_noun_agreement_1', 'determiner_noun_agreement_2', 'determiner_noun_agreement_irregular_1', 'determiner_noun_agreement_irregular_2', 'determiner_noun_agreement_with_adj_2', 'determiner_noun_agreement_with_adj_irregular_1', 'determiner_noun_agreement_with_adj_irregular_2', 'determiner_noun_agreement_with_adjective_1', 'distractor_agreement_relational_noun', 'distractor_agreement_relative_clause', 'drop_argument', 'ellipsis_n_bar_1', 'ellipsis_n_bar_2', 'existential_there_object_raising', 'existential_there_quantifiers_1', 'existential_there_quantifiers_2', 'existential_there_subject_raising', 'expletive_it_object_raising', 'inchoative', 'intransitive', 'irregular_past_participle_adjectives', 'irregular_past_participle_verbs', 'irregular_plural_subject_verb_agreement_1', 'irregular_plural_subject_verb_agreement_2', 'left_branch_island_echo_question', 'left_branch_island_simple_question', 'matrix_question_npi_licensor_present', 'npi_present_1', 'npi_present_2', 'only_npi_licensor_present', 'only_npi_scope', 'passive_1', 'passive_2', 'principle_A_c_command', 'principle_A_case_1', 'principle_A_case_2', 'principle_A_domain_1', 'principle_A_domain_2', 'principle_A_domain_3', 'principle_A_reconstruction', 'regular_plural_subject_verb_agreement_1', 'regular_plural_subject_verb_agreement_2', 'sentential_negation_npi_licensor_present', 'sentential_negation_npi_scope', 'sentential_subject_island', 'superlative_quantifiers_1', 'superlative_quantifiers_2', 'tough_vs_raising_1', 'tough_vs_raising_2', 'transitive', 'wh_island', 'wh_questions_object_gap', 'wh_questions_subject_gap', 'wh_questions_subject_gap_long_distance', 'wh_vs_that_no_gap', 'wh_vs_that_no_gap_long_distance', 'wh_vs_that_with_gap', 'wh_vs_that_with_gap_long_distance']", + "needs_custom_loader": false + }, + { + "name": "ceval", + "hf_id": "ceval/ceval-exam", + "hf_subsample": "computer_science", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'computer_science' not found. Available: ['accountant', 'advanced_mathematics', 'art_studies', 'basic_medicine', 'business_administration', 'chinese_language_and_literature', 'civil_servant', 'clinical_medicine', 'college_chemistry', 'college_economics', 'college_physics', 'college_programming', 'computer_architecture', 'computer_network', 'discrete_mathematics', 'education_science', 'electrical_engineer', 'environmental_impact_assessment_engineer', 'fire_engineer', 'high_school_biology', 'high_school_chemistry', 'high_school_chinese', 'high_school_geography', 'high_school_history', 'high_school_mathematics', 'high_school_physics', 'high_school_politics', 'ideological_and_moral_cultivation', 'law', 'legal_professional', 'logic', 'mao_zedong_thought', 'marxism', 'metrology_engineer', 'middle_school_biology', 'middle_school_chemistry', 'middle_school_geography', 'middle_school_history', 'middle_school_mathematics', 'middle_school_physics', 'middle_school_politics', 'modern_chinese_history', 'operating_system', 'physician', 'plant_protection', 'probability_and_statistics', 'professional_tour_guide', 'sports_science', 'tax_accountant', 'teacher_qualification', 'urban_and_rural_planner', 'veterinary_medicine']", + "needs_custom_loader": true + }, + { + "name": "mmmlu", + "hf_id": "openai/MMMLU", + "hf_subsample": "EN_US", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'EN_US' not found. Available: ['default', 'AR_XY', 'BN_BD', 'DE_DE', 'ES_LA', 'FR_FR', 'HI_IN', 'ID_ID', 'IT_IT', 'JA_JP', 'KO_KR', 'PT_BR', 'SW_KE', 'YO_NG', 'ZH_CN']", + "needs_custom_loader": true + }, + { + "name": "gpqa", + "hf_id": "Idavidrein/gpqa", + "hf_subsample": "gpqa_main", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'Idavidrein/gpqa' is a gated dataset on the Hub. You must be authenticated to access it.", + "needs_custom_loader": true + }, + { + "name": "scotus_classification", + "hf_id": "coastalcph/lex_glue", + "hf_subsample": "scotus", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "scitail", + "hf_id": "allenai/scitail", + "hf_subsample": "dgem_format", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "hypothesis": "text", + "label": "text", + "hypothesis_graph_structure": "text" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "qasper", + "hf_id": "allenai/qasper", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found qasper.py", + "needs_custom_loader": false + }, + { + "name": "customer_support_audio", + "hf_id": "HumynLabs/e-commerce-customersupport-english-audio", + "hf_subsample": null, + "task": "audio_classification", + "status": "skipped_no_yaml", + "columns": { + "audio": "audio" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + }, + { + "name": "gift_eval_pretrain", + "hf_id": "Salesforce/GiftEvalPretrain", + "hf_subsample": null, + "task": "time_series_forecasting", + "status": "needs_custom_loader", + "columns": { + "item_id": "text", + "start": "text", + "freq": "text", + "target": "_list", + "past_feat_dynamic_real": "_list" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "chronos_electricity", + "hf_id": "autogluon/chronos_datasets", + "hf_subsample": "electricity_hourly", + "task": "time_series_forecasting", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'electricity_hourly' not found. Available: ['dominick', 'electricity_15min', 'ercot', 'exchange_rate', 'm4_daily', 'm4_hourly', 'm4_monthly', 'm4_quarterly', 'm4_weekly', 'm4_yearly', 'm5', 'mexico_city_bikes', 'monash_australian_electricity', 'monash_car_parts', 'monash_cif_2016', 'monash_covid_deaths', 'monash_electricity_hourly', 'monash_electricity_weekly', 'monash_fred_md', 'monash_hospital', 'monash_kdd_cup_2018', 'monash_london_smart_meters', 'monash_m1_monthly', 'monash_m1_quarterly', 'monash_m1_yearly', 'monash_m3_monthly', 'monash_m3_quarterly', 'monash_m3_yearly', 'monash_nn5_weekly', 'monash_pedestrian_counts', 'monash_rideshare', 'monash_saugeenday', 'monash_temperature_rain', 'monash_tourism_monthly', 'monash_tourism_quarterly', 'monash_tourism_yearly', 'monash_traffic', 'monash_weather', 'nn5', 'solar', 'solar_1h', 'taxi_1h', 'taxi_30min', 'training_corpus_kernel_synth_1m', 'training_corpus_tsmixup_10m', 'uber_tlc_daily', 'uber_tlc_hourly', 'ushcn_daily', 'weatherbench_daily', 'weatherbench_hourly_10m_u_component_of_wind', 'weatherbench_hourly_10m_v_component_of_wind', 'weatherbench_hourly_2m_temperature', 'weatherbench_hourly_geopotential', 'weatherbench_hourly_potential_vorticity', 'weatherbench_hourly_relative_humidity', 'weatherbench_hourly_specific_humidity', 'weatherbench_hourly_temperature', 'weatherbench_hourly_toa_incident_solar_radiation', 'weatherbench_hourly_total_cloud_cover', 'weatherbench_hourly_total_precipitation', 'weatherbench_hourly_u_component_of_wind', 'weatherbench_hourly_v_component_of_wind', 'weatherbench_hourly_vorticity', 'weatherbench_weekly', 'wiki_daily_100k', 'wind_farms_daily', 'wind_farms_hourly']", + "needs_custom_loader": true + }, + { + "name": "tox21", + "hf_id": "DeepChem/tox21", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'DeepChem/tox21' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "bbbp", + "hf_id": "DeepChem/bbbp", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'DeepChem/bbbp' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "esol", + "hf_id": "DeepChem/esol", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'DeepChem/esol' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "lipophilicity", + "hf_id": "DeepChem/lipo", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'DeepChem/lipo' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "jigsaw_toxic_multi", + "hf_id": "jigsaw-toxic-comment-classification-challenge/jigsaw_toxic_comments", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'jigsaw-toxic-comment-classification-challenge/jigsaw_toxic_comments' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "semeval2018_emotion", + "hf_id": "sem_eval_2018_task_1/sem_eval_2018_task_1", + "hf_subsample": "subtask5.english", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'sem_eval_2018_task_1/sem_eval_2018_task_1' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "reuters21578", + "hf_id": "rceborg/reuters-21578", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'rceborg/reuters-21578' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "rvl_cdip", + "hf_id": "rvl-cdip/rvl_cdip", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'rvl-cdip/rvl_cdip' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "rendered_sst2", + "hf_id": "nateraw/rendered-sst2", + "hf_subsample": null, + "task": "image_classification", + "status": "auto_generated", + "columns": { + "image": "image", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "invoice_data", + "hf_id": "katanaml-org/invoices-donut-data-v1", + "hf_subsample": null, + "task": "image_classification", + "status": "needs_custom_loader", + "columns": { + "image": "image", + "ground_truth": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "tabular_benchmark_num_clf", + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "clf_num", + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'clf_num' not found. Available: ['clf_cat_albert', 'clf_cat_compas-two-years', 'clf_cat_covertype', 'clf_cat_default-of-credit-card-clients', 'clf_cat_electricity', 'clf_cat_eye_movements', 'clf_cat_road-safety', 'clf_num_Bioresponse', 'clf_num_Diabetes130US', 'clf_num_Higgs', 'clf_num_MagicTelescope', 'clf_num_MiniBooNE', 'clf_num_bank-marketing', 'clf_num_california', 'clf_num_covertype', 'clf_num_credit', 'clf_num_default-of-credit-card-clients', 'clf_num_electricity', 'clf_num_eye_movements', 'clf_num_heloc', 'clf_num_house_16H', 'clf_num_jannis', 'clf_num_pol', 'reg_cat_Airlines_DepDelay_1M', 'reg_cat_Allstate_Claims_Severity', 'reg_cat_Bike_Sharing_Demand', 'reg_cat_Brazilian_houses', 'reg_cat_Mercedes_Benz_Greener_Manufacturing', 'reg_cat_SGEMM_GPU_kernel_performance', 'reg_cat_abalone', 'reg_cat_analcatdata_supreme', 'reg_cat_delays_zurich_transport', 'reg_cat_diamonds', 'reg_cat_house_sales', 'reg_cat_medical_charges', 'reg_cat_nyc-taxi-green-dec-2016', 'reg_cat_particulate-matter-ukair-2017', 'reg_cat_seattlecrime6', 'reg_cat_topo_2_1', 'reg_cat_visualizing_soil', 'reg_num_Ailerons', 'reg_num_Bike_Sharing_Demand', 'reg_num_Brazilian_houses', 'reg_num_MiamiHousing2016', 'reg_num_abalone', 'reg_num_cpu_act', 'reg_num_delays_zurich_transport', 'reg_num_diamonds', 'reg_num_elevators', 'reg_num_house_16H', 'reg_num_house_sales', 'reg_num_houses', 'reg_num_medical_charges', 'reg_num_nyc-taxi-green-dec-2016', 'reg_num_pol', 'reg_num_sulfur', 'reg_num_superconduct', 'reg_num_wine_quality', 'reg_num_yprop_4_1']", + "needs_custom_loader": false + }, + { + "name": "california_housing_sklearn", + "hf_id": "scikit-learn/california-housing", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'scikit-learn/california-housing' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "diabetes_readmission", + "hf_id": "imodels/diabetes-readmission", + "hf_subsample": null, + "task": "tabular_classification", + "status": "auto_generated", + "columns": { + "time_in_hospital": "number", + "num_lab_procedures": "number", + "num_procedures": "number", + "num_medications": "number", + "number_outpatient": "number", + "number_emergency": "number", + "number_inpatient": "number", + "number_diagnoses": "number", + "change": "number", + "diabetesMed": "number", + "race:AfricanAmerican": "number", + "race:Asian": "number", + "race:Caucasian": "number", + "race:Hispanic": "number", + "race:Other": "number", + "gender:Female": "number", + "gender:Male": "number", + "age:70+": "number", + "age:[0-10)": "number", + "age:[10-20)": "number", + "age:[20-50)": "number", + "age:[50-70)": "number", + "admission_type_id:Elective": "number", + "admission_type_id:Emergency": "number", + "admission_type_id:New Born": "number", + "admission_type_id:Trauma Center": "number", + "discharge_disposition_id:Discharged to Home": "number", + "discharge_disposition_id:Other": "number", + "admission_source_id:Emergency": "number", + "admission_source_id:Other": "number", + "admission_source_id:Referral": "number", + "admission_source_id:Transfer": "number", + "medical_specialty:Cardiology": "number", + "medical_specialty:Emergency/Trauma": "number", + "medical_specialty:Family/GeneralPractice": "number", + "medical_specialty:Gastroenterology": "number", + "medical_specialty:Hematology/Oncology": "number", + "medical_specialty:InternalMedicine": "number", + "medical_specialty:Nephrology": "number", + "medical_specialty:ObstetricsandGynecology": "number", + "medical_specialty:Orthopedics": "number", + "medical_specialty:Other": "number", + "medical_specialty:Psychiatry": "number", + "medical_specialty:Pulmonology": "number", + "medical_specialty:Radiology": "number", + "medical_specialty:Surgery-Cardiovascular/Thoracic": "number", + "medical_specialty:Surgery-General": "number", + "medical_specialty:Urology": "number", + "diag_1:Circulatory": "number", + "diag_1:Diabetes": "number", + "diag_1:Digestive": "number", + "diag_1:Genitourinary": "number", + "diag_1:Infectious": "number", + "diag_1:Injury": "number", + "diag_1:Mental": "number", + "diag_1:Musculoskeletal": "number", + "diag_1:Neoplasms": "number", + "diag_1:Non-diabetes endocrine/metabolic": "number", + "diag_1:Other": "number", + "diag_1:Respiratory": "number", + "diag_1:Skin": "number", + "diag_2:Circulatory": "number", + "diag_2:Diabetes": "number", + "diag_2:Digestive": "number", + "diag_2:Genitourinary": "number", + "diag_2:Infectious": "number", + "diag_2:Injury": "number", + "diag_2:Mental": "number", + "diag_2:Musculoskeletal": "number", + "diag_2:Neoplasms": "number", + "diag_2:Non-diabetes endocrine/metabolic": "number", + "diag_2:Other": "number", + "diag_2:Respiratory": "number", + "diag_2:Skin": "number", + "diag_3:Circulatory": "number", + "diag_3:Diabetes": "number", + "diag_3:Digestive": "number", + "diag_3:Genitourinary": "number", + "diag_3:Infectious": "number", + "diag_3:Injury": "number", + "diag_3:Mental": "number", + "diag_3:Musculoskeletal": "number", + "diag_3:Neoplasms": "number", + "diag_3:Non-diabetes endocrine/metabolic": "number", + "diag_3:Other": "number", + "diag_3:Respiratory": "number", + "diag_3:Skin": "number", + "metformin:Down": "number", + "metformin:No": "number", + "metformin:Steady": "number", + "metformin:Up": "number", + "repaglinide:Down": "number", + "repaglinide:No": "number", + "repaglinide:Steady": "number", + "repaglinide:Up": "number", + "nateglinide:Down": "number", + "nateglinide:No": "number", + "nateglinide:Steady": "number", + "nateglinide:Up": "number", + "chlorpropamide:Down": "number", + "chlorpropamide:No": "number", + "chlorpropamide:Steady": "number", + "chlorpropamide:Up": "number", + "glimepiride:Down": "number", + "glimepiride:No": "number", + "glimepiride:Steady": "number", + "glimepiride:Up": "number", + "glipizide:Down": "number", + "glipizide:No": "number", + "glipizide:Steady": "number", + "glipizide:Up": "number", + "glyburide:Down": "number", + "glyburide:No": "number", + "glyburide:Steady": "number", + "glyburide:Up": "number", + "pioglitazone:Down": "number", + "pioglitazone:No": "number", + "pioglitazone:Steady": "number", + "pioglitazone:Up": "number", + "rosiglitazone:Down": "number", + "rosiglitazone:No": "number", + "rosiglitazone:Steady": "number", + "rosiglitazone:Up": "number", + "acarbose:Down": "number", + "acarbose:No": "number", + "acarbose:Steady": "number", + "acarbose:Up": "number", + "miglitol:Down": "number", + "miglitol:No": "number", + "miglitol:Steady": "number", + "miglitol:Up": "number", + "tolazamide:No": "number", + "tolazamide:Steady": "number", + "tolazamide:Up": "number", + "insulin:Down": "number", + "insulin:No": "number", + "insulin:Steady": "number", + "insulin:Up": "number", + "glyburide-metformin:Down": "number", + "glyburide-metformin:No": "number", + "glyburide-metformin:Steady": "number", + "glyburide-metformin:Up": "number", + "A1Cresult:>7": "number", + "A1Cresult:>8": "number", + "A1Cresult:None": "number", + "A1Cresult:Norm": "number", + "max_glu_serum:>200": "number", + "max_glu_serum:>300": "number", + "max_glu_serum:None": "number", + "max_glu_serum:Norm": "number", + "readmitted": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "time_in_hospital" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "compas_recidivism", + "hf_id": "imodels/compas-recidivism", + "hf_subsample": null, + "task": "tabular_classification", + "status": "auto_generated", + "columns": { + "age": "number", + "priors_count": "number", + "days_b_screening_arrest": "number", + "c_jail_time": "number", + "juv_fel_count": "number", + "juv_other_count": "number", + "juv_misd_count": "number", + "c_charge_degree:F": "number", + "c_charge_degree:M": "number", + "race:African-American": "number", + "race:Asian": "number", + "race:Caucasian": "number", + "race:Hispanic": "number", + "race:Native_American": "number", + "race:Other": "number", + "age_cat:25_-_45": "number", + "age_cat:Greater_than_45": "number", + "age_cat:Less_than_25": "number", + "sex:Female": "number", + "sex:Male": "number", + "is_recid": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "age" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "credit_card_default", + "hf_id": "imodels/credit-card", + "hf_subsample": null, + "task": "tabular_classification", + "status": "auto_generated", + "columns": { + "limit_bal": "number", + "age": "number", + "pay_0": "number", + "pay_2": "number", + "pay_3": "number", + "pay_4": "number", + "pay_5": "number", + "pay_6": "number", + "bill_amt1": "number", + "bill_amt2": "number", + "bill_amt3": "number", + "bill_amt4": "number", + "bill_amt5": "number", + "bill_amt6": "number", + "pay_amt1": "number", + "pay_amt2": "number", + "pay_amt3": "number", + "pay_amt4": "number", + "pay_amt5": "number", + "pay_amt6": "number", + "sex:1": "number", + "sex:2": "number", + "education:0": "number", + "education:1": "number", + "education:2": "number", + "education:3": "number", + "education:4": "number", + "education:5": "number", + "education:6": "number", + "marriage:0": "number", + "marriage:1": "number", + "marriage:2": "number", + "marriage:3": "number", + "default.payment.next.month": "category" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "limit_bal" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "bbh", + "hf_id": "lukaemon/bbh", + "hf_subsample": "boolean_expressions", + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "input": "text", + "target": "text" + }, + "rows": -1, + "splits": [ + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "winogender", + "hf_id": "lighteval/winogender_schemas", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'lighteval/winogender_schemas' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "social_iqa", + "hf_id": "keirp/social_i_qa", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'keirp/social_i_qa' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": true + }, + { + "name": "math500", + "hf_id": "HuggingFaceH4/MATH-500", + "hf_subsample": null, + "task": "text_generation", + "status": "auto_generated", + "columns": { + "problem": "text", + "solution": "text", + "answer": "text", + "subject": "text", + "level": "category", + "unique_id": "text" + }, + "rows": -1, + "splits": [ + "test" + ], + "yaml_written": true, + "output_cols": [ + "answer" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "conll2012", + "hf_id": "di-mi/conll2012_ontonotesv5", + "hf_subsample": "english_v12", + "task": "ner", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'di-mi/conll2012_ontonotesv5' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "docmatix", + "hf_id": "HuggingFaceM4/Docmatix", + "hf_subsample": null, + "task": "visual_question_answering", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: Config name is missing.\nPlease pick one among the available configs: ['images', 'pdf', 'zero-shot-exp']\nExample of usage:\n\t`load_dataset('HuggingFaceM4/Docmatix', 'images')`", + "needs_custom_loader": false + }, + { + "name": "amazon_reviews_2023", + "hf_id": "McAuley-Lab/Amazon-Reviews-2023", + "hf_subsample": "raw_review_All_Beauty", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found Amazon-Reviews-2023.py", + "needs_custom_loader": false + }, + { + "name": "amazon_counterfactual", + "hf_id": "mteb/amazon_counterfactual_classification", + "hf_subsample": "en", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mteb/amazon_counterfactual_classification' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "banking77", + "hf_id": "mteb/banking77", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category", + "label_text": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "mteb_emotion", + "hf_id": "mteb/emotion", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category", + "label_text": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "sst5_setfit", + "hf_id": "SetFit/sst5", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category", + "label_text": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "enron_spam", + "hf_id": "SetFit/enron_spam", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "message_id": "category", + "text": "text", + "label": "category", + "label_text": "text", + "subject": "text", + "message": "text", + "date": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "customer_reviews", + "hf_id": "SetFit/CR", + "hf_subsample": null, + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category", + "label_text": "text" + }, + "rows": -1, + "splits": [ + "train", + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "clinc150", + "hf_id": "mteb/clinc150", + "hf_subsample": "small", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mteb/clinc150' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "cord_v2", + "hf_id": "naver-clova-ix/cord-v2", + "hf_subsample": null, + "task": "image_classification", + "status": "needs_custom_loader", + "columns": { + "image": "image", + "ground_truth": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "tabular_benchmark_cat_reg", + "hf_id": "inria-soda/tabular-benchmark", + "hf_subsample": "reg_cat", + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'reg_cat' not found. Available: ['clf_cat_albert', 'clf_cat_compas-two-years', 'clf_cat_covertype', 'clf_cat_default-of-credit-card-clients', 'clf_cat_electricity', 'clf_cat_eye_movements', 'clf_cat_road-safety', 'clf_num_Bioresponse', 'clf_num_Diabetes130US', 'clf_num_Higgs', 'clf_num_MagicTelescope', 'clf_num_MiniBooNE', 'clf_num_bank-marketing', 'clf_num_california', 'clf_num_covertype', 'clf_num_credit', 'clf_num_default-of-credit-card-clients', 'clf_num_electricity', 'clf_num_eye_movements', 'clf_num_heloc', 'clf_num_house_16H', 'clf_num_jannis', 'clf_num_pol', 'reg_cat_Airlines_DepDelay_1M', 'reg_cat_Allstate_Claims_Severity', 'reg_cat_Bike_Sharing_Demand', 'reg_cat_Brazilian_houses', 'reg_cat_Mercedes_Benz_Greener_Manufacturing', 'reg_cat_SGEMM_GPU_kernel_performance', 'reg_cat_abalone', 'reg_cat_analcatdata_supreme', 'reg_cat_delays_zurich_transport', 'reg_cat_diamonds', 'reg_cat_house_sales', 'reg_cat_medical_charges', 'reg_cat_nyc-taxi-green-dec-2016', 'reg_cat_particulate-matter-ukair-2017', 'reg_cat_seattlecrime6', 'reg_cat_topo_2_1', 'reg_cat_visualizing_soil', 'reg_num_Ailerons', 'reg_num_Bike_Sharing_Demand', 'reg_num_Brazilian_houses', 'reg_num_MiamiHousing2016', 'reg_num_abalone', 'reg_num_cpu_act', 'reg_num_delays_zurich_transport', 'reg_num_diamonds', 'reg_num_elevators', 'reg_num_house_16H', 'reg_num_house_sales', 'reg_num_houses', 'reg_num_medical_charges', 'reg_num_nyc-taxi-green-dec-2016', 'reg_num_pol', 'reg_num_sulfur', 'reg_num_superconduct', 'reg_num_wine_quality', 'reg_num_yprop_4_1']", + "needs_custom_loader": false + }, + { + "name": "mice_protein", + "hf_id": "openml/miceprotein", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'openml/miceprotein' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "anneal", + "hf_id": "openml/anneal", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'openml/anneal' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "vehicle_silhouettes", + "hf_id": "openml/vehicle", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'openml/vehicle' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "pokemon_classification", + "hf_id": "keremberke/pokemon-classification", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found pokemon-classification.py", + "needs_custom_loader": false + }, + { + "name": "flowers102", + "hf_id": "dcdmllm/Flowers102", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'dcdmllm/Flowers102' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "snacks", + "hf_id": "Matthijs/snacks", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found snacks.py", + "needs_custom_loader": false + }, + { + "name": "chest_xray", + "hf_id": "mrm8488/chest-xrays-indiana-university", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mrm8488/chest-xrays-indiana-university' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "imagenette", + "hf_id": "fastai/imagenette", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'fastai/imagenette' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "tomato_disease", + "hf_id": "fcakyon/tomato-disease", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'fcakyon/tomato-disease' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "isic2019", + "hf_id": "andrewmvd/isic-2019", + "hf_subsample": null, + "task": "image_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'andrewmvd/isic-2019' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "speech_commands", + "hf_id": "speech_commands/speech_commands", + "hf_subsample": "v0.02", + "task": "audio_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'speech_commands/speech_commands' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "urbansound8k", + "hf_id": "UrbanSound8K/UrbanSound8K", + "hf_subsample": null, + "task": "audio_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'UrbanSound8K/UrbanSound8K' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "ravdess", + "hf_id": "narad/RAVDESS", + "hf_subsample": null, + "task": "audio_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DataFilesNotFoundError: No (supported) data files found in narad/RAVDESS", + "needs_custom_loader": false + }, + { + "name": "emodb", + "hf_id": "renumics/emodb", + "hf_subsample": null, + "task": "audio_classification", + "status": "auto_generated", + "columns": { + "age": "number", + "gender": "category", + "emotion": "category", + "audio": "audio" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "emotion" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "mls_german", + "hf_id": "facebook/multilingual_librispeech", + "hf_subsample": "german", + "task": "automatic_speech_recognition", + "status": "auto_generated", + "columns": { + "audio": "audio", + "original_path": "text", + "begin_time": "number", + "end_time": "number", + "transcript": "text", + "audio_duration": "number", + "speaker_id": "text", + "chapter_id": "text", + "file": "text", + "id": "text" + }, + "rows": -1, + "splits": [ + "dev", + "test", + "train", + "9_hours", + "1_hours" + ], + "yaml_written": true, + "output_cols": [ + "begin_time" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "common_voice_11", + "hf_id": "mozilla-foundation/common_voice_11_0", + "hf_subsample": "en", + "task": "automatic_speech_recognition", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mozilla-foundation/common_voice_11_0' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "speech_massive", + "hf_id": "FBK-MT/Speech-MASSIVE", + "hf_subsample": "en-US", + "task": "audio_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: BuilderConfig 'en-US' not found. Available: ['all', 'ar-SA', 'de-DE', 'es-ES', 'fr-FR', 'hu-HU', 'ko-KR', 'nl-NL', 'pl-PL', 'pt-PT', 'ru-RU', 'tr-TR', 'vi-VN']", + "needs_custom_loader": false + }, + { + "name": "cantonese_asr", + "hf_id": "CanCLID/zoengjyutgaai", + "hf_subsample": null, + "task": "automatic_speech_recognition", + "status": "auto_generated", + "columns": { + "id": "text", + "episode_id": "category", + "audio": "audio", + "audio_duration": "number", + "transcription": "text" + }, + "rows": -1, + "splits": [ + "saamgwokjinji", + "seoiwuzyun", + "mouzaakdung", + "lukdinggei" + ], + "yaml_written": true, + "output_cols": [ + "episode_id" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "naver_news_summary", + "hf_id": "daekeun-ml/naver-news-summarization-ko", + "hf_subsample": null, + "task": "summarization", + "status": "auto_generated", + "columns": { + "date": "text", + "category": "text", + "press": "text", + "title": "text", + "document": "text", + "link": "text", + "summary": "text" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": true, + "output_cols": [ + "category" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "cnn_dailymail_ccdv", + "hf_id": "ccdv/cnn_dailymail", + "hf_subsample": "3.0.0", + "task": "summarization", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "RuntimeError: Dataset scripts are no longer supported, but found cnn_dailymail.py", + "needs_custom_loader": false + }, + { + "name": "glue_diagnostic", + "hf_id": "nyu-mll/glue", + "hf_subsample": "ax", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "premise": "text", + "hypothesis": "text", + "label": "category", + "idx": "category" + }, + "rows": -1, + "splits": [ + "test" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "fever_gold", + "hf_id": "copenlu/fever_gold_evidence", + "hf_subsample": null, + "task": "text_classification", + "status": "needs_custom_loader", + "columns": { + "claim": "text", + "label": "text", + "evidence": "_list", + "id": "text", + "verifiable": "text", + "original_id": "category" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "dyda_dialog_acts", + "hf_id": "silicone/silicone", + "hf_subsample": "dyda_da", + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'silicone/silicone' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "ai2d_diagrams", + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "ai2d", + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "images": "_list", + "texts": "_list" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "textvqa", + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "textvqa", + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "images": "_list", + "texts": "_list" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "vqav2", + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "vqav2", + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "images": "_list", + "texts": "_list" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "docvqa", + "hf_id": "HuggingFaceM4/the_cauldron", + "hf_subsample": "docvqa", + "task": "visual_question_answering", + "status": "needs_custom_loader", + "columns": { + "images": "_list", + "texts": "_list" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "winoground", + "hf_id": "facebook/winoground", + "hf_subsample": null, + "task": "visual_question_answering", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'facebook/winoground' is a gated dataset on the Hub. You must be authenticated to access it.", + "needs_custom_loader": false + }, + { + "name": "acronym_identification", + "hf_id": "amirveyseh/acronym_identification", + "hf_subsample": null, + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "id": "text", + "tokens": "_list", + "labels": "_list" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "wikiann_de", + "hf_id": "unimelb-nlp/wikiann", + "hf_subsample": "de", + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "tokens": "_list", + "ner_tags": "_list", + "langs": "_list", + "spans": "_list" + }, + "rows": -1, + "splits": [ + "validation", + "test", + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "wikiann_zh", + "hf_id": "unimelb-nlp/wikiann", + "hf_subsample": "zh", + "task": "ner", + "status": "needs_custom_loader", + "columns": { + "tokens": "_list", + "ner_tags": "_list", + "langs": "_list", + "spans": "_list" + }, + "rows": -1, + "splits": [ + "validation", + "test", + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "admet_benchmark", + "hf_id": "datamol-io/polaris-admet-benchmark", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'datamol-io/polaris-admet-benchmark' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "obesity_prediction", + "hf_id": "mstz/obesity", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/obesity' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "dry_bean", + "hf_id": "mstz/dry_bean", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/dry_bean' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "rice_classification", + "hf_id": "mstz/rice", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/rice' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "water_quality", + "hf_id": "mstz/water_quality", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/water_quality' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "room_occupancy", + "hf_id": "mstz/room_occupancy", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/room_occupancy' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "online_shoppers", + "hf_id": "mstz/online_shoppers", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/online_shoppers' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "airline_satisfaction", + "hf_id": "mstz/airline_passenger_satisfaction", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/airline_passenger_satisfaction' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "spaceship_titanic", + "hf_id": "mstz/spaceship_titanic", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/spaceship_titanic' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "stroke_prediction", + "hf_id": "mstz/stroke_prediction", + "hf_subsample": null, + "task": "tabular_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/stroke_prediction' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "student_performance", + "hf_id": "mstz/student_performance", + "hf_subsample": null, + "task": "regression", + "status": "auto_generated", + "columns": { + "is_male": "binary", + "ethnicity": "text", + "parental_level_of_education": "category", + "has_standard_lunch": "binary", + "has_completed_preparation_test": "binary", + "reading_score": "category", + "writing_score": "category", + "has_passed_math_exam": "category" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": true, + "output_cols": [ + "is_male" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "used_cars_price", + "hf_id": "mstz/used_cars", + "hf_subsample": null, + "task": "regression", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'mstz/used_cars' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "belebele_fr", + "hf_id": "facebook/belebele", + "hf_subsample": "fra_Latn", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "link": "text", + "question_number": "category", + "flores_passage": "text", + "question": "text", + "mc_answer1": "text", + "mc_answer2": "text", + "mc_answer3": "text", + "mc_answer4": "text", + "correct_answer_num": "text", + "dialect": "text", + "ds": "text" + }, + "rows": -1, + "splits": [ + "test" + ], + "yaml_written": true, + "output_cols": [ + "question_number" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "duorc", + "hf_id": "ibm/duorc", + "hf_subsample": "SelfRC", + "task": "extractive_qa", + "status": "needs_custom_loader", + "columns": { + "plot_id": "text", + "plot": "text", + "title": "text", + "question_id": "text", + "question": "text", + "answers": "_list", + "no_answer": "binary" + }, + "rows": -1, + "splits": [ + "train", + "validation", + "test" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": true + }, + { + "name": "nq_open", + "hf_id": "nq_open/nq_open", + "hf_subsample": null, + "task": "text_generation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'nq_open/nq_open' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "web_questions", + "hf_id": "web_questions/web_questions", + "hf_subsample": null, + "task": "text_generation", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "DatasetNotFoundError: Dataset 'web_questions/web_questions' doesn't exist on the Hub or cannot be accessed.", + "needs_custom_loader": false + }, + { + "name": "tweeteval_stance", + "hf_id": "cardiffnlp/tweet_eval", + "hf_subsample": "stance_abortion", + "task": "text_classification", + "status": "auto_generated", + "columns": { + "text": "text", + "label": "category" + }, + "rows": -1, + "splits": [ + "train", + "test", + "validation" + ], + "yaml_written": true, + "output_cols": [ + "label" + ], + "error": null, + "needs_custom_loader": false + }, + { + "name": "claim_stance", + "hf_id": "ibm/claim_stance", + "hf_subsample": null, + "task": "text_classification", + "status": "error", + "columns": {}, + "rows": -1, + "splits": [], + "yaml_written": false, + "output_cols": [], + "error": "ValueError: Config name is missing.\nPlease pick one among the available configs: ['claim_stance', 'claim_stance_topic']\nExample of usage:\n\t`load_dataset('ibm/claim_stance', 'claim_stance')`", + "needs_custom_loader": false + }, + { + "name": "chartnet", + "hf_id": "ibm-granite/ChartNet", + "hf_subsample": null, + "task": "visual_question_answering", + "status": "skipped_no_yaml", + "columns": { + "id": "text", + "image": "image", + "code": "text", + "csv": "text", + "summary": "text", + "chart_type": "text", + "library": "text" + }, + "rows": -1, + "splits": [ + "train" + ], + "yaml_written": false, + "output_cols": [], + "error": null, + "needs_custom_loader": false, + "debug_output_cols": null + } +] diff --git a/scripts/smoke_results.json b/scripts/smoke_results.json new file mode 100644 index 0000000..54b456b --- /dev/null +++ b/scripts/smoke_results.json @@ -0,0 +1,3542 @@ +[ + { + "name": "acronym_identification", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "adult_census_income", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "adult_income_hf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "ae_price_prediction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "ag_news_hf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "agnews", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "ai2d_diagrams", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "allstate_claims_severity", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "alpaca", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "amazon_employee_access_challenge", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "amazon_polarity", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "amazon_review_polarity", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "amazon_reviews", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "ames_housing", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "anli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "arc_challenge", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "arc_easy", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "banking77", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "bbcnews", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "bbh", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "beans", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "beavertails", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "belebele", + "status": "pass", + "error": null, + "rows": 900 + }, + { + "name": "belebele_fr", + "status": "pass", + "error": null, + "rows": 900 + }, + { + "name": "billsum", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "bnp_claims_management", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "bookprice_prediction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "boolq", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "boolq_standalone", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "california_house_price", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "california_housing", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "camseq", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "cantonese_asr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "cifar10", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "cifar100", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "civil_comments", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "climate_sentiment", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "cnn_dailymail", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "code_alpaca", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "code_search_net", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "codexglue_code_to_text", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "cola", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "commitment_bank", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "commonsense_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "compas_recidivism", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "connect4", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "consumer_complaints", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "copa", + "status": "pass", + "error": null, + "rows": 400 + }, + { + "name": "cord_v2", + "status": "pass", + "error": null, + "rows": 800 + }, + { + "name": "credit_card_default", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "creditcard_fraud", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "customer_churn_prediction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "customer_reviews", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "data_scientist_salary", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "dbpedia", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "dbpedia_14", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "diabetes_readmission", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "dialogsum", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "docvqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "duorc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "electricity", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "electricity_tabular", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "emodb", + "status": "pass", + "error": null, + "rows": 535 + }, + { + "name": "emotion", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "enron_spam", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "esc50", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "eurosat", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "fake_job_postings2", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "fake_news_detection", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "fashion_mnist", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "fever", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "fever_gold", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "few_nerd", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "flickr8k", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "food101", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "forest_cover", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "go_emotions", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "go_emotions_multiclass", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "goemotions", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "goodbooks_books", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "google_qa_answer_type_reason_explanation", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "google_qa_question_type_reason_explanation", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "google_quest_qa", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "gsm8k", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gtsrb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "heart_failure", + "status": "pass", + "error": null, + "rows": 299 + }, + { + "name": "hellaswag", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "higgs", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "hotpot_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hugging_face", + "status": "skipped", + "error": null, + "rows": 0 + }, + { + "name": "ieee_fraud", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "imbalanced_insurance", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "imdb", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "imdb_genre_prediction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "imdb_sentiment", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "insurance_lite", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "invoice_data", + "status": "pass", + "error": null, + "rows": 425 + }, + { + "name": "iris", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "irony", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "jc_penney_products", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "jigsaw_unintended_bias", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "jigsaw_unintended_bias100k", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "kdd_appetency", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "kdd_churn", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "kdd_upselling", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "kick_starter_funding", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "klue_sts", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "klue_topic", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "language_identification", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "lex_glue_ecthr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "lex_glue_eurlex", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "librispeech", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "math500", + "status": "pass", + "error": null, + "rows": 500 + }, + { + "name": "mathvista", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mbpp", + "status": "pass", + "error": null, + "rows": 374 + }, + { + "name": "measuring_hate_speech", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "medmcqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "melbourne_airbnb", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "mercari_price_suggestion", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "mercari_price_suggestion100K", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "mercedes_benz_greener", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "mls_german", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mmlu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mmlu_pro", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mnist", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "mnli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mrpc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "msmarco_passage", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_emotion", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "multi_nli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "multinerd", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "multirc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mushroom_edibility", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "natural_questions", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "natural_questions_hard_negatives", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "naval", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "naver_news_summary", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "news_channel", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "news_popularity2", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "newyorker_caption_contest", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "noshow_appointments", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "numerai28pt6", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "ohsumed_7400", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "ohsumed_cmu", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "openbookqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "opus100_en_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "opus100_en_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "opus_books_en_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "otto_group_product", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "oxford_pets", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "paws", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "peoples_speech", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "pii_masking", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "poker_hand", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "porto_seguro_safe_driver", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "product_sentiment_machine_hack", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "protein", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "pubmed_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "qnli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "qqp", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "rendered_sst2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "reuters_cmu", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "reuters_r8", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "rossman_store_sales", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "rotten_tomatoes", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "rte", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "samsum", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "santander_customer_satisfaction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "santander_customer_transaction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "santander_value_prediction", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "sarcastic_headlines", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "sarcos", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "scienceqa_vqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sciq", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "scitail", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "scotus_classification", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "snli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "squad", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "squad_v2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sst2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sst2_hf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sst3", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sst5", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sst5_setfit", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "stanford_cars", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts_benchmark", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "stsb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "student_performance", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sun397", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "superglue_rte", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "svhn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "synthetic_fraud", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "talkingdata_adtrack_fraud", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "telco_customer_churn", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "temperature", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "textvqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tiny_imagenet", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "titanic", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "trivia_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "truthful_qa", + "status": "pass", + "error": null, + "rows": 817 + }, + { + "name": "tweet_sentiment_extraction", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweeteval_emotion", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweeteval_hate", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweeteval_irony", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweeteval_offensive", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweeteval_sentiment", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweeteval_stance", + "status": "pass", + "error": null, + "rows": 587 + }, + { + "name": "twitter_bots", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "voxpopuli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "vqa_rad", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "vqav2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "walmart_recruiting", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "wic", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wikiann", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wikiann_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wikiann_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wikiann_zh", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wikiart", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wine_reviews", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "winobias", + "status": "pass", + "error": null, + "rows": 396 + }, + { + "name": "winograd_schema", + "status": "pass", + "error": null, + "rows": 554 + }, + { + "name": "winogrande", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wmt14_de_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wmt15", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "wmt16_de_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wmt19_de_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wnli", + "status": "pass", + "error": null, + "rows": 635 + }, + { + "name": "women_clothing_review", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "xnli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xsum", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "yahoo_answers", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "yelp_review_full", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "yelp_review_polarity", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "yelp_reviews", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "yosemite", + "status": "skipped", + "error": "Not an HF dataset (pre-existing Ludwig dataset)", + "rows": 0 + }, + { + "name": "aqua_rat", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "numinamath", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "bitext_customer_support", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "phinc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "nemotron_safety", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "orca_math", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "python_code_instructions", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "race", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "ami_asr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "flashrag_2wikimultihop", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "indic_glue", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "big_patent", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "multi30k", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "codex_thinking", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "qasc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mnist_ylecun", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "naturelm_audio", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gaia_rrlyrae", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "paws_x", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "imagenet_100", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "medical_flashcards", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "farstail_nli", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hermes_function_calling", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mmsulab", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gaia_cepheids", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gaiasky_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "amazon_massive_intent", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "brazilian_toxic_tweets", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "kmmlu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "factcheck", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "abjad_kids", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "nq_open", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "drop", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mini_imagenet", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "amazon_science_massive", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "minds14", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "m3cot", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "nli_zh_all", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "blimp", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "cmrc2018", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "alpaca_gpt4_zh", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "handwritten_crossouts", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mobile_mold", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "intuitive_physics", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "nemotron_pii", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "merit", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "graid_bdd", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "aegis_safety", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "clue_afqmc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "code_defect_detection", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "bigbench", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "banking77_legacy", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "code_contests", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "arxiv_abstracts_2021", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "bornholm_bitext", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gaia_spectroscopic_binaries", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "kvasir_vqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gaia_young_stellar_objects", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "ccnews", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_bg_cs", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "pubmed_summarization", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "map_trace", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hc3_chinese", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "flores_101", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "m_mmlu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "govreport_summarization", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "amazon_massive_scenario", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "eurosat_rgb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "bitext_customer_intent", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "alpaca_gpt4", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "moral_stories", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "ai_generated_ecommerce", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "databricks_dolly_15k", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "databench_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "audioset_balanced", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "kilt_nq", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "imdb_mteb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "multiun_ar_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "arxiv_summarization", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hc3", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "amazon_reviews_2023", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mmlu_lighteval", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "clinc_oos", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "coig_cqia", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "openfake", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "or_bench", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "path_vqa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "ravnursson_asr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "repid", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "reward_bench", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "scandisent", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setimes_bg_bs", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sib200", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sickr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sms_spam", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "speech_massive", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "spotify_tracks", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "stackoverflow_posts", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "stocks_daily_price", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts12", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts13", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts14", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts15", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts16", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts17", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "sts22", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "stsb_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "stsb_sentencetransformers", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "synthia", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tadabur", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "taix_ray", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tobacco_document", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tofu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "toxic_chat", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "twitter_financial_news_topic", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "universal_dependencies", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "vukuzenzele", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "web_questions", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wiki_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wmt_t2t_de_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "world_speech_asr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "yahoo_answers_topics", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "yelp_polarity", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "aeslc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "allocine", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "ambig_qa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "app_reviews", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "climate_fever", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "financial_phrasebank", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "germeval18", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hate_speech18", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hatespeech_offensive", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_polarity", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_imdb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "alpaca_cleaned", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "cnn_dm_hf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "dair_emotion", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "dolly_15k", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_bg_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_cs_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_da_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_de_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_el_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_it", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_nl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_pl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_pt", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_ro", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "europarl_en_sv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "fineweb_edu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "gsm8k_openai", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "helpsteer2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "hh_rlhf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "iris_sklearn", + "status": "pass", + "error": null, + "rows": 150 + }, + { + "name": "lmsys_arena", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_sts17_ar", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "mteb_sts17_de", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "mteb_sts17_en", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "mteb_sts17_es", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "mteb_sts17_fr", + "status": "pass", + "error": null, + "rows": 250 + }, + { + "name": "mteb_toxic_convo", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_tweet_sentiment", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "news_category", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "no_robots", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "oasst1", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "orca_dpo_pairs", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "persuasion", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "poem_sentiment", + "status": "pass", + "error": null, + "rows": 892 + }, + { + "name": "poem_sentiment_hf", + "status": "pass", + "error": null, + "rows": 892 + }, + { + "name": "setfit_ag_news", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setfit_amazon_polarity", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setfit_emotion", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setfit_mrpc", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setfit_sst2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setfit_subj", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "setfit_yelp_review", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "tweet_eval_emoji", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "wildchat", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "winogrande_hf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xnli_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xnli_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xnli_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xnli_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xnli_zh", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "xsum_hf", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_reviews_class_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_reviews_class_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_reviews_class_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_reviews_class_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_reviews_class_ja", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_amazon_reviews_class_zh", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_biosses", + "status": "pass", + "error": null, + "rows": 100 + }, + { + "name": "mteb_massive_intent_ar", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_zh_cn", + "status": "pass", + "error": null, + "rows": 105 + }, + { + "name": "mteb_sts22_ar", + "status": "pass", + "error": null, + "rows": 262 + }, + { + "name": "mteb_sts22_de", + "status": "pass", + "error": null, + "rows": 756 + }, + { + "name": "mteb_sts22_de_en", + "status": "pass", + "error": null, + "rows": 535 + }, + { + "name": "mteb_sts22_de_fr", + "status": "pass", + "error": null, + "rows": 74 + }, + { + "name": "mteb_sts22_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_sts22_es", + "status": "pass", + "error": null, + "rows": 561 + }, + { + "name": "mteb_sts22_es_en", + "status": "pass", + "error": null, + "rows": 365 + }, + { + "name": "mteb_sts22_es_it", + "status": "pass", + "error": null, + "rows": 212 + }, + { + "name": "mteb_sts22_fr", + "status": "pass", + "error": null, + "rows": 72 + }, + { + "name": "mteb_sts22_it", + "status": "pass", + "error": null, + "rows": 407 + }, + { + "name": "mteb_sts22_pl", + "status": "pass", + "error": null, + "rows": 309 + }, + { + "name": "mteb_sts22_pl_en", + "status": "pass", + "error": null, + "rows": 48 + }, + { + "name": "mteb_sts22_ru", + "status": "pass", + "error": null, + "rows": 265 + }, + { + "name": "mteb_sts22_tr", + "status": "pass", + "error": null, + "rows": 390 + }, + { + "name": "mteb_sts22_zh", + "status": "pass", + "error": null, + "rows": 637 + }, + { + "name": "mteb_sts22_zh_en", + "status": "pass", + "error": null, + "rows": 161 + }, + { + "name": "mteb_stsbenchmark", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_financial_phrasebank", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_jd_review", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_domain_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_domain_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_domain_es", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_domain_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_domain_hi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_domain_th", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_intent_de2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_intent_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_intent_es2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_intent_fr2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_intent_hi2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_mtop_intent_th2", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_online_shopping", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_poem_sentiment", + "status": "pass", + "error": null, + "rows": 892 + }, + { + "name": "mteb_sentiment_hindi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_tnews", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_tweet_topic", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_waimai", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_yahoo_answers", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_af", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_am", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_az", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_bn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_cy", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_da", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_el", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_fa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_fi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_he", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_hi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_hu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_hy", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_id", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_is", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_it", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ja", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_jv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ka", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_km", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_kn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ko", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_lv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ml", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_mn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ms", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_my", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_nb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_nl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_pl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_pt", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ro", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ru", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_sl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_sq", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_sv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_sw", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ta", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_te", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_th", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_tl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_tr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_ur", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_vi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_zh_cn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_intent_zh_tw", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_af", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_am", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ar", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_az", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_bn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_cy", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_da", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_el", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_fa", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_fi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_he", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_hi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_hu", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_hy", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_id", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_is", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_it", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ja", + "status": "pass", + "error": null, + "rows": 527 + }, + { + "name": "mteb_massive_scenario_jv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ka", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_km", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_kn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ko", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_lv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ml", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_mn", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ms", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_my", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_nb", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_nl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_pl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_pt", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ro", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ru", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_sl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_sq", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_sv", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_sw", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ta", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_te", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_th", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_tl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_tr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_ur", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_vi", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_massive_scenario_zh_tw", + "status": "pass", + "error": null, + "rows": 581 + }, + { + "name": "mteb_cyrillic_turkic", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_frenk_en", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_frenk_hr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_frenk_sl", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_georeview", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_greek_legal", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_ita_casehold", + "status": "pass", + "error": null, + "rows": 792 + }, + { + "name": "mteb_kor_sarcasm", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_language_class", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_multilingual_sentiment", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_naija_senti_hau", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_naija_senti_ibo", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_naija_senti_pcm", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_naija_senti_yor", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_nepali_news", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_nordic_lang", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_sensitive_topics", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_swiss_judgement_de", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_swiss_judgement_fr", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_swiss_judgement_it", + "status": "pass", + "error": null, + "rows": 1000 + }, + { + "name": "mteb_turkish_product", + "status": "pass", + "error": null, + "rows": 1000 + } +] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..1d26b58 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,114 @@ +# Test Guide + +Assuming your CWD is the Ludwig repo root. + +## Basic + +```bash +pytest -vs tests +``` + +## Private Tests + +These tests connect to services like remote filesystems (Minio / S3), which can be run locally using Docker. + +```bash +# prepare test services +docker-compose -f tests/docker-compose.yml up + +# run all tests +RUN_PRIVATE=1 pytest -vs tests +``` + +## Slow Tests + +These tests are very slow, and should typically be run on GPU machines. + +```bash +RUN_SLOW=1 pytest -vs tests +``` + +## Running GitHub Actions Locally + +It is possible to run the CI test suite locally by executing the `pytest` action using +[act](https://github.com/nektos/act). + +First start up the local minio container, if it is not already running. Then call `act -j pytest` to run the test suite. + +``` +# Start minio container in background +docker-compose -f tests/docker-compose.yml up -d + +# Run local test suite +RUN_PRIVATE=1 act -j pytest +``` + +## Tests that use ray clusters + +Use the distributed pytest decorator to make sure that the test runs on CI jobs with the right ray dependencies installed. + +```python +@pytest.mark.distributed +def test_something(ray_cluster_2_cpu): + pass +``` + +Use module-level pytest fixtures to share ray cluster startup and teardown overhead at the module level. List of fixtures are found in `conftest.py`, for example: + +```python +@pytest.fixture(scope="module") +def ray_cluster_2cpu(request): + with _ray_start(request, num_cpus=2): + yield +``` + +## Grouped Integration Tests + +To leverage more runners to cut Ludwig CI time down, we partition `tests/integration_tests` into 3 groups (A, B, default). Each group should take on a roughly equal share of testing time, which at the time of writing is ~45 minutes each. + +To define a new group and use it in tests: + +1. Define a new pytest marker in `pytest.ini`. + +```ini +integration_tests_a: mark a test to be run as part of integration tests, group A. +integration_tests_b: mark a test to be run as part of integration tests, group B. +# (new) +integration_tests_c: mark a test to be run as part of integration tests, group C. +``` + +2. Use the marker in a test file under `tests/integration_tests/`. + +```python +import pytest + +pytestmark = pytest.mark.integration_tests_c +``` + +If there's already a `pytestmark` declaration, turn it into a list. + +```python +import pytest + +pytestmark = [pytest.mark.distributed, pytest.mark.integration_tests_c] +``` + +If there's a specific test to include in the group, decorate the test function. + +```python +@pytest.mark.integration_tests_c +def test_something(): + pass +``` + +3. Create a new GHA to run pytest with that marker. + +You can use [this change](https://github.com/ludwig-ai/ludwig/pull/3391/files#diff-2500680f4bc6c1b75c3d4b36372bf4d64c5f603b90bfd7a5186f66a20329d16aR189-R245) as a reference. + +NOTE: Be sure to update other Integration Test GHA pytest jobs to exclude tests under the new marker. + +To check which tests would be run under the `pytest` command without actually running them, use `--collect-only`. + +```sh +pytest -m "not distributed and not slow and not combinatorial and not llm and integration_tests_c" --junitxml pytest.xml tests/integration_tests --collect-only +``` diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..42715bb --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6311355 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,246 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import contextlib +import os +import tempfile +import time +import uuid +from unittest import mock + +import pytest + +from ludwig.constants import ( + BATCH_SIZE, + COMBINER, + EPOCHS, + HYPEROPT, + INPUT_FEATURES, + NAME, + OUTPUT_FEATURES, + TRAINER, + TYPE, +) +from ludwig.hyperopt.run import hyperopt +from tests.integration_tests.utils import category_feature, generate_data, text_feature + +TEST_SUITE_TIMEOUT_S = int(os.environ.get("LUDWIG_TEST_SUITE_TIMEOUT_S", 3600)) + + +explicit_int_markers = { + "integration_tests_a", + "integration_tests_b", + "integration_tests_c", + "integration_tests_d", + "integration_tests_e", + "integration_tests_f", + "integration_tests_g", + "integration_tests_h", + "integration_tests_i", +} + + +def pytest_sessionstart(session): + session.start_time = time.time() + + +def pytest_collection_modifyitems(config, items): + for item in items: + if all(False for x in item.iter_markers() if x.name in explicit_int_markers): + item.add_marker("integration_tests_j") + + +@pytest.fixture(autouse=True) +def check_session_time(request): + elapsed = time.time() - request.session.start_time + if elapsed > TEST_SUITE_TIMEOUT_S: + request.session.shouldstop = "time limit reached: %0.2f seconds" % elapsed + + +@pytest.fixture(autouse=True) +def setup_tests(request): + if "distributed" not in request.keywords: + # Only run this patch if we're running distributed tests, otherwise Ray will not be installed + # and this will fail. + # See: https://stackoverflow.com/a/38763328 + yield + return + + with mock.patch("ludwig.backend.ray.init_ray_local") as mock_init_ray_local: + mock_init_ray_local.side_effect = RuntimeError("Ray must be initialized explicitly when running tests") + yield mock_init_ray_local + + +@pytest.fixture() +def csv_filename(): + """Yields a csv filename for holding temporary data.""" + with tempfile.TemporaryDirectory() as tmpdir: + csv_filename = os.path.join(tmpdir, uuid.uuid4().hex[:10].upper() + ".csv") + yield csv_filename + + +@pytest.fixture() +def yaml_filename(): + """Yields a yaml filename for holding a temporary config.""" + with tempfile.TemporaryDirectory() as tmpdir: + yaml_filename = os.path.join(tmpdir, "model_def_" + uuid.uuid4().hex[:10].upper() + ".yaml") + yield yaml_filename + + +@pytest.fixture(scope="module") +def hyperopt_results_single_parameter(ray_cluster_4cpu): + """This fixture is used by hyperopt visualization tests in test_visualization_api.py.""" + config, rel_path = _get_sample_config() + config[HYPEROPT] = { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.0001, + "upper": 0.01, + } + }, + "goal": "minimize", + "output_feature": config[OUTPUT_FEATURES][0][NAME], + "validation_metrics": "loss", + "executor": { + "type": "ray", + "num_samples": 2, + }, + "search_alg": { + "type": "variant_generator", + }, + } + # Prevent resume from failure since this results in failures in other tests + hyperopt(config, dataset=rel_path, output_directory="results", experiment_name="hyperopt_test", resume=False) + return os.path.join(os.path.abspath("results"), "hyperopt_test") + + +@pytest.fixture(scope="module") +def hyperopt_results_multiple_parameters(ray_cluster_4cpu): + """This fixture is used by hyperopt visualization tests in test_visualization_api.py.""" + config, rel_path = _get_sample_config() + output_feature_name = config[OUTPUT_FEATURES][0][NAME] + config[HYPEROPT] = { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.0001, + "upper": 0.01, + }, + output_feature_name + ".decoder.fc_output_size": {"space": "choice", "categories": [32, 64, 128, 256]}, + output_feature_name + ".decoder.num_fc_layers": {"space": "randint", "lower": 1, "upper": 6}, + }, + "goal": "minimize", + "output_feature": output_feature_name, + "validation_metrics": "loss", + "executor": { + "type": "ray", + "num_samples": 2, + }, + "search_alg": { + "type": "variant_generator", + }, + } + # Prevent resume from failure since this results in failures in other tests + hyperopt(config, dataset=rel_path, output_directory="results", experiment_name="hyperopt_test", resume=False) + return os.path.join(os.path.abspath("results"), "hyperopt_test") + + +@pytest.fixture(scope="module") +def ray_cluster_2cpu(request): + with _ray_start(request, num_cpus=2): + yield + + +@pytest.fixture(scope="module") +def ray_cluster_4cpu(request): + with _ray_start(request, num_cpus=4): + yield + + +@pytest.fixture(scope="module") +def ray_cluster_5cpu(request): + with _ray_start(request, num_cpus=5): + yield + + +@pytest.fixture(scope="module") +def ray_cluster_7cpu(request): + with _ray_start(request, num_cpus=7): + yield + + +@contextlib.contextmanager +def _ray_start(request, **kwargs): + try: + import ray + except ImportError: + if "distributed" in request.keywords: + raise + + # Allow this fixture to run in environments where Ray is not installed + # for parameterized tests that mix Ray with non-Ray backends + yield None + return + + init_kwargs = _get_default_ray_kwargs() + init_kwargs.update(kwargs) + # HACK(geoffrey): `hyperopt_resources` is a required resource for hyperopt to prevent deadlocks in Ludwig tests. + # For context, if there are 4 hyperopt trials scheduled and 7 CPUs available, then the trial driver will require + # some resource to run *in addition* to the resources required by the trainer downstream. If we use 1 CPU + # (default trial driver request), then the trial will be scheduled on 1 CPU and the trainer will later request + # an additional 1 CPU. Across all 4 trials, this will possibly consume >7 CPUs, causing a deadlock since + # Ray Datasets will not be able to grab resources for data preprocessing. + # + # By adding a `hyperopt_resources` resource, we can ensure that the trial driver will be scheduled without + # consuming any CPU resources. This allows each trial's trainer to request 1 CPU without starving Ray Datasets. + # TODO(geoffrey): remove for Ray 2.2 + res = ray.init(**init_kwargs, resources={"hyperopt_resources": 1000}) + try: + yield res + finally: + ray.shutdown() + # Delete the cluster address just in case. + if hasattr(ray._private.utils, "reset_ray_address"): + ray._private.utils.reset_ray_address() + + +def _get_default_ray_kwargs(): + ray_kwargs = { + "num_cpus": 1, + "object_store_memory": 150 * 1024 * 1024, + "dashboard_port": None, + "include_dashboard": False, + "namespace": "default_test_namespace", + "ignore_reinit_error": True, + } + return ray_kwargs + + +def _get_sample_config(): + """Returns a sample config.""" + input_features = [ + text_feature(name="utterance", encoder={"cell_type": "lstm", "reduce_output": "sum"}), + category_feature(encoder={"vocab_size": 2}, reduce_input="sum"), + ] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + csv_filename = uuid.uuid4().hex[:10].upper() + ".csv" + rel_path = generate_data(input_features, output_features, csv_filename) + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat", "num_fc_layers": 2}, + TRAINER: {EPOCHS: 2, "learning_rate": 0.001, BATCH_SIZE: 128}, + } + return config, rel_path diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..611725c --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' + +services: + minio: + image: 'minio/minio:latest' + volumes: + - minio_storage:/data + ports: + - 9000:9000 + - 9001:9001 + environment: + - MINIO_ACCESS_KEY=minio + - MINIO_SECRET_KEY=minio123 + command: server --console-address ":9001" /data +volumes: + minio_storage: diff --git a/tests/integration_tests/__init__.py b/tests/integration_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration_tests/parameter_update_utils.py b/tests/integration_tests/parameter_update_utils.py new file mode 100644 index 0000000..11167ca --- /dev/null +++ b/tests/integration_tests/parameter_update_utils.py @@ -0,0 +1,129 @@ +import logging +from collections.abc import Callable + +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.utils.torch_utils import LudwigModule + +logger = logging.getLogger(__name__) + + +class ParameterUpdateError(Exception): + pass + + +def check_module_parameters_updated( + module: LudwigModule, + module_input_args: tuple, + module_target: torch.Tensor, + loss_function: Callable | None = None, + max_steps: int = 1, + learning_rate: float = 0.001, +) -> tuple: + """ + Reports on the number of parameters in a Ludwig component and their update status. + Args: + module: (LudwigModel) model to be tested. + module_input_args: (tuple) input for model + module_target: (Tensor) target values for computing loss and parameter updates + loss_function: (None or Callable) Optional for module specific loss calculation + max_steps: (int, default=1) maximum number of steps allowed to test for parameter + updates. + learning_rate: (float, default=0.001) learning rate for the optimizer + + Returns: Tuple(frozen_parameters, trainable_parameters, parameters_updated, not_updated) + frozen_parameters: count of frozen parameters + trainable_parameters: count of trainable parameters + parameters_updated: count of trainable parameters that were updated + not_updated: list of parameters that were not updated + + """ + # setup + if loss_function is None: + loss_function = torch.nn.MSELoss() + + # Ensure module and all inputs are on the same device + from ludwig.utils.torch_utils import get_torch_device + + device = get_torch_device() + module = module.to(device) + + def _move_to_device(arg): + if isinstance(arg, torch.Tensor): + return arg.to(device) + if isinstance(arg, dict): + return {k: _move_to_device(v) for k, v in arg.items()} + if isinstance(arg, (list, tuple)): + return type(arg)(_move_to_device(v) for v in arg) + return arg + + module_input_args = tuple(_move_to_device(arg) for arg in module_input_args) + module_target = module_target.to(device) + + optimizer = torch.optim.SGD(module.parameters(), lr=learning_rate) + module.train(True) + + trainable_parameter_list = [] + frozen_parameter_list = [] + parameter_updated = [] + parameters_not_updated = [] + for step in range(max_steps): + # make pass through model + module_output = module(*module_input_args) + + # check for any frozen parameters + frozen_parameter_list = [] + trainable_parameter_list = [] + for p in module.named_parameters(): + if p[1].requires_grad: + trainable_parameter_list.append(p) + else: + frozen_parameter_list.append(p) + + # check parameter updates only if there are some unfrozen parameters + if len(trainable_parameter_list) > 0: + # do update of model parameters + optimizer.zero_grad() + if isinstance(module_output, torch.Tensor): + module_target = module_target.to(device=module_output.device) + loss = loss_function(module_output, module_target) + elif isinstance(module_output, dict): + if "logits" in module_output: + module_target = module_target.to(device=module_output["logits"].device) + loss = loss_function(module_output["logits"], module_target) + elif ENCODER_OUTPUT in module_output: + module_target = module_target.to(device=module_output[ENCODER_OUTPUT].device) + loss = loss_function(module_output[ENCODER_OUTPUT], module_target) + elif "combiner_output" in module_output: + module_target = module_target.to(device=module_output["combiner_output"].device) + loss = loss_function(module_output["combiner_output"], module_target) + elif isinstance(module_output, (list, tuple)): + module_target = module_target.to(device=module_output[0].device) + loss = loss_function(module_output[0], module_target) + else: + raise ValueError(f"Unexpected output type. Module type found is {type(module_output)}") + + loss.backward() + optimizer.step() + + # check for parameter updates + parameter_updated = [] + # create tuple for each parameter: (parameter name, update indicator True/False) + # parameter is deemed updated if the gradient is not None and the gradient has non-zero value + for p in module.named_parameters(): + parameter_updated.append((p[0], (p[1].grad is not None) and (not torch.all(p[1].grad == 0)))) + else: + parameter_updated = [] + + parameters_not_updated = [] + for p in parameter_updated: + # if not updated, record parameter name + if not p[1]: + parameters_not_updated.append(p[0]) + + trainable_parameters = len(trainable_parameter_list) + parameters_updated = sum(p[1] for p in parameter_updated) + frozen_parameters = len(frozen_parameter_list) + + return frozen_parameters, trainable_parameters, parameters_updated, parameters_not_updated diff --git a/tests/integration_tests/scripts/run_train_aim.py b/tests/integration_tests/scripts/run_train_aim.py new file mode 100644 index 0000000..659f511 --- /dev/null +++ b/tests/integration_tests/scripts/run_train_aim.py @@ -0,0 +1,46 @@ +import argparse +import os +import sys +import tempfile +from unittest.mock import Mock + +# Comet must be imported before the libraries it wraps +import aim # noqa + +from ludwig.contribs.aim import AimCallback +from tests.integration_tests.utils import category_feature, generate_data, image_feature, run_experiment + +PATH_HERE = os.path.abspath(os.path.dirname(__file__)) +PATH_ROOT = os.path.join(PATH_HERE, "..", "..", "..") +sys.path.insert(0, os.path.abspath(PATH_ROOT)) + + +def run(csv_filename): + callback = AimCallback() + + # Wrap these methods so we can check that they were called + callback.on_train_init = Mock(side_effect=callback.on_train_init) + callback.on_train_start = Mock(side_effect=callback.on_train_start) + + # Image Inputs + with tempfile.TemporaryDirectory() as tmpdir: + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Inputs & Outputs + input_features = [image_feature(folder=image_dest_folder)] + output_features = [category_feature(output_feature=True)] + rel_path = generate_data(input_features, output_features, csv_filename) + + # Run experiment + run_experiment(input_features, output_features, dataset=rel_path, callbacks=[callback]) + + # Check that these methods were called at least once + callback.on_train_init.assert_called() + callback.on_train_start.assert_called() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--csv-filename", required=True) + args = parser.parse_args() + run(args.csv_filename) diff --git a/tests/integration_tests/scripts/run_train_comet.py b/tests/integration_tests/scripts/run_train_comet.py new file mode 100644 index 0000000..52842b4 --- /dev/null +++ b/tests/integration_tests/scripts/run_train_comet.py @@ -0,0 +1,79 @@ +# Tests the following end-to-end: +# +# 1. Comet is imported +# 2. Conflicting modules (i.e., TensorFlow) are not imported +# 3. Overridden methods are called (train_init, train_model, etc.) and run without error +# +# This test runs in an isolated environment to ensure TensorFlow imports are not leaked +# from previous tests. + +import argparse +import os +import sys +import tempfile +from unittest.mock import Mock, patch + +# Comet must be imported before the libraries it wraps +import comet_ml # noqa + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, TRAINER +from ludwig.contribs.comet import CometCallback + +# Bad key will ensure Comet is initialized, but nothing is uploaded externally. +os.environ["COMET_API_KEY"] = "key" + +# Add tests dir to the import path +PATH_HERE = os.path.abspath(os.path.dirname(__file__)) +PATH_ROOT = os.path.join(PATH_HERE, "..", "..", "..") +sys.path.insert(0, os.path.abspath(PATH_ROOT)) + +from tests.integration_tests.utils import category_feature, generate_data, image_feature # noqa + +parser = argparse.ArgumentParser() +parser.add_argument("--csv-filename", required=True) + + +def run(csv_filename): + with tempfile.TemporaryDirectory() as tmpdir: + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Inputs & Outputs + input_features = [image_feature(folder=image_dest_folder)] + output_features = [category_feature(output_feature=True)] + data_csv = generate_data(input_features, output_features, csv_filename) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + callback = CometCallback() + model = LudwigModel(config, callbacks=[callback]) + + # Wrap these methods so we can check that they were called + callback.on_train_init = Mock(side_effect=callback.on_train_init) + callback.on_train_start = Mock(side_effect=callback.on_train_start) + + with patch("comet_ml.Experiment.log_asset_data") as mock_log_asset_data: + # Training with csv + _, _, _ = model.train(dataset=data_csv, output_directory=os.path.join(tmpdir, "output")) + model.predict(dataset=data_csv) + + # Verify that the experiment was created successfully + assert callback.cometml_experiment is not None + + # Check that these methods were called at least once + callback.on_train_init.assert_called() + callback.on_train_start.assert_called() + + # Check that we ran `train_model`, which calls into `log_assert_data`, successfully + mock_log_asset_data.assert_called() + + +if __name__ == "__main__": + args = parser.parse_args() + run(args.csv_filename) diff --git a/tests/integration_tests/scripts/run_train_wandb.py b/tests/integration_tests/scripts/run_train_wandb.py new file mode 100644 index 0000000..5496a4e --- /dev/null +++ b/tests/integration_tests/scripts/run_train_wandb.py @@ -0,0 +1,56 @@ +# Tests the following end-to-end: +# +# 1. W&B is imported +# 2. Overridden methods are called (train_init, train_model, etc.) and run without error +# +# This test runs in an isolated environment because W&B make breaking changes to the +# global interpreter state that will otherwise cause subsequent tests to fail. + +import argparse +import os +import sys +import tempfile +from unittest.mock import Mock + +from ludwig.contribs.wandb import WandbCallback + +PATH_HERE = os.path.abspath(os.path.dirname(__file__)) +PATH_ROOT = os.path.join(PATH_HERE, "..", "..", "..") +sys.path.insert(0, os.path.abspath(PATH_ROOT)) + +from tests.integration_tests.utils import category_feature, generate_data, image_feature, run_experiment # noqa + +parser = argparse.ArgumentParser() +parser.add_argument("--csv-filename", required=True) + + +def run(csv_filename): + callback = WandbCallback() + + # Wrap these methods so we can check that they were called + callback.on_train_init = Mock(side_effect=callback.on_train_init) + callback.on_train_start = Mock(side_effect=callback.on_train_start) + + # disable sync to cloud + os.environ["WANDB_MODE"] = "dryrun" + + with tempfile.TemporaryDirectory() as tmpdir: + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Inputs & Outputs + input_features = [image_feature(folder=image_dest_folder)] + output_features = [category_feature(output_feature=True)] + rel_path = generate_data(input_features, output_features, csv_filename) + + # Run experiment + run_experiment(input_features, output_features, dataset=rel_path, callbacks=[callback]) + + # Check that these methods were called at least once + callback.on_train_init.assert_called() + callback.on_train_start.assert_called() + + +if __name__ == "__main__": + args = parser.parse_args() + run(args.csv_filename) diff --git a/tests/integration_tests/synthetic_test_data.py b/tests/integration_tests/synthetic_test_data.py new file mode 100644 index 0000000..f7a7d85 --- /dev/null +++ b/tests/integration_tests/synthetic_test_data.py @@ -0,0 +1,71 @@ +"""Utilities for producing synthetic test data that is convergence-friendly.""" + +from collections import namedtuple + +import numpy as np +import pandas as pd +from sklearn.model_selection import train_test_split + +RANDOM_SEED = 42 +NUMBER_OBSERVATIONS = 200 + +GeneratedData = namedtuple("GeneratedData", "train_df validation_df test_df") + + +def get_feature_configs(): + input_features = [ + {"name": "x", "type": "number"}, + ] + output_features = [ + { + "name": "y", + "type": "number", + "loss": {"type": "mean_squared_error"}, + "decoder": { + "num_fc_layers": 2, + "fc_output_size": 64, + }, + } + ] + + return input_features, output_features + + +def get_generated_data(): + # function generates simple training data that guarantee convergence + # within 30 epochs for suitable config + + # generate data + np.random.seed(RANDOM_SEED) + x = np.array(range(NUMBER_OBSERVATIONS)).reshape(-1, 1) + y = 2 * x + 1 + np.random.normal(size=x.shape[0]).reshape(-1, 1) + raw_df = pd.DataFrame(np.concatenate((x, y), axis=1), columns=["x", "y"]) + + # create training data + train, valid_test = train_test_split(raw_df, train_size=0.7) + + # create validation and test data + validation, test = train_test_split(valid_test, train_size=0.5) + + return GeneratedData(train, validation, test) + + +def get_generated_data_for_optimizer(): + # function generates simple training data that guarantee convergence + # within 30 epochs for suitable config + + # generate data + np.random.seed(RANDOM_SEED) + x = np.array(range(NUMBER_OBSERVATIONS)).reshape(-1, 1) + y = 2 * x + 1 + np.random.normal(size=x.shape[0]).reshape(-1, 1) + raw_df = pd.DataFrame(np.concatenate((x, y), axis=1), columns=["x", "y"]) + raw_df["x"] = (raw_df["x"] - raw_df["x"].min()) / (raw_df["x"].max() - raw_df["x"].min()) + raw_df["y"] = (raw_df["y"] - raw_df["y"].min()) / (raw_df["y"].max() - raw_df["y"].min()) + + # create training data + train, valid_test = train_test_split(raw_df, train_size=0.7) + + # create validation and test data + validation, test = train_test_split(valid_test, train_size=0.5) + + return GeneratedData(train, validation, test) diff --git a/tests/integration_tests/test_api.py b/tests/integration_tests/test_api.py new file mode 100644 index 0000000..b2b67f8 --- /dev/null +++ b/tests/integration_tests/test_api.py @@ -0,0 +1,804 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import json +import os +import shutil +from unittest import mock + +import pytest +import torch +import yaml + +from ludwig.api import LudwigModel +from ludwig.callbacks import Callback +from ludwig.constants import BATCH_SIZE, ENCODER, TRAINER, TYPE +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME +from ludwig.utils.data_utils import read_csv +from tests.integration_tests.utils import ( + category_feature, + generate_data, + get_weights, + image_feature, + run_api_experiment, + sequence_feature, + text_feature, +) + + +def run_api_experiment_separated_datasets(input_features, output_features, data_csv): + """Helper method to avoid code repetition in running an experiment. + + :param input_features: input schema + :param output_features: output schema + :param data_csv: path to data + :return: None + """ + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + model = LudwigModel(config) + + # Training with dataframe + data_df = read_csv(data_csv) + train_df = data_df.sample(frac=0.8) + test_df = data_df.drop(train_df.index).sample(frac=0.5) + validation_df = data_df.drop(train_df.index).drop(test_df.index) + + basename, ext = os.path.splitext(data_csv) + train_fname = basename + ".train" + ext + val_fname = basename + ".validation" + ext + test_fname = basename + ".test" + ext + output_dirs = [] + + try: + train_df.to_csv(train_fname) + validation_df.to_csv(val_fname) + test_df.to_csv(test_fname) + + # Training with csv + _, _, output_dir = model.train( + training_set=train_fname, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + output_dirs.append(output_dir) + + _, _, output_dir = model.train( + training_set=train_fname, + validation_set=val_fname, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + output_dirs.append(output_dir) + + _, _, output_dir = model.train( + training_set=train_fname, + validation_set=val_fname, + test_set=test_fname, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + output_dirs.append(output_dir) + + _, output_dir = model.predict(dataset=test_fname) + output_dirs.append(output_dir) + + finally: + # Remove results/intermediate data saved to disk + os.remove(train_fname) + os.remove(val_fname) + os.remove(test_fname) + for output_dir in output_dirs: + shutil.rmtree(output_dir, ignore_errors=True) + + output_dirs = [] + try: + _, _, output_dir = model.train( + training_set=train_df, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + output_dirs.append(output_dir) + + _, _, output_dir = model.train( + training_set=train_df, + validation_set=validation_df, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + output_dirs.append(output_dir) + + _, _, output_dir = model.train( + training_set=train_df, + validation_set=validation_df, + test_set=test_df, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + output_dirs.append(output_dir) + + _, output_dir = model.predict(dataset=data_df) + output_dirs.append(output_dir) + + finally: + for output_dir in output_dirs: + shutil.rmtree(output_dir, ignore_errors=True) + + +def test_api_intent_classification(csv_filename): + # Single sequence input, single category output + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + # Test representative encoders (embed=simple, rnn=recurrent, transformer=attention) + for encoder in ["embed", "rnn", "transformer"]: + input_features[0][ENCODER][TYPE] = encoder + run_api_experiment(input_features, output_features, data_csv=rel_path) + + +def test_api_intent_classification_separated(csv_filename): + # Single sequence input, single category output + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + # Test representative encoders (embed=simple, rnn=recurrent, transformer=attention) + for encoder in ["embed", "rnn", "transformer"]: + input_features[0][ENCODER][TYPE] = encoder + run_api_experiment_separated_datasets(input_features, output_features, data_csv=rel_path) + + +def test_api_train_online(csv_filename): + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + data_csv = generate_data(input_features, output_features, csv_filename) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + } + model = LudwigModel(config) + + for _ in range(2): + model.train_online(dataset=data_csv) + model.predict(dataset=data_csv) + + +def test_api_training_set(tmpdir): + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + } + model = LudwigModel(config) + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv) + model.predict(dataset=test_csv) + + # Train again, this time the HDF5 cache will be used + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv) + + +def test_api_training_determinism(tmpdir): + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + "trainer": {BATCH_SIZE: 128}, # batch size must be fixed for determinism + } + + # Train the model 3 times: + # + # 1. seed x + # 2. seed y + # 3. seed x + # + # Check that models (1) and (3) produce the same weights, + # but (1) and (2) do not + rand_x = 42 + rand_y = 24 + + model_1 = LudwigModel(config) + model_1.train(dataset=data_csv, output_directory=tmpdir, random_seed=rand_x) + + model_2 = LudwigModel(config) + model_2.train(dataset=data_csv, output_directory=tmpdir, random_seed=rand_y) + + model_3 = LudwigModel(config) + model_3.train(dataset=data_csv, output_directory=tmpdir, random_seed=rand_x) + + model_weights_1 = get_weights(model_1.model) + model_weights_2 = get_weights(model_2.model) + model_weights_3 = get_weights(model_3.model) + + divergence = False + for weight_1, weight_2 in zip(model_weights_1, model_weights_2): + if not torch.allclose(weight_1, weight_2): + divergence = True + break + assert divergence, "model_1 and model_2 have identical weights with different seeds!" + + for weight_1, weight_3 in zip(model_weights_1, model_weights_3): + assert torch.allclose(weight_1, weight_3) + + +def run_api_commands( + input_features, + output_features, + data_csv, + output_dir, + skip_save_training_description=False, + skip_save_training_statistics=False, + skip_save_model=False, + skip_save_progress=False, + skip_save_log=False, + skip_save_processed_input=False, + skip_save_unprocessed_output=False, + skip_save_predictions=False, + skip_save_eval_stats=False, + skip_collect_predictions=False, + skip_collect_overall_stats=False, +): + """Helper method to avoid code repetition in running an experiment. + + :param input_features: input schema + :param output_features: output schema + :param data_csv: path to data + :return: None + """ + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + model = LudwigModel(config) + + # Training with csv + model.train( + dataset=data_csv, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + output_directory=output_dir, + ) + model.predict( + dataset=data_csv, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + output_directory=output_dir, + ) + model.evaluate( + dataset=data_csv, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + collect_predictions=not skip_collect_predictions, + collect_overall_stats=not skip_collect_overall_stats, + output_directory=output_dir, + ) + model.experiment( + dataset=data_csv, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + skip_collect_predictions=skip_collect_predictions, + skip_collect_overall_stats=skip_collect_overall_stats, + output_directory=output_dir, + ) + + +@pytest.mark.parametrize( + "skip_save_training_description,skip_save_training_statistics,skip_save_model," + "skip_save_progress,skip_save_log,skip_save_processed_input", + [ + (False, False, False, False, False, False), # all saving enabled + (True, True, True, True, True, True), # all saving disabled + (True, False, True, False, True, False), # alternating pattern + ], + ids=["all_save", "all_skip", "mixed"], +) +def test_api_skip_parameters_train( + tmpdir, + csv_filename, + skip_save_training_description, + skip_save_training_statistics, + skip_save_model, + skip_save_progress, + skip_save_log, + skip_save_processed_input, +): + # Single sequence input, single category output + input_features = [category_feature(encoder={"vocab_size": 5})] + output_features = [category_feature(decoder={"vocab_size": 5})] + + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename)) + run_api_commands( + input_features, + output_features, + data_csv=rel_path, + output_dir=tmpdir, + skip_save_training_description=skip_save_training_description, + skip_save_training_statistics=skip_save_training_statistics, + skip_save_model=skip_save_model, + skip_save_progress=skip_save_progress, + skip_save_log=skip_save_log, + skip_save_processed_input=skip_save_processed_input, + ) + + +@pytest.mark.parametrize("skip_save_unprocessed_output", [False, True]) +@pytest.mark.parametrize("skip_save_predictions", [False, True]) +def test_api_skip_parameters_predict( + tmpdir, + csv_filename, + skip_save_unprocessed_output, + skip_save_predictions, +): + # Single sequence input, single category output + input_features = [category_feature(encoder={"vocab_size": 5})] + output_features = [category_feature(decoder={"vocab_size": 5})] + + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename)) + run_api_commands( + input_features, + output_features, + data_csv=rel_path, + output_dir=tmpdir, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + ) + + +@pytest.mark.parametrize( + "skip_save_unprocessed_output,skip_save_predictions,skip_save_eval_stats," + "skip_collect_predictions,skip_collect_overall_stats", + [ + (False, False, False, False, False), # all saving enabled + (True, True, True, True, True), # all saving disabled + (True, False, True, False, True), # alternating pattern + ], + ids=["all_save", "all_skip", "mixed"], +) +def test_api_skip_parameters_evaluate( + tmpdir, + csv_filename, + skip_save_unprocessed_output, + skip_save_predictions, + skip_save_eval_stats, + skip_collect_predictions, + skip_collect_overall_stats, +): + # Single sequence input, single category output + input_features = [category_feature(encoder={"vocab_size": 5})] + output_features = [category_feature(decoder={"vocab_size": 5})] + + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename)) + run_api_commands( + input_features, + output_features, + data_csv=rel_path, + output_dir=tmpdir, + skip_save_unprocessed_output=skip_save_unprocessed_output, + skip_save_predictions=skip_save_predictions, + skip_save_eval_stats=skip_save_eval_stats, + skip_collect_predictions=skip_collect_predictions, + skip_collect_overall_stats=skip_collect_overall_stats, + ) + + +@pytest.mark.parametrize( + "epochs,batch_size,num_examples,steps_per_checkpoint", + [ + (1, 8, 16, 1), + (2, 4, 32, 2), + (2, 8, 16, 2), + ], + ids=["small", "large", "mixed"], +) +def test_api_callbacks(tmpdir, csv_filename, epochs, batch_size, num_examples, steps_per_checkpoint): + mock_callback = mock.Mock(wraps=Callback()) + + steps_per_epoch = num_examples / batch_size + total_checkpoints = (steps_per_epoch / steps_per_checkpoint) * epochs + total_batches = epochs * (num_examples / batch_size) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + "epochs": epochs, + "batch_size": batch_size, + "steps_per_checkpoint": steps_per_checkpoint, + "early_stop": 0, # Disable early stopping. + }, + } + model = LudwigModel(config, callbacks=[mock_callback]) + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv) + + assert mock_callback.on_epoch_start.call_count == epochs + assert mock_callback.on_epoch_end.call_count == epochs + + assert mock_callback.should_early_stop.call_count == total_checkpoints + + assert mock_callback.on_validation_start.call_count == total_checkpoints + assert mock_callback.on_validation_end.call_count == total_checkpoints + + assert mock_callback.on_test_start.call_count == total_checkpoints + assert mock_callback.on_test_end.call_count == total_checkpoints + + assert mock_callback.on_batch_start.call_count == total_batches + assert mock_callback.on_batch_end.call_count == total_batches + + assert mock_callback.on_eval_end.call_count == total_checkpoints + assert mock_callback.on_eval_start.call_count == total_checkpoints + + +@pytest.mark.parametrize( + "epochs,batch_size,num_examples,checkpoints_per_epoch", + [ + (1, 8, 32, 1), + (2, 4, 64, 2), + (2, 8, 32, 4), + ], + ids=["single_checkpoint", "multi_checkpoint", "frequent_checkpoint"], +) +def test_api_callbacks_checkpoints_per_epoch( + tmpdir, csv_filename, epochs, batch_size, num_examples, checkpoints_per_epoch +): + mock_callback = mock.Mock(wraps=Callback()) + + total_checkpoints = epochs * checkpoints_per_epoch + total_batches = epochs * (num_examples / batch_size) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + "epochs": epochs, + "batch_size": batch_size, + "checkpoints_per_epoch": checkpoints_per_epoch, + "early_stop": 0, # Disable early stopping. + }, + } + model = LudwigModel(config, callbacks=[mock_callback]) + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv) + + assert mock_callback.on_epoch_start.call_count == epochs + assert mock_callback.on_epoch_end.call_count == epochs + + assert mock_callback.should_early_stop.call_count == total_checkpoints + + assert mock_callback.on_validation_start.call_count == total_checkpoints + assert mock_callback.on_validation_end.call_count == total_checkpoints + + assert mock_callback.on_test_start.call_count == total_checkpoints + assert mock_callback.on_test_end.call_count == total_checkpoints + + assert mock_callback.on_batch_start.call_count == total_batches + assert mock_callback.on_batch_end.call_count == total_batches + + assert mock_callback.on_eval_end.call_count == total_checkpoints + assert mock_callback.on_eval_start.call_count == total_checkpoints + + +def test_api_callbacks_default_train_steps(tmpdir, csv_filename): + # Default for train_steps is -1: use epochs. + train_steps = None + epochs = 3 + batch_size = 8 + num_examples = 20 + mock_callback = mock.Mock(wraps=Callback()) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": epochs, "train_steps": train_steps, "batch_size": batch_size}, + } + model = LudwigModel(config, callbacks=[mock_callback]) + model.train( + training_set=generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + ) + + assert mock_callback.on_epoch_start.call_count == epochs + + +def test_api_callbacks_fixed_train_steps(tmpdir, csv_filename): + train_steps = 4 + batch_size = 8 + num_examples = 20 + mock_callback = mock.Mock(wraps=Callback()) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"train_steps": train_steps, "batch_size": batch_size}, + } + model = LudwigModel(config, callbacks=[mock_callback]) + model.train( + training_set=generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + ) + + # With 20 examples (14 train at 70% split), batch_size=8, steps_per_epoch=2. + # So 4 train steps => 2 epochs. + assert mock_callback.on_epoch_start.call_count == 2 + + +def test_api_callbacks_fixed_train_steps_partial_epochs(tmpdir, csv_filename): + # If train_steps is set manually, epochs is ignored. + train_steps = 3 + epochs = 2 + batch_size = 8 + num_examples = 20 + mock_callback = mock.Mock(wraps=Callback()) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": epochs, "train_steps": train_steps, "batch_size": batch_size}, + } + model = LudwigModel(config, callbacks=[mock_callback]) + model.train( + training_set=generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + ) + + # With 20 examples, batch_size=8, steps_per_epoch=2. 3 train steps => 1 full epoch. + assert mock_callback.on_epoch_end.call_count == 1 + + +def test_api_callbacks_batch_size_1(tmpdir, csv_filename): + epochs = 1 + batch_size = 1 + num_examples = 16 + mock_callback = mock.Mock(wraps=Callback()) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": epochs, "batch_size": batch_size}, + } + model = LudwigModel(config, callbacks=[mock_callback]) + model.train( + training_set=generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + ) + + # There are exactly 1 epoch start, even with batch_size = 1. + assert mock_callback.on_epoch_start.call_count == 1 + assert mock_callback.on_epoch_end.call_count == 1 + assert mock_callback.on_batch_start.call_count == 16 + assert mock_callback.on_batch_end.call_count == 16 + + +def test_api_callbacks_fixed_train_steps_less_than_one_epoch(tmpdir, csv_filename): + # If train_steps is set manually, epochs is ignored. + # With 80 examples at 70% split = 56 train examples, batch_size=8 => 7 steps per epoch. + # train_steps=6 < 7, so less than one full epoch. + train_steps = total_batches = 6 + steps_per_checkpoint = 2 + batch_size = 8 + num_examples = 80 + mock_callback = mock.Mock(wraps=Callback()) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + "train_steps": train_steps, + "steps_per_checkpoint": steps_per_checkpoint, + "batch_size": batch_size, + }, + } + model = LudwigModel(config, callbacks=[mock_callback]) + model.train( + training_set=generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + ) + + assert mock_callback.on_epoch_start.call_count == 1 + assert mock_callback.on_epoch_end.call_count == 0 + # The total number of batches is the number of train_steps + assert mock_callback.on_batch_end.call_count == total_batches + # The total number of evals is the number of times checkpoints are made + assert mock_callback.on_eval_end.call_count == train_steps // steps_per_checkpoint + + +def test_saved_weights_in_checkpoint(tmpdir): + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + text_feature(), + image_feature(image_dest_folder), + ] + output_features = [category_feature(name="class", output_feature=True)] + + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {BATCH_SIZE: 128}, + } + model = LudwigModel(config) + _, _, output_dir = model.train( + training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir + ) + + config_save_path = os.path.join(output_dir, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME) + with open(config_save_path) as f: + saved_config = json.load(f) + saved_input_features = saved_config["input_features"] + for saved_input_feature in saved_input_features: + assert "encoder" in saved_input_feature + input_feature_encoder = saved_input_feature["encoder"] + assert "saved_weights_in_checkpoint" in input_feature_encoder + assert input_feature_encoder["saved_weights_in_checkpoint"] + + +def test_constant_metadata(tmpdir): + input_features = [category_feature(encoder={"vocab_size": 5})] + output_features = [category_feature(name="class", decoder={"vocab_size": 5}, output_feature=True)] + + data_csv1 = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset1.csv")) + val_csv1 = shutil.copyfile(data_csv1, os.path.join(tmpdir, "validation1.csv")) + test_csv1 = shutil.copyfile(data_csv1, os.path.join(tmpdir, "test1.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + } + model = LudwigModel(config) + model.train(training_set=data_csv1, validation_set=val_csv1, test_set=test_csv1, output_directory=tmpdir) + metadata1 = model.training_set_metadata + + data_csv2 = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset2.csv"), num_examples=10) + val_csv2 = shutil.copyfile(data_csv2, os.path.join(tmpdir, "validation2.csv")) + test_csv2 = shutil.copyfile(data_csv2, os.path.join(tmpdir, "test2.csv")) + model.train(training_set=data_csv2, validation_set=val_csv2, test_set=test_csv2, output_directory=tmpdir) + metadata2 = model.training_set_metadata + + assert metadata1 == metadata2 + + +@pytest.mark.integration_tests_i +@pytest.mark.parametrize( + "input_max_sequence_length, global_max_sequence_length, expect_raise", + [ + (5, "null", True), + ("null", 5, True), + (5, 5, True), + (100, 100, False), + (100, "null", False), + ("null", "null", False), + ], +) +def test_llm_template_too_long(tmpdir, input_max_sequence_length, global_max_sequence_length, expect_raise): + zero_shot_config = yaml.safe_load(f""" + model_type: llm + base_model: hf-internal-testing/tiny-random-GPTJForCausalLM + + input_features: + - name: instruction + type: text + preprocessing: + max_sequence_length: {input_max_sequence_length} + + output_features: + - name: output + type: text + + preprocessing: + global_max_sequence_length: {global_max_sequence_length} + """) + zero_shot_config["prompt"] = {} + zero_shot_config["prompt"]["template"] = ( + "This is a very long template that is longer than the max sequence length {instruction}" + ) + + input_features = [text_feature(name="instruction")] + output_features = [text_feature(name="output", output_feature=True)] + data_csv1 = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset1.csv")) + model = LudwigModel(zero_shot_config) + + if expect_raise: + with pytest.raises(ValueError): + model.preprocess(dataset=data_csv1, output_directory=tmpdir) + else: + model.preprocess(dataset=data_csv1, output_directory=tmpdir) diff --git a/tests/integration_tests/test_audio_feature.py b/tests/integration_tests/test_audio_feature.py new file mode 100644 index 0000000..b570360 --- /dev/null +++ b/tests/integration_tests/test_audio_feature.py @@ -0,0 +1,40 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.features.audio_feature import AudioInputFeature +from ludwig.schema.features.audio_feature import AudioInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from tests.integration_tests.utils import audio_feature + +BATCH_SIZE = 2 +SEQ_SIZE = 20 +AUDIO_W_SIZE = 16 +DEFAULT_OUTPUT_SIZE = 256 + + +@pytest.mark.parametrize("enc_encoder", ["stacked_cnn", "rnn"]) +def test_audio_feature(enc_encoder): + # synthetic audio tensor + audio_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE, AUDIO_W_SIZE], dtype=torch.float32) + + # generate audio feature config + audio_feature_config = audio_feature( + folder=".", encoder={"type": enc_encoder, "max_sequence_length": SEQ_SIZE, "embedding_size": AUDIO_W_SIZE} + ) + + # instantiate audio input feature object + audio_feature_config, _ = load_config_with_kwargs(AudioInputFeatureConfig, audio_feature_config) + audio_input_feature = AudioInputFeature(audio_feature_config) + + # pass synthetic audio tensor through the audio input feature + encoder_output = audio_input_feature(audio_tensor) + + # confirm correctness of the the audio encoder output + assert isinstance(encoder_output, dict) + assert ENCODER_OUTPUT in encoder_output + assert isinstance(encoder_output[ENCODER_OUTPUT], torch.Tensor) + if enc_encoder == "passthrough": + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, AUDIO_W_SIZE) + else: + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, DEFAULT_OUTPUT_SIZE) diff --git a/tests/integration_tests/test_automl.py b/tests/integration_tests/test_automl.py new file mode 100644 index 0000000..5bf72d5 --- /dev/null +++ b/tests/integration_tests/test_automl.py @@ -0,0 +1,382 @@ +import os +import tempfile +from unittest import mock + +import numpy as np +import pandas as pd +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import COLUMN, ENCODER, INPUT_FEATURES, NAME, OUTPUT_FEATURES, PREPROCESSING, SPLIT, TYPE +from ludwig.schema.model_types.base import ModelConfig +from ludwig.types import FeatureConfigDict, ModelConfigDict +from ludwig.utils.misc_utils import merge_dict +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + generate_data, + image_feature, + minio_test_creds, + number_feature, + private_param, + remote_tmpdir, + text_feature, +) + +ray = pytest.importorskip("ray") + +import dask.dataframe as dd # noqa E402 +from ray.tune.experiment.trial import Trial # noqa E402 + +from ludwig.automl import auto_train, create_auto_config, train_with_config # noqa E402 +from ludwig.automl.automl import OUTPUT_DIR # noqa E402 +from ludwig.hyperopt.execution import RayTuneExecutor # noqa E402 + +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_e, pytest.mark.integration_tests_g] + + +def to_name_set(features: list[FeatureConfigDict]) -> set[str]: + """Returns the list of feature names.""" + return {feature[NAME] for feature in features} + + +def merge_lists(a_features: list, b_features: list): + for idx in range(max(len(a_features), len(b_features))): + if idx >= len(a_features): + a_features.append(b_features[idx]) + elif idx < len(b_features): + a_features[idx] = merge_dict(a_features[idx], b_features[idx]) + + +def merge_dict_with_features(a: ModelConfigDict, b: ModelConfigDict) -> ModelConfigDict: + merge_lists(a[INPUT_FEATURES], b.get(INPUT_FEATURES, [])) + merge_lists(a[OUTPUT_FEATURES], b.get(OUTPUT_FEATURES, [])) + + b = b.copy() + if INPUT_FEATURES in b: + del b[INPUT_FEATURES] + if OUTPUT_FEATURES in b: + del b[OUTPUT_FEATURES] + + return merge_dict(a, b) + + +def check_types( + config: ModelConfigDict, input_features: list[FeatureConfigDict], output_features: list[FeatureConfigDict] +): + actual_features = config.get(INPUT_FEATURES, []) + config.get(OUTPUT_FEATURES, []) + expected_features = {f[NAME]: f for f in input_features + output_features} + assert len(actual_features) == len(expected_features) + for actual_feature in actual_features: + expected_feature = expected_features[actual_feature[NAME]] + assert actual_feature[TYPE] == expected_feature[TYPE], ( + f"{actual_feature[NAME]}: actual type {actual_feature[TYPE]} != {expected_feature[TYPE]}" + ) + + +@pytest.fixture(scope="module") +def test_data_tabular_large(): + with tempfile.TemporaryDirectory() as tmpdir: + input_features = [ + number_feature(), + number_feature(), + category_feature(encoder={"vocab_size": 3}), + category_feature(encoder={"vocab_size": 3}), + ] + output_features = [category_feature(decoder={"vocab_size": 3})] + dataset_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=50 + ) + yield input_features, output_features, dataset_csv + + +@pytest.fixture(scope="module") +def test_data_tabular_small(): + with tempfile.TemporaryDirectory() as tmpdir: + input_features = [ + number_feature(), + category_feature(encoder={"vocab_size": 3}), + ] + output_features = [category_feature(decoder={"vocab_size": 3})] + dataset_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=50 + ) + yield input_features, output_features, dataset_csv + + +@pytest.fixture(scope="module") +def test_data_image(): + with tempfile.TemporaryDirectory() as tmpdir: + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature(folder=image_dest_folder), + ] + output_features = [binary_feature()] + dataset_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=20 + ) + yield input_features, output_features, dataset_csv + + +@pytest.fixture(scope="module") +def test_data_text(): + with tempfile.TemporaryDirectory() as tmpdir: + input_features = [ + text_feature(preprocessing={"tokenizer": "space"}), + ] + output_features = [binary_feature()] + dataset_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=20 + ) + yield input_features, output_features, dataset_csv + + +@pytest.fixture(scope="module") +def test_data_multimodal(): + with tempfile.TemporaryDirectory() as tmpdir: + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature(folder=image_dest_folder), + text_feature(preprocessing={"tokenizer": "space"}), + number_feature(), + category_feature(encoder={"vocab_size": 3}), + category_feature(encoder={"vocab_size": 5}), + ] + output_features = [binary_feature()] + dataset_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=20 + ) + yield input_features, output_features, dataset_csv + + +@pytest.mark.distributed +@pytest.mark.parametrize( + "test_data,expectations", + [ + ("test_data_tabular_large", {"combiner": {"type": "ft_transformer"}}), + ("test_data_tabular_small", {"combiner": {"type": "concat"}}), + ("test_data_image", {"combiner": {"type": "concat"}}), + ( + "test_data_text", + { + "input_features": [{"type": "text", "encoder": {"type": "bert"}}], + "combiner": {"type": "concat"}, + "trainer": { + "batch_size": "auto", + "learning_rate": 1e-05, + "epochs": 10, + "optimizer": {"type": "adamw"}, + "learning_rate_scheduler": {"warmup_fraction": 0.1}, + "use_mixed_precision": True, + }, + "defaults": { + "text": { + "encoder": { + "type": "bert", + "trainable": True, + } + } + }, + }, + ), + ( + "test_data_multimodal", + { + "input_features": [{"type": "image"}, {"type": "text", "encoder": {"type": "embed"}}], + "combiner": {"type": "concat"}, + }, + ), + ], + ids=["tabular_large", "tabular_small", "image", "text", "multimodal"], +) +def test_create_auto_config(test_data, expectations, ray_cluster_2cpu, request): + test_data = request.getfixturevalue(test_data) + input_features, output_features, dataset_csv = test_data + targets = [feature[NAME] for feature in output_features] + df = dd.read_csv(dataset_csv) + config = create_auto_config(df, targets, time_limit_s=600, backend="ray") + + # Ensure our configs are using the latest Ludwig schema + ModelConfig.from_dict(config) + + assert to_name_set(config[INPUT_FEATURES]) == to_name_set(input_features) + assert to_name_set(config[OUTPUT_FEATURES]) == to_name_set(output_features) + check_types(config, input_features, output_features) + + expected = merge_dict_with_features(config, expectations) + assert config == expected + + +def _get_sample_df(class_probs): + nrows = 1000 + thresholds = np.cumsum((class_probs * nrows).astype(int)) + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + + def get_category(v): + if v < thresholds[0]: + return 0 + if thresholds[0] <= v < thresholds[1]: + return 1 + return 2 + + df["category"] = df.index.map(get_category).astype(np.int8) + return df + + +@pytest.mark.distributed +def test_autoconfig_preprocessing_balanced(): + df = _get_sample_df(np.array([0.33, 0.33, 0.34])) + + config = create_auto_config(dataset=df, target="category", time_limit_s=1) + + # Ensure our configs are using the latest Ludwig schema + ModelConfig.from_dict(config) + + assert PREPROCESSING not in config + + +@pytest.mark.distributed +def test_autoconfig_preprocessing_imbalanced(): + df = _get_sample_df(np.array([0.6, 0.2, 0.2])) + + config = create_auto_config(dataset=df, target="category", time_limit_s=1) + + # Ensure our configs are using the latest Ludwig schema + ModelConfig.from_dict(config) + + assert PREPROCESSING in config + assert SPLIT in config[PREPROCESSING] + assert config[PREPROCESSING][SPLIT] == {TYPE: "stratify", COLUMN: "category"} + + +@pytest.mark.distributed +def test_autoconfig_preprocessing_text_image(tmpdir): + image_dest_folder = os.path.join(tmpdir, "generated_images") + + input_features = [text_feature(preprocessing={"tokenizer": "space"}), image_feature(folder=image_dest_folder)] + output_features = [category_feature(output_feature=True)] + + # Generate Dataset + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + df = pd.read_csv(rel_path) + target = df.columns[-1] + + config = create_auto_config(dataset=df, target=target, time_limit_s=1) + + # Ensure our configs are using the latest Ludwig schema + ModelConfig.from_dict(config) + + # Check no features shuffled around + assert len(input_features) == 2 + assert len(output_features) == 1 + + # Check encoders are properly nested + assert isinstance(config[INPUT_FEATURES][0][ENCODER], dict) + assert isinstance(config[INPUT_FEATURES][1][ENCODER], dict) + + # Check automl default encoders are properly set + assert config[INPUT_FEATURES][0][ENCODER][TYPE] == "bert" + assert config[INPUT_FEATURES][1][ENCODER][TYPE] == "stacked_cnn" + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.parametrize("time_budget", [120, 1], ids=["high", "low"]) +def test_train_with_config(time_budget, test_data_tabular_large, ray_cluster_2cpu, tmpdir): + _run_train_with_config(time_budget, test_data_tabular_large, tmpdir) + + +@pytest.mark.distributed +def test_auto_train(test_data_tabular_large, ray_cluster_2cpu, tmpdir): + _, ofeatures, dataset_csv = test_data_tabular_large + local_output_directory_path: str = f"{tmpdir!s}/{OUTPUT_DIR}" + results = auto_train( + dataset=dataset_csv, + target=ofeatures[0][NAME], + time_limit_s=120, + output_directory=local_output_directory_path, + user_config={"hyperopt": {"executor": {"num_samples": 2}}}, + ) + + analysis = results.experiment_analysis + for trial in analysis.trials: + assert trial.status != Trial.ERROR, f"Error in trial {trial}" + + +@pytest.mark.slow +@pytest.mark.parametrize("fs_protocol,bucket", [private_param(("s3", "ludwig-tests"))], ids=["s3"]) +def test_train_with_config_remote(fs_protocol, bucket, test_data_tabular_large, ray_cluster_2cpu): + backend = { + "type": "local", + "credentials": { + "artifacts": minio_test_creds(), + }, + } + + with remote_tmpdir(fs_protocol, bucket) as tmpdir: + _run_train_with_config(200, test_data_tabular_large, tmpdir, backend=backend) + + +def _run_train_with_config(time_budget, test_data, tmpdir, **kwargs): + input_features, output_features, dataset_csv = test_data + config = { + "input_features": input_features, + "output_features": output_features, + "trainer": {"epochs": 2}, + "hyperopt": { + "search_alg": { + "type": "variant_generator", + "random_state": 42, + }, + "executor": { + "type": "ray", + "time_budget_s": time_budget, + "cpu_resources_per_trial": 1, + "num_samples": 2, + "scheduler": { + "type": "async_hyperband", + "max_t": time_budget, + "time_attr": "time_total_s", + "grace_period": min(72, time_budget), + "reduction_factor": 5, + }, + }, + "parameters": { + "trainer.batch_size": { + "space": "choice", + "categories": [64, 128, 256], + }, + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + }, + }, + } + + fn = RayTuneExecutor._evaluate_best_model + with mock.patch("ludwig.hyperopt.execution.RayTuneExecutor._evaluate_best_model") as mock_fn: + # We need to check that _evaluate_best_model is called when the time_budget is low + # as this code path should be triggered when the trial was early stopped + mock_fn.side_effect = fn + + outdir = os.path.join(tmpdir, "output") + results = train_with_config(dataset_csv, config, output_directory=outdir, **kwargs) + try: + best_model = results.best_model + except ValueError: + # ValueError is raised when best_model can't be found. This typically + # happens when the time_budget is low and the trial is stopped early, + # resulting in no evaluations happening (and no scores being reported back to RayTune). + # So RayTune has no way of determining what the best model is. + best_model = None + + if time_budget > 1: + assert isinstance(best_model, LudwigModel) + assert best_model.config_obj.trainer.early_stop == -1 + # assert mock_fn.call_count == 1 + else: + assert best_model is None + assert mock_fn.call_count == 0 diff --git a/tests/integration_tests/test_cache_manager.py b/tests/integration_tests/test_cache_manager.py new file mode 100644 index 0000000..e01e5b2 --- /dev/null +++ b/tests/integration_tests/test_cache_manager.py @@ -0,0 +1,106 @@ +import os +import tempfile +from pathlib import Path + +import pandas as pd +import pytest + +from ludwig.constants import CHECKSUM, META, TEST, TRAINING, VALIDATION +from ludwig.data.cache.manager import alphanum, PreprocessedDataCache +from ludwig.data.cache.types import CacheableDataframe, wrap +from ludwig.data.dataset.pandas import PandasDatasetManager +from ludwig.globals import TRAINING_PREPROC_FILE_NAME +from tests.integration_tests.utils import category_feature, LocalTestBackend, sequence_feature + + +@pytest.fixture +def change_test_dir(tmpdir, monkeypatch): + monkeypatch.chdir(tmpdir) + + +@pytest.mark.parametrize("use_df", [True, False], ids=["df", "filename"]) +@pytest.mark.parametrize("use_split", [True, False], ids=["split", "no_split"]) +@pytest.mark.parametrize("use_cache_dir", [True, False], ids=["cache_dir", "no_cache_dir"]) +def test_cache_dataset(use_cache_dir, use_split, use_df, tmpdir, change_test_dir): + dataset_manager = PandasDatasetManager(backend=LocalTestBackend()) + cache_dir = os.path.join(tmpdir, "cache") if use_cache_dir else None + manager = PreprocessedDataCache(dataset_manager, cache_dir=cache_dir) + + config = { + "input_features": [sequence_feature(encoder={"reduce_output": "sum"})], + "output_features": [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")], + "combiner": {"type": "concat", "output_size": 14}, + "preprocessing": {}, + } + + def touch(basename): + path = os.path.join(tmpdir, f"{basename}.csv") + Path(path).touch() + return path + + def create_dataset(name): + if use_df: + return CacheableDataframe(df=pd.DataFrame(), name=name, checksum=name) + else: + return wrap(touch(name)) + + dataset = training_set = test_set = validation_set = None + if not use_split: + dataset = create_dataset("dataset") + cache_key = manager.get_cache_key(dataset, config) + else: + training_set = create_dataset("train") + test_set = create_dataset("test") + validation_set = create_dataset("validation") + cache_key = manager.get_cache_key(training_set, config) + + training_set_metadata = { + CHECKSUM: cache_key, + } + + cache = manager.get_dataset_cache(config, dataset, training_set, test_set, validation_set) + cache_map = cache.cache_map + assert len(cache_map) == 4 + + train_path = os.path.join(cache_dir, alphanum(cache_key)) if use_cache_dir else os.path.join(tmpdir, "dataset") + test_path = val_path = train_path + + if use_split and not use_cache_dir: + train_path = os.path.join(tmpdir, "train") + test_path = os.path.join(tmpdir, "test") + val_path = os.path.join(tmpdir, "validation") + + # CacheableDataframe has no backing file, so its cache directory is the system tempdir. + # CacheablePath uses the file's own directory (tmpdir). + if use_df and not use_cache_dir: + train_path = os.path.join(tempfile.gettempdir(), "dataset") + test_path = val_path = train_path + if use_split: + train_path = os.path.join(tempfile.gettempdir(), "train") + test_path = os.path.join(tempfile.gettempdir(), "test") + val_path = os.path.join(tempfile.gettempdir(), "validation") + + data_ext = dataset_manager.data_format # "parquet" + assert cache_map[META] == f"{train_path}.meta.json" + assert cache_map[TRAINING] == f"{train_path}.{TRAINING_PREPROC_FILE_NAME}" + assert cache_map[TEST] == f"{test_path}.test.{data_ext}" + assert cache_map[VALIDATION] == f"{val_path}.validation.{data_ext}" + + for cache_path in cache_map.values(): + assert not os.path.exists(cache_path) + + training_set = pd.DataFrame() + test_set = pd.DataFrame() + validation_set = pd.DataFrame() + + if use_cache_dir: + os.makedirs(cache_dir) + cache.put(training_set, test_set, validation_set, training_set_metadata) + + for cache_path in cache_map.values(): + assert os.path.exists(cache_path) + + cache.delete() + + for cache_path in cache_map.values(): + assert not os.path.exists(cache_path) diff --git a/tests/integration_tests/test_cached_preprocessing.py b/tests/integration_tests/test_cached_preprocessing.py new file mode 100644 index 0000000..240f90d --- /dev/null +++ b/tests/integration_tests/test_cached_preprocessing.py @@ -0,0 +1,148 @@ +import os + +import numpy as np +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import MODEL_ECD, PREPROCESSING, PROC_COLUMN, TRAINER +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + generate_data, + number_feature, + run_test_suite, + text_feature, +) + + +def _onehot_encoding_config(tmpdir): + input_features = [ + number_feature(), + category_feature(encoder={"type": "onehot"}), + ] + output_features = [binary_feature()] + + data_csv_path = os.path.join(tmpdir, "dataset.csv") + dataset = generate_data(input_features, output_features, data_csv_path) + config = {"input_features": input_features, "output_features": output_features, TRAINER: {"train_steps": 1}} + return config, dataset + + +def test_onehot_encoding(tmpdir): + config, dataset = _onehot_encoding_config(tmpdir) + run_test_suite(config, dataset, "local") + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_onehot_encoding_ray(tmpdir, ray_cluster_2cpu): + config, dataset = _onehot_encoding_config(tmpdir) + run_test_suite(config, dataset, "ray") + + +def _hf_text_embedding_config(tmpdir): + input_features = [ + number_feature(), + text_feature( + encoder={ + "type": "auto_transformer", + "pretrained_model_name_or_path": "hf-internal-testing/tiny-bert-for-token-classification", + }, + preprocessing={"cache_encoder_embeddings": True}, + ), + ] + output_features = [binary_feature()] + + data_csv_path = os.path.join(tmpdir, "dataset.csv") + dataset = generate_data(input_features, output_features, data_csv_path) + config = {"input_features": input_features, "output_features": output_features, TRAINER: {"train_steps": 1}} + return config, dataset + + +def test_hf_text_embedding(tmpdir): + config, dataset = _hf_text_embedding_config(tmpdir) + run_test_suite(config, dataset, "local") + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_hf_text_embedding_ray(tmpdir, ray_cluster_2cpu): + config, dataset = _hf_text_embedding_config(tmpdir) + run_test_suite(config, dataset, "ray") + + +@pytest.mark.parametrize("cache_encoder_embeddings", [True, False, None]) +def test_onehot_encoding_preprocessing(cache_encoder_embeddings, tmpdir): + vocab_size = 5 + input_features = [ + category_feature(encoder={"type": "onehot", "vocab_size": vocab_size}), + number_feature(), + ] + output_features = [binary_feature()] + + if cache_encoder_embeddings is not None: + if PREPROCESSING not in input_features[0]: + input_features[0][PREPROCESSING] = {} + input_features[0][PREPROCESSING]["cache_encoder_embeddings"] = cache_encoder_embeddings + + # Need sufficiently high number of examples to ensure at least one of each category type appears + data_csv_path = os.path.join(tmpdir, "dataset.csv") + num_examples = 100 + dataset_fp = generate_data(input_features, output_features, data_csv_path, num_examples) + config = { + "model_type": MODEL_ECD, + "input_features": input_features, + "output_features": output_features, + } + + # Run preprocessing + ludwig_model = LudwigModel(config, backend="local") + proc_dataset = ludwig_model.preprocess(training_set=dataset_fp) + + # Check preprocessed output + proc_df = ludwig_model.backend.df_engine.compute(proc_dataset.training_set.to_df()) + proc_col = input_features[0][PROC_COLUMN] + proc_series = proc_df[proc_col] + + # ECD will not cache embeddings by default, but will if set to `cache_encoder_embeddings=true` + expected_cache_encoder_embeddings = cache_encoder_embeddings or False + if expected_cache_encoder_embeddings: + assert proc_series.values.dtype == "object" + data = np.stack(proc_series.values) + assert data.shape == (num_examples, vocab_size) + + # Only one element in each row should be 1 + assert all(x == 1 for x in data.sum(axis=1)) + else: + assert proc_series.values.dtype == "int8" + data = proc_series.to_numpy() + assert data.shape == (num_examples,) + + +def test_hf_text_embedding_tied(tmpdir): + input_features = [ + text_feature( + encoder={ + "type": "auto_transformer", + "pretrained_model_name_or_path": "hf-internal-testing/tiny-bert-for-token-classification", + }, + preprocessing={"cache_encoder_embeddings": True}, + ), + text_feature( + encoder={ + "type": "auto_transformer", + "pretrained_model_name_or_path": "hf-internal-testing/tiny-bert-for-token-classification", + }, + preprocessing={"cache_encoder_embeddings": True}, + ), + ] + input_features[1]["tied"] = input_features[0]["name"] + output_features = [binary_feature()] + + data_csv_path = os.path.join(tmpdir, "dataset.csv") + dataset = generate_data(input_features, output_features, data_csv_path) + + config = {"input_features": input_features, "output_features": output_features, TRAINER: {"epochs": 1}} + run_test_suite(config, dataset, "local") diff --git a/tests/integration_tests/test_class_imbalance_feature.py b/tests/integration_tests/test_class_imbalance_feature.py new file mode 100644 index 0000000..f4dd0f2 --- /dev/null +++ b/tests/integration_tests/test_class_imbalance_feature.py @@ -0,0 +1,184 @@ +import contextlib +import os +import shutil + +import numpy as np +import pandas as pd +import pytest + +from ludwig.api import LudwigModel +from ludwig.backend import LocalBackend +from tests.integration_tests.utils import create_data_set_to_use, RAY_BACKEND_CONFIG, spawn + +try: + import ray + + from ludwig.backend.ray import RayBackend +except ImportError: + ray = None + +rs = np.random.RandomState(42) + + +@contextlib.contextmanager +def ray_start(num_cpus=2, num_gpus=None): + res = ray.init( + num_cpus=num_cpus, + num_gpus=num_gpus, + include_dashboard=False, + object_store_memory=150 * 1024 * 1024, + ) + try: + yield res + finally: + ray.shutdown() + # Delete the cluster address just in case. + if hasattr(ray._private.utils, "reset_ray_address"): + ray._private.utils.reset_ray_address() + + +@spawn +def run_test_imbalance_ray( + tmpdir, + input_df, + config, + balance, + num_cpus=2, + num_gpus=None, +): + with ray_start(num_cpus=num_cpus, num_gpus=num_gpus): + csv_filename = os.path.join(tmpdir, "dataset.csv") + input_df.to_csv(csv_filename) + dataset_parquet = create_data_set_to_use("parquet", csv_filename) + + model = LudwigModel(config, backend=RAY_BACKEND_CONFIG, callbacks=None) + output_dir = None + + try: + _, output_dataset, output_dir = model.train( + dataset=dataset_parquet, + training_set=None, + validation_set=None, + test_set=None, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_log=True, + ) + finally: + # Remove results/intermediate data saved to disk + shutil.rmtree(output_dir, ignore_errors=True) + + input_train_set = input_df.sample(frac=0.7, replace=False) + processed_len = output_dataset[0].ds.count() + processed_target_pos = output_dataset[0].ds.sum(on="Label_mZFLky") + processed_target_neg = output_dataset[0].ds.count() - output_dataset[0].ds.sum(on="Label_mZFLky") + assert len(input_train_set) == 140 + assert 0.05 <= len(input_train_set[input_train_set["Label"] == 1]) / len(input_train_set) <= 0.15 + assert round(processed_target_pos / processed_target_neg, 1) == 0.5 + assert model.backend.df_engine.parallelism == RAY_BACKEND_CONFIG["processor"]["parallelism"] + assert isinstance(model.backend, RayBackend) + + if balance == "oversample_minority": + assert len(input_train_set) < processed_len + + if balance == "undersample_majority": + assert len(input_train_set) > processed_len + + +def run_test_imbalance_local( + input_df, + config, + balance, +): + model = LudwigModel(config) + _, output_dataset, output_dir = model.train( + input_df, + skip_save_model=True, + skip_save_log=True, + skip_save_progress=True, + skip_save_processed_input=True, + skip_save_training_description=True, + skip_save_training_statistics=True, + ) + + input_train_set = input_df.sample(frac=0.7, replace=False) + processed_len = output_dataset[0].size + processed_target_pos = sum(output_dataset[0].dataset["Label_2Xl8CP"]) + processed_target_neg = len(output_dataset[0].dataset["Label_2Xl8CP"]) - processed_target_pos + assert len(input_train_set) == 140 + assert 0.05 <= len(input_train_set[input_train_set["Label"] == 1]) / len(input_train_set) <= 0.15 + assert round(processed_target_pos / processed_target_neg, 1) == 0.5 + assert isinstance(model.backend, LocalBackend) + + if balance == "oversample_minority": + assert len(input_train_set) < processed_len + assert 55 <= processed_target_pos <= 75 + assert 110 <= processed_target_neg <= 150 + + if balance == "undersample_majority": + assert len(input_train_set) > processed_len + assert 7 <= processed_target_pos <= 20 + assert 14 <= processed_target_neg <= 40 + + +@pytest.mark.parametrize( + "balance", + ["oversample_minority", "undersample_majority"], +) +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.skip(reason="Flaky") +def test_imbalance_ray(balance): + config = { + "input_features": [ + {"name": "Index", "column": "Index", "type": "numerical"}, + {"name": "random_1", "column": "random_1", "type": "numerical"}, + {"name": "random_2", "column": "random_2", "type": "numerical"}, + ], + "output_features": [{"name": "Label", "column": "Label", "type": "binary"}], + "trainer": {"epochs": 2, "batch_size": 8}, + "preprocessing": {}, + } + split_col = np.concatenate((np.zeros(140), np.ones(20), np.full(40, 2))) + rs.shuffle(split_col) + df = pd.DataFrame( + { + "Index": np.arange(0, 200, 1), + "random_1": np.random.randint(0, 50, 200), + "random_2": np.random.choice(["Type A", "Type B", "Type C", "Type D"], 200), + "Label": np.concatenate((np.zeros(180), np.ones(20))), + "split": split_col, + } + ) + + config["preprocessing"][balance] = 0.5 + run_test_imbalance_ray(df, config, balance) + + +@pytest.mark.parametrize( + "balance", + ["oversample_minority", "undersample_majority"], +) +def test_imbalance_local(balance): + config = { + "input_features": [ + {"name": "Index", "column": "Index", "type": "number"}, + {"name": "random_1", "column": "random_1", "type": "number"}, + {"name": "random_2", "column": "random_2", "type": "category"}, + ], + "output_features": [{"name": "Label", "column": "Label", "type": "binary"}], + "trainer": {"epochs": 2, "batch_size": 8}, + "preprocessing": {}, + } + df = pd.DataFrame( + { + "Index": np.arange(0, 200, 1), + "random_1": np.random.randint(0, 50, 200), + "random_2": np.random.choice(["Type A", "Type B", "Type C", "Type D"], 200), + "Label": np.concatenate((np.zeros(180), np.ones(20))), + } + ) + + config["preprocessing"][balance] = 0.5 + run_test_imbalance_local(df, config, balance) diff --git a/tests/integration_tests/test_cli.py b/tests/integration_tests/test_cli.py new file mode 100644 index 0000000..78e4808 --- /dev/null +++ b/tests/integration_tests/test_cli.py @@ -0,0 +1,418 @@ +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import json +import os +import os.path +import pathlib +import shutil +import subprocess +import sys + +import pytest +import yaml + +from ludwig.constants import ( + BATCH_SIZE, + COMBINER, + EVAL_BATCH_SIZE, + INPUT_FEATURES, + NAME, + OUTPUT_FEATURES, + PREPROCESSING, + TRAINER, +) +from ludwig.globals import MODEL_FILE_NAME +from ludwig.types import FeatureConfigDict +from ludwig.utils.data_utils import load_yaml +from tests.integration_tests.utils import category_feature, generate_data, number_feature, sequence_feature + +pytestmark = pytest.mark.integration_tests_f + + +def _run_commands(commands, **ludwig_kwargs): + for arg_name, value in ludwig_kwargs.items(): + commands += ["--" + arg_name, value] + cmdline = " ".join(commands) + print(cmdline) + completed_process = subprocess.run(cmdline, shell=True, stdout=subprocess.PIPE, env=os.environ.copy()) + assert completed_process.returncode == 0 + + return completed_process + + +def _run_ludwig(command, **ludwig_kwargs): + ludwig_bin = os.path.join(os.path.dirname(sys.executable), "ludwig") + commands = [ludwig_bin, command] + return _run_commands(commands, **ludwig_kwargs) + + +def _prepare_data(csv_filename, config_filename): + # Single sequence input, single category output + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 3}, reduce_input="sum")] + + # Generate test data + dataset_filename = generate_data(input_features, output_features, csv_filename) + + # generate config file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128, EVAL_BATCH_SIZE: 128}, + } + + with open(config_filename, "w") as f: + yaml.dump(config, f) + + return dataset_filename + + +def _prepare_hyperopt_data(csv_filename, config_filename): + # Single sequence input, single category output + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + dataset_filename = generate_data(input_features, output_features, csv_filename) + + # generate config file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 4}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + "hyperopt": { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.0001, + "upper": 0.01, + } + }, + "goal": "minimize", + "output_feature": output_features[0]["name"], + "validation_metrics": "loss", + "executor": { + "type": "ray", + "num_samples": 2, + }, + "search_alg": { + "type": "variant_generator", + }, + }, + } + + with open(config_filename, "w") as f: + yaml.dump(config, f) + + return dataset_filename + + +def test_train_cli_dataset(tmpdir, csv_filename): + """Test training using `ludwig train --dataset`.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + + +def test_train_cli_gpu_memory_limit(tmpdir, csv_filename): + """Test training using `ludwig train --dataset --gpu_memory_limit`.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig( + "train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir), gpu_memory_limit="0.5" + ) + + +def test_train_cli_training_set(tmpdir, csv_filename): + """Test training using `ludwig train --training_set`.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + validation_filename = shutil.copyfile(dataset_filename, os.path.join(tmpdir, "validation.csv")) + test_filename = shutil.copyfile(dataset_filename, os.path.join(tmpdir, "test.csv")) + _run_ludwig( + "train", + training_set=dataset_filename, + validation_set=validation_filename, + test_set=test_filename, + config=config_filename, + output_directory=str(tmpdir), + ) + + +def test_export_mlflow_cli(tmpdir, csv_filename): + """Test export_mlflow cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + _run_ludwig( + "export_mlflow", + model_path=os.path.join(tmpdir, "experiment_run", MODEL_FILE_NAME), + output_path=os.path.join(tmpdir, "data/results/mlflow"), + ) + + +def test_experiment_cli(tmpdir, csv_filename): + """Test experiment cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("experiment", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + + +def test_predict_cli(tmpdir, csv_filename): + """Test predict cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + _run_ludwig( + "predict", + dataset=dataset_filename, + model=os.path.join(tmpdir, "experiment_run", MODEL_FILE_NAME), + output_directory=os.path.join(tmpdir, "predictions"), + ) + + +def test_evaluate_cli(tmpdir, csv_filename): + """Test evaluate cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + _run_ludwig( + "evaluate", + dataset=dataset_filename, + model=os.path.join(tmpdir, "experiment_run", MODEL_FILE_NAME), + output_directory=os.path.join(tmpdir, "predictions"), + ) + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_hyperopt_cli(tmpdir, csv_filename): + """Test hyperopt cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_hyperopt_data(csv_filename, config_filename) + _run_ludwig("hyperopt", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + + +def test_visualize_cli(tmpdir, csv_filename): + """Test Ludwig 'visualize' cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + _run_ludwig( + "visualize", + visualization="learning_curves", + model_names="run", + training_statistics=os.path.join(tmpdir, "experiment_run", "training_statistics.json"), + output_directory=os.path.join(tmpdir, "visualizations"), + ) + + +def test_collect_summary_activations_weights_cli(tmpdir, csv_filename): + """Test collect_summary cli.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("train", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir)) + assert _run_ludwig("collect_summary", model=os.path.join(tmpdir, "experiment_run", MODEL_FILE_NAME)) + + +@pytest.mark.parametrize( + "model_name", + [ + "alexnet", + "convnext_base", + "convnext_large", + "convnext_small", + "convnext_tiny", + "densenet121", + "densenet161", + "densenet169", + "openai-community/gpt2", + "facebook/opt-125m", + ], +) +def test_collect_summary_pretrained_model_cli(model_name): + """Test collect_summary pretrained model cli.""" + assert _run_ludwig("collect_summary", pretrained_model=model_name) + + +def test_synthesize_dataset_cli(tmpdir, csv_filename): + """Test synthesize_data cli.""" + # test depends on default setting of --dataset_size + # if this parameter is specified, _run_ludwig fails when + # attempting to build the cli parameter structure + _run_ludwig( + "synthesize_dataset", + output_path=os.path.join(tmpdir, csv_filename), + features="'[ \ + {name: text, type: text}, \ + {name: category, type: category}, \ + {name: number, type: number}, \ + {name: binary, type: binary}, \ + {name: set, type: set}, \ + {name: bag, type: bag}, \ + {name: sequence, type: sequence}, \ + {name: timeseries, type: timeseries}, \ + {name: date, type: date}, \ + {name: h3, type: h3}, \ + {name: vector, type: vector}, \ + {name: audio, type: audio}, \ + {name: image, type: image} \ + ]'", + ) + + +def test_preprocess_cli(tmpdir, csv_filename): + """Test preprocess `ludwig preprocess.""" + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + _run_ludwig("preprocess", dataset=dataset_filename, preprocessing_config=config_filename) + + +@pytest.mark.parametrize( + "second_seed_offset,random_seed,type_of_run", + [ + (0, 42, "train"), # same seed train: should be reproducible + (1, 42, "train"), # different seed train: should diverge + (0, 42, "experiment"), # same seed experiment: should be reproducible + ], + ids=["same_seed_train", "diff_seed_train", "same_seed_experiment"], +) +def test_reproducible_cli_runs( + type_of_run: str, random_seed: int, second_seed_offset: int, csv_filename: str, tmpdir: pathlib.Path +) -> None: + """ + Test for reproducible training using `ludwig experiment|train --dataset`. + Args: + type_of_run(str): type of run, either train or experiment + csv_filename(str): file path of dataset to use + random_seed(int): random seed integer to use for test + second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different + seed for the second run. + tmpdir (pathlib.Path): temporary directory path + + Returns: None + """ + config_filename = os.path.join(tmpdir, "config.yaml") + dataset_filename = _prepare_data(csv_filename, config_filename) + + # run first model + _run_ludwig( + type_of_run, + dataset=dataset_filename, + config=config_filename, + output_directory=str(tmpdir), + skip_save_processed_input="", # skip saving preprocessed inputs for reproducibility + experiment_name="reproducible", + model_name="run1", + random_seed=str(random_seed), + ) + + # run second model with same seed + _run_ludwig( + type_of_run, + dataset=dataset_filename, + config=config_filename, + output_directory=str(tmpdir), + skip_save_processed_input="", # skip saving preprocessed inputs for reproducibility + experiment_name="reproducible", + model_name="run2", + random_seed=str(random_seed + second_seed_offset), + ) + + # retrieve training statistics and compare + with open(os.path.join(tmpdir, "reproducible_run1", "training_statistics.json")) as f: + training1 = json.load(f) + with open(os.path.join(tmpdir, "reproducible_run2", "training_statistics.json")) as f: + training2 = json.load(f) + + if second_seed_offset == 0: + # same seeds should result in same output + assert training1 == training2 + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert training1 != training2 + + # if type_of_run is experiment check test statistics and compare + if type_of_run == "experiment": + with open(os.path.join(tmpdir, "reproducible_run1", "test_statistics.json")) as f: + test1 = json.load(f) + with open(os.path.join(tmpdir, "reproducible_run2", "test_statistics.json")) as f: + test2 = json.load(f) + + if second_seed_offset == 0: + # same seeds should result in same output + assert test1 == test2 + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert test1 != test2 + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_init_config(tmpdir): + """Test initializing a config from a dataset and a target.""" + input_features = [ + number_feature(), + number_feature(), + category_feature(encoder={"vocab_size": 3}), + category_feature(encoder={"vocab_size": 3}), + ] + output_features = [category_feature(decoder={"vocab_size": 3})] + dataset_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=20) + output_config_path = os.path.join(tmpdir, "config.yaml") + + _run_ludwig("init_config", dataset=dataset_csv, target=output_features[0][NAME], output=output_config_path) + + config = load_yaml(output_config_path) + + def to_name_set(features: list[FeatureConfigDict]) -> set[str]: + return {feature[NAME] for feature in features} + + assert to_name_set(config[INPUT_FEATURES]) == to_name_set(input_features) + assert to_name_set(config[OUTPUT_FEATURES]) == to_name_set(output_features) + + +@pytest.mark.skip(reason="https://github.com/ludwig-ai/ludwig/issues/3377") +def test_render_config(tmpdir): + """Test rendering a full config from a partial user config.""" + user_config_path = os.path.join(tmpdir, "config.yaml") + input_features = [ + number_feature(), + number_feature(), + category_feature(encoder={"vocab_size": 3}), + category_feature(encoder={"vocab_size": 3}), + ] + output_features = [category_feature(decoder={"vocab_size": 3})] + + user_config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + } + + with open(user_config_path, "w") as f: + yaml.dump(user_config, f) + + output_config_path = os.path.join(tmpdir, "rendered.yaml") + _run_ludwig("render_config", config=user_config_path, output=output_config_path) + + rendered_config = load_yaml(output_config_path) + assert len(rendered_config[INPUT_FEATURES]) == len(user_config[INPUT_FEATURES]) + assert len(rendered_config[OUTPUT_FEATURES]) == len(user_config[OUTPUT_FEATURES]) + assert TRAINER in rendered_config + assert COMBINER in rendered_config + assert PREPROCESSING in rendered_config diff --git a/tests/integration_tests/test_collect.py b/tests/integration_tests/test_collect.py new file mode 100644 index 0000000..a3d4574 --- /dev/null +++ b/tests/integration_tests/test_collect.py @@ -0,0 +1,112 @@ +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os +import shutil + +import numpy as np +import torch + +from ludwig.api import LudwigModel +from ludwig.collect import collect_activations, collect_weights, print_model_summary +from ludwig.constants import BATCH_SIZE, ENCODER, TRAINER, TYPE +from ludwig.globals import MODEL_FILE_NAME +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.utils import category_feature, ENCODERS, generate_data, sequence_feature + +DEVICE = get_torch_device() + + +def _prepare_data(csv_filename): + # Single sequence input, single category output + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + input_features[0][ENCODER][TYPE] = ENCODERS[0] + + # Generate test data + data_csv = generate_data(input_features, output_features, csv_filename) + return input_features, output_features, data_csv + + +def _train(input_features, output_features, data_csv, **kwargs): + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + model = LudwigModel(config) + _, _, output_dir = model.train(dataset=data_csv, **kwargs) + return model, output_dir + + +def _get_layers(model_path): + model = LudwigModel.load(model_path) + return [name for name, _ in model.model.named_children()] + + +def _collect_activations(model_path, layers, csv_filename, output_directory): + return collect_activations(model_path, layers, dataset=csv_filename, output_directory=output_directory) + + +def test_collect_weights(tmpdir, csv_filename): + output_dir = None + try: + model, output_dir = _train(*_prepare_data(csv_filename)) + model_path = os.path.join(output_dir, MODEL_FILE_NAME) + + # 1 for the encoder (embeddings). + # 2 for the decoder classifier (w and b). + weights = [w for _, w in model.model.collect_weights()] + assert len(weights) == 3 + + # Load model from disk to ensure correct weight names + model_loaded = LudwigModel.load(model_path) + tensor_names = [name for name, w in model_loaded.collect_weights()] + assert len(tensor_names) == 3 + + filenames = collect_weights(model_path, tensor_names, tmpdir) + assert len(filenames) == 3 + + for weight, filename in zip(weights, filenames): + saved_weight = np.load(filename) + assert torch.allclose(weight, torch.from_numpy(saved_weight).to(DEVICE), rtol=1.0e-4), filename + finally: + if output_dir: + shutil.rmtree(output_dir, ignore_errors=True) + + +def test_collect_activations(tmpdir, csv_filename): + output_dir = None + try: + model, output_dir = _train(*_prepare_data(csv_filename)) + model_path = os.path.join(output_dir, MODEL_FILE_NAME) + + # [last_hidden, logits, projection_input] + filenames = _collect_activations( + model_path, [name for name, _ in model.model.named_children()], csv_filename, tmpdir + ) + assert len(filenames) == 3 + finally: + if output_dir: + shutil.rmtree(output_dir, ignore_errors=True) + + +def test_print_model_summary(csv_filename): + output_dir = None + model, output_dir = _train(*_prepare_data(csv_filename)) + model_path = os.path.join(output_dir, MODEL_FILE_NAME) + print_model_summary(model_path) diff --git a/tests/integration_tests/test_config_global_defaults.py b/tests/integration_tests/test_config_global_defaults.py new file mode 100644 index 0000000..e71d0c4 --- /dev/null +++ b/tests/integration_tests/test_config_global_defaults.py @@ -0,0 +1,88 @@ +import logging + +from ludwig.constants import ( + BATCH_SIZE, + CATEGORY, + COMBINER, + DECODER, + DEFAULTS, + ENCODER, + EPOCHS, + FILL_WITH_CONST, + INPUT_FEATURES, + LOSS, + OUTPUT_FEATURES, + PREPROCESSING, + TEXT, + TRAINER, + TYPE, +) +from ludwig.schema.model_config import ModelConfig +from tests.integration_tests.utils import category_feature, generate_data, run_experiment, text_feature + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + + +def _prepare_data(csv_filename: str) -> tuple[dict, str]: + input_features = [ + text_feature(name="title", reduce_output="sum"), + text_feature(name="summary"), + category_feature(vocab_size=3), + category_feature(vocab_size=3), + ] + + output_features = [text_feature(name="article", embedding_size=3, output_feature=True)] + + dataset = generate_data(input_features, output_features, csv_filename) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat", "num_fc_layers": 2}, + TRAINER: {EPOCHS: 1, "learning_rate": 0.001, BATCH_SIZE: 128}, + DEFAULTS: { + CATEGORY: { + PREPROCESSING: {"missing_value_strategy": FILL_WITH_CONST, "fill_value": ""}, + ENCODER: {TYPE: "sparse"}, + DECODER: {"norm_params": None, "dropout": 0.1, "use_bias": True}, + }, + TEXT: { + PREPROCESSING: {"most_common": 10, "padding_symbol": ""}, + ENCODER: {TYPE: "rnn"}, + DECODER: {TYPE: "generator", "num_fc_layers": 2, "dropout": 0.1}, + LOSS: {"confidence_penalty": 0.1}, + }, + }, + } + + return config, dataset + + +def test_run_experiment_with_global_default_parameters(csv_filename): + config, dataset = _prepare_data(csv_filename) + + run_experiment(config=config, dataset=dataset) + + +def test_global_defaults_with_encoder_dependencies(): + input_features = [text_feature(name="title", reduce_output="sum")] + output_features = [category_feature(name="article", embedding_size=3, output_feature=True)] + del input_features[0][ENCODER] + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + DEFAULTS: { + TEXT: { + ENCODER: {TYPE: "bert"}, + } + }, + } + + # Config should populate with the additional required fields for bert + updated_config = ModelConfig.from_dict(config).to_dict() + + assert updated_config[INPUT_FEATURES][0][ENCODER][TYPE] == "bert" + assert updated_config[INPUT_FEATURES][0][ENCODER]["pretrained_model_name_or_path"] == "bert-base-uncased" diff --git a/tests/integration_tests/test_contrib_aim.py b/tests/integration_tests/test_contrib_aim.py new file mode 100644 index 0000000..e9438c9 --- /dev/null +++ b/tests/integration_tests/test_contrib_aim.py @@ -0,0 +1,28 @@ +import logging +import os +import subprocess +import sys + +import pytest + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +TEST_SCRIPT = os.path.join(os.path.dirname(__file__), "scripts", "run_train_aim.py") + + +@pytest.mark.skip(reason="Aim integration not compatible with Aim 4.0.") +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_contrib_experiment(csv_filename, tmpdir): + aim_test_path = os.path.join(tmpdir, "results") + os.makedirs(aim_test_path, exist_ok=True) + + os.environ["AIM_TEST_PATH"] = aim_test_path + subprocess.call(["chmod", "-R", "a+w", os.environ["AIM_TEST_PATH"]]) + + cmdline = [sys.executable, TEST_SCRIPT, "--csv-filename", csv_filename] + print(cmdline) + exit_code = subprocess.call(" ".join(cmdline), shell=True, env=os.environ.copy()) + assert exit_code == 0 diff --git a/tests/integration_tests/test_contrib_comet.py b/tests/integration_tests/test_contrib_comet.py new file mode 100644 index 0000000..73d2cee --- /dev/null +++ b/tests/integration_tests/test_contrib_comet.py @@ -0,0 +1,34 @@ +import importlib.util +import logging +import os +import subprocess +import sys + +import pytest + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +TEST_SCRIPT = os.path.join(os.path.dirname(__file__), "scripts", "run_train_comet.py") + + +@pytest.mark.skipif( + not importlib.util.find_spec("pkg_resources"), + reason="comet_ml requires pkg_resources (removed in setuptools 82+)", +) +@pytest.mark.skipif( + not importlib.util.find_spec("imp"), + reason="comet_ml requires imp module (removed in Python 3.12)", +) +def test_contrib_experiment(csv_filename): + cmdline = [sys.executable, TEST_SCRIPT, "--csv-filename", csv_filename] + exit_code = subprocess.call(" ".join(cmdline), shell=True, env=os.environ.copy()) + assert exit_code == 0 + + +if __name__ == "__main__": + """To run tests individually, run: + + ```python -m pytest tests/integration_tests/test_contrib_comet.py::test_name``` + """ diff --git a/tests/integration_tests/test_contrib_wandb.py b/tests/integration_tests/test_contrib_wandb.py new file mode 100644 index 0000000..d705683 --- /dev/null +++ b/tests/integration_tests/test_contrib_wandb.py @@ -0,0 +1,27 @@ +import logging +import os +import subprocess +import sys + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +TEST_SCRIPT = os.path.join(os.path.dirname(__file__), "scripts", "run_train_wandb.py") + + +def test_contrib_experiment(csv_filename, tmpdir): + wandb_dir = os.path.join(tmpdir, "results") + os.makedirs(wandb_dir, exist_ok=True) + os.environ["WANDB_DIR"] = wandb_dir + subprocess.call(["chmod", "-R", "a+w", os.environ["WANDB_DIR"]]) + cmdline = [sys.executable, TEST_SCRIPT, "--csv-filename", csv_filename] + exit_code = subprocess.call(" ".join(cmdline), shell=True, env=os.environ.copy()) + assert exit_code == 0 + + +if __name__ == "__main__": + """To run tests individually, run: + + ```python -m pytest tests/integration_tests/test_contrib_wandb.py::test_name``` + """ diff --git a/tests/integration_tests/test_custom_components.py b/tests/integration_tests/test_custom_components.py new file mode 100644 index 0000000..1531122 --- /dev/null +++ b/tests/integration_tests/test_custom_components.py @@ -0,0 +1,187 @@ +import os +import tempfile + +import torch +from torch import nn, Tensor + +from ludwig.api import LudwigModel +from ludwig.combiners.combiners import Combiner, register_combiner +from ludwig.constants import BATCH_SIZE, ENCODER_OUTPUT, LOGITS, MINIMIZE, NUMBER, TRAINER +from ludwig.decoders.base import Decoder +from ludwig.decoders.registry import register_decoder +from ludwig.encoders.base import Encoder +from ludwig.encoders.registry import register_encoder +from ludwig.modules.loss_modules import LogitsInputsMixin, register_loss +from ludwig.modules.metric_modules import LossMetric, register_metric +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.base import BaseCombinerConfig +from ludwig.schema.combiners.utils import register_combiner_config +from ludwig.schema.decoders.base import BaseDecoderConfig +from ludwig.schema.decoders.utils import register_decoder_config +from ludwig.schema.encoders.base import BaseEncoderConfig +from ludwig.schema.encoders.utils import register_encoder_config +from ludwig.schema.features.loss.loss import BaseLossConfig +from ludwig.schema.features.loss.loss import register_loss as register_loss_schema +from tests.integration_tests.utils import ( + category_feature, + generate_data, + LocalTestBackend, + number_feature, + sequence_feature, +) + + +@register_encoder_config("custom_number_encoder", NUMBER) +class CustomNumberEncoderConfig(BaseEncoderConfig): + type: str = "custom_number_encoder" + + input_size: int = schema_utils.PositiveInteger(default=1, description="") + + +@register_decoder_config("custom_number_decoder", NUMBER) +class CustomNumberDecoderConfig(BaseDecoderConfig): + type: str = "custom_number_decoder" + + input_size: int = schema_utils.PositiveInteger(default=1, description="") + + +@register_loss_schema([NUMBER]) +class CustomLossConfig(BaseLossConfig): + type: str = "custom_loss" + + +@register_combiner_config("custom_combiner") +class CustomTestCombinerConfig(BaseCombinerConfig): + type: str = "custom_combiner" + + foo: bool = schema_utils.Boolean(default=False, description="") + + +@register_combiner(CustomTestCombinerConfig) +class CustomTestCombiner(Combiner): + def __init__(self, input_features: dict = None, config: CustomTestCombinerConfig = None, **kwargs): + super().__init__(input_features) + self.foo = config.foo + + def forward(self, inputs: dict) -> dict: # encoder outputs + if not self.foo: + raise ValueError("expected foo to be True") + + # minimal transformation from inputs to outputs + encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs] + hidden = torch.cat(encoder_outputs, 1) + return_data = {"combiner_output": hidden} + + return return_data + + +@register_encoder("custom_number_encoder", NUMBER) +class CustomNumberEncoder(Encoder): + def __init__(self, input_size, **kwargs): + super().__init__() + self.input_size = input_size + + def forward(self, inputs, **kwargs): + return {ENCODER_OUTPUT: inputs} + + @property + def input_shape(self) -> torch.Size: + return torch.Size([self.input_size]) + + @property + def output_shape(self) -> torch.Size: + return self.input_shape + + @staticmethod + def get_schema_cls(): + return CustomNumberEncoderConfig + + +@register_decoder("custom_number_decoder", NUMBER) +class CustomNumberDecoder(Decoder): + def __init__(self, input_size, **kwargs): + super().__init__() + self.input_size = input_size + + @property + def input_shape(self): + return torch.Size([self.input_size]) + + def forward(self, inputs, **kwargs): + return torch.mean(inputs, 1) + + @staticmethod + def get_schema_cls(): + return CustomNumberDecoderConfig + + +@register_loss(CustomLossConfig) +class CustomLoss(nn.Module, LogitsInputsMixin): + def __init__(self, config: CustomLossConfig): + super().__init__() + + def forward(self, preds: Tensor, target: Tensor) -> Tensor: + return torch.mean(torch.square(preds - target)) + + @staticmethod + def get_schema_cls(): + return CustomLossConfig + + +@register_metric("custom_loss", [NUMBER], MINIMIZE, LOGITS) +class CustomLossMetric(LossMetric): + def __init__(self, config: CustomLossConfig, **kwargs): + super().__init__() + self.loss_fn = CustomLoss(config) + + def get_current_value(self, preds: Tensor, target: Tensor): + return self.loss_fn(preds, target) + + +def test_custom_combiner(): + _run_test(combiner={"type": "custom_combiner", "foo": True}) + + +def test_custom_encoder_decoder(): + input_features = [ + sequence_feature(encoder={"reduce_output": "sum"}), + number_feature(encoder={"type": "custom_number_encoder"}), + ] + output_features = [ + number_feature(decoder={"type": "custom_number_decoder"}), + ] + _run_test(input_features=input_features, output_features=output_features) + + +def test_custom_loss_metric(): + output_features = [ + number_feature(loss={"type": "custom_loss"}), + ] + _run_test(output_features=output_features) + + +def _run_test(input_features=None, output_features=None, combiner=None): + with tempfile.TemporaryDirectory() as tmpdir: + input_features = input_features or [ + sequence_feature(encoder={"reduce_output": "sum"}), + number_feature(), + ] + output_features = output_features or [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + combiner = combiner or {"type": "concat"} + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": combiner, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + model = LudwigModel(config, backend=LocalTestBackend()) + _, _, output_directory = model.train( + dataset=data_csv, + output_directory=tmpdir, + ) + model.predict(dataset=data_csv, output_directory=output_directory) diff --git a/tests/integration_tests/test_date_feature.py b/tests/integration_tests/test_date_feature.py new file mode 100644 index 0000000..cbe91c7 --- /dev/null +++ b/tests/integration_tests/test_date_feature.py @@ -0,0 +1,105 @@ +import datetime +import time + +import pandas as pd +import pytest +from dateutil.parser import parse + +from ludwig.api import LudwigModel +from ludwig.constants import ( + BACKEND, + BINARY, + DATE, + EPOCHS, + FILL_WITH_CONST, + INPUT_FEATURES, + MISSING_VALUE_STRATEGY, + NAME, + OUTPUT_FEATURES, + PREPROCESSING, + RAY, + TRAINER, + TYPE, +) +from ludwig.utils.date_utils import create_vector_from_datetime_obj + +ray = pytest.importorskip("ray") + +pytestmark = [ + pytest.mark.distributed, + pytest.mark.distributed_f, +] + + +@pytest.fixture(scope="module") +def string_date_df() -> "pd.DataFrame": + df = pd.DataFrame.from_dict( + { + "date_feature": [str(datetime.datetime.now()) for i in range(100)], + "binary_feature": [i % 2 for i in range(100)], + } + ) + return df + + +@pytest.fixture(scope="module") +def int_date_df() -> "pd.DataFrame": + df = pd.DataFrame.from_dict( + { + "date_feature": [time.time_ns() for i in range(100)], + "binary_feature": [i % 2 for i in range(100)], + } + ) + return df + + +@pytest.fixture(scope="module") +def float_date_df() -> "pd.DataFrame": + df = pd.DataFrame.from_dict( + { + "date_feature": [time.time() for i in range(100)], + "binary_feature": [i % 2 for i in range(100)], + } + ) + return df + + +@pytest.mark.parametrize( + "date_df", + [ + pytest.param("string_date_df", id="string_date"), + pytest.param("int_date_df", id="int_date"), + pytest.param("float_date_df", id="float_date"), + ], +) +def test_date_feature_formats(date_df, request, ray_cluster_2cpu): + df = request.getfixturevalue(date_df) + + config = { + INPUT_FEATURES: [ + { + NAME: "date_feature", + TYPE: DATE, + PREPROCESSING: {MISSING_VALUE_STRATEGY: FILL_WITH_CONST, "fill_value": "1970-01-01 00:00:00"}, + } + ], + OUTPUT_FEATURES: [{NAME: "binary_feature", TYPE: BINARY}], + TRAINER: {EPOCHS: 2}, + BACKEND: {TYPE: RAY, "processor": {TYPE: "dask"}}, + } + + fill_value = create_vector_from_datetime_obj(parse("1970-01-01 00:00:00")) + + model = LudwigModel(config) + preprocessed = model.preprocess(df) + + # Because parsing errors are suppressed, we want to ensure that the data was preprocessed correctly. Sample data is + # drawn from the current time, so the recorded years should not match the fill value's year. + for date in preprocessed.training_set.to_df().compute().iloc[:, 0].values: + assert date[0] != fill_value[0] + + for date in preprocessed.validation_set.to_df().compute().iloc[:, 0].values: + assert date[0] != fill_value[0] + + for date in preprocessed.test_set.to_df().compute().iloc[:, 0].values: + assert date[0] != fill_value[0] diff --git a/tests/integration_tests/test_dependencies.py b/tests/integration_tests/test_dependencies.py new file mode 100644 index 0000000..19fb308 --- /dev/null +++ b/tests/integration_tests/test_dependencies.py @@ -0,0 +1,140 @@ +import logging + +import pytest +import torch + +from ludwig.combiners.combiners import ConcatCombiner +from ludwig.constants import CATEGORY, DECODER, NUMBER, SEQUENCE, TYPE +from ludwig.models.base import BaseModel +from ludwig.modules.reduction_modules import SequenceReducer +from ludwig.schema.model_config import ModelConfig +from ludwig.utils import output_feature_utils +from tests.integration_tests.utils import generate_output_features_with_dependencies, number_feature + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +BATCH_SIZE = 16 +SEQ_SIZE = 12 +HIDDEN_SIZE = 128 +OTHER_HIDDEN_SIZE = 32 +OTHER_HIDDEN_SIZE2 = 64 + + +# unit test for dependency concatenation +# tests both single and multiple dependencies +@pytest.mark.parametrize( + "dependent_hidden_shape2", + [ + None, + [BATCH_SIZE, OTHER_HIDDEN_SIZE2], + [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE2], + [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE], + ], +) +@pytest.mark.parametrize( + "dependent_hidden_shape", [[BATCH_SIZE, OTHER_HIDDEN_SIZE], [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE]] +) +@pytest.mark.parametrize("hidden_shape", [[BATCH_SIZE, HIDDEN_SIZE], [BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE]]) +@pytest.mark.parametrize( + # todo: re-add 'attention' after further research in implication of torch + # migration + "reduce_dependencies", + ["sum", "mean", "avg", "max", "concat", "last"], +) +def test_multiple_dependencies(reduce_dependencies, hidden_shape, dependent_hidden_shape, dependent_hidden_shape2): + # setup at least for a single dependency + hidden_layer = torch.randn(hidden_shape, dtype=torch.float32) + other_hidden_layer = torch.randn(dependent_hidden_shape, dtype=torch.float32) + other_dependencies = { + "feature_name": other_hidden_layer, + } + + # setup dummy output feature to be root of dependency list + num_feature_defn = number_feature() + num_feature_defn["loss"] = {"type": "mean_squared_error"} + num_feature_defn["dependencies"] = ["feature_name"] + if len(dependent_hidden_shape) > 2: + num_feature_defn["reduce_dependencies"] = reduce_dependencies + + # Based on specification calculate expected resulting hidden size for + # with one dependencies + if reduce_dependencies == "concat" and len(hidden_shape) == 2 and len(dependent_hidden_shape) == 3: + expected_hidden_size = HIDDEN_SIZE + OTHER_HIDDEN_SIZE * SEQ_SIZE + else: + expected_hidden_size = HIDDEN_SIZE + OTHER_HIDDEN_SIZE + + # set up if multiple dependencies specified, setup second dependent feature + if dependent_hidden_shape2: + other_hidden_layer2 = torch.randn(dependent_hidden_shape2, dtype=torch.float32) + other_dependencies["feature_name2"] = other_hidden_layer2 + num_feature_defn["dependencies"].append("feature_name2") + if len(dependent_hidden_shape2) > 2: + num_feature_defn["reduce_dependencies"] = reduce_dependencies + + # Based on specification calculate marginal increase in resulting + # hidden size with two dependencies + if reduce_dependencies == "concat" and len(hidden_shape) == 2 and len(dependent_hidden_shape2) == 3: + expected_hidden_size += dependent_hidden_shape2[-1] * SEQ_SIZE + else: + expected_hidden_size += dependent_hidden_shape2[-1] + + # Set up dependency reducers. + dependency_reducers = torch.nn.ModuleDict() + for feature_name in other_dependencies: + dependency_reducers[feature_name] = SequenceReducer(reduce_mode=reduce_dependencies) + + # test dependency concatenation + num_feature_defn["input_size"] = expected_hidden_size + results = output_feature_utils.concat_dependencies( + "num_feature", num_feature_defn["dependencies"], dependency_reducers, hidden_layer, other_dependencies + ) + + # confirm size of resulting concat_dependencies() call + if len(hidden_shape) > 2: + assert results.shape == (BATCH_SIZE, SEQ_SIZE, expected_hidden_size) + else: + assert results.shape == (BATCH_SIZE, expected_hidden_size) + + +@pytest.mark.parametrize( + "output_feature_defs", + [ + generate_output_features_with_dependencies("number_feature", ["category_feature"]), + generate_output_features_with_dependencies("number_feature", ["category_feature", "sequence_feature"]), + generate_output_features_with_dependencies("sequence_feature", ["category_feature", "number_feature"]), + ], +) +def test_construct_output_features_with_dependencies(output_feature_defs): + # Add keys to output_feature_defs which would have been derived from data. + def add_data_derived_keys(output_feature_def): + if DECODER not in output_feature_def: + output_feature_def[DECODER] = {} + if output_feature_def[TYPE] == CATEGORY: + output_feature_def["num_classes"] = 2 + elif output_feature_def[TYPE] == NUMBER: + output_feature_def[DECODER][TYPE] = "regressor" + elif output_feature_def[TYPE] == SEQUENCE: + output_feature_def[DECODER]["max_sequence_length"] = 5 + return output_feature_def + + output_feature_defs = [add_data_derived_keys(of) for of in output_feature_defs] + # Gets name of output feature which has dependencies. + dep_feature_name = [of for of in output_feature_defs if len(of.get("dependencies", [])) > 0][0]["name"] + # Creates a dummy input feature and combiner. + config = { + "input_features": [number_feature()], + "output_features": output_feature_defs, + "combiner": {"type": "concat", "output_size": 1}, + } + config_obj = ModelConfig.from_dict(config) + input_features = BaseModel.build_inputs(config_obj.input_features) + combiner = ConcatCombiner(input_features=input_features, config=config_obj.combiner) + output_features = BaseModel.build_outputs(config_obj.output_features, combiner) + # Gets the output feature object which has dependencies. + feature_with_deps = output_features[dep_feature_name] + n_dependencies = len(feature_with_deps.dependencies) + assert n_dependencies > 0 + # Each synthetic output feature has output size 1, so total size is 1 + n_dependencies. + assert feature_with_deps.fc_stack.input_shape == torch.Size([1 + n_dependencies]) diff --git a/tests/integration_tests/test_experiment.py b/tests/integration_tests/test_experiment.py new file mode 100644 index 0000000..5773ff6 --- /dev/null +++ b/tests/integration_tests/test_experiment.py @@ -0,0 +1,1172 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import contextlib +import logging +import os +import shutil +import uuid +from collections import namedtuple + +import pandas as pd +import pytest +import torchvision +import yaml + +from ludwig.api import LudwigModel +from ludwig.backend import LOCAL_BACKEND +from ludwig.callbacks import Callback +from ludwig.constants import BATCH_SIZE, COLUMN, ENCODER, H3, NAME, PREPROCESSING, TRAINER, TYPE +from ludwig.data.concatenate_datasets import concatenate_df +from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df +from ludwig.encoders.registry import get_encoder_classes +from ludwig.error import ConfigValidationError +from ludwig.experiment import experiment_cli +from ludwig.globals import MODEL_FILE_NAME +from ludwig.predict import predict_cli +from ludwig.utils.data_utils import read_csv +from ludwig.utils.defaults import default_random_seed +from tests.integration_tests.utils import ( + audio_feature, + bag_feature, + binary_feature, + category_distribution_feature, + category_feature, + create_data_set_to_use, + date_feature, + ENCODERS, + generate_data, + generate_output_features_with_dependencies, + generate_output_features_with_dependencies_complex, + h3_feature, + image_feature, + LocalTestBackend, + number_feature, + run_experiment, + sequence_feature, + set_feature, + text_feature, + timeseries_feature, + vector_feature, +) + +pytestmark = pytest.mark.integration_tests_h + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + + +@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer", "tf_idf"]) +def test_experiment_text_feature_non_pretrained(encoder, csv_filename): + input_features = [ + text_feature(encoder={"vocab_size": 30, "min_len": 1, "type": encoder}, preprocessing={"tokenizer": "space"}) + ] + output_features = [category_feature(decoder={"vocab_size": 2})] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +def run_experiment_with_encoder(encoder, csv_filename): + # Run in a subprocess to clear TF and prevent OOM + # This also allows us to use GPU resources + input_features = [text_feature(encoder={"vocab_size": 30, "min_len": 1, "type": encoder})] + output_features = [category_feature(decoder={"vocab_size": 2})] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer"]) +def test_experiment_seq_seq_generator(csv_filename, encoder): + input_features = [text_feature(encoder={"type": encoder, "reduce_output": None})] + output_features = [text_feature(decoder={"type": "generator"}, output_feature=True)] + rel_path = generate_data(input_features, output_features, csv_filename) + + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer"]) +def test_experiment_seq_seq_tagger(csv_filename, encoder): + input_features = [text_feature(encoder={"type": encoder, "reduce_output": None})] + output_features = [text_feature(decoder={"type": "tagger"}, reduce_input=None)] + rel_path = generate_data(input_features, output_features, csv_filename) + + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("encoder", ["cnnrnn", "stacked_cnn"]) +def test_experiment_seq_seq_tagger_fails_for_non_length_preserving_encoders(csv_filename, encoder): + input_features = [text_feature(encoder={"type": encoder, "reduce_output": None})] + output_features = [text_feature(decoder={"type": "tagger"}, reduce_input=None)] + rel_path = generate_data(input_features, output_features, csv_filename) + + with pytest.raises(ValueError): + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_seq_seq_model_def_file(csv_filename, yaml_filename): + # seq-to-seq test to use config file instead of dictionary + input_features = [text_feature(encoder={"reduce_output": None, "type": "embed"})] + output_features = [text_feature(decoder={"vocab_size": 3, "type": "tagger"}, reduce_input=None)] + + # Save the config to a yaml file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + with open(yaml_filename, "w") as yaml_out: + yaml.safe_dump(config, yaml_out) + + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(None, None, dataset=rel_path, config=yaml_filename) + + +def test_experiment_seq_seq_train_test_valid(tmpdir): + # seq-to-seq test to use train, test, validation files + input_features = [text_feature(encoder={"reduce_output": None, "type": "rnn"})] + output_features = [text_feature(decoder={"vocab_size": 3, "type": "tagger"}, reduce_input=None)] + + train_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "train.csv")) + test_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "test.csv"), 20) + valdation_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "val.csv"), 20) + + run_experiment( + input_features, output_features, training_set=train_csv, test_set=test_csv, validation_set=valdation_csv + ) + + # Save intermediate output + run_experiment( + input_features, output_features, training_set=train_csv, test_set=test_csv, validation_set=valdation_csv + ) + + +@pytest.mark.parametrize("encoder", ["embed", "rnn", "transformer"]) +def test_experiment_multi_input_intent_classification(csv_filename, encoder): + # Multiple inputs, Single category output + input_features = [ + text_feature(encoder={"vocab_size": 10, "min_len": 1, "representation": "sparse"}), + category_feature(encoder={"vocab_size": 10}), + ] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + input_features[0][ENCODER][TYPE] = encoder + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_with_torch_module_dict_feature_name(csv_filename): + input_features = [category_feature(name="type")] + output_features = [category_feature(name="to", output_feature=True)] + rel_path = generate_data(input_features, output_features, csv_filename) + + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_multiclass_with_class_weights(csv_filename): + # Multiple inputs, Single category output + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 3}, loss={"class_weights": [0, 1, 2]})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_multilabel_with_class_weights(csv_filename): + # Multiple inputs, Single category output + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [set_feature(decoder={"vocab_size": 3}, loss={"class_weights": [0, 1, 2, 3]})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize( + "output_features", + [ + # baseline test case + [ + category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}), + sequence_feature(decoder={"vocab_size": 10, "max_len": 5}), + number_feature(), + ], + # use generator as decoder + [ + category_feature(decoder={"vocab_size": 2, "reduce_input": "sum"}), + sequence_feature(decoder={"vocab_size": 10, "max_len": 5, "type": "generator"}), + number_feature(), + ], + # Generator decoder and reduce_input = None + [ + category_feature(decoder={"vocab_size": 2, "reduce_input": "sum"}), + sequence_feature(decoder={"max_len": 5, "type": "generator"}, reduce_input=None), + number_feature(normalization="minmax"), + ], + # output features with dependencies single dependency + generate_output_features_with_dependencies("number_feature", ["category_feature"]), + # output features with dependencies multiple dependencies + generate_output_features_with_dependencies("number_feature", ["category_feature", "sequence_feature"]), + # output features with dependencies multiple dependencies + generate_output_features_with_dependencies("sequence_feature", ["category_feature", "number_feature"]), + # output features with dependencies + generate_output_features_with_dependencies("category_feature", ["sequence_feature"]), + generate_output_features_with_dependencies_complex(), + ], +) +def test_experiment_multiple_seq_seq(csv_filename, output_features): + input_features = [ + text_feature(encoder={"vocab_size": 100, "min_len": 1, "type": "stacked_cnn"}), + number_feature(normalization="zscore"), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + output_features = output_features + + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize( + "num_channels,image_source,in_memory,skip_save_processed_input", + [ + (3, "file", True, True), + (1, "file", False, False), + (3, "tensor", True, False), + ], + ids=["file_in_memory_3ch", "file_on_disk_1ch", "tensor_in_memory_3ch"], +) +def test_basic_image_feature(num_channels, image_source, in_memory, skip_save_processed_input, tmpdir): + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={ + "in_memory": in_memory, + "height": 12, + "width": 12, + "num_channels": num_channels, + "num_processes": 5, + }, + encoder={ + "type": "stacked_cnn", + "output_size": 16, + "num_filters": 8, + }, + ) + ] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})] + + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + if image_source == "file": + # use images from file + run_experiment( + input_features, output_features, dataset=rel_path, skip_save_processed_input=skip_save_processed_input + ) + else: + # import image from file and store in dataframe as tensors. + df = pd.read_csv(rel_path) + image_feature_name = input_features[0]["name"] + df[image_feature_name] = df[image_feature_name].apply(lambda x: torchvision.io.read_image(x)) + + run_experiment(input_features, output_features, dataset=df, skip_save_processed_input=skip_save_processed_input) + + +def test_experiment_infer_image_metadata(tmpdir): + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Resnet encoder + input_features = [ + image_feature(folder=image_dest_folder, encoder={"type": "stacked_cnn", "output_size": 16, "num_filters": 8}), + text_feature(encoder={"type": "embed", "min_len": 1}), + number_feature(normalization="zscore"), + ] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}), number_feature()] + + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + # remove image preprocessing section to force inferring image meta data + input_features[0].pop("preprocessing") + + run_experiment(input_features, output_features, dataset=rel_path) + + +ImageParams = namedtuple("ImageTestParams", "image_encoder in_memory_flag skip_save_processed_input") + + +@pytest.mark.parametrize( + "image_params", + [ + ImageParams("stacked_cnn", True, True), + ImageParams("stacked_cnn", False, False), + ], +) +def test_experiment_image_inputs(image_params: ImageParams, tmpdir): + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Resnet encoder + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + encoder={"type": "resnet", "output_size": 16, "num_filters": 8}, + ), + text_feature(encoder={"type": "embed", "min_len": 1}), + number_feature(normalization="zscore"), + ] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}), number_feature()] + + input_features[0]["encoder"]["type"] = image_params.image_encoder + input_features[0]["preprocessing"]["in_memory"] = image_params.in_memory_flag + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + run_experiment( + input_features, + output_features, + dataset=rel_path, + skip_save_processed_input=image_params.skip_save_processed_input, + ) + + +# Primary focus of this test is to determine if exceptions are raised for different data set formats and in_memory +# setting. + + +@pytest.mark.parametrize( + "train_format,train_in_memory,test_format,test_in_memory", + [ + ("csv", True, "csv", True), + ("df", False, "df", False), + ("csv", False, "df", True), + ], + ids=["csv_inmem", "df_ondisk", "csv_to_df_mixed"], +) +def test_experiment_image_dataset(train_format, train_in_memory, test_format, test_in_memory, tmpdir): + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + encoder={"type": "stacked_cnn", "output_size": 16, "num_filters": 8}, + ), + ] + output_features = [ + category_feature(decoder={"reduce_input": "sum", "vocab_size": 2}), + ] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + "preprocessing": {}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # create temporary name for train and test data sets + train_csv_filename = os.path.join(tmpdir, "train_" + uuid.uuid4().hex[:10].upper() + ".csv") + test_csv_filename = os.path.join(tmpdir, "test_" + uuid.uuid4().hex[:10].upper() + ".csv") + + # setup training data format to test + train_data = generate_data(input_features, output_features, train_csv_filename) + config["input_features"][0]["preprocessing"]["in_memory"] = train_in_memory + training_set_metadata = None + + # define Ludwig model + backend = LocalTestBackend() + model = LudwigModel( + config=config, + backend=backend, + ) + + train_dataset_to_use = create_data_set_to_use(train_format, train_data) + + model.train(dataset=train_dataset_to_use, training_set_metadata=training_set_metadata) + + model.config_obj.input_features.to_list()[0]["preprocessing"]["in_memory"] = test_in_memory + + # setup test data format to test + test_data = generate_data(input_features, output_features, test_csv_filename) + + test_dataset_to_use = create_data_set_to_use(test_format, test_data) + + # run functions with the specified data format + model.evaluate(dataset=test_dataset_to_use) + model.predict(dataset=test_dataset_to_use) + + +DATA_FORMATS_TO_TEST = [ + "csv", + "df", + "dict", + "excel", + "feather", + "fwf", + "html", + "json", + "jsonl", + "parquet", + "pickle", + "stata", + "tsv", +] + + +@pytest.mark.parametrize("data_format", DATA_FORMATS_TO_TEST) +def test_experiment_dataset_formats(data_format, csv_filename): + # primary focus of this test is to determine if exceptions are + # raised for different data set formats and in_memory setting + + input_features = [number_feature(), category_feature()] + output_features = [category_feature(output_feature=True), number_feature()] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + "preprocessing": {}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # setup training data format to test + raw_data = generate_data(input_features, output_features, csv_filename) + + # define Ludwig model + model = LudwigModel(config=config) + + dataset_to_use = create_data_set_to_use(data_format, raw_data) + + # pickle auto-dispatch by extension is disabled (CWE-502); must opt in explicitly. + explicit_format = data_format if data_format == "pickle" else None + model.train(dataset=dataset_to_use, data_format=explicit_format, random_seed=default_random_seed) + + # # run functions with the specified data format + model.evaluate(dataset=dataset_to_use, data_format=explicit_format) + model.predict(dataset=dataset_to_use, data_format=explicit_format) + + +def test_experiment_audio_inputs(tmpdir): + # Audio Inputs + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + + input_features = [audio_feature(folder=audio_dest_folder)] + output_features = [binary_feature()] + + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_tied_weights(csv_filename): + # Single sequence input, single category output + input_features = [ + text_feature(name="text_feature1", encoder={"min_len": 1, "type": "cnnrnn", "reduce_output": "sum"}), + text_feature( + name="text_feature2", encoder={"min_len": 1, "type": "cnnrnn", "reduce_output": "sum"}, tied="text_feature1" + ), + ] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + for encoder in ENCODERS: + input_features[0][ENCODER][TYPE] = encoder + input_features[1][ENCODER][TYPE] = encoder + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_tied_weights_sequence_combiner(csv_filename): + """Tests that tied weights work with sequence combiners if `sequence_length` is provided. + + Addresses https://github.com/ludwig-ai/ludwig/issues/3220 + """ + input_features = [ + text_feature( + name="feature1", + encoder={ + "max_len": 5, + "reduce_output": None, + }, + preprocessing={"sequence_length": 10}, + ), + text_feature( + name="feature2", + encoder={ + "max_len": 3, + "reduce_output": None, + }, + preprocessing={"sequence_length": 10}, + tied="feature1", + ), + ] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})] + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "sequence"}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(config=config, dataset=rel_path) + + +@pytest.mark.parametrize( + "enc_cell_type,attention", + [("lstm", True), ("rnn", False), ("gru", True)], + ids=["lstm_attn", "rnn_no_attn", "gru_attn"], +) +def test_sequence_tagger(enc_cell_type, attention, csv_filename): + # Define input and output features + input_features = [ + sequence_feature(encoder={"max_len": 10, "type": "rnn", "cell_type": enc_cell_type, "reduce_output": None}) + ] + output_features = [ + sequence_feature(decoder={"max_len": 10, "type": "tagger", "attention": attention}, reduce_input=None) + ] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + # run the experiment + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_sequence_tagger_text(csv_filename): + # Define input and output features + input_features = [text_feature(encoder={"max_len": 10, "type": "rnn", "reduce_output": None})] + output_features = [ + sequence_feature( + decoder={"max_len": 10, "type": "tagger"}, + reduce_input=None, + ) + ] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + # run the experiment + run_experiment(input_features, output_features, dataset=rel_path) + + +""" +@pytest.mark.distributed +@pytest.mark.distributed_d +def test_sequence_tagger_text_ray(csv_filename, ray_cluster_2cpu): + # Define input and output features + input_features = [text_feature(encoder={"max_len": 10, "type": "rnn", "reduce_output": None})] + output_features = [ + sequence_feature( + decoder={"max_len": 10, "type": "tagger"}, + reduce_input=None, + ) + ] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + # run the experiment + run_experiment(input_features, output_features, dataset=rel_path, backend="ray") +""" + + +def test_experiment_sequence_combiner_with_reduction_fails(csv_filename): + config = { + "input_features": [ + sequence_feature( + name="seq1", + encoder={ + "min_len": 5, + "max_len": 5, + "type": "embed", + "cell_type": "lstm", + "reduce_output": "sum", + }, + ), + sequence_feature( + name="seq2", + encoder={ + "min_len": 5, + "max_len": 5, + "type": "embed", + "cell_type": "lstm", + "reduce_output": "sum", + }, + ), + category_feature(encoder={"vocab_size": 5}), + ], + "output_features": [category_feature(decoder={"reduce_input": "sum", "vocab_size": 5})], + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + "combiner": { + "type": "sequence", + "encoder": {"type": "rnn"}, + "main_sequence_feature": "seq1", + "reduce_output": None, + }, + } + + # Generate test data + rel_path = generate_data(config["input_features"], config["output_features"], csv_filename) + + # Encoding sequence features with 'embed' should fail with SequenceConcatCombiner, since at least one sequence + # feature should be rank 3. + with pytest.raises(TypeError): + run_experiment(config=config, dataset=rel_path) + + +@pytest.mark.parametrize("sequence_encoder", ["rnn", "transformer"]) +def test_experiment_sequence_combiner(sequence_encoder, csv_filename): + config = { + "input_features": [ + sequence_feature( + name="seq1", + encoder={ + "min_len": 5, + "max_len": 5, + "type": sequence_encoder, + "cell_type": "lstm", + "reduce_output": None, + }, + ), + sequence_feature( + name="seq2", + encoder={ + "min_len": 5, + "max_len": 5, + "type": sequence_encoder, + "cell_type": "lstm", + "reduce_output": None, + }, + ), + category_feature(vocab_size=5), + ], + "output_features": [category_feature(decoder={"reduce_input": "sum", "vocab_size": 5})], + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + "combiner": { + "type": "sequence", + "encoder": {"type": "rnn"}, + "main_sequence_feature": "seq1", + "reduce_output": None, + }, + } + + # Generate test data + rel_path = generate_data(config["input_features"], config["output_features"], csv_filename) + + run_experiment(config=config, dataset=rel_path) + + +def test_experiment_model_resume(tmpdir): + # Single sequence input, single category output + # Tests saving a model file, loading it to rerun training and predict + input_features = [sequence_feature(encoder={"type": "rnn", "reduce_output": "sum"})] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})] + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + _, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=tmpdir) + + experiment_cli(config, dataset=rel_path, model_resume_path=output_dir) + + predict_cli(os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path) + shutil.rmtree(output_dir, ignore_errors=True) + + +@pytest.mark.slow +@pytest.mark.parametrize( + "dist_strategy", + [ + pytest.param("accelerate", id="accelerate", marks=pytest.mark.distributed), + ], +) +def test_experiment_model_resume_distributed(tmpdir, dist_strategy, ray_cluster_4cpu): + _run_experiment_model_resume_distributed(tmpdir, dist_strategy) + + +def _run_experiment_model_resume_distributed(tmpdir, dist_strategy): + # Single sequence input, single category output + # Tests saving a model file, loading it to rerun training and predict + input_features = [number_feature()] + output_features = [category_feature(output_feature=True)] + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 8}, + TRAINER: {"epochs": 1, BATCH_SIZE: 128}, + "backend": {"type": "ray", "trainer": {"strategy": dist_strategy, "num_workers": 2}}, + } + + _, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=os.path.join(tmpdir, "results1")) + + experiment_cli( + config, dataset=rel_path, model_resume_path=output_dir, output_directory=os.path.join(tmpdir, "results2") + ) + + predict_cli( + os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path, output_directory=os.path.join(tmpdir, "results3") + ) + + +@pytest.mark.parametrize( + "missing_file", + ["training_progress.json", "training_checkpoints"], + ids=["training_progress", "training_checkpoints"], +) +def test_experiment_model_resume_missing_file(tmpdir, missing_file): + # Single sequence input, single category output + # Tests saving a model file, loading it to rerun training and predict + input_features = [sequence_feature(encoder={"type": "rnn", "reduce_output": "sum"})] + output_features = [category_feature(decoder={"reduce_input": "sum", "vocab_size": 2})] + + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + _, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=tmpdir) + + try: + # Remove file to simulate failure during first epoch of training which prevents + # training_checkpoints to be empty and training_progress.json to not be created + missing_file_path = os.path.join(output_dir, MODEL_FILE_NAME, missing_file) + if missing_file == "training_progress.json": + os.remove(missing_file_path) + else: + shutil.rmtree(missing_file_path) + finally: + # Training should start a fresh model training run without any errors + experiment_cli(config, dataset=rel_path, model_resume_path=output_dir) + + predict_cli(os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path) + shutil.rmtree(output_dir, ignore_errors=True) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_d +def test_experiment_model_resume_before_1st_epoch_distributed(tmpdir, ray_cluster_4cpu): + # Single sequence input, single category output + # Tests saving a model file, loading it to rerun training and predict + input_features = [number_feature()] + output_features = [category_feature(output_feature=True)] + # Generate test data + training_set = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 8}, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + "backend": {"type": "ray", "trainer": {"strategy": "accelerate", "num_workers": 2}}, + } + + class InducedFailureCallback(Callback): + """Class that defines the methods necessary to hook into process.""" + + def on_resume_training(self, is_coordinator): + if is_coordinator: + raise RuntimeError("Induced failure") + + class NoFailureCallback(Callback): + """Class that defines the methods necessary to hook into process.""" + + def on_resume_training(self, is_coordinator): + pass + + try: + # Define Ludwig model object that drive model training + model = LudwigModel(config=config, logging_level=logging.INFO, callbacks=[InducedFailureCallback()]) + model.train( + dataset=training_set, + experiment_name="simple_experiment", + model_name="simple_model_incomplete", + skip_save_processed_input=True, + output_directory=os.path.join(tmpdir, "results1"), + ) + except Exception: + model = LudwigModel(config=config, logging_level=logging.INFO, callbacks=[NoFailureCallback()]) + model.train( + dataset=training_set, + skip_save_processed_input=True, + model_resume_path=os.path.join(tmpdir, "results1"), + ) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_d +def test_tabnet_with_batch_size_1(tmpdir, ray_cluster_4cpu): + input_features = [number_feature()] + output_features = [category_feature(output_feature=True)] + training_set = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "tabnet"}, + TRAINER: {"train_steps": 1, BATCH_SIZE: 1}, + "backend": {"type": "ray", "trainer": {"strategy": "accelerate", "num_workers": 2}}, + } + model = LudwigModel(config=config, logging_level=logging.INFO) + model.train( + dataset=training_set, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + + +def test_experiment_various_feature_types(csv_filename): + input_features = [binary_feature(), bag_feature()] + output_features = [set_feature(decoder={"max_len": 3, "vocab_size": 5})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_timeseries(csv_filename): + input_features = [timeseries_feature()] + output_features = [binary_feature()] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "transformer" + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_visual_question_answering(tmpdir): + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={"in_memory": True, "height": 32, "width": 32, "num_channels": 3, "num_processes": 5}, + encoder={ + "type": "stacked_cnn", + }, + ), + text_feature(encoder={"type": "embed", "min_len": 1}), + ] + output_features = [sequence_feature(decoder={"type": "generator", "cell_type": "lstm"})] + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_image_resizing_num_channel_handling(tmpdir): + """This test creates two image datasets with 3 channels and 1 channel. The combination of this data is used to + train a model. This checks the cases where the user may or may not specify a number of channels in the config. + + :param csv_filename: + :return: + """ + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Resnet encoder + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={"in_memory": True, "height": 32, "width": 32, "num_channels": 3, "num_processes": 5}, + encoder={ + "type": "stacked_cnn", + }, + ), + text_feature(encoder={"type": "embed", "min_len": 1}), + number_feature(normalization="minmax"), + ] + output_features = [binary_feature(), number_feature()] + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset1.csv"), num_examples=20) + + df1 = read_csv(rel_path) + + input_features[0]["preprocessing"]["num_channels"] = 1 + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset2.csv"), num_examples=20) + df2 = read_csv(rel_path) + + df = concatenate_df(df1, df2, None, LOCAL_BACKEND) + df.to_csv(rel_path, index=False) + + # Here the user specifies number of channels. Exception shouldn't be thrown + run_experiment(input_features, output_features, dataset=rel_path) + + del input_features[0]["preprocessing"]["num_channels"] + + # User doesn't specify num channels, but num channels is inferred. Exception shouldn't be thrown + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("encoder", ["wave", "embed"]) +def test_experiment_date(encoder, csv_filename): + input_features = [date_feature()] + output_features = [category_feature(decoder={"vocab_size": 2})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + input_features[0][ENCODER] = {TYPE: encoder} + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("encoder", get_encoder_classes(H3).keys()) +def test_experiment_h3(encoder, csv_filename): + input_features = [h3_feature()] + output_features = [binary_feature()] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + input_features[0][ENCODER] = {TYPE: encoder} + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_vector_feature(csv_filename): + input_features = [vector_feature()] + output_features = [binary_feature()] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_vector_feature_infer_size(csv_filename): + input_features = [vector_feature()] + output_features = [vector_feature()] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + # Unset vector_size so it needs to be inferred + del input_features[0][PREPROCESSING] + del output_features[0][PREPROCESSING] + + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("encoder", ["parallel_cnn", "dense", "passthrough"]) +def test_forecasting_row_major(csv_filename, encoder): + input_features = [timeseries_feature(encoder={"type": encoder})] + output_features = [timeseries_feature(decoder={"type": "projector"})] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14, "flatten_inputs": True}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, config=config, dataset=rel_path) + + +def test_forecasting_column_major(csv_filename): + input_feature = timeseries_feature(preprocessing={"window_size": 3}) + input_features = [input_feature] + + # Ensure output feature has the same column and the input feature + output_feature = timeseries_feature( + name=input_feature[COLUMN], preprocessing={"horizon": 2}, decoder={"type": "projector"} + ) + output_feature[NAME] = f"{input_feature[NAME]}_out" + output_features = [output_feature] + + # Generate test data in column-major format. This is just a dataframe of numbers with the same column name + # as expected by the timeseries input feature + column_major_feature = number_feature(name=input_feature[COLUMN]) + csv_filename = generate_data([column_major_feature], [], csv_filename) + + input_df = pd.read_csv(csv_filename) + + model, eval_stats, train_stats, preprocessed_data, output_directory = run_experiment( + input_features, output_features, dataset=csv_filename + ) + train_set, val_set, test_set, _ = preprocessed_data + + print(input_df) + # print(train_set.to_df()) + + horizon_df = model.forecast(input_df, horizon=5) + print(horizon_df) + + +@pytest.mark.parametrize("reduce_output", [("sum"), (None)], ids=["sum", "none"]) +def test_experiment_text_output_feature_with_tagger_decoder(csv_filename, reduce_output): + """Test that the tagger decoder works with text output features when reduce_output is set to None.""" + input_features = [text_feature(encoder={"type": "parallel_cnn", "reduce_output": reduce_output})] + output_features = [text_feature(output_feature=True, decoder={"type": "tagger"})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + with pytest.raises(ConfigValidationError) if reduce_output == "sum" else contextlib.nullcontext(): + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize("reduce_output", [("sum"), (None)], ids=["sum", "none"]) +def test_experiment_sequence_output_feature_with_tagger_decoder(csv_filename, reduce_output): + """Test that the tagger decoder works with sequence output features when reduce_output is set to None.""" + input_features = [text_feature(encoder={"type": "parallel_cnn", "reduce_output": reduce_output})] + output_features = [sequence_feature(output_feature=True, decoder={"type": "tagger"})] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + with pytest.raises(ConfigValidationError) if reduce_output == "sum" else contextlib.nullcontext(): + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_category_input_feature_with_tagger_decoder(csv_filename): + """Test that the tagger decoder doesn't work with category input features.""" + input_features = [category_feature()] + output_features = [sequence_feature(output_feature=True, decoder={"type": "tagger"})] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14, "reduce_output": None}, + } + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + with pytest.raises(ConfigValidationError): + run_experiment(config=config, dataset=rel_path) + + +def test_experiment_category_distribution_feature(csv_filename): + vocab = ["a", "b", "c"] + input_features = [vector_feature()] + output_features = [ + category_distribution_feature( + preprocessing={ + "vocab": vocab, + } + ) + ] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + input_df = pd.read_csv(rel_path) + + # set batch_size=auto to ensure we produce the correct shaped synthetic data + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: "auto"}, + } + model, _, _, _, _ = run_experiment(input_features, output_features, dataset=rel_path, config=config) + preds, _ = model.predict(input_df) + + # Check that predictions are category values drawn from the vocab, not distributions + assert all(v in vocab for v in preds[f"{output_features[0][NAME]}_predictions"].values) + + +def test_experiment_ordinal_category(csv_filename): + input_features = [category_feature(num_classes=5), number_feature()] + output_features = [category_feature(output_feature=True, loss={"type": "corn"})] + + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + +def test_experiment_feature_names_with_non_word_chars(tmpdir): + config = yaml.safe_load(""" +input_features: + - name: Pclass (new) + type: category + - name: review.text + type: category + - name: other_feature + type: category + tied: review.text + +output_features: + - name: Survived (new) + type: binary + - name: Thrived + type: binary + dependencies: + - Survived (new) + +combiner: + type: comparator + entity_1: + - Pclass (new) + - other_feature + entity_2: + - review.text + +""") + + df = build_synthetic_dataset_df(120, config) + model = LudwigModel(config, logging_level=logging.INFO) + + model.train(dataset=df, output_directory=tmpdir) + + +def test_text_output_feature_cols(tmpdir, csv_filename): + """Test ensures that there are 4 output columns when model.predict() is called for text output features.""" + input_features = [text_feature(encoder={"type": "parallel_cnn"})] + output_features = [text_feature(output_feature=True)] + + # Generate test data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename)) + + config = { + "input_features": input_features, + "output_features": output_features, + "trainer": {"train_steps": 2, "batch_size": 5}, + } + + model = LudwigModel(config, logging_level=logging.INFO) + model.train(dataset=rel_path, output_directory=tmpdir) + predict_output = model.predict(dataset=rel_path)[0] + + assert len(predict_output.columns) == 4 + + predict_df_headers = {col_name.split("_")[2] for col_name in list(predict_output.columns)} + assert predict_df_headers == {"predictions", "probability", "probabilities", "response"} diff --git a/tests/integration_tests/test_explain.py b/tests/integration_tests/test_explain.py new file mode 100644 index 0000000..bf06cef --- /dev/null +++ b/tests/integration_tests/test_explain.py @@ -0,0 +1,267 @@ +import logging +import os + +import numpy as np +import pandas as pd +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, BINARY, CATEGORY, MINIMUM_BATCH_SIZE, MODEL_ECD, TYPE +from ludwig.explain.captum import IntegratedGradientsExplainer +from ludwig.explain.explainer import Explainer +from ludwig.explain.explanation import Explanation +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + date_feature, + generate_data, + image_feature, + LocalTestBackend, + number_feature, + sequence_feature, + set_feature, + text_feature, + timeseries_feature, + vector_feature, +) + +try: + from ludwig.explain.captum_ray import RayIntegratedGradientsExplainer +except ImportError: + RayIntegratedGradientsExplainer = None + +pytestmark = pytest.mark.integration_tests_h + + +def test_explanation_dataclass(): + explanation = Explanation(target="target") + + feature_attributions_for_label_1 = np.array([1, 2, 3]) + feature_attributions_for_label_2 = np.array([4, 5, 6]) + + # test add() + explanation.add(["f1", "f2", "f3"], feature_attributions_for_label_1) + + with pytest.raises(ValueError, match="Expected feature attributions of shape"): + # test add() with wrong shape + explanation.add(["f1", "f2", "f3", "f4"], np.array([1, 2, 3, 4])) + + explanation.add(["f1", "f2", "f3"], feature_attributions_for_label_2) + + # test to_array() + explanation_array = explanation.to_array() + assert np.array_equal(explanation_array, [[1, 2, 3], [4, 5, 6]]) + + +def test_abstract_explainer_instantiation(): + with pytest.raises(TypeError, match="Can't instantiate abstract class Explainer"): + Explainer(None, inputs_df=None, sample_df=None, target=None) + + +@pytest.mark.parametrize( + "explainer_class, model_type", + [ + (IntegratedGradientsExplainer, MODEL_ECD), + ], +) +@pytest.mark.parametrize( + "output_feature", + [binary_feature(), number_feature(), category_feature(decoder={"vocab_size": 3})], + ids=["binary", "number", "category"], +) +@pytest.mark.parametrize( + "additional_config", + [ + pytest.param({}, id="default"), + pytest.param({"preprocessing": {"split": {"type": "fixed", "column": "split"}}}, id="fixed_split"), + ], +) +def test_explainer_api(explainer_class, model_type, output_feature, additional_config, tmpdir): + run_test_explainer_api(explainer_class, model_type, [output_feature], additional_config, tmpdir) + + +@pytest.mark.distributed +@pytest.mark.distributed_d +@pytest.mark.parametrize( + "output_feature", + [binary_feature(), number_feature(), category_feature(decoder={"vocab_size": 3})], + ids=["binary", "number", "category"], +) +def test_explainer_api_ray(output_feature, tmpdir, ray_cluster_2cpu): + from ludwig.explain.captum_ray import RayIntegratedGradientsExplainer + + run_test_explainer_api( + RayIntegratedGradientsExplainer, + "ecd", + [output_feature], + {}, + tmpdir, + resources_per_task={"num_cpus": 1}, + num_workers=1, + ) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_d +def test_explainer_api_ray_minimum_batch_size(tmpdir, ray_cluster_2cpu): + from ludwig.explain.captum_ray import RayIntegratedGradientsExplainer + + run_test_explainer_api( + RayIntegratedGradientsExplainer, + "ecd", + [binary_feature()], + {}, + tmpdir, + resources_per_task={"num_cpus": 1}, + num_workers=1, + batch_size=MINIMUM_BATCH_SIZE, + ) + + +@pytest.mark.flaky(reruns=2, reruns_delay=5) +@pytest.mark.parametrize("cache_encoder_embeddings", [True]) +@pytest.mark.parametrize( + "explainer_class,model_type", + [ + pytest.param(IntegratedGradientsExplainer, MODEL_ECD, id="ecd_local"), + pytest.param(RayIntegratedGradientsExplainer, MODEL_ECD, id="ecd_ray", marks=pytest.mark.distributed), + ], +) +def test_explainer_text_hf(explainer_class, model_type, cache_encoder_embeddings, tmpdir, ray_cluster_2cpu): + input_features = [ + text_feature( + encoder={ + "type": "auto_transformer", + "pretrained_model_name_or_path": "hf-internal-testing/tiny-bert-for-token-classification", + }, + preprocessing={"cache_encoder_embeddings": cache_encoder_embeddings}, + ) + ] + run_test_explainer_api(explainer_class, model_type, [binary_feature()], {}, tmpdir, input_features=input_features) + + +@pytest.mark.parametrize( + "explainer_class,model_type", + [ + pytest.param(IntegratedGradientsExplainer, MODEL_ECD, id="ecd_local"), + pytest.param(RayIntegratedGradientsExplainer, MODEL_ECD, id="ecd_ray", marks=pytest.mark.distributed), + ], +) +def test_explainer_text_tied_weights(explainer_class, model_type, tmpdir): + text_feature_1 = text_feature() + text_feature_2 = text_feature(tied=text_feature_1["name"]) + input_features = [text_feature_1, text_feature_2] + run_test_explainer_api(explainer_class, model_type, [binary_feature()], {}, tmpdir, input_features=input_features) + + +def run_test_explainer_api( + explainer_class, + model_type, + output_features, + additional_config, + tmpdir, + input_features=None, + batch_size=128, + **kwargs, +): + image_dest_folder = os.path.join(tmpdir, "generated_images") + + if input_features is None: + input_features = [ + # Include a non-canonical name that's not a valid key for a vanilla pytorch ModuleDict: + # https://github.com/pytorch/pytorch/issues/71203 + {"name": "type", "type": "binary"}, + number_feature(), + category_feature(encoder={TYPE: "onehot", "reduce_output": "sum"}), + category_feature(encoder={TYPE: "passthrough", "reduce_output": "sum"}), + ] + # TODO(travis): need unit tests to test the get_embedding_layer() of every encoder to ensure it is + # compatible with the explainer + input_features += [ + category_feature(encoder={"type": "dense", "reduce_output": "sum"}), + text_feature(encoder={"vocab_size": 3}), + vector_feature(), + timeseries_feature(), + image_feature(folder=image_dest_folder), + # audio_feature(os.path.join(tmpdir, "generated_audio")), # NOTE: works but takes a long time + # sequence_feature(encoder={"vocab_size": 3}), + date_feature(), + # h3_feature(), + set_feature(encoder={"vocab_size": 3}), + # bag_feature(encoder={"vocab_size": 3}), + ] + + # Generate data + csv_filename = os.path.join(tmpdir, "training.csv") + generate_data(input_features, output_features, csv_filename, num_examples=20) + df = pd.read_csv(csv_filename) + if "split" in additional_config.get("preprocessing", {}): + df["split"] = np.random.randint(0, 3, df.shape[0]) + + # Train model + config = {"input_features": input_features, "output_features": output_features, "model_type": model_type} + config["trainer"] = {"train_steps": 1, BATCH_SIZE: batch_size} + config.update(additional_config) + + model = LudwigModel(config, logging_level=logging.WARNING, backend=LocalTestBackend()) + model.train(df) + + # Explain model + explainer = explainer_class(model, inputs_df=df, sample_df=df, target=output_features[0]["name"], **kwargs) + + is_binary = output_features[0].get("type") == BINARY + is_category = output_features[0].get("type") == CATEGORY + + vocab_size = 1 + if is_binary: + vocab_size = 2 + elif is_category: + vocab_size = output_features[0].get("decoder", {}).get("vocab_size") + + assert explainer.is_binary_target == is_binary + assert explainer.is_category_target == is_category + assert explainer.vocab_size == vocab_size + + explanations_result = explainer.explain() + + # Verify shapes. + assert explanations_result.global_explanation.to_array().shape == (vocab_size, len(input_features)) + + assert len(explanations_result.row_explanations) == len(df) + for e in explanations_result.row_explanations: + assert e.to_array().shape == (vocab_size, len(input_features)) + + assert len(explanations_result.expected_values) == vocab_size + + +@pytest.mark.parametrize( + "output_feature", + [set_feature(decoder={"vocab_size": 3}), vector_feature()], + ids=["set", "vector"], +) +def test_explainer_api_nonscalar_outputs(output_feature, tmpdir): + run_test_explainer_api(IntegratedGradientsExplainer, MODEL_ECD, [output_feature], {}, tmpdir) + + +def test_explainer_api_text_outputs(tmpdir): + input_features = [text_feature(encoder={"type": "parallel_cnn", "reduce_output": None})] + output_features = [text_feature(output_feature=True, decoder={"type": "tagger"})] + run_test_explainer_api( + IntegratedGradientsExplainer, MODEL_ECD, output_features, {}, tmpdir, input_features=input_features + ) + + +@pytest.mark.parametrize( + "explainer_class,model_type", + [ + pytest.param(IntegratedGradientsExplainer, MODEL_ECD, id="ecd_local"), + pytest.param(RayIntegratedGradientsExplainer, MODEL_ECD, id="ecd_ray", marks=pytest.mark.distributed), + ], +) +@pytest.mark.parametrize("encoder_type", ["embed", "rnn", "transformer"]) +def test_explainer_sequence_feature(explainer_class, model_type, encoder_type, tmpdir): + input_features = [sequence_feature()] + input_features[0]["encoder"] = {"type": encoder_type} + output_features = [binary_feature()] + run_test_explainer_api(explainer_class, model_type, output_features, {}, tmpdir, input_features=input_features) diff --git a/tests/integration_tests/test_graph_execution.py b/tests/integration_tests/test_graph_execution.py new file mode 100644 index 0000000..495175e --- /dev/null +++ b/tests/integration_tests/test_graph_execution.py @@ -0,0 +1,67 @@ +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pytest + +from tests.integration_tests.utils import ( + category_feature, + generate_data, + generate_output_features_with_dependencies, + number_feature, + run_experiment, + sequence_feature, + set_feature, + text_feature, +) + + +@pytest.mark.parametrize( + "output_features", + [ + # baseline test case + [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + sequence_feature(decoder={"vocab_size": 10, "max_len": 5}), + number_feature(), + ], + # use generator as decoder + [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + sequence_feature(decoder={"vocab_size": 10, "max_len": 5, "type": "generator"}), + number_feature(), + ], + # Generator decoder and reduce_input = None + [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + sequence_feature(decoder={"max_len": 5, "type": "generator"}, reduce_input=None), + number_feature(normalization="minmax"), + ], + # output features with dependencies single dependency + generate_output_features_with_dependencies("number_feature", ["category_feature"]), + # output features with dependencies multiple dependencies + generate_output_features_with_dependencies("number_feature", ["category_feature", "sequence_feature"]), + ], +) +def test_experiment_multiple_seq_seq(csv_filename, output_features): + input_features = [ + text_feature(encoder={"vocab_size": 100, "min_len": 1, "type": "stacked_cnn"}), + number_feature(normalization="zscore"), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + output_features = output_features + + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) diff --git a/tests/integration_tests/test_hyperopt.py b/tests/integration_tests/test_hyperopt.py new file mode 100644 index 0000000..7a5b6ab --- /dev/null +++ b/tests/integration_tests/test_hyperopt.py @@ -0,0 +1,661 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import json +import os +import os.path +import uuid + +import pytest + +from ludwig.backend import initialize_backend +from ludwig.constants import ( + ACCURACY, + AUTO, + BATCH_SIZE, + CATEGORY, + COMBINER, + EXECUTOR, + HYPEROPT, + INPUT_FEATURES, + MAX_CONCURRENT_TRIALS, + MODEL_ECD, + MODEL_TYPE, + NAME, + OUTPUT_FEATURES, + RAY, + TEXT, + TRAINER, + TYPE, + VALIDATION, +) +from ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME, MODEL_FILE_NAME +from ludwig.hyperopt.results import HyperoptResults +from ludwig.hyperopt.run import hyperopt +from ludwig.hyperopt.utils import update_hyperopt_params_with_defaults +from ludwig.schema.model_config import ModelConfig +from ludwig.utils import fs_utils +from ludwig.utils.data_utils import load_json, use_credentials +from tests.integration_tests.utils import category_feature, generate_data, minio_test_creds, remote_tmpdir, text_feature + +ray = pytest.importorskip("ray") + +from ludwig.hyperopt.execution import get_build_hyperopt_executor, RayTuneExecutor # noqa + +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_c, pytest.mark.integration_tests_c] + +RANDOM_SEARCH_SIZE = 2 + +HYPEROPT_CONFIG = { + "parameters": { + # using only float parameter as common in all search algorithms + "trainer.learning_rate": {"space": "loguniform", "lower": 0.001, "upper": 0.1}, + }, + "goal": "minimize", + "executor": {TYPE: "ray", "num_samples": 2, "scheduler": {TYPE: "fifo"}}, + "search_alg": {TYPE: "variant_generator"}, +} + +SEARCH_ALGS_FOR_TESTING = [ + # None, + # "variant_generator", + "random", + "bohb", + # "hyperopt", + # "ax", + # "bayesopt", + # "blendsearch", + # "cfo", + # "dragonfly", + # "hebo", + # "skopt", + # "optuna", +] + +SCHEDULERS_FOR_TESTING = [ + "fifo", + "asynchyperband", + # "async_hyperband", + # "median_stopping_rule", + # "medianstopping", + # "hyperband", + # "hb_bohb", + # "pbt", + # "pb2", commented out for now: https://github.com/ray-project/ray/issues/24815 + # "resource_changing", +] + + +def _setup_ludwig_config(dataset_fp: str, model_type: str = MODEL_ECD) -> tuple[dict, str]: + input_features = [category_feature(encoder={"vocab_size": 3})] + output_features = [category_feature(decoder={"vocab_size": 3})] + + rel_path = generate_data(input_features, output_features, dataset_fp, num_examples=30) + + trainer_cfg = {"learning_rate": 0.001} + if model_type == MODEL_ECD: + trainer_cfg["epochs"] = 2 + else: + trainer_cfg["num_boost_round"] = 2 + # Disable feature filtering to avoid having no features due to small test dataset, + # see https://stackoverflow.com/a/66405983/5222402 + trainer_cfg["feature_pre_filter"] = False + + config = { + MODEL_TYPE: model_type, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat"}, + TRAINER: trainer_cfg, + } + + config = ModelConfig.from_dict(config).to_dict() + + return config, rel_path + + +@pytest.mark.parametrize("search_alg", SEARCH_ALGS_FOR_TESTING) +@pytest.mark.parametrize("model_type", [MODEL_ECD]) +def test_hyperopt_search_alg( + search_alg, + model_type, + csv_filename, + tmpdir, + ray_cluster_7cpu, + validate_output_feature=False, + validation_metric=None, + split="validation", +): + config, rel_path = _setup_ludwig_config(csv_filename, model_type) + + hyperopt_config = HYPEROPT_CONFIG.copy() + + # finalize hyperopt config settings + if search_alg == "dragonfly": + hyperopt_config["search_alg"] = { + TYPE: search_alg, + "domain": "euclidean", + "optimizer": "random", + } + elif search_alg is None: + hyperopt_config["search_alg"] = {} + else: + hyperopt_config["search_alg"] = { + TYPE: search_alg, + } + + if validate_output_feature: + hyperopt_config["output_feature"] = config[OUTPUT_FEATURES][0][NAME] + if validation_metric: + hyperopt_config["validation_metric"] = validation_metric + + update_hyperopt_params_with_defaults(hyperopt_config) + + backend = initialize_backend("local") + if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO: + hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config) + + parameters = hyperopt_config["parameters"] + output_feature = hyperopt_config["output_feature"] + metric = hyperopt_config["metric"] + goal = hyperopt_config["goal"] + executor = hyperopt_config["executor"] + search_alg = hyperopt_config["search_alg"] + + hyperopt_executor = get_build_hyperopt_executor(RAY)( + parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor + ) + results = hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir) + assert isinstance(results, HyperoptResults) + + with hyperopt_executor._get_best_model_path( + results.experiment_analysis.best_trial, results.experiment_analysis + ) as path: + assert path is not None + assert isinstance(path, str) + + +@pytest.mark.parametrize("model_type", [MODEL_ECD]) +def test_hyperopt_executor_with_metric(model_type, csv_filename, tmpdir, ray_cluster_7cpu): + test_hyperopt_search_alg( + "variant_generator", + model_type, + csv_filename, + tmpdir, + ray_cluster_7cpu, + validate_output_feature=True, + validation_metric=ACCURACY, + ) + + +@pytest.mark.parametrize("split", [VALIDATION]) +def test_hyperopt_with_split(split, csv_filename, tmpdir, ray_cluster_7cpu): + test_hyperopt_search_alg( + search_alg="variant_generator", + model_type=MODEL_ECD, + csv_filename=csv_filename, + tmpdir=tmpdir, + ray_cluster_7cpu=ray_cluster_7cpu, + split=split, + ) + + +@pytest.mark.parametrize("scheduler", SCHEDULERS_FOR_TESTING) +@pytest.mark.parametrize("model_type", [MODEL_ECD]) +def test_hyperopt_scheduler( + scheduler, model_type, csv_filename, tmpdir, ray_cluster_7cpu, validate_output_feature=False, validation_metric=None +): + config, rel_path = _setup_ludwig_config(csv_filename, model_type) + + hyperopt_config = HYPEROPT_CONFIG.copy() + + # finalize hyperopt config settings + if scheduler == "pb2": + # setup scheduler hyperparam_bounds parameter + min = hyperopt_config["parameters"]["trainer.learning_rate"]["lower"] + max = hyperopt_config["parameters"]["trainer.learning_rate"]["upper"] + hyperparam_bounds = { + "trainer.learning_rate": [min, max], + } + hyperopt_config["executor"]["scheduler"] = { + TYPE: scheduler, + "hyperparam_bounds": hyperparam_bounds, + } + else: + hyperopt_config["executor"]["scheduler"] = { + TYPE: scheduler, + } + + if validate_output_feature: + hyperopt_config["output_feature"] = config[OUTPUT_FEATURES][0][NAME] + if validation_metric: + hyperopt_config["validation_metric"] = validation_metric + + backend = initialize_backend("local") + update_hyperopt_params_with_defaults(hyperopt_config) + if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO: + hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config) + + parameters = hyperopt_config["parameters"] + split = hyperopt_config["split"] + output_feature = hyperopt_config["output_feature"] + metric = hyperopt_config["metric"] + goal = hyperopt_config["goal"] + executor = hyperopt_config["executor"] + search_alg = hyperopt_config["search_alg"] + + # TODO: Determine if we still need this if-then-else construct + if search_alg[TYPE] in {""}: + with pytest.raises(ImportError): + get_build_hyperopt_executor(RAY)( + parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor + ) + else: + hyperopt_executor = get_build_hyperopt_executor(RAY)( + parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor + ) + raytune_results = hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir) + assert isinstance(raytune_results, HyperoptResults) + + +def _run_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, backend, ray_cluster_7cpu): + input_features = [category_feature(encoder={"vocab_size": 3})] + output_features = [category_feature(decoder={"vocab_size": 3})] + + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat"}, + TRAINER: {"epochs": 1, "learning_rate": 0.001, BATCH_SIZE: 128}, + "backend": backend, + } + + output_feature_name = output_features[0][NAME] + + if search_space == "random": + # random search will be size of num_samples + search_parameters = { + "trainer.learning_rate": { + "lower": 0.0001, + "upper": 0.01, + "space": "loguniform", + }, + output_feature_name + ".decoder.fc_layers": { + "space": "choice", + "categories": [ + [{"output_size": 8}, {"output_size": 4}], + [{"output_size": 8}], + [{"output_size": 4}], + ], + }, + output_feature_name + ".decoder.fc_output_size": {"space": "choice", "categories": [4, 8, 12]}, + } + else: + # grid search space will be product each parameter size + search_parameters = { + "trainer.learning_rate": {"space": "grid_search", "values": [0.001, 0.01]}, + output_feature_name + ".decoder.fc_output_size": {"space": "grid_search", "values": [4, 8]}, + } + + hyperopt_configs = { + "parameters": search_parameters, + "goal": "minimize", + "output_feature": output_feature_name, + "validation_metrics": "loss", + "executor": { + TYPE: "ray", + "num_samples": 1 if search_space == "grid" else RANDOM_SEARCH_SIZE, + "max_concurrent_trials": 1, + }, + "search_alg": {TYPE: "variant_generator"}, + } + + # add hyperopt parameter space to the config + config[HYPEROPT] = hyperopt_configs + + experiment_name = f"test_hyperopt_{uuid.uuid4().hex}" + hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name) + if search_space == "random": + assert hyperopt_results.experiment_analysis.results_df.shape[0] == RANDOM_SEARCH_SIZE + else: + # compute size of search space for grid search + grid_search_size = 1 + for k, v in search_parameters.items(): + grid_search_size *= len(v["values"]) + assert hyperopt_results.experiment_analysis.results_df.shape[0] == grid_search_size + + # check for return results + assert isinstance(hyperopt_results, HyperoptResults) + + # check for existence of the hyperopt statistics file + with use_credentials(minio_test_creds()): + assert fs_utils.path_exists(os.path.join(tmpdir, experiment_name, HYPEROPT_STATISTICS_FILE_NAME)) + for trial in hyperopt_results.experiment_analysis.trials: + assert fs_utils.path_exists( + os.path.join(tmpdir, experiment_name, f"trial_{trial.trial_id}"), + ) + + # Verify best trial has a valid checkpoint + best_trial = hyperopt_results.experiment_analysis.best_trial + assert best_trial is not None + + +@pytest.mark.slow +@pytest.mark.parametrize("search_space", ["random", "grid"]) +def test_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, ray_cluster_7cpu): + _run_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, "local", ray_cluster_7cpu) + + +@pytest.mark.xfail( + reason="PyArrow S3 C++ client uses chunked transfer encoding for multipart uploads, " + "which MinIO rejects with HTTP 411 MissingContentLength. Requires real AWS S3.", + strict=False, +) +def test_hyperopt_sync_remote(csv_filename, ray_cluster_7cpu, monkeypatch): + """Test hyperopt with remote S3 (MinIO) storage for trial results.""" + # Override AWS env vars so PyArrow's S3 client (used by Ray Tune internally) + # connects to MinIO instead of real AWS S3 + minio_endpoint = os.environ.get("LUDWIG_MINIO_ENDPOINT", "http://localhost:9000") + monkeypatch.setenv("AWS_ACCESS_KEY_ID", os.environ.get("LUDWIG_MINIO_ACCESS_KEY", "minio")) + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", os.environ.get("LUDWIG_MINIO_SECRET_KEY", "minio123")) + monkeypatch.setenv("AWS_ENDPOINT_URL", minio_endpoint) + monkeypatch.setenv("AWS_EC2_METADATA_DISABLED", "true") + + backend = { + "type": "local", + "credentials": { + "artifacts": minio_test_creds(), + }, + } + + with remote_tmpdir("s3", "test") as tmpdir: + _run_hyperopt_run_hyperopt( + csv_filename, + "random", + tmpdir, + backend, + ray_cluster_7cpu, + ) + + +def test_hyperopt_with_feature_specific_parameters(csv_filename, tmpdir, ray_cluster_7cpu): + input_features = [ + text_feature(name="utterance", reduce_output="sum"), + category_feature(vocab_size=3), + ] + + output_features = [category_feature(vocab_size=3, output_feature=True)] + + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30) + + filter_size_search_space = [5, 7] + embedding_size_search_space = [4, 8, 12] + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat", "num_fc_layers": 2}, + TRAINER: {"epochs": 1, "learning_rate": 0.001, BATCH_SIZE: 128}, + HYPEROPT: { + "parameters": { + input_features[0][NAME] + ".encoder.filter_size": { + "space": "choice", + "categories": filter_size_search_space, + }, + input_features[1][NAME] + ".encoder.embedding_size": { + "space": "choice", + "categories": embedding_size_search_space, + }, + }, + "goal": "minimize", + "output_feature": output_features[0][NAME], + "validation_metrics": "loss", + "executor": {TYPE: "ray", "num_samples": 1}, + "search_alg": {TYPE: "variant_generator"}, + }, + } + + hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name="test_hyperopt") + hyperopt_results_df = hyperopt_results.experiment_analysis.results_df + + model_parameters = json.load( + open( + os.path.join( + hyperopt_results_df.iloc[0]["trial_dir"], + "test_hyperopt_run", + MODEL_FILE_NAME, + "model_hyperparameters.json", + ) + ) + ) + + for input_feature in model_parameters[INPUT_FEATURES]: + if input_feature[TYPE] == TEXT: + assert input_feature["encoder"]["filter_size"] in filter_size_search_space + elif input_feature[TYPE] == CATEGORY: + assert input_feature["encoder"]["embedding_size"] in embedding_size_search_space + + +def test_hyperopt_old_config(csv_filename, tmpdir, ray_cluster_7cpu): + old_config = { + "ludwig_version": "0.4", + INPUT_FEATURES: [ + {"name": "cat1", TYPE: "category", "encoder": {"vocab_size": 2}}, + {"name": "num1", TYPE: "number"}, + ], + OUTPUT_FEATURES: [ + {"name": "bin1", TYPE: "binary"}, + ], + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + HYPEROPT: { + EXECUTOR: { + TYPE: "ray", + "time_budget_s": 200, + "cpu_resources_per_trial": 1, + }, + "sampler": { + TYPE: "ray", + "scheduler": { + TYPE: "async_hyperband", + "max_t": 200, + "time_attr": "time_total_s", + "grace_period": 72, + "reduction_factor": 5, + }, + "search_alg": { + TYPE: "variant_generator", + }, + "num_samples": 2, + }, + "parameters": { + "trainer.batch_size": { + "space": "choice", + "categories": [64, 128, 256], + }, + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + }, + }, + } + + input_features = old_config[INPUT_FEATURES] + output_features = old_config[OUTPUT_FEATURES] + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30) + + hyperopt(old_config, dataset=rel_path, output_directory=tmpdir, experiment_name="test_hyperopt") + + +def test_hyperopt_nested_parameters(csv_filename, tmpdir, ray_cluster_7cpu): + config = { + INPUT_FEATURES: [ + {"name": "cat1", TYPE: "category", "encoder": {"vocab_size": 2}}, + {"name": "num1", TYPE: "number"}, + ], + OUTPUT_FEATURES: [ + {"name": "bin1", TYPE: "binary"}, + ], + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + HYPEROPT: { + EXECUTOR: { + TYPE: "ray", + "time_budget_s": 200, + "cpu_resources_per_trial": 1, + "num_samples": 2, + "scheduler": {TYPE: "fifo"}, + }, + "search_alg": {TYPE: "variant_generator"}, + "parameters": { + ".": { + "space": "choice", + "categories": [ + { + "combiner": { + "type": "tabnet", + "bn_virtual_bs": 32, + }, + "trainer": { + "learning_rate_scaling": "sqrt", + "learning_rate_scheduler": { + "decay": "exponential", + "decay_steps": 20000, + "decay_rate": 0.8, + }, + "optimizer": {"type": "adam"}, + }, + }, + { + "combiner": {"type": "concat"}, + "trainer": {"learning_rate_scaling": "linear"}, + }, + ], + }, + "trainer.learning_rate": {"space": "choice", "categories": [0.7, 0.42]}, + }, + }, + } + + input_features = config[INPUT_FEATURES] + output_features = config[OUTPUT_FEATURES] + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30) + + results = hyperopt( + config, + dataset=rel_path, + output_directory=tmpdir, + experiment_name="test_hyperopt_nested_params", + ) + + results_df = results.experiment_analysis.results_df + assert len(results_df) == 2 + + for _, trial_meta in results_df.iterrows(): + trial_dir = trial_meta["trial_dir"] + trial_config = load_json( + os.path.join(trial_dir, "test_hyperopt_nested_params_run", MODEL_FILE_NAME, "model_hyperparameters.json") + ) + + assert len(trial_config[INPUT_FEATURES]) == len(config[INPUT_FEATURES]) + assert len(trial_config[OUTPUT_FEATURES]) == len(config[OUTPUT_FEATURES]) + + assert trial_config[COMBINER][TYPE] in {"tabnet", "concat"} + if trial_config[COMBINER][TYPE] == "tabnet": + assert trial_config[COMBINER]["bn_virtual_bs"] == 32 + assert trial_config[TRAINER]["learning_rate_scaling"] == "sqrt" + assert trial_config[TRAINER]["learning_rate_scheduler"]["decay"] == "exponential" + assert trial_config[TRAINER]["learning_rate_scheduler"]["decay_steps"] == 20000 + assert trial_config[TRAINER]["learning_rate_scheduler"]["decay_rate"] == 0.8 + assert trial_config[TRAINER]["optimizer"]["type"] == "adam" + else: + assert trial_config[TRAINER]["learning_rate_scaling"] == "linear" + + assert trial_config[TRAINER]["learning_rate"] in {0.7, 0.42} + + +@pytest.mark.slow +def test_hyperopt_without_config_defaults(csv_filename, tmpdir, ray_cluster_7cpu): + input_features = [category_feature(encoder={"vocab_size": 3})] + output_features = [category_feature(decoder={"vocab_size": 3})] + + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat"}, + TRAINER: {"train_steps": 5, "learning_rate": 0.001, BATCH_SIZE: 128}, + # Missing search_alg and executor, but should still work + HYPEROPT: { + "parameters": { + "trainer.learning_rate": { + "lower": 0.0001, + "upper": 0.01, + "space": "loguniform", + } + }, + "goal": "minimize", + "output_feature": output_features[0]["name"], + "metric": "loss", + "executor": {"type": "ray", "num_samples": 2}, + }, + } + + experiment_name = f"test_hyperopt_{uuid.uuid4().hex}" + hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name) + assert hyperopt_results.experiment_analysis.results_df.shape[0] == 2 + + +@pytest.mark.slow +def test_hyperopt_with_time_budget(csv_filename, tmpdir, ray_cluster_7cpu): + """Tests that incomplete checkpoints created by RayTune when time budget is hit doesn't throw errors because of + missing .tune_metadata files in the checkpoint directories.""" + input_features = [text_feature()] + output_features = [category_feature(output_feature=True)] + + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=30) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat"}, + HYPEROPT: { + "goal": "minimize", + "metric": "loss", + "output_feature": output_features[0]["name"], + "search_alg": {TYPE: "variant_generator"}, + "executor": { + "type": "ray", + # Ensure there is enough time for some trials to start and also for some to terminate + # to reproduce the exact issue of missing .tune_metadata files. + "time_budget_s": 30, + "cpu_resources_per_trial": 1, + "num_samples": 4, + "scheduler": {TYPE: "fifo"}, + }, + "parameters": { + "trainer.learning_rate": { + "lower": 0.0001, + "upper": 0.01, + "space": "loguniform", + } + }, + }, + } + + experiment_name = f"test_hyperopt_{uuid.uuid4().hex}" + hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name) diff --git a/tests/integration_tests/test_hyperopt_ray.py b/tests/integration_tests/test_hyperopt_ray.py new file mode 100644 index 0000000..eccd70c --- /dev/null +++ b/tests/integration_tests/test_hyperopt_ray.py @@ -0,0 +1,380 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import json +import logging +import os.path + +import mlflow +import pandas as pd +import pytest +from mlflow.tracking import MlflowClient + +from ludwig.backend import initialize_backend +from ludwig.callbacks import Callback +from ludwig.constants import ACCURACY, AUTO, BATCH_SIZE, EXECUTOR, MAX_CONCURRENT_TRIALS, TRAINER +from ludwig.contribs.mlflow import MlflowCallback +from ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME +from ludwig.hyperopt.results import HyperoptResults +from ludwig.hyperopt.run import hyperopt +from ludwig.hyperopt.utils import update_hyperopt_params_with_defaults +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.automl.utils import get_model_type +from tests.integration_tests.utils import category_feature, generate_data, text_feature + +try: + import ray + from ray.tune import Callback as TuneCallback + from ray.tune.experiment.trial import Trial + + from ludwig.hyperopt.execution import get_build_hyperopt_executor +except ImportError: + ray = None + Trial = None + TuneCallback = object # needed to set up HyperoptTestCallback when not distributed + +pytestmark = pytest.mark.integration_tests_h + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +HYPEROPT_CONFIG = { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + "combiner.num_fc_layers": {"space": "randint", "lower": 0, "upper": 2}, + "utterance.encoder.norm": {"space": "grid_search", "values": ["layer", "batch"]}, + "utterance.encoder.fc_layers": { + "space": "choice", + "categories": [ + [{"output_size": 16}, {"output_size": 8}], + [{"output_size": 16}], + [{"output_size": 8}], + ], + }, + }, + "goal": "minimize", +} + + +SCENARIOS = [ + {"executor": {"type": "ray"}, "search_alg": {"type": "variant_generator"}}, + {"executor": {"type": "ray", "num_samples": 2}, "search_alg": {"type": "variant_generator"}}, + { + "executor": { + "type": "ray", + "num_samples": 3, + "scheduler": { + "type": "hb_bohb", + "time_attr": "training_iteration", + "reduction_factor": 4, + "max_t": 2, + }, + }, + "search_alg": {"type": "bohb"}, + }, +] + + +def _get_config(search_alg: dict, executor: dict, epochs: int): + input_features = [ + text_feature(name="utterance", encoder={"cell_type": "lstm", "reduce_output": "sum"}), + category_feature(encoder={"vocab_size": 2}, reduce_input="sum"), + ] + + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum", output_feature=True)] + + return { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"epochs": epochs, "learning_rate": 0.001, BATCH_SIZE: 128}, + "hyperopt": { + **HYPEROPT_CONFIG, + "executor": executor, + "search_alg": search_alg, + }, + } + + +class HyperoptTestCallback(TuneCallback): + def __init__(self, exp_name: str, model_type: str): + self.exp_name = exp_name + self.model_type = model_type + self.trial_ids = set() + self.trial_status = {} + self.user_config = {} + self.rendered_config = {} + + def on_trial_start(self, iteration: int, trials: list["Trial"], trial: "Trial", **info): + super().on_trial_start(iteration, trials, trial, **info) + self.trial_ids.add(trial.trial_id) + + def on_trial_complete(self, iteration: int, trials: list["Trial"], trial: "Trial", **info): + super().on_trial_complete(iteration, trials, trial, **info) + self.trial_status[trial.trial_id] = trial.status + + model_hyperparameters = os.path.join( + trial.local_path, f"{self.exp_name}_{self.model_type}", MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME + ) + if os.path.isfile(model_hyperparameters): + try: + with open(model_hyperparameters) as f: + config = json.load(f) + assert config, f"Trial {trial} rendered config was empty." + self.rendered_config[trial.trial_id] = True + except OSError: + logging.exception("Could not load rendered config from trial logdir.") + + model_hyperparameters = os.path.join(trial.local_path, "trial_hyperparameters.json") + if os.path.isfile(model_hyperparameters): + try: + with open(model_hyperparameters) as f: + config = json.load(f) + assert config, "Trial {trial} user config was empty." + self.rendered_config[trial.trial_id] = True + except OSError: + logging.exception("Could not load rendered config from trial logdir.") + + +def run_hyperopt_executor( + search_alg, + executor, + epochs, + csv_filename, + tmpdir, + validate_output_feature=False, + validation_metric=None, + use_split=True, +): + config = _get_config(search_alg, executor, epochs) + rel_path = generate_data(config["input_features"], config["output_features"], csv_filename) + + if not use_split: + df = pd.read_csv(rel_path) + df["split"] = 0 + df.to_csv(rel_path) + + config = ModelConfig.from_dict(config).to_dict() + + hyperopt_config = config["hyperopt"] + + if validate_output_feature: + hyperopt_config["output_feature"] = config["output_features"][0]["name"] + if validation_metric: + hyperopt_config["validation_metric"] = validation_metric + + backend = initialize_backend("local") + update_hyperopt_params_with_defaults(hyperopt_config) + if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO: + hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config) + + parameters = hyperopt_config["parameters"] + if search_alg.get("type", "") == "bohb": + # bohb does not support grid_search search space + del parameters["utterance.encoder.norm"] + hyperopt_config["parameters"] = parameters + + split = hyperopt_config["split"] + output_feature = hyperopt_config["output_feature"] + metric = hyperopt_config["metric"] + goal = hyperopt_config["goal"] + search_alg = hyperopt_config["search_alg"] + executor = hyperopt_config["executor"] + + hyperopt_executor = get_build_hyperopt_executor(executor["type"])( + parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor + ) + + hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir, backend=backend) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_c +@pytest.mark.parametrize("scenario", SCENARIOS) +def test_hyperopt_executor(scenario, csv_filename, tmpdir, ray_cluster_4cpu): + search_alg = scenario["search_alg"] + executor = scenario["executor"] + scheduler = executor.get("scheduler", {}) + if scheduler.get("type") == "hb_bohb": + # When using the hb_bohb scheduler, num_epochs must equal max_t + epochs = scheduler.get("max_t", 81) + else: + epochs = 1 + run_hyperopt_executor(search_alg, executor, epochs, csv_filename, tmpdir) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_c +@pytest.mark.parametrize("use_split", [True, False], ids=["split", "no_split"]) +def test_hyperopt_executor_with_metric(use_split, csv_filename, tmpdir, ray_cluster_4cpu): + run_hyperopt_executor( + {"type": "variant_generator"}, # search_alg + {"type": "ray", "num_samples": 2}, # executor + 1, + csv_filename, + tmpdir, + validate_output_feature=True, + validation_metric=ACCURACY, + use_split=use_split, + ) + + +@pytest.mark.distributed +@pytest.mark.distributed_c +@pytest.mark.parametrize( + "backend", + [ + "local", + pytest.param("ray", marks=pytest.mark.xfail(reason="Nested Ray actors exceed 4-CPU CI cluster resources")), + ], +) +def test_hyperopt_run_hyperopt(csv_filename, backend, tmpdir, ray_cluster_4cpu): + input_features = [ + text_feature(name="utterance", encoder={"cell_type": "lstm", "reduce_output": "sum"}), + category_feature(encoder={"vocab_size": 2}, reduce_input="sum"), + ] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum", output_feature=True)] + + rel_path = generate_data(input_features, output_features, csv_filename) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"train_steps": 3, "learning_rate": 0.001, BATCH_SIZE: 128}, + "backend": { + "type": backend, + }, + } + + output_feature_name = output_features[0]["name"] + + hyperopt_configs = { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + output_feature_name + ".decoder.fc_output_size": {"space": "randint", "lower": 8, "upper": 16}, + output_feature_name + ".decoder.num_fc_layers": {"space": "randint", "lower": 0, "upper": 1}, + }, + "goal": "minimize", + "output_feature": output_feature_name, + "validation_metrics": "loss", + "executor": { + "type": "ray", + "num_samples": 2, + "cpu_resources_per_trial": 1, + "max_concurrent_trials": 1, + }, + "search_alg": {"type": "variant_generator"}, + } + + @ray.remote(num_cpus=0) + class Event: + def __init__(self): + self._set = False + + def is_set(self): + return self._set + + def set(self): + self._set = True + + # Used to trigger a cancel event in the trial, which should subsequently be retried + event = Event.remote() + + class CancelCallback(Callback): + def on_epoch_start(self, trainer, progress_tracker, save_path: str): + if progress_tracker.epoch == 1 and not ray.get(event.is_set.remote()): + ray.get(event.set.remote()) + raise KeyboardInterrupt() + + # add hyperopt parameter space to the config + config["hyperopt"] = hyperopt_configs + + # run for one epoch, then cancel, then resume from where we left off + run_hyperopt(config, rel_path, tmpdir, callbacks=[CancelCallback()]) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_c +def test_hyperopt_ray_mlflow(csv_filename, tmpdir, ray_cluster_4cpu): + mlflow_uri = f"file://{tmpdir}/mlruns" + mlflow.set_tracking_uri(mlflow_uri) + client = MlflowClient(tracking_uri=mlflow_uri) + + num_samples = 2 + config = _get_config( + {"type": "variant_generator"}, # search_alg + {"type": "ray", "num_samples": num_samples}, # executor + 1, # epochs + ) + + rel_path = generate_data(config["input_features"], config["output_features"], csv_filename) + + exp_name = "mlflow_test" + run_hyperopt(config, rel_path, tmpdir, experiment_name=exp_name, callbacks=[MlflowCallback(mlflow_uri)]) + + experiment = client.get_experiment_by_name(exp_name) + assert experiment is not None + + runs = client.search_runs([experiment.experiment_id]) + assert len(runs) > 0 + + for run in runs: + artifacts = [f.path for f in client.list_artifacts(run.info.run_id, "")] + assert "config.yaml" in artifacts + assert MODEL_FILE_NAME in artifacts + + +def run_hyperopt( + config, + rel_path, + tmpdir, + experiment_name="ray_hyperopt", + callbacks=None, +): + tune_test_callback = HyperoptTestCallback(experiment_name, get_model_type(config)) + + hyperopt_results = hyperopt( + config, + dataset=rel_path, + output_directory=tmpdir, + experiment_name=experiment_name, + callbacks=callbacks, + tune_callbacks=[tune_test_callback], + ) + + # check for return results + assert isinstance(hyperopt_results, HyperoptResults) + + # check for existence of the hyperopt statistics file + assert os.path.isfile(os.path.join(tmpdir, experiment_name, HYPEROPT_STATISTICS_FILE_NAME)) + + # check for evidence that the HyperoptTestCallback was active + assert len(tune_test_callback.trial_ids) > 0 + for t in tune_test_callback.trial_ids: + if tune_test_callback.trial_status.get(t) == "terminated": + assert tune_test_callback.user_config[t].get() + assert tune_test_callback.rendered_config[t].get() diff --git a/tests/integration_tests/test_input_feature_tied.py b/tests/integration_tests/test_input_feature_tied.py new file mode 100644 index 0000000..906c045 --- /dev/null +++ b/tests/integration_tests/test_input_feature_tied.py @@ -0,0 +1,150 @@ +from collections import namedtuple + +import pytest + +from ludwig.models.base import BaseModel +from ludwig.schema.model_config import ModelConfig +from tests.integration_tests.utils import ( + category_feature, + generate_data, + number_feature, + run_experiment, + sequence_feature, + text_feature, +) + +# InputFeatureOptions namedtuple structure: +# feature_type: input feature type, e.g., number, category, etc. +# feature_options: None or dictionary of required input feature specification +# tie_features: boolean, True to tie features, False not to tie features +InputFeatureOptions = namedtuple("InputFeatureOptions", "feature_type feature_options tie_features") + + +# micro level test confirms the encoders for tied input features are sharing +# the same encoder. Include negative tests to confirm untied input features +# do not share the same encoder. +# note: vocab parameter, below, is made up to facilitate creating input encoders +@pytest.mark.parametrize( + "input_feature_options", + [ + # tie input features, encoders should be the same + InputFeatureOptions("number", {"encoder": {"type": "passthrough"}}, True), + InputFeatureOptions( + "number", {"encoder": {"type": "passthrough"}, "preprocessing": {"normalization": "zscore"}}, True + ), + InputFeatureOptions("binary", {"encoder": {"type": "passthrough"}}, True), + InputFeatureOptions("category", {"encoder": {"type": "dense", "vocab": ["a", "b", "c"]}}, True), + InputFeatureOptions("set", {"encoder": {"type": "embed", "vocab": ["a", "b", "c"]}}, True), + InputFeatureOptions( + "sequence", {"encoder": {"type": "parallel_cnn", "max_sequence_length": 10, "vocab": ["x", "y", "z"]}}, True + ), + InputFeatureOptions( + "text", {"encoder": {"type": "parallel_cnn", "max_sequence_length": 10, "vocab": ["a", "b", "c"]}}, True + ), + InputFeatureOptions( + "timeseries", {"encoder": {"type": "parallel_cnn", "max_sequence_length": 10, "should_embed": False}}, True + ), + InputFeatureOptions( + "audio", + { + "encoder": { + "type": "parallel_cnn", + "embedding_size": 64, + "max_sequence_length": 16, + "should_embed": False, + } + }, + True, + ), + # do not tie input features, encoders should be different + InputFeatureOptions("number", {"encoder": {"type": "passthrough"}}, False), + InputFeatureOptions( + "number", {"encoder": {"type": "passthrough"}, "preprocessing": {"normalization": "zscore"}}, False + ), + InputFeatureOptions("binary", {"encoder": {"type": "passthrough"}}, False), + InputFeatureOptions("category", {"encoder": {"type": "dense", "vocab": ["a", "b", "c"]}}, False), + InputFeatureOptions("set", {"encoder": {"type": "embed", "vocab": ["a", "b", "c"]}}, False), + InputFeatureOptions( + "sequence", + {"encoder": {"type": "parallel_cnn", "max_sequence_length": 10, "vocab": ["x", "y", "z"]}}, + False, + ), + InputFeatureOptions( + "text", {"encoder": {"type": "parallel_cnn", "max_sequence_length": 10, "vocab": ["a", "b", "c"]}}, False + ), + InputFeatureOptions( + "timeseries", {"encoder": {"type": "parallel_cnn", "max_sequence_length": 10, "should_embed": False}}, False + ), + InputFeatureOptions( + "audio", + { + "encoder": { + "type": "parallel_cnn", + "embedding_size": 64, + "max_sequence_length": 16, + "should_embed": False, + } + }, + False, + ), + ], +) +def test_tied_micro_level(input_feature_options): + # build input feature config + input_feature_configs = list() + + input_feature_configs.append({"name": "input_feature_1", "type": input_feature_options.feature_type}) + input_feature_configs[0].update(input_feature_options.feature_options) + + input_feature_configs.append({"name": "input_feature_2", "type": input_feature_options.feature_type}) + input_feature_configs[1].update(input_feature_options.feature_options) + + # add tied option to the second feature + if input_feature_options.tie_features: + input_feature_configs[1]["tied"] = "input_feature_1" + + config_obj = ModelConfig.from_dict( + {"input_features": input_feature_configs, "output_features": [{"name": "dummy_feature", "type": "binary"}]} + ) + + input_features = BaseModel.build_inputs(input_feature_configs=config_obj.input_features) + + if input_feature_options.tie_features: + # should be same encoder + assert input_features["input_feature_1"].encoder_obj is input_features["input_feature_2"].encoder_obj + else: + # no tied parameter, encoders should be different + assert input_features["input_feature_1"].encoder_obj is not input_features["input_feature_2"].encoder_obj + + +# TiedUseCase namedtuple structure: +# input_feature: Ludwig synthetic data creation function. +# output_feature: Ludwig synthetic data creation function +TiedUseCase = namedtuple("TiedUseCase", "input_feature output_feature") + + +# Macro level test ensures no exceptions are raised during a full_experiment() +@pytest.mark.parametrize( + "tied_use_case", + [ + TiedUseCase(number_feature, number_feature), + TiedUseCase(text_feature, category_feature), + TiedUseCase(sequence_feature, sequence_feature), + ], +) +def test_tied_macro_level(tied_use_case: TiedUseCase, csv_filename: str): + input_features = [ + number_feature(), # Other feature + tied_use_case.input_feature(), # first feature to be tied + tied_use_case.input_feature(), # second feature to be tied + category_feature(), # other feature + ] + # tie second feature to first feature + input_features[2]["tied"] = input_features[1]["name"] + + # setup output feature + output_features = [tied_use_case.output_feature(output_feature=True)] + + # Generate test data and run full_experiment + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) diff --git a/tests/integration_tests/test_kfold_cv.py b/tests/integration_tests/test_kfold_cv.py new file mode 100644 index 0000000..fc8b35a --- /dev/null +++ b/tests/integration_tests/test_kfold_cv.py @@ -0,0 +1,271 @@ +import logging +import os +import os.path +from collections import namedtuple + +import pytest +import yaml + +from ludwig.api import kfold_cross_validate +from ludwig.constants import BATCH_SIZE, TRAINER +from ludwig.experiment import kfold_cross_validate_cli +from ludwig.utils.data_utils import load_json +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + create_data_set_to_use, + generate_data, + number_feature, + sequence_feature, + text_feature, +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +FeaturesToUse = namedtuple("FeaturesToUse", "input_features output_features") + +FEATURES_TO_TEST = [ + FeaturesToUse( + # input feature + [number_feature(normalization="zscore"), number_feature(normalization="zscore")], + # output feature + [number_feature()], + ), + FeaturesToUse( + # input feature + [number_feature(normalization="zscore"), number_feature(normalization="zscore")], + # output feature + [binary_feature()], + ), + FeaturesToUse( + # input feature + [number_feature(normalization="zscore"), number_feature(normalization="zscore")], + # output feature + [category_feature(decoder={"vocab_size": 4}, reduce_input="sum", output_feature=True)], + ), + FeaturesToUse( + # input feature + # [sequence_feature(min_len=5, max_len=10, encoder="rnn", cell_type="lstm", reduce_output=None)], + [number_feature(normalization="zscore"), number_feature(normalization="zscore")], + # output feature + [ + sequence_feature( + decoder={ + "min_len": 5, + "max_len": 10, + "type": "generator", + "cell_type": "lstm", + "attention": "bahdanau", + }, + reduce_input=None, + output_feature=True, + ) + ], + ), + FeaturesToUse( + # input feature + [ + sequence_feature( + encoder={"min_len": 5, "max_len": 10, "type": "rnn", "cell_type": "lstm", "reduce_output": None} + ) + ], + # output feature + [sequence_feature(decoder={"max_len": 10, "type": "tagger"}, reduce_input=None, output_feature=True)], + ), + FeaturesToUse( + # input feature + [number_feature(normalization="zscore"), number_feature(normalization="zscore")], + # output feature + [text_feature(output_feature=True)], + ), +] + + +@pytest.mark.parametrize("features_to_use", FEATURES_TO_TEST) +def test_kfold_cv_cli(tmpdir, features_to_use: FeaturesToUse): + # k-fold cross validation cli + num_folds = 3 + + training_data_fp = os.path.join(tmpdir, "train.csv") + config_fp = os.path.join(tmpdir, "config.yaml") + results_dir = os.path.join(tmpdir, "results") + statistics_fp = os.path.join(results_dir, "kfold_training_statistics.json") + indices_fp = os.path.join(results_dir, "kfold_split_indices.json") + + # generate synthetic data for the test + input_features = features_to_use.input_features + + output_features = features_to_use.output_features + + generate_data(input_features, output_features, training_data_fp) + + # generate config file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + with open(config_fp, "w") as f: + yaml.dump(config, f) + + # run k-fold cv + kfold_cross_validate_cli( + k_fold=num_folds, + config=config_fp, + dataset=training_data_fp, + output_directory=results_dir, + logging_level="warn", + ) + + # check for expected results + # check for existence and structure of statistics file + assert os.path.isfile(statistics_fp) + + # check for required keys + cv_statistics = load_json(statistics_fp) + for key in ["fold_" + str(i + 1) for i in range(num_folds)] + ["overall"]: + assert key in cv_statistics + + # check for existence and structure of split indices file + assert os.path.isfile(indices_fp) + + # check for required keys + cv_indices = load_json(indices_fp) + for key in ["fold_" + str(i + 1) for i in range(num_folds)]: + assert key in cv_indices + + +def test_kfold_cv_api_from_file(tmpdir): + # k-fold_cross_validate api with config file + num_folds = 3 + + # setup required data structures for test + training_data_fp = os.path.join(tmpdir, "train.csv") + config_fp = os.path.join(tmpdir, "config.yaml") + + # generate synthetic data for the test + input_features = [number_feature(normalization="zscore"), number_feature(normalization="zscore")] + + output_features = [category_feature(decoder={"vocab_size": 3}, reduce_input="sum")] + + generate_data(input_features, output_features, training_data_fp) + + # generate config file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + with open(config_fp, "w") as f: + yaml.dump(config, f) + + # test kfold_cross_validate api with config file + + # execute k-fold cross validation run + kfold_cv_stats, kfold_split_indices = kfold_cross_validate(3, config=config_fp, dataset=training_data_fp) + + # correct structure for results from kfold cv + for key in ["fold_" + str(i + 1) for i in range(num_folds)] + ["overall"]: + assert key in kfold_cv_stats + + for key in ["fold_" + str(i + 1) for i in range(num_folds)]: + assert key in kfold_split_indices + + +def test_kfold_cv_api_in_memory(tmpdir): + # k-fold_cross_validate api with in-memory config + num_folds = 3 + + # setup required data structures for test + training_data_fp = os.path.join(tmpdir, "train.csv") + + # generate synthetic data for the test + input_features = [number_feature(normalization="zscore"), number_feature(normalization="zscore")] + + output_features = [number_feature()] + + generate_data(input_features, output_features, training_data_fp) + + # generate config file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # test kfold_cross_validate api with config in-memory + + # execute k-fold cross validation run + kfold_cv_stats, kfold_split_indices = kfold_cross_validate(3, config=config, dataset=training_data_fp) + + # correct structure for results from kfold cv + for key in ["fold_" + str(i + 1) for i in range(num_folds)] + ["overall"]: + assert key in kfold_cv_stats + + for key in ["fold_" + str(i + 1) for i in range(num_folds)]: + assert key in kfold_split_indices + + +DATA_FORMATS_FOR_KFOLDS = [ + "csv", + "df", + "dict", + "excel", + "feather", + "fwf", + "html", + "json", + "jsonl", + "parquet", + "pickle", + "stata", + "tsv", +] + + +@pytest.mark.parametrize("data_format", DATA_FORMATS_FOR_KFOLDS) +def test_kfold_cv_dataset_formats(tmpdir, data_format): + # k-fold_cross_validate api with in-memory config + num_folds = 3 + + # setup required data structures for test + training_data_fp = os.path.join(tmpdir, "train.csv") + + # generate synthetic data for the test + input_features = [number_feature(normalization="zscore"), number_feature(normalization="zscore")] + + output_features = [number_feature()] + + generate_data(input_features, output_features, training_data_fp) + dataset_to_use = create_data_set_to_use(data_format, training_data_fp) + + # generate config file + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # test kfold_cross_validate api with config in-memory + + # execute k-fold cross validation run + # pickle auto-dispatch by extension is disabled (CWE-502); pass explicitly. + explicit_format = data_format if data_format == "pickle" else None + kfold_cv_stats, kfold_split_indices = kfold_cross_validate( + 3, config=config, dataset=dataset_to_use, data_format=explicit_format + ) + + # correct structure for results from kfold cv + for key in ["fold_" + str(i + 1) for i in range(num_folds)] + ["overall"]: + assert key in kfold_cv_stats + + for key in ["fold_" + str(i + 1) for i in range(num_folds)]: + assert key in kfold_split_indices diff --git a/tests/integration_tests/test_llm.py b/tests/integration_tests/test_llm.py new file mode 100644 index 0000000..3c28ae2 --- /dev/null +++ b/tests/integration_tests/test_llm.py @@ -0,0 +1,1514 @@ +from __future__ import annotations + +import copy +import json +import os +import pathlib +from typing import Any + +import numpy as np +import pandas as pd +import pytest +import torch +import yaml + +import ludwig.error as ludwig_error +from ludwig.api import LudwigModel +from ludwig.constants import ( + ADAPTER, + BACKEND, + BASE_MODEL, + BATCH_SIZE, + COMBINER, + EPOCHS, + EVAL_BATCH_SIZE, + GENERATION, + INPUT_FEATURES, + MERGE_ADAPTER_INTO_BASE_MODEL, + MODEL_ECD, + MODEL_LLM, + MODEL_TYPE, + OUTPUT_FEATURES, + POSTPROCESSOR, + PREPROCESSING, + PRETRAINED_ADAPTER_WEIGHTS, + PROGRESSBAR, + PROMPT, + QUANTIZATION, + TARGET_MODULES, + TRAINER, + TYPE, +) +from ludwig.globals import MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME +from ludwig.models.llm import LLM +from ludwig.schema.model_types.base import ModelConfig +from ludwig.utils.fs_utils import list_file_names_in_directory +from ludwig.utils.types import DataFrame +from tests.integration_tests.utils import category_feature, generate_data, text_feature + +pytestmark = pytest.mark.llm + + +LOCAL_BACKEND = {"type": "local"} +TEST_MODEL_NAME = "hf-internal-testing/tiny-random-GPTJForCausalLM" +MAX_NEW_TOKENS_TEST_DEFAULT = 5 + +RAY_BACKEND = { + "type": "ray", + "processor": { + "parallelism": 1, + }, + "trainer": { + "use_gpu": False, + "num_workers": 2, + "resources_per_worker": { + "CPU": 1, + "GPU": 0, + }, + }, +} + + +def get_num_non_empty_tokens(iterable): + """Returns the number of non-empty tokens.""" + return len(list(filter(bool, iterable))) + + +@pytest.fixture(scope="module") +def local_backend(): + return LOCAL_BACKEND + + +@pytest.fixture(scope="module") +def ray_backend(): + return RAY_BACKEND + + +def get_dataset(): + data = [ + {"review": "I loved this movie!", "output": "positive"}, + {"review": "The food was okay, but the service was terrible.", "output": "negative"}, + {"review": "I can't believe how rude the staff was.", "output": "negative"}, + {"review": "This book was a real page-turner.", "output": "positive"}, + {"review": "The hotel room was dirty and smelled bad.", "output": "negative"}, + {"review": "I had a great experience at this restaurant.", "output": "positive"}, + {"review": "The concert was amazing!", "output": "positive"}, + {"review": "The traffic was terrible on my way to work this morning.", "output": "negative"}, + {"review": "The customer service was excellent.", "output": "positive"}, + {"review": "I was disappointed with the quality of the product.", "output": "negative"}, + ] + df = pd.DataFrame(data) + return df + + +def get_generation_config(): + return { + "temperature": 0.1, + "top_p": 0.75, + "top_k": 40, + "num_beams": 4, + "max_new_tokens": MAX_NEW_TOKENS_TEST_DEFAULT, + } + + +def convert_preds(preds: DataFrame): + if isinstance(preds, pd.DataFrame): + return preds.to_dict(orient="list") + return preds.compute().to_dict(orient="list") + + +@pytest.mark.llm +@pytest.mark.parametrize( + "backend", + [ + pytest.param(LOCAL_BACKEND, id="local"), + pytest.param(RAY_BACKEND, id="ray"), + ], +) +def test_llm_text_to_text(tmpdir, backend, ray_cluster_4cpu): + """Test that the LLM model can train and predict with text inputs and text outputs.""" + input_features = [ + { + "name": "Question", + "type": "text", + "encoder": {"type": "passthrough"}, + } + ] + output_features = [text_feature(output_feature=True, name="Answer", decoder={"type": "text_extractor"})] + + csv_filename = os.path.join(tmpdir, "training.csv") + dataset_filename = generate_data(input_features, output_features, csv_filename, num_examples=20) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + GENERATION: get_generation_config(), + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + BACKEND: backend, + } + + model = LudwigModel(config) + model.train(dataset=dataset_filename, output_directory=str(tmpdir), skip_save_processed_input=True) + + preds, _ = model.predict(dataset=dataset_filename, output_directory=str(tmpdir), split="test") + preds = convert_preds(preds) + + assert "Answer_predictions" in preds + assert "Answer_probabilities" in preds + assert "Answer_probability" in preds + assert "Answer_response" in preds + + assert preds["Answer_predictions"] + assert preds["Answer_probabilities"] + assert preds["Answer_probability"] + assert preds["Answer_response"] + + # Check that in-line generation parameters are used. Original prediction uses max_new_tokens = 5. + assert get_num_non_empty_tokens(preds["Answer_predictions"][0]) <= MAX_NEW_TOKENS_TEST_DEFAULT + original_max_new_tokens = model.model.generation.max_new_tokens + + # This prediction uses max_new_tokens = 2. + preds, _ = model.predict( + dataset=dataset_filename, + output_directory=str(tmpdir), + split="test", + generation_config={"min_new_tokens": 2, "max_new_tokens": 3}, + ) + preds = convert_preds(preds) + print(preds["Answer_predictions"][0]) + num_non_empty_tokens = get_num_non_empty_tokens(preds["Answer_predictions"][0]) + assert 2 <= num_non_empty_tokens <= 3 + + # Check that the state of the model is unchanged. + assert model.model.generation.max_new_tokens == original_max_new_tokens + + +@pytest.mark.llm +@pytest.mark.parametrize( + "backend", + [ + pytest.param(LOCAL_BACKEND, id="local"), + pytest.param(RAY_BACKEND, id="ray"), + ], +) +def test_llm_zero_shot_classification(tmpdir, backend, ray_cluster_4cpu): + input_features = [ + { + "name": "review", + "type": "text", + } + ] + output_features = [ + category_feature( + name="output", + preprocessing={ + "fallback_label": "neutral", + }, + # How can we avoid using r here for regex, since it is technically an implementation detail? + decoder={ + "type": "category_extractor", + "match": { + "positive": {"type": "contains", "value": "positive"}, + "neutral": {"type": "regex", "value": r"\bneutral\b"}, + "negative": {"type": "contains", "value": "negative"}, + }, + }, + ) + ] + + df = get_dataset() + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + GENERATION: get_generation_config(), + PROMPT: {"task": "This is a review of a restaurant. Classify the sentiment."}, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + BACKEND: backend, + } + + model = LudwigModel(config) + model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=True) + + prediction_df = pd.DataFrame( + [ + {"review": "The food was amazing!", "output": "positive"}, + {"review": "The service was terrible.", "output": "negative"}, + {"review": "The food was okay.", "output": "neutral"}, + ] + ) + + preds, _ = model.predict(dataset=prediction_df, output_directory=str(tmpdir)) + preds = convert_preds(preds) + + assert preds + + +@pytest.mark.llm +@pytest.mark.parametrize( + "backend", + [ + pytest.param(LOCAL_BACKEND, id="local"), + pytest.param(RAY_BACKEND, id="ray"), + ], +) +def test_llm_few_shot_classification(tmpdir, backend, csv_filename, ray_cluster_4cpu): + input_features = [ + text_feature( + output_feature=False, + name="body", + encoder={"type": "passthrough"}, # need to use the default encoder for LLMTextInputFeatureConfig + ) + ] + output_features = [ + category_feature( + output_feature=True, + name="output", + preprocessing={ + "fallback_label": "3", + }, + decoder={ + "type": "category_extractor", + "match": { + "1": {"type": "contains", "value": "1"}, + "2": {"type": "contains", "value": "2"}, + "3": {"type": "contains", "value": "3"}, + "4": {"type": "contains", "value": "4"}, + "5": {"type": "contains", "value": "5"}, + }, + }, + ) + ] + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + GENERATION: get_generation_config(), + PROMPT: { + "retrieval": {"type": "random", "k": 3}, + "task": ( + "Given the sample input, complete this sentence by replacing XXXX: The review rating is XXXX. " + "Choose one value in this list: [1, 2, 3, 4, 5]." + ), + }, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + PREPROCESSING: { + "split": {TYPE: "fixed"}, + }, + BACKEND: {**backend, "cache_dir": str(tmpdir)}, + } + + dataset_path = generate_data( + input_features, + output_features, + filename=csv_filename, + num_examples=25, + nan_percent=0.1, + with_split=True, + ) + df = pd.read_csv(dataset_path) + df["output"] = np.random.choice([1, 2, 3, 4, 5], size=len(df)).astype(str) # ensure labels match the feature config + df.to_csv(dataset_path, index=False) + + model = LudwigModel(config) + model.train(dataset=dataset_path, output_directory=str(tmpdir), skip_save_processed_input=True) + + # TODO: fix LLM model loading + # model = LudwigModel.load(os.path.join(results.output_directory, "model"), backend=backend) + preds, _ = model.predict(dataset=dataset_path) + preds = convert_preds(preds) + + assert preds + + +def _prepare_finetuning_test( + csv_filename: str, finetune_strategy: str, backend: dict, adapter_args: dict +) -> tuple[dict, str]: + input_features = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features = [text_feature(name="output")] + + train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25) + prediction_df = pd.DataFrame( + [ + {"input": "The food was amazing!", "output": "positive"}, + {"input": "The service was terrible.", "output": "negative"}, + {"input": "The food was okay.", "output": "neutral"}, + ] + ) + + model_name = TEST_MODEL_NAME + if finetune_strategy == "adalora": + # Adalora isn't supported for GPT-J model types, so use tiny bart + model_name = "hf-internal-testing/tiny-random-BartModel" + elif finetune_strategy == "adaption_prompt": + # At the time of writing this test, Adaption Prompt fine-tuning is only supported for Llama models + model_name = "yujiepan/llama-2-tiny-random" + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: model_name, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + GENERATION: {"max_new_tokens": 64}, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: "auto", + EVAL_BATCH_SIZE: "auto", + EPOCHS: 2, + }, + BACKEND: backend, + } + + if finetune_strategy is not None: + config[ADAPTER] = { + TYPE: finetune_strategy, + **adapter_args, + } + + return train_df, prediction_df, config + + +def _finetune_strategy_requires_cuda(finetune_strategy_name: str, quantization_args: dict | None) -> bool: + """This method returns whether a given finetine_strategy requires CUDA. + + For all finetune strategies, except "qlora", the decision is based just on the name of the finetine_strategy; in the + case of qlora, if the quantization dictionary is non-empty (i.e., contains quantization specifications), then the + original finetine_strategy name of "lora" is interpreted as "qlora" and used in the lookup, based on the list of + finetine strategies requiring CUDA. + """ + cuda_only_finetune_strategy_names: list[str] = [ + "prompt_tuning", + "prefix_tuning", + "p_tuning", + "qlora", + ] + + if finetune_strategy_name == "lora" and quantization_args: + finetune_strategy_name = "qlora" + + return finetune_strategy_name in cuda_only_finetune_strategy_names + + +def _verify_lm_lora_finetuning_layers( + attention_layer: torch.nn.Module, + target_modules: set[str], + merge_adapter_into_base_model: bool, + model_weights_directory: str, + expected_lora_in_features: int, + expected_lora_out_features: int, + expected_file_names: list[str], +) -> None: + """This method verifies that LoRA finetuning layers have correct types and shapes, depending on whether the + optional "model.merge_and_unload()" method (based on the "merge_adapter_into_base_model" directive) was + executed. + + If merge_adapter_into_base_model is True, then all specified LoRA projection layers in the attention layer must + contain square weight matrices (with the dimensions expected_lora_in_features by expected_lora_in_features). + However, if merge_adapter_into_base_model is False, then the LoRA part of the attention layer must include Lora_A + and Lora_B children layers for each specified projection, such that the product of Lora_A and Lora_B is a square + matrix (with the dimensions expected_lora_in_features by expected_lora_in_features) for each specified projection. + """ + from peft.tuners.lora.layer import LoraLayer + + expected_lora_num_features_orig: tuple[int] = (expected_lora_in_features, expected_lora_out_features) + + file_names: list[str] = list_file_names_in_directory(directory_name=model_weights_directory) + assert set(file_names) == set(expected_file_names) + + target_module_name: str + target_module_obj: LoraLayer | torch.nn.Linear + + # Not providing default value to "getattr()" so that error is raised if incorrect projection layer name is supplied. + + for target_module_name in target_modules: + target_module_obj = getattr(attention_layer, target_module_name) + if merge_adapter_into_base_model: + assert isinstance(target_module_obj, torch.nn.Linear) + else: + assert isinstance(target_module_obj, LoraLayer) + + if merge_adapter_into_base_model: + # If LoRA A & B layers are merged, they must have no children layers, and projection matrices must be square. + for target_module_name in target_modules: + target_module_obj = getattr(attention_layer, target_module_name) + assert not list(target_module_obj.children()) + assert (target_module_obj.in_features, target_module_obj.out_features) == ( + expected_lora_in_features, + expected_lora_out_features, + ) + else: + # If LoRA A & B layers are not merged, their children layers must be correctly-dimensioned projection matrices. + expected_lora_num_features: tuple[int] + target_named_children: dict[str, torch.nn.Module] + lora_matrix_name: str + idx: int + for target_module_name in target_modules: + target_module_obj = getattr(attention_layer, target_module_name) + target_named_children = dict(target_module_obj.named_children()) + + for idx, lora_matrix_name in enumerate(["lora_A", "lora_B"]): + assert isinstance(target_named_children[lora_matrix_name]["default"], torch.nn.Linear) + + # LoRA A and B matrix dimensions are transposes of one another so that their product is square matrix. + expected_lora_num_features = ( + expected_lora_num_features_orig + if idx % 2 == 0 + else (expected_lora_num_features_orig[1], expected_lora_num_features_orig[0]) + ) + assert ( + target_named_children[lora_matrix_name]["default"].in_features, + target_named_children[lora_matrix_name]["default"].out_features, + ) == expected_lora_num_features + + +@pytest.mark.llm +def test_llm_qat_torchao_end_to_end(tmpdir, csv_filename): + """End-to-end smoke test for torchao quantization-aware training (QAT) on an LLM. + + Fine-tunes ``hf-internal-testing/tiny-random-GPTJForCausalLM`` for a single epoch with + ``quantization.backend: torchao``, ``mode: int8_weight_only``, ``qat: true`` and verifies: + + * QAT observers are inserted before training (``_torchao_qat_prepared`` is set after + ``prepare_for_training``). + * Training completes without errors. + * Save applies the conversion — after ``model.save_pretrained`` runs, the saved + checkpoint reflects the quantized weights, the ``_torchao_quantized`` flag is set, + and the model is reloadable for inference. + + Paired with ``adapter: lora`` because Ludwig requires an adapter whenever quantization + is active on a finetune trainer (matches the existing QLoRA integration test pattern). + """ + pytest.importorskip("torchao", reason="torchao required for QAT tests") + + input_features = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features = [text_feature(name="output")] + train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=12) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + GENERATION: {"max_new_tokens": 16}, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 2, + EVAL_BATCH_SIZE: 2, + EPOCHS: 1, + }, + ADAPTER: {TYPE: "lora", "r": 4, "alpha": 8}, + QUANTIZATION: {"backend": "torchao", "mode": "int8_weight_only", "qat": True}, + BACKEND: LOCAL_BACKEND, + } + + output_directory: str = str(tmpdir) + model_directory: pathlib.Path = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME + + model = LudwigModel(config) + model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False) + + # QAT observers should have been inserted before training. + assert getattr(model.model, "_torchao_qat_prepared", False), "QAT preparation did not run" + # Save-time conversion should have fired. + assert getattr(model.model, "_torchao_quantized", False), "save-time quantization conversion did not run" + + # Reload and verify inference runs through the QAT-converted model. + reloaded = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND) + prediction_df = pd.DataFrame([{"input": "Hello world", "output": ""}]) + preds, _ = reloaded.predict(dataset=prediction_df, output_directory=output_directory) + preds = convert_preds(preds) + assert preds + + +@pytest.mark.llm +def test_llm_multi_adapter_registration_and_merge(tmpdir, csv_filename): + """End-to-end smoke test for the ``adapters:`` multi-adapter config. + + Registers two named LoRA adapters on a tiny GPTJ, runs a single fine-tune epoch, + attaches a TIES-merged adapter built from both sources, and verifies that: + + * all three adapters (``a``, ``b``, ``merged``) exist on the loaded model, + * the active adapter after init matches ``adapters.active`` (``merged``), and + * predictions can be generated through the merged adapter. + + Uses ``hf-internal-testing/tiny-random-GPTJForCausalLM`` — the smallest practical + causal LM in the Ludwig test suite — to keep wall-time low even on CPU runners. + """ + import peft as _peft # noqa: F401 (fail the test early on minimal installs) + + input_features = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features = [text_feature(name="output")] + train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=12) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + GENERATION: {"max_new_tokens": 16}, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 2, + EVAL_BATCH_SIZE: 2, + EPOCHS: 1, + }, + "adapters": { + "adapters": { + "adapter_a": {"type": "lora", "r": 4, "alpha": 8}, + "adapter_b": {"type": "lora", "r": 4, "alpha": 8}, + }, + "merge": { + "name": "merged", + "sources": ["adapter_a", "adapter_b"], + "weights": [0.5, 0.5], + "combination_type": "ties", + "density": 0.5, + }, + "active": "merged", + }, + BACKEND: LOCAL_BACKEND, + } + + output_directory: str = str(tmpdir) + model_directory: pathlib.Path = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME + + model = LudwigModel(config) + model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False) + + # All three named adapters must be present on the PEFT-wrapped model. + peft_adapters = set(model.model.model.peft_config.keys()) + assert {"adapter_a", "adapter_b", "merged"}.issubset(peft_adapters), f"missing adapters: {peft_adapters}" + + # The active adapter after initialization should be the merged one we requested. + active = model.model.model.active_adapter + if isinstance(active, (list, tuple, set)): + active = next(iter(active)) + assert active == "merged", f"expected active=merged, got {active!r}" + + # Reload round-trip: the saved model's PEFT dir should carry all three adapters. + reloaded = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND) + reloaded_peft_adapters = set(reloaded.model.model.peft_config.keys()) + assert {"adapter_a", "adapter_b", "merged"}.issubset(reloaded_peft_adapters) + + # Generation through the merged adapter should run to completion. + prediction_df = pd.DataFrame([{"input": "The food was amazing!", "output": ""}]) + preds, _ = reloaded.predict(dataset=prediction_df, output_directory=output_directory) + preds = convert_preds(preds) + assert preds + + +# TODO(arnav): p-tuning and prefix tuning have errors when enabled that seem to stem from distributed training: +# +# prefix tuning: +# Sizes of tensors must match except in dimension 1. Expected size 320 but got size 32 for tensor number 1 in the list. +# +# p-tuning: +# 'PromptEncoder' object has no attribute 'mlp_head' +@pytest.mark.llm +@pytest.mark.parametrize( + "backend", + [ + pytest.param(LOCAL_BACKEND, id="local"), + # TODO(Arnav): Re-enable once we can run tests on GPUs + # This is because fine-tuning requires Ray with a distributed strategy, and distributed + # training requires GPUs + # pytest.param(RAY_BACKEND, id="ray"), + ], +) +@pytest.mark.parametrize( + "finetune_strategy,adapter_args", + [ + pytest.param( + None, + {}, + id="full", + ), + pytest.param( + "lora", + {}, + id="lora-defaults", + ), + pytest.param( + "lora", + {"r": 4, "dropout": 0.1}, + id="lora-modified-defaults", + ), + pytest.param( + "lora", + {TARGET_MODULES: ["q_proj", "k_proj", "v_proj"]}, + id="lora-target-modules", + ), + pytest.param( + "lora", + {"use_rslora": True}, + id="lora-rslora-enabled", + ), + pytest.param( + "lora", + {"use_dora": True}, + id="lora-dora-enabled", + ), + pytest.param( + "lora", + {"use_rslora": True, "use_dora": True}, + id="lora-rslora-and-dora-enabled", + ), + pytest.param( + "lora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}}, + id="lora_merged", + ), + pytest.param( + "lora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}}, + id="lora_not_merged", + ), + pytest.param( + "adalora", + {}, + id="adalora-defaults", + ), + pytest.param( + "adalora", + {"init_r": 8, "beta1": 0.8}, + id="adalora-modified-defaults", + ), + pytest.param( + "adalora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}}, + id="adalora_merged", + ), + pytest.param( + "adalora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}}, + id="adalora_not_merged", + ), + # TODO: 02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix + # "TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')" + # (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938). + # + # pytest.param( + # "adaption_prompt", + # {}, + # id="adaption_prompt-defaults", + # ), + # pytest.param( + # "adaption_prompt", + # {"adapter_len": 6, "adapter_layers": 1}, + # id="adaption_prompt-modified-defaults", + # ), + pytest.param( + "ia3", + {}, + id="ia3-defaults", + ), + pytest.param( + "ia3", + {"init_ia3_weights": False}, + id="ia3-modified-defaults", + ), + # pytest.param( + # "prompt_tuning", + # { + # "num_virtual_tokens": 8, + # "prompt_tuning_init": "RANDOM", + # }, + # id="prompt_tuning_init_random", + # ), + # pytest.param( + # "prompt_tuning", + # { + # "num_virtual_tokens": 8, + # "prompt_tuning_init": "TEXT", + # "prompt_tuning_init_text": "Classify if the review is positive, negative, or neutral: ", + # }, + # id="prompt_tuning_init_text", + # ), + # pytest.param( + # "prefix_tuning", + # { + # "num_virtual_tokens": 8, + # }, + # id="prefix_tuning", + # ), + # pytest.param( + # "p_tuning", + # {"num_virtual_tokens": 8, "encoder_reparameterization_type": "MLP"}, + # id="p_tuning_mlp_reparameterization", + # ), + # pytest.param( + # "p_tuning", + # {"num_virtual_tokens": 8, "encoder_reparameterization_type": "LSTM"}, + # id="p_tuning_lstm_reparameterization", + # ), + ], +) +def test_llm_finetuning_strategies(tmpdir, csv_filename, backend, finetune_strategy, adapter_args): + train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetune_strategy, backend, adapter_args) + + output_directory: str = str(tmpdir) + model_directory: str = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME + + model = LudwigModel(config) + model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False) + + # Make sure we can load the saved model and then use it for predictions + model = LudwigModel.load(str(model_directory), backend=backend) + + base_model = LLM(ModelConfig.from_dict(config)) + assert not _compare_models(base_model, model.model) + + preds, _ = model.predict(dataset=prediction_df, output_directory=output_directory) + preds = convert_preds(preds) + + assert preds + + +@pytest.mark.llm +@pytest.mark.parametrize( + "finetune_strategy,adapter_args,quantization", + [ + pytest.param( + "lora", + {}, + {"bits": 4}, + id="qlora-4bit", + ), + pytest.param( + "lora", + {}, + {"bits": 8}, + id="qlora-8bit", + ), + ], +) +def test_llm_finetuning_strategies_quantized(tmpdir, csv_filename, finetune_strategy, adapter_args, quantization): + pytest.importorskip("bitsandbytes", reason="bitsandbytes required for quantization tests") + if ( + _finetune_strategy_requires_cuda(finetune_strategy_name=finetune_strategy, quantization_args=quantization) + and not (torch.cuda.is_available() and torch.cuda.device_count()) > 0 + ): + pytest.skip("Skip: quantization requires GPU and none are available.") + + backend = LOCAL_BACKEND + + train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetune_strategy, backend, adapter_args) + config["backend"] = backend + config[QUANTIZATION] = quantization + + model = LudwigModel(config) + model.train(dataset=train_df, output_directory=str(tmpdir), skip_save_processed_input=False) + + # Make sure we can load the saved model and then use it for predictions + model = LudwigModel.load(os.path.join(str(tmpdir), "api_experiment_run", MODEL_FILE_NAME)) + + base_model = LLM(ModelConfig.from_dict(config)) + assert not _compare_models(base_model, model.model) + + preds, _ = model.predict(dataset=prediction_df, output_directory=str(tmpdir)) + preds = convert_preds(preds) + + assert preds + + +@pytest.mark.llm +@pytest.mark.skipif(torch.cuda.device_count() == 0, reason="test requires at least 1 gpu") +@pytest.mark.skipif(not torch.cuda.is_available(), reason="test requires gpu support") +@pytest.mark.parametrize( + "finetune_strategy,adapter_args,quantization,error_raised", + [ + pytest.param( + "lora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}}, + {"bits": 4}, + ( + ImportError, + "Using `load_in_8bit=True` requires Accelerate: `pip install accelerate` and the latest version of bitsandbytes `pip install -i https://test.pypi.org/simple/ bitsandbytes` or pip install bitsandbytes` ", + ), + id="qlora-4bit-not-merged", + ), + pytest.param( + "lora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}}, + {"bits": 8}, + ( + ImportError, + "Using `load_in_8bit=True` requires Accelerate: `pip install accelerate` and the latest version of bitsandbytes `pip install -i https://test.pypi.org/simple/ bitsandbytes` or pip install bitsandbytes` ", + ), + id="qlora-8bit-merged", + ), + pytest.param( + "lora", + {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}}, + {"bits": 8}, + ( + ImportError, + "Using `load_in_8bit=True` requires Accelerate: `pip install accelerate` and the latest version of bitsandbytes `pip install -i https://test.pypi.org/simple/ bitsandbytes` or pip install bitsandbytes` ", + ), + id="qlora-8bit-not-merged", + ), + ], +) +def test_llm_lora_finetuning_merge_and_unload_quantized_accelerate_required( + csv_filename, finetune_strategy, adapter_args, quantization, error_raised +): + pytest.importorskip("bitsandbytes", reason="bitsandbytes required for quantization tests") + input_features: list[dict] = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features: list[dict] = [text_feature(name="output")] + + config: dict = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 8, + EPOCHS: 2, + }, + ADAPTER: { + TYPE: finetune_strategy, + **adapter_args, + }, + QUANTIZATION: quantization, + } + + model = LudwigModel(config) + + error_class: type + error_message: str + error_class, error_message = error_raised + with pytest.raises(error_class) as excinfo: + train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=3) + model.train(dataset=train_df) + + assert str(excinfo.value) == error_message + + +@pytest.mark.llm +def test_llm_lora_finetuning_merge_and_unload_4_bit_quantization_not_supported(local_backend: dict): + input_features: list[dict] = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features: list[dict] = [text_feature(name="output")] + finetune_strategy: str = "lora" + + config: dict = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 8, + EPOCHS: 2, + }, + ADAPTER: { + TYPE: finetune_strategy, + POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}, + }, + QUANTIZATION: {"bits": 4}, + BACKEND: local_backend, + } + + expected_error_class: type = ludwig_error.ConfigValidationError + expected_error_message: str = """This operation will entail merging LoRA layers on a 4-bit quantized model. \ +Calling "save_pretrained()" on that model is currently unsupported. If you want to merge the LoRA adapter weights \ +into the base model, you need to use 8-bit quantization or do non-quantized based training by removing the \ +quantization section from your Ludwig configuration.""" + with pytest.raises(expected_error_class) as excinfo: + _ = LudwigModel(config) + + assert str(excinfo.value) == expected_error_message + + +@pytest.mark.llm +@pytest.mark.parametrize( + "backend", + [ + pytest.param(LOCAL_BACKEND, id="local"), + # TODO: Re-enable once we can run tests on GPUs + # This is because fine-tuning requires Ray with a distributed strategy, and distributed + # training requires GPUs + # pytest.param(RAY_BACKEND, id="ray"), + ], +) +@pytest.mark.parametrize( + "target_modules,merge_adapter_into_base_model,expected_lora_in_features,expected_lora_out_features,expected_file_names", + [ + pytest.param( + None, + False, + 32, + 8, + [ + "README.md", + "adapter_config.json", + "adapter_model.safetensors", + ], + id="lora_default_not_merged", + ), + pytest.param( + None, + True, + 32, + 32, + [ + "README.md", + "adapter_config.json", + "adapter_model.safetensors", + "config.json", + "generation_config.json", + "model.safetensors", + "tokenizer.json", + "tokenizer_config.json", + ], + id="lora_default_merged", + ), + pytest.param( + ["q_proj", "k_proj", "v_proj"], + False, + 32, + 8, + [ + "README.md", + "adapter_config.json", + "adapter_model.safetensors", + ], + id="lora_custom_not_merged", + ), + pytest.param( + ["q_proj", "k_proj", "v_proj"], + True, + 32, + 32, + [ + "README.md", + "adapter_config.json", + "adapter_model.safetensors", + "config.json", + "generation_config.json", + "model.safetensors", + "tokenizer.json", + "tokenizer_config.json", + ], + id="lora_custom_merged", + ), + ], +) +def test_llm_lora_finetuning_merge_and_unload( + tmpdir: str, + csv_filename: str, + backend: dict, + target_modules: list[str] | set[str] | None, + merge_adapter_into_base_model: bool, + expected_lora_in_features: int, + expected_lora_out_features: int, + expected_file_names: list[str], +): + from peft.tuners.lora.config import LoraConfig + from peft.tuners.lora.model import LoraModel + + finetune_strategy: str = "lora" + + adapter_args: dict = { + POSTPROCESSOR: { + MERGE_ADAPTER_INTO_BASE_MODEL: merge_adapter_into_base_model, + }, + } + # If "target_modules" is None, then ["q_proj", "v_proj"] is used (HuggingFace Transformers/PEFT internal default). + if target_modules: + adapter_args[TARGET_MODULES] = target_modules + + train_df, prediction_df, config = _prepare_finetuning_test( + csv_filename=csv_filename, finetune_strategy=finetune_strategy, backend=backend, adapter_args=adapter_args + ) + + output_directory: str = str(tmpdir) + model_directory: str = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME + model_weights_directory: str = ( + pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME / MODEL_WEIGHTS_FILE_NAME + ) + + model = LudwigModel(config) + model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False) + + # Get actual "target_modules" from trained model (to be used in assertions). + lora_model: LoraModel = model.model.model.base_model + peft_config: dict = lora_model.peft_config + lora_config: LoraConfig = peft_config["default"] + target_modules = lora_config.target_modules + + _verify_lm_lora_finetuning_layers( + attention_layer=model.model.model.base_model.model.transformer.h[1].attn, + target_modules=target_modules, + merge_adapter_into_base_model=merge_adapter_into_base_model, + model_weights_directory=model_weights_directory, + expected_lora_in_features=expected_lora_in_features, + expected_lora_out_features=expected_lora_out_features, + expected_file_names=expected_file_names, + ) + + # Make sure we can load the saved model and verify that the LoRA layers have expected shapes. + model = LudwigModel.load(str(model_directory), backend=backend) + _verify_lm_lora_finetuning_layers( + attention_layer=model.model.model.base_model.model.transformer.h[1].attn, + target_modules=target_modules, + merge_adapter_into_base_model=merge_adapter_into_base_model, + model_weights_directory=model_weights_directory, + expected_lora_in_features=expected_lora_in_features, + expected_lora_out_features=expected_lora_out_features, + expected_file_names=expected_file_names, + ) + + +@pytest.mark.llm +@pytest.mark.parametrize("use_adapter", [True, False], ids=["with_adapter", "without_adapter"]) +def test_llm_training_with_gradient_checkpointing(tmpdir, csv_filename, use_adapter): + input_features = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features = [text_feature(name="output")] + + df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "hf-internal-testing/tiny-random-BartModel", + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 8, + EPOCHS: 1, + "enable_gradient_checkpointing": True, + }, + } + + if use_adapter: + config[ADAPTER] = {TYPE: "lora"} + + model = LudwigModel(config) + assert model.config_obj.trainer.enable_gradient_checkpointing + + model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=False) + + +@pytest.mark.llm +def test_lora_wrap_on_init(): + from peft import PeftModel + from transformers import PreTrainedModel + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 8, + EPOCHS: 2, + }, + } + config_obj = ModelConfig.from_dict(config) + model = LLM(config_obj) + assert isinstance(model.model, PreTrainedModel) + assert not isinstance(model.model, PeftModel) + + # Now add adapter + config[ADAPTER] = { + TYPE: "lora", + } + config_obj = ModelConfig.from_dict(config) + model = LLM(config_obj) + # We need to explicitly make this call since we now load the adapter + # in the trainer as opposed to the point of LLM model initialization. + model.prepare_for_training() + assert not isinstance(model.model, PreTrainedModel) + assert isinstance(model.model, PeftModel) + + +def test_llama_rope_scaling(): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 8, + EPOCHS: 2, + }, + "model_parameters": { + "rope_scaling": { + "rope_type": "dynamic", + "factor": 2.0, + } + }, + } + config_obj = ModelConfig.from_dict(config) + model = LLM(config_obj) + + assert model.model.config.rope_scaling + assert model.model.config.rope_scaling["rope_type"] == "dynamic" + assert model.model.config.rope_scaling["factor"] == 2.0 + + +def test_default_max_sequence_length(): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: TEST_MODEL_NAME, + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 8, + EPOCHS: 2, + }, + ADAPTER: {TYPE: "lora", PRETRAINED_ADAPTER_WEIGHTS: "Infernaught/test_adapter_weights"}, + BACKEND: {TYPE: "local"}, + } + config_obj = ModelConfig.from_dict(config) + assert config_obj.input_features[0].preprocessing.max_sequence_length is None + assert config_obj.output_features[0].preprocessing.max_sequence_length is None + + +@pytest.mark.llm +@pytest.mark.parametrize( + "adapter", + [ + "lora", + "adalora", + # TODO: 02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix + # "TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')" + # (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938). + # + # "adaption_prompt", + ], +) +def test_load_pretrained_adapter_weights(adapter): + from peft import PeftModel + from transformers import PreTrainedModel + + if adapter == "lora": + weights = "Infernaught/test_adapter_weights" + base_model = TEST_MODEL_NAME + elif adapter == "adalora": + weights = "Infernaught/test_adalora_weights" + base_model = "HuggingFaceH4/tiny-random-LlamaForCausalLM" + elif adapter == "adaption_prompt": + weights = "Infernaught/test_ap_weights" + base_model = "HuggingFaceH4/tiny-random-LlamaForCausalLM" + else: + raise () + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: base_model, + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + TRAINER: { + TYPE: "none", + BATCH_SIZE: 8, + EPOCHS: 2, + }, + ADAPTER: {TYPE: adapter, PRETRAINED_ADAPTER_WEIGHTS: weights}, + BACKEND: {TYPE: "local"}, + } + config_obj = ModelConfig.from_dict(config) + model = LLM(config_obj) + + assert model.config_obj.adapter.pretrained_adapter_weights + assert model.config_obj.adapter.pretrained_adapter_weights == weights + + model.prepare_for_training() + assert not isinstance(model.model, PreTrainedModel) + assert isinstance(model.model, PeftModel) + + config_obj = ModelConfig.from_dict(config) + assert config_obj.input_features[0].preprocessing.max_sequence_length is None + assert config_obj.output_features[0].preprocessing.max_sequence_length is None + + +def _compare_models(model_1: torch.nn.Module, model_2: torch.nn.Module) -> bool: + # For a full explanation of this 8-bit workaround, see https://github.com/ludwig-ai/ludwig/pull/3606 + + # TODO: Uncomment "filter_for_weight_format()" method definition and enable its usage once GPU tests are set up. + # def filter_for_weight_format(i): + # """Remove bitsandbytes metadata keys added on state dict creation. + # + # 8-bit quantized models that have been put on gpu will have a set of `weight_format` keys in their state dict. + # These contain strings that are used to reshape quantized tensors, however these have no impact until the state + # dict is loaded into a model. These keys were causing `torch.equal` to raise an exception, so we skip them in + # the evaluation. + # """ + # return "weight_format" not in i[0] + + # model_1_filtered_state_dict = filter(filter_for_weight_format, model_1.state_dict().items()) + # model_2_filtered_state_dict = filter(filter_for_weight_format, model_2.state_dict().items()) + + # Source: https://discuss.pytorch.org/t/check-if-models-have-same-weights/4351/6 + + if model_1.__class__.__name__ != model_2.__class__.__name__: + return False + + if ( + hasattr(model_1, "model") + and hasattr(model_2, "model") + and not _compare_models(model_1=model_1.model, model_2=model_2.model) + ): + return False + + for key_item_1, key_item_2 in zip(model_1.state_dict().items(), model_2.state_dict().items()): + if not torch.equal(key_item_1[1], key_item_2[1]): + return False + + return True + + +def test_global_max_sequence_length_for_llms(): + """Ensures that user specified global_max_sequence_length can never be greater than the model's context + length.""" + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + } + config_obj = ModelConfig.from_dict(config) + model = LLM(config_obj) + + # Default value is set based on model's context_len + assert model.global_max_sequence_length == 2048 + + # Override to a larger value in the config + config["preprocessing"] = {"global_max_sequence_length": 4096} + config_obj = ModelConfig.from_dict(config) + model = LLM(config_obj) + + # Check that the value can never be larger than the model's context_len + assert model.global_max_sequence_length == 2048 + + +def test_local_path_loading(tmpdir): + """Tests that local paths can be used to load models.""" + + from huggingface_hub import snapshot_download + + # Download the model to a local directory + local_path: str = f"{tmpdir!s}/test_local_path_loading" + repo_id: str = "HuggingFaceH4/tiny-random-LlamaForCausalLM" + os.makedirs(local_path, exist_ok=True) + snapshot_download(repo_id=repo_id, local_dir=local_path) + + # Load the model using the local path + config1 = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: local_path, + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + } + config_obj1 = ModelConfig.from_dict(config1) + model1 = LLM(config_obj1) + + # Load the model using the repo id + config2 = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: repo_id, + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + } + config_obj2 = ModelConfig.from_dict(config2) + model2 = LLM(config_obj2) + + # Check that the models are the same + assert _compare_models(model1.model, model2.model) + + +@pytest.mark.parametrize( + "finetuning_strategy, embedding_noise", + [ + pytest.param(None, 0, id="full_finetuning_without_noise"), + pytest.param(None, 5, id="full_finetuning_with_noise"), + pytest.param("lora", 0, id="lora_without_noise"), + pytest.param("lora", 5, id="lora_with_noise"), + ], +) +def test_llm_finetuning_with_embedding_noise( + tmpdir, + csv_filename, + finetuning_strategy, + embedding_noise, +): + train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetuning_strategy, LOCAL_BACKEND, {}) + + # Add embedding noise + if embedding_noise: + config["model_parameters"] = {"neftune_noise_alpha": embedding_noise} + + model = LudwigModel(config) + + if embedding_noise: + assert model.config_obj.model_parameters.neftune_noise_alpha == embedding_noise + + output_directory: str = str(tmpdir) + model_directory: str = pathlib.Path(output_directory) / "api_experiment_run" / MODEL_FILE_NAME + model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False) + + # Make sure we can load the saved model and then use it for predictions + model = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND) + + base_model = LLM(ModelConfig.from_dict(config)) + assert not _compare_models(base_model, model.model) + + preds, _ = model.predict(dataset=prediction_df, output_directory=output_directory) + preds = convert_preds(preds) + + assert preds + + +@pytest.fixture() +def llm_encoder_config() -> dict[str, Any]: + encoder_config = { + TYPE: "llm", + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + } + + return encoder_config + + +@pytest.mark.parametrize( + "adapter,quantization", + [ + (None, None), + ("lora", None), + ("lora", {"bits": 4}), + ("lora", {"bits": 8}), + ("adalora", None), + ("adalora", {"bits": 4}), + ("adalora", {"bits": 8}), + ], + ids=["FFT", "LoRA", "LoRA 4-bit", "LoRA 8-bit", "AdaLoRA", "AdaLoRA 4-bit", "AdaLoRA 8-bit"], +) +def test_llm_encoding(llm_encoder_config, adapter, quantization, tmpdir): + if quantization: + pytest.importorskip("bitsandbytes", reason="bitsandbytes required for quantization tests") + if ( + _finetune_strategy_requires_cuda( + finetune_strategy_name="lora" if adapter else None, quantization_args=quantization + ) + and not (torch.cuda.is_available() and torch.cuda.device_count()) > 0 + ): + pytest.skip("Skip: quantization requires GPU and none are available.") + + dataset_path = os.path.join(tmpdir, "llm_classification_data.csv") + + config = { + MODEL_TYPE: MODEL_ECD, + OUTPUT_FEATURES: [category_feature(name="output")], + COMBINER: {TYPE: "sequence"}, + TRAINER: {EPOCHS: 1}, + } + + encoder_config = copy.deepcopy(llm_encoder_config) + + if adapter: + encoder_config[ADAPTER] = {TYPE: adapter} + if quantization: + encoder_config[QUANTIZATION] = quantization + config[BACKEND] = LOCAL_BACKEND + + config[INPUT_FEATURES] = [text_feature(name="input", encoder=encoder_config)] + + generate_data(input_features=config[INPUT_FEATURES], output_features=config[OUTPUT_FEATURES], filename=dataset_path) + + model = LudwigModel(config) + model.train(dataset=dataset_path, output_directory=str(tmpdir)) + + +def test_llm_batch_size_tuning(): + dataset = pd.DataFrame({"instruction": ["a"] * 100, "output": ["a"] * 100}) + config = yaml.safe_load(""" + model_type: llm + input_features: + - name: instruction + type: text + output_features: + - name: output + type: text + prompt: + template: >- + {instruction} + adapter: + type: lora + trainer: + type: finetune + optimizer: + type: adam + batch_size: auto + train_steps: 1 + learning_rate: 0.0002 + eval_batch_size: 2 + backend: + type: local + base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM + """) + model = LudwigModel(config=config) + model.train(dataset=dataset) + assert model.config_obj.trainer.batch_size > 1 + + +@pytest.mark.llm +def test_llm_used_tokens(tmpdir): + input_features = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features = [text_feature(name="output")] + + df = pd.read_json("https://raw.githubusercontent.com/sahil280114/codealpaca/master/data/code_alpaca_20k.json").head( + 10 + ) + + # df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "hf-internal-testing/tiny-random-BartModel", + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + TYPE: "finetune", + BATCH_SIZE: 1, + EPOCHS: 3, + "enable_gradient_checkpointing": True, + }, + } + + config[ADAPTER] = {TYPE: "lora"} + + model = LudwigModel(config) + assert model.config_obj.trainer.enable_gradient_checkpointing + + model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=False) + + with open( + os.path.join(str(tmpdir), "api_experiment_run", MODEL_FILE_NAME, "training_progress.json"), encoding="utf-8" + ) as f: + progress_tracker = json.load(f) + + assert progress_tracker["cumulative_step_token_usage"]["11"] == progress_tracker["total_tokens_used"] == 621 + assert progress_tracker["checkpoint_to_epoch"] == {"1": 1, "2": 1, "3": 2, "4": 2, "5": 3, "6": 3} + assert progress_tracker["checkpoint_to_step"] == {"1": 4, "2": 4, "3": 8, "4": 8, "5": 12, "6": 12} + assert progress_tracker["cumulative_checkpoint_token_usage"] == { + "1": 207, + "2": 207, + "3": 414, + "4": 414, + "5": 621, + "6": 621, + } + assert progress_tracker["incremental_checkpoint_token_usage"] == { + "1": 207, + "2": 0, + "3": 207, + "4": 0, + "5": 207, + "6": 0, + } diff --git a/tests/integration_tests/test_missing_value_strategy.py b/tests/integration_tests/test_missing_value_strategy.py new file mode 100644 index 0000000..e0592a1 --- /dev/null +++ b/tests/integration_tests/test_missing_value_strategy.py @@ -0,0 +1,202 @@ +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import os +import random + +import numpy as np +import pandas as pd +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, COLUMN, DROP_ROW, FILL_WITH_MEAN, PREPROCESSING, PROC_COLUMN, TRAINER +from ludwig.globals import MODEL_FILE_NAME +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + generate_data, + LocalTestBackend, + number_feature, + read_csv_with_nan, + sequence_feature, + set_feature, + text_feature, + vector_feature, +) + + +def test_missing_value_prediction(tmpdir, csv_filename): + random.seed(1) + np.random.seed(1) + input_features = [ + category_feature( + encoder={"vocab_size": 2}, reduce_input="sum", preprocessing=dict(missing_value_strategy="fill_with_mode") + ) + ] + output_features = [binary_feature()] + + dataset = pd.read_csv(generate_data(input_features, output_features, csv_filename)) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + } + model = LudwigModel(config) + _, _, output_dir = model.train(dataset=dataset, output_directory=tmpdir) + + # Set the input column to None, we should be able to replace the missing value with the mode + # from the training set + dataset[input_features[0]["name"]] = None + model.predict(dataset=dataset) + + model = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME)) + model.predict(dataset=dataset) + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=[pytest.mark.distributed, pytest.mark.distributed_f]), + ], +) +def test_missing_values_fill_with_mean(backend, csv_filename, tmpdir, ray_cluster_2cpu): + data_csv_path = os.path.join(tmpdir, csv_filename) + + kwargs = {PREPROCESSING: {"missing_value_strategy": FILL_WITH_MEAN}} + input_features = [ + number_feature(**kwargs), + binary_feature(), + category_feature(encoder={"vocab_size": 3}), + ] + output_features = [binary_feature()] + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # run preprocessing + ludwig_model = LudwigModel(config, backend=backend) + ludwig_model.preprocess(dataset=training_data_csv_path) + + +def test_missing_values_drop_rows(csv_filename, tmpdir): + data_csv_path = os.path.join(tmpdir, csv_filename) + + kwargs = {PREPROCESSING: {"missing_value_strategy": DROP_ROW}} + input_features = [ + number_feature(), + binary_feature(), + category_feature(encoder={"vocab_size": 3}), + ] + output_features = [ + binary_feature(**kwargs), + number_feature(**kwargs), + category_feature(decoder={"vocab_size": 3}, **kwargs), + sequence_feature(decoder={"vocab_size": 3}, **kwargs), + text_feature(decoder={"vocab_size": 3}, **kwargs), + set_feature(decoder={"vocab_size": 3}, **kwargs), + vector_feature(**kwargs), + ] + backend = LocalTestBackend() + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = read_csv_with_nan(training_data_csv_path, nan_percent=0.1) + + # run preprocessing + ludwig_model = LudwigModel(config, backend=backend) + ludwig_model.preprocess(dataset=df) + + +@pytest.mark.parametrize( + "backend,outlier_strategy,outlier_threshold", + [ + pytest.param("local", None, 3.0, id="local_none"), + pytest.param("local", "fill_with_mean", 1.0, id="local_mean_strict"), + pytest.param("local", "fill_with_const", 3.0, id="local_const_relaxed"), + pytest.param( + "ray", "fill_with_mean", 3.0, id="ray_mean", marks=[pytest.mark.distributed, pytest.mark.distributed_f] + ), + ], +) +def test_outlier_strategy(outlier_strategy, outlier_threshold, backend, tmpdir, ray_cluster_2cpu): + fill_value = 42 + kwargs = { + PREPROCESSING: { + "outlier_strategy": outlier_strategy, + "outlier_threshold": outlier_threshold, + "fill_value": fill_value, + } + } + input_features = [ + number_feature(**kwargs), + ] + output_features = [binary_feature()] + + # Values that will be 1 and 3 std deviations from the mean, respectively + sigma1, sigma1_idx = -150, 4 + sigma3, sigma3_idx = 300, 11 + + num_col = np.array([77, 24, 29, 29, sigma1, 71, 46, 95, 20, 52, 85, sigma3, 74, 10, 98, 53, 110, 94, 62, 13]) + expected_fill_value = num_col.mean() if outlier_strategy == "fill_with_mean" else fill_value + + input_col = input_features[0][COLUMN] + output_col = output_features[0][COLUMN] + + bin_col = np.array([1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0], dtype=np.bool_) + dataset_df = pd.DataFrame( + data={ + input_col: num_col, + output_col: bin_col, + } + ) + + dataset_fp = os.path.join(tmpdir, "dataset.csv") + dataset_df.to_csv(dataset_fp) + + config = { + "input_features": input_features, + "output_features": output_features, + } + + # Run preprocessing + ludwig_model = LudwigModel(config, backend=backend) + proc_dataset = ludwig_model.preprocess(training_set=dataset_fp) + + # Check preprocessed output + proc_df = ludwig_model.backend.df_engine.compute(proc_dataset.training_set.to_df()) + proc_col = input_features[0][PROC_COLUMN] + + assert len(proc_df) == len(dataset_df) + + # Check that values over 1 std are replaced + if outlier_strategy is not None and outlier_threshold <= 1.0: + assert np.isclose(proc_df[proc_col][sigma1_idx], expected_fill_value) + else: + assert np.isclose(proc_df[proc_col][sigma1_idx], dataset_df[input_col][sigma1_idx]) + + # Check that values over 3 std are replaced + if outlier_strategy is not None and outlier_threshold <= 3.0: + assert np.isclose(proc_df[proc_col][sigma3_idx], expected_fill_value) + else: + assert np.isclose(proc_df[proc_col][sigma3_idx], dataset_df[input_col][sigma3_idx]) diff --git a/tests/integration_tests/test_mlflow.py b/tests/integration_tests/test_mlflow.py new file mode 100644 index 0000000..7dbccae --- /dev/null +++ b/tests/integration_tests/test_mlflow.py @@ -0,0 +1,201 @@ +import os +import shutil +import uuid +from unittest import mock + +import mlflow +import pandas as pd +import pytest +import yaml +from mlflow.tracking import MlflowClient + +from ludwig.api import LudwigModel +from ludwig.constants import TRAINER +from ludwig.contribs.mlflow import MlflowCallback +from ludwig.export import export_mlflow +from ludwig.globals import MODEL_FILE_NAME +from ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version +from tests.integration_tests.utils import category_feature, FakeRemoteBackend, generate_data, sequence_feature + + +def run_mlflow_callback_test(mlflow_client, config, training_data, val_data, test_data, tmpdir, exp_name=None): + ludwig_exp_name = "mlflow_test" + callback = MlflowCallback() + wrapped_callback = mock.Mock(wraps=callback) + + model = LudwigModel(config, callbacks=[wrapped_callback], backend=FakeRemoteBackend()) + model.train( + training_set=training_data, validation_set=val_data, test_set=test_data, experiment_name=ludwig_exp_name + ) + expected_df, _ = model.predict(test_data) + + # Check mlflow artifacts + assert callback.experiment_id is not None + assert callback.run is not None + + mlflow_exp_name = exp_name or ludwig_exp_name + experiment = mlflow.get_experiment_by_name(mlflow_exp_name) + assert experiment.experiment_id == callback.experiment_id + + df = mlflow.search_runs([experiment.experiment_id]) + assert len(df) == 1 + + run_id = df.run_id[0] + assert run_id == callback.run.info.run_id + + run = mlflow.get_run(run_id) + expected_status = "FINISHED" if exp_name is None else "RUNNING" + assert run.info.status == expected_status + assert wrapped_callback.on_trainer_train_setup.call_count == 1 + assert wrapped_callback.on_trainer_train_teardown.call_count == 1 + + artifacts = [f.path for f in mlflow_client.list_artifacts(callback.run.info.run_id, "")] + local_dir = f"{tmpdir}/local_artifacts" + os.makedirs(local_dir) + + assert "config.yaml" in artifacts + local_config_path = mlflow_client.download_artifacts(callback.run.info.run_id, "config.yaml", local_dir) + + with open(local_config_path) as f: + config_artifact = yaml.safe_load(f) + assert config_artifact == upgrade_config_dict_to_latest_version(config) + + model_path = f"runs:/{callback.run.info.run_id}/model" + loaded_model = mlflow.pyfunc.load_model(model_path) + + assert "ludwig" in loaded_model.metadata.flavors + flavor = loaded_model.metadata.flavors["ludwig"] + config = model.config + + def compare_features(key): + assert len(config[key]) == len(flavor["ludwig_schema"][key]) + for feature, schema_feature in zip(config[key], flavor["ludwig_schema"][key]): + assert feature["name"] == schema_feature["name"] + assert feature["type"] == schema_feature["type"] + + compare_features("input_features") + compare_features("output_features") + + test_df = pd.read_csv(test_data) + pred_df = loaded_model.predict(test_df) + assert pred_df.equals(expected_df) + return run + + +def run_mlflow_callback_test_without_artifacts(mlflow_client, config, training_data, val_data, test_data): + exp_name = "mlflow_test_without_artifacts" + callback = MlflowCallback(log_artifacts=False) + wrapped_callback = mock.Mock(wraps=callback) + + model = LudwigModel(config, callbacks=[wrapped_callback], backend=FakeRemoteBackend()) + model.train(training_set=training_data, validation_set=val_data, test_set=test_data, experiment_name=exp_name) + expected_df, _ = model.predict(test_data) + + # Check mlflow artifacts + artifacts = [f.path for f in mlflow_client.list_artifacts(callback.run.info.run_id, "")] + assert len(artifacts) == 0 + + +@pytest.mark.parametrize("external_run", [False, True], ids=["internal_run", "external_run"]) +def test_mlflow(tmpdir, external_run): + epochs = 2 + batch_size = 8 + num_examples = 32 + + input_features = [sequence_feature(reduce_output="sum")] + output_features = [category_feature(vocab_size=2, reduce_input="sum", output_feature=True)] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": epochs, "batch_size": batch_size}, + } + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "train.csv"), num_examples=num_examples + ) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + mlflow_uri = f"file://{tmpdir}/mlruns" + mlflow.set_tracking_uri(mlflow_uri) + client = MlflowClient(tracking_uri=mlflow_uri) + + exp_name = None + run = None + if external_run: + # Start a run here and make sure it's still active when training completes + exp_name = f"ext_experiment_{uuid.uuid4().hex}" + exp_id = mlflow.create_experiment(name=exp_name) + run = mlflow.start_run(experiment_id=exp_id, run_name=f"ext_run_{uuid.uuid4().hex}") + + callback_run = run_mlflow_callback_test(client, config, data_csv, val_csv, test_csv, tmpdir, exp_name=exp_name) + + if not external_run: + run_mlflow_callback_test_without_artifacts(client, config, data_csv, val_csv, test_csv) + else: + assert run.info.run_id == callback_run.info.run_id + + active_run = mlflow.active_run() + assert active_run is not None + assert run.info.run_id == active_run.info.run_id + + mlflow.end_run() + + +def test_export_mlflow_local(tmpdir): + epochs = 2 + batch_size = 8 + num_examples = 32 + + input_features = [sequence_feature(reduce_output="sum")] + output_features = [category_feature(vocab_size=2, reduce_input="sum", output_feature=True)] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": epochs, "batch_size": batch_size}, + } + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "train.csv"), num_examples=num_examples + ) + + exp_name = "mlflow_test" + output_dir = os.path.join(tmpdir, "output") + model = LudwigModel(config, backend=FakeRemoteBackend()) + _, _, output_directory = model.train(training_set=data_csv, experiment_name=exp_name, output_directory=output_dir) + + model_path = os.path.join(output_directory, MODEL_FILE_NAME) + output_path = os.path.join(tmpdir, "data/results/mlflow") + export_mlflow(model_path, output_path) + assert set(os.listdir(output_path)) == {"MLmodel", MODEL_FILE_NAME, "conda.yaml"} + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_mlflow_ray(tmpdir, ray_cluster_2cpu): + epochs = 2 + batch_size = 8 + num_examples = 32 + + input_features = [sequence_feature(reduce_output="sum")] + output_features = [category_feature(vocab_size=2, reduce_input="sum", output_feature=True)] + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": epochs, "batch_size": batch_size}, + } + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "train.csv"), num_examples=num_examples + ) + + exp_name = "mlflow_test" + output_dir = os.path.join(tmpdir, "output") + model = LudwigModel(config, callbacks=[MlflowCallback()], backend="ray") + _, _, output_directory = model.train(training_set=data_csv, experiment_name=exp_name, output_directory=output_dir) diff --git a/tests/integration_tests/test_model_save_and_load.py b/tests/integration_tests/test_model_save_and_load.py new file mode 100644 index 0000000..fa05256 --- /dev/null +++ b/tests/integration_tests/test_model_save_and_load.py @@ -0,0 +1,446 @@ +import os +import os.path +import random + +import numpy as np +import pandas as pd +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, ENCODER, LOSS, NAME, PREPROCESSING, TRAINER, TRAINING, TYPE +from ludwig.data.split import get_splitter +from ludwig.globals import MODEL_FILE_NAME +from ludwig.modules.loss_modules import MSELoss +from ludwig.schema.features.loss.loss import MSELossConfig +from ludwig.utils.data_utils import read_csv +from tests.integration_tests.utils import ( + audio_feature, + bag_feature, + binary_feature, + category_feature, + date_feature, + generate_data, + h3_feature, + image_feature, + LocalTestBackend, + number_feature, + sequence_feature, + set_feature, + text_feature, + timeseries_feature, + vector_feature, +) + + +def test_model_load_from_checkpoint(tmpdir, csv_filename, tmp_path): + torch.manual_seed(1) + random.seed(1) + np.random.seed(1) + + input_features = [ + binary_feature(), + number_feature(), + ] + + output_features = [ + binary_feature(), + ] + + data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 1, BATCH_SIZE: 2}, + } + backend = LocalTestBackend() + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + data_df = read_csv(data_csv_path) + splitter = get_splitter("random") + training_set, validation_set, test_set = splitter.split(data_df, backend) + ludwig_model1 = LudwigModel(config, backend=backend) + _, _, output_dir = ludwig_model1.train( + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + output_directory="results", # results_dir + ) + + model_dir = os.path.join(output_dir, MODEL_FILE_NAME) + ludwig_model_loaded = LudwigModel.load(model_dir, backend=backend, from_checkpoint=True) + preds_1, _ = ludwig_model1.predict(dataset=validation_set) + + def check_model_equal(ludwig_model2): + # Compare model predictions + preds_2, _ = ludwig_model2.predict(dataset=validation_set) + assert set(preds_1.keys()) == set(preds_2.keys()) + for key in preds_1: + assert preds_1[key].dtype == preds_2[key].dtype, key + assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key + # assert preds_2[key].dtype == preds_3[key].dtype, key + # assert list(preds_2[key]) == list(preds_3[key]), key + + # Compare model weights + for if_name in ludwig_model1.model.input_features: + if1 = ludwig_model1.model.input_features.get(if_name) + if2 = ludwig_model2.model.input_features.get(if_name) + for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()): + assert torch.allclose(if1_w, if2_w) + + c1 = ludwig_model1.model.combiner + c2 = ludwig_model2.model.combiner + for c1_w, c2_w in zip(c1.parameters(), c2.parameters()): + assert torch.allclose(c1_w, c2_w) + + for of_name in ludwig_model1.model.output_features: + of1 = ludwig_model1.model.output_features.get(of_name) + of2 = ludwig_model2.model.output_features.get(of_name) + for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()): + assert torch.allclose(of1_w, of2_w) + + check_model_equal(ludwig_model_loaded) + + +def test_model_save_reload_api(tmpdir, csv_filename, tmp_path): + torch.manual_seed(1) + random.seed(1) + np.random.seed(1) + + image_dest_folder = os.path.join(tmpdir, "generated_images") + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + + input_features = [ + binary_feature(), + number_feature(), + category_feature(encoder={"vocab_size": 3}), + sequence_feature(encoder={"vocab_size": 3}), + text_feature( + encoder={"vocab_size": 3, "type": "rnn", "cell_type": "lstm", "num_layers": 2, "bidirectional": False} + ), + vector_feature(), + image_feature(image_dest_folder, encoder={"type": "mlp_mixer", "patch_size": 12}), + audio_feature(audio_dest_folder, encoder={"type": "stacked_cnn"}), + timeseries_feature(encoder={"type": "parallel_cnn"}), + sequence_feature(encoder={"vocab_size": 3, "type": "stacked_parallel_cnn"}), + date_feature(), + h3_feature(), + set_feature(encoder={"vocab_size": 3}), + bag_feature(encoder={"vocab_size": 3}), + ] + + output_features = [ + binary_feature(), + number_feature(), + category_feature(decoder={"vocab_size": 3}, output_feature=True), + sequence_feature(decoder={"vocab_size": 3}, output_feature=True), + text_feature(decoder={"vocab_size": 3}, output_feature=True), + set_feature(decoder={"vocab_size": 3}, output_feature=True), + vector_feature(), + ] + + # Generate test data + data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20) + + ############# + # Train model + ############# + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + } + + data_df = read_csv(data_csv_path) + splitter = get_splitter("random") + training_set, validation_set, test_set = splitter.split(data_df, LocalTestBackend()) + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + # perform initial model training + backend = LocalTestBackend() + ludwig_model1 = LudwigModel(config, backend=backend) + _, _, output_dir = ludwig_model1.train( + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + output_directory="results", # results_dir + ) + + preds_1, _ = ludwig_model1.predict(dataset=validation_set) + + def check_model_equal(ludwig_model2): + # Compare model predictions + preds_2, _ = ludwig_model2.predict(dataset=validation_set) + assert set(preds_1.keys()) == set(preds_2.keys()) + for key in preds_1: + assert preds_1[key].dtype == preds_2[key].dtype, key + assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key + # assert preds_2[key].dtype == preds_3[key].dtype, key + # assert list(preds_2[key]) == list(preds_3[key]), key + + # Compare model weights + for if_name in ludwig_model1.model.input_features: + if1 = ludwig_model1.model.input_features.get(if_name) + if2 = ludwig_model2.model.input_features.get(if_name) + for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()): + assert torch.allclose(if1_w, if2_w) + + c1 = ludwig_model1.model.combiner + c2 = ludwig_model2.model.combiner + for c1_w, c2_w in zip(c1.parameters(), c2.parameters()): + assert torch.allclose(c1_w, c2_w) + + for of_name in ludwig_model1.model.output_features: + of1 = ludwig_model1.model.output_features.get(of_name) + of2 = ludwig_model2.model.output_features.get(of_name) + for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()): + assert torch.allclose(of1_w, of2_w) + + ludwig_model1.save(tmpdir) + ludwig_model_loaded = LudwigModel.load(tmpdir, backend=backend) + check_model_equal(ludwig_model_loaded) + + # Test loading the model from the experiment directory + ludwig_model_exp = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME), backend=backend) + check_model_equal(ludwig_model_exp) + + +def test_model_weights_match_training(tmpdir, csv_filename): + np.random.seed(1) + + input_features = [number_feature()] + output_features = [number_feature()] + output_feature_name = output_features[0][NAME] + + # Generate test data + data_csv_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=20) + + config = { + "input_features": input_features, + "output_features": output_features, + "trainer": { + "epochs": 3, + "batch_size": 32, + "evaluate_training_set": True, # needed to ensure exact training metrics computed + }, + } + + model = LudwigModel( + config=config, + ) + + training_stats, _, _ = model.train(training_set=data_csv_path, random_seed=1919) + + # generate predicitons from training data + df = pd.read_csv(data_csv_path) + predictions = model.predict(df) + + # compute loss on predictions from training data + loss_function = MSELoss(MSELossConfig()) + loss = loss_function( + torch.tensor(predictions[0][output_feature_name + "_predictions"].values), # predictions + torch.tensor(df[output_feature_name].values), # target + ).type(torch.float32) + + # get last loss value from training + last_training_loss = torch.tensor(training_stats[TRAINING][output_feature_name][LOSS][-1]) + + # loss from predictions should match last loss value recorded during training + assert torch.isclose(loss, last_training_loss), ( + "Model predictions on training set did not generate same loss value as in training. " + "Need to confirm that weights were correctly captured in model." + ) + + +@pytest.mark.parametrize("torch_encoder, variant", [("resnet", 18), ("googlenet", "base")]) +def test_model_save_reload_tv_model(torch_encoder, variant, tmpdir, csv_filename, tmp_path): + torch.manual_seed(1) + random.seed(1) + np.random.seed(1) + + image_dest_folder = os.path.join(tmpdir, "generated_images") + + input_features = [ + image_feature(image_dest_folder), + ] + input_features[0][ENCODER] = { + TYPE: torch_encoder, + "model_variant": variant, + } + input_features[0][PREPROCESSING]["height"] = 128 + input_features[0][PREPROCESSING]["width"] = 128 + + output_features = [ + category_feature(decoder={"vocab_size": 3}), + ] + + # Generate test data + data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20) + + ############# + # Train model + ############# + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + } + + data_df = read_csv(data_csv_path) + splitter = get_splitter("random") + training_set, validation_set, test_set = splitter.split(data_df, LocalTestBackend()) + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + # perform initial model training + backend = LocalTestBackend() + ludwig_model1 = LudwigModel(config, backend=backend) + _, _, output_dir = ludwig_model1.train( + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + output_directory="results", # results_dir + ) + + preds_1, _ = ludwig_model1.predict(dataset=validation_set) + + def check_model_equal(ludwig_model2): + # Compare model predictions + preds_2, _ = ludwig_model2.predict(dataset=validation_set) + assert set(preds_1.keys()) == set(preds_2.keys()) + for key in preds_1: + assert preds_1[key].dtype == preds_2[key].dtype, key + assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key + # assert preds_2[key].dtype == preds_3[key].dtype, key + # assert list(preds_2[key]) == list(preds_3[key]), key + + # Compare model weights + for if_name in ludwig_model1.model.input_features: + if1 = ludwig_model1.model.input_features.get(if_name) + if2 = ludwig_model2.model.input_features.get(if_name) + for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()): + assert torch.allclose(if1_w, if2_w) + + c1 = ludwig_model1.model.combiner + c2 = ludwig_model2.model.combiner + for c1_w, c2_w in zip(c1.parameters(), c2.parameters()): + assert torch.allclose(c1_w, c2_w) + + for of_name in ludwig_model1.model.output_features: + of1 = ludwig_model1.model.output_features.get(of_name) + of2 = ludwig_model2.model.output_features.get(of_name) + for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()): + assert torch.allclose(of1_w, of2_w) + + ludwig_model1.save(tmpdir) + ludwig_model_loaded = LudwigModel.load(tmpdir, backend=backend) + + # confirm model structure and weights are the same + check_model_equal(ludwig_model_loaded) + + # Test loading the model from the experiment directory + ludwig_model_exp = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME), backend=backend) + + # confirm model structure and weights are the same + check_model_equal(ludwig_model_exp) + + +@pytest.mark.slow +def test_model_save_reload_hf_model(tmpdir, csv_filename, tmp_path): + torch.manual_seed(1) + random.seed(1) + np.random.seed(1) + + input_features = [ + text_feature( + encoder={ + "vocab_size": 3, + "type": "bert", + } + ), + ] + + output_features = [ + category_feature(decoder={"vocab_size": 3}), + ] + + # Generate test data + data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20) + + ############# + # Train model + ############# + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + } + + data_df = read_csv(data_csv_path) + splitter = get_splitter("random") + training_set, validation_set, test_set = splitter.split(data_df, LocalTestBackend()) + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + # perform initial model training + backend = LocalTestBackend() + ludwig_model1 = LudwigModel(config, backend=backend) + _, _, output_dir = ludwig_model1.train( + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + output_directory="results", # results_dir + ) + + preds_1, _ = ludwig_model1.predict(dataset=validation_set) + + def check_model_equal(ludwig_model2): + # Compare model predictions + preds_2, _ = ludwig_model2.predict(dataset=validation_set) + assert set(preds_1.keys()) == set(preds_2.keys()) + for key in preds_1: + assert preds_1[key].dtype == preds_2[key].dtype, key + assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key + # assert preds_2[key].dtype == preds_3[key].dtype, key + # assert list(preds_2[key]) == list(preds_3[key]), key + + # Compare model weights + for if_name in ludwig_model1.model.input_features: + if1 = ludwig_model1.model.input_features.get(if_name) + if2 = ludwig_model2.model.input_features.get(if_name) + for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()): + assert torch.allclose(if1_w, if2_w) + + c1 = ludwig_model1.model.combiner + c2 = ludwig_model2.model.combiner + for c1_w, c2_w in zip(c1.parameters(), c2.parameters()): + assert torch.allclose(c1_w, c2_w) + + for of_name in ludwig_model1.model.output_features: + of1 = ludwig_model1.model.output_features.get(of_name) + of2 = ludwig_model2.model.output_features.get(of_name) + for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()): + assert torch.allclose(of1_w, of2_w) + + ludwig_model1.save(tmpdir) + ludwig_model_loaded = LudwigModel.load(tmpdir, backend=backend) + + # confirm model structure and weights are the same + check_model_equal(ludwig_model_loaded) + + # Test loading the model from the experiment directory + ludwig_model_exp = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME), backend=backend) + + # confirm model structure and weights are the same + check_model_equal(ludwig_model_exp) diff --git a/tests/integration_tests/test_model_training_options.py b/tests/integration_tests/test_model_training_options.py new file mode 100644 index 0000000..d914dd1 --- /dev/null +++ b/tests/integration_tests/test_model_training_options.py @@ -0,0 +1,496 @@ +import json +import logging +import os.path +import re + +import numpy as np +import pandas as pd +import pytest +import torch + +from ludwig import globals as global_vars +from ludwig.api import LudwigModel +from ludwig.backend import LOCAL_BACKEND +from ludwig.constants import ( + BATCH_SIZE, + CATEGORY, + DEFAULTS, + EPOCHS, + INPUT_FEATURES, + OUTPUT_FEATURES, + PREPROCESSING, + TRAINER, + TRAINING, +) +from ludwig.contribs.mlflow import MlflowCallback +from ludwig.experiment import experiment_cli +from ludwig.features.number_feature import numeric_transformation_registry +from ludwig.globals import DESCRIPTION_FILE_NAME, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME, TRAINING_PREPROC_FILE_NAME +from ludwig.utils.data_utils import load_json, replace_file_extension +from ludwig.utils.misc_utils import get_from_registry +from ludwig.utils.package_utils import LazyLoader +from tests.integration_tests import synthetic_test_data +from tests.integration_tests.utils import category_feature, generate_data, LocalTestBackend + +mlflow = LazyLoader("mlflow", globals(), "mlflow") + +RANDOM_SEED = 42 + + +@pytest.mark.parametrize("early_stop", [3, 5]) +def test_early_stopping(early_stop, tmp_path): + input_features, output_features = synthetic_test_data.get_feature_configs() + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"epochs": 20, "early_stop": early_stop, "batch_size": 16, "learning_rate": 0.01}, + } + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + # run experiment + generated_data = synthetic_test_data.get_generated_data() + _, _, _, _, output_dir = experiment_cli( + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + output_directory=str(results_dir), + config=config, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_model=True, + skip_save_log=True, + ) + + # test existence of required files + train_stats_fp = os.path.join(output_dir, "training_statistics.json") + metadata_fp = os.path.join(output_dir, DESCRIPTION_FILE_NAME) + assert os.path.isfile(train_stats_fp) + assert os.path.isfile(metadata_fp) + + # retrieve results so we can validate early stopping + with open(train_stats_fp) as f: + train_stats = json.load(f) + with open(metadata_fp) as f: + metadata = json.load(f) + + # get early stopping value + early_stop_value = metadata["config"][TRAINER]["early_stop"] + + # retrieve validation losses + vald_losses_data = train_stats["validation"]["combined"]["loss"] + + last_evaluation = len(vald_losses_data) - 1 + best_evaluation = np.argmin(vald_losses_data) + + assert last_evaluation - best_evaluation == early_stop_value + + +@pytest.mark.parametrize("skip_save_progress", [False]) +@pytest.mark.parametrize("skip_save_model", [False, True]) +def test_model_progress_save(skip_save_progress, skip_save_model, tmp_path): + input_features, output_features = synthetic_test_data.get_feature_configs() + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + # run experiment + generated_data = synthetic_test_data.get_generated_data() + _, _, _, _, output_dir = experiment_cli( + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + output_directory=str(results_dir), + config=config, + skip_save_processed_input=True, + skip_save_progress=skip_save_progress, + skip_save_unprocessed_output=True, + skip_save_model=skip_save_model, + skip_save_log=True, + ) + + # ========== Check for required result data sets ============= + model_dir = os.path.join(output_dir, MODEL_FILE_NAME) + files = [f for f in os.listdir(model_dir) if re.match(MODEL_WEIGHTS_FILE_NAME, f)] + if skip_save_model: + assert len(files) == 0 + else: + assert len(files) == 1 + + training_checkpoints_dir = os.path.join(output_dir, MODEL_FILE_NAME, "training_checkpoints") + training_checkpoints = os.listdir(training_checkpoints_dir) + if skip_save_progress: + assert len(training_checkpoints) == 0 + else: + assert len(training_checkpoints) > 0 + + +@pytest.mark.parametrize("optimizer", ["sgd", "adam"]) +def test_resume_training(optimizer, tmp_path): + input_features, output_features = synthetic_test_data.get_feature_configs() + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"epochs": 2, "batch_size": 16, "optimizer": {"type": optimizer}}, + } + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + generated_data = synthetic_test_data.get_generated_data() + _, _, _, _, output_dir1 = experiment_cli( + config, + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + ) + + config[TRAINER]["epochs"] = 5 + + experiment_cli( + config, + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + model_resume_path=output_dir1, + ) + + _, _, _, _, output_dir2 = experiment_cli( + config, + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + ) + + # compare learning curves with and without resuming + ts1 = load_json(os.path.join(output_dir1, "training_statistics.json")) + ts2 = load_json(os.path.join(output_dir2, "training_statistics.json")) + print("ts1", ts1) + print("ts2", ts2) + assert ts1[TRAINING]["combined"]["loss"] == ts2[TRAINING]["combined"]["loss"] + + # compare predictions with and without resuming + y_pred1 = np.load(os.path.join(output_dir1, "y_predictions.npy")) + y_pred2 = np.load(os.path.join(output_dir2, "y_predictions.npy")) + print("y_pred1", y_pred1) + print("y_pred2", y_pred2) + assert np.all(np.isclose(y_pred1, y_pred2)) + + +@pytest.mark.parametrize("optimizer", ["sgd", "adam"]) +def test_resume_training_mlflow(optimizer, tmp_path): + input_features, output_features = synthetic_test_data.get_feature_configs() + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"epochs": 2, "batch_size": 16, "eval_batch_size": 2, "optimizer": {"type": optimizer}}, + } + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + mlflow_uri = f"file://{tmp_path}/mlruns" + experiment_name = optimizer + "_experiment" + + generated_data = synthetic_test_data.get_generated_data() + _, _, _, _, output_dir1 = experiment_cli( + config, + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + callbacks=[MlflowCallback(mlflow_uri)], + experiment_name=experiment_name, + ) + # Can't change any artifact spec on a run once it has been logged to mlflow, so skipping changing epochs + + _, _, _, _, output_dir2 = experiment_cli( + config, + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + model_resume_path=output_dir1, + callbacks=[MlflowCallback(mlflow_uri)], + experiment_name=experiment_name, + ) + + # make sure there is only one mlflow run id + experiment = mlflow.get_experiment_by_name(experiment_name) + previous_runs = mlflow.search_runs([experiment.experiment_id]) + assert len(previous_runs) == 1 + + +@pytest.mark.parametrize("optimizer_type", ["sgd", "adam", "adamw", "adagrad", "rmsprop"]) +def test_optimizers(optimizer_type, tmp_path): + if (optimizer_type in {"lars", "lamb", "lion"}) and ( + not torch.cuda.is_available() or torch.cuda.device_count() == 0 + ): + pytest.skip("Skip: lars, lamb, and lion optimizers require GPU and none are available.") + + if ("paged" in optimizer_type or "8bit" in optimizer_type) and ( + not torch.cuda.is_available() or torch.cuda.device_count() == 0 + ): + pytest.skip("Skip: paged and 8-bit optimizers require GPU and none are available.") + + input_features, output_features = synthetic_test_data.get_feature_configs() + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: {"epochs": 2, "batch_size": 16, "evaluate_training_set": True, "optimizer": {"type": optimizer_type}}, + } + + # special handling for adadelta and lbfgs, break out of local minima + if optimizer_type == "adadelta": + config[TRAINER]["learning_rate"] = 0.1 + if optimizer_type == "lbfgs": + config[TRAINER]["learning_rate"] = 0.05 + + model = LudwigModel(config) + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + # run experiment + generated_data = synthetic_test_data.get_generated_data_for_optimizer() + train_stats, preprocessed_data, output_directory = model.train( + training_set=generated_data.train_df, + output_directory=str(results_dir), + config=config, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_model=True, + skip_save_log=True, + ) + + # retrieve training losses for first and last entries. + train_losses = train_stats[TRAINING]["combined"]["loss"] + last_entry = len(train_losses) + + # ensure train loss for last entry is less than to the first entry. + np.testing.assert_array_less(train_losses[last_entry - 1], train_losses[0]) + + +def test_regularization(tmp_path): + input_features, output_features = synthetic_test_data.get_feature_configs() + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat"}, + TRAINER: { + "epochs": 1, + "batch_size": 16, + "regularization_lambda": 1, + }, + } + + # create sub-directory to store results + results_dir = tmp_path / "results" + results_dir.mkdir() + + regularization_losses = [] + generated_data = synthetic_test_data.get_generated_data() + for regularizer in [None, "l1", "l2", "l1_l2"]: + np.random.seed(RANDOM_SEED) + torch.manual_seed(RANDOM_SEED) + + # setup regularization parameters + config[TRAINER]["regularization_type"] = regularizer + + # run experiment + _, _, _, _, output_dir = experiment_cli( + training_set=generated_data.train_df, + validation_set=generated_data.validation_df, + test_set=generated_data.test_df, + output_directory=str(results_dir), + config=config, + experiment_name="regularization", + model_name=str(regularizer), + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_model=True, + skip_save_log=True, + ) + + # test existence of required files + train_stats_fp = os.path.join(output_dir, "training_statistics.json") + metadata_fp = os.path.join(output_dir, DESCRIPTION_FILE_NAME) + assert os.path.isfile(train_stats_fp) + assert os.path.isfile(metadata_fp) + + # retrieve results so we can compare training loss with regularization + with open(train_stats_fp) as f: + train_stats = json.load(f) + + # retrieve training losses for all epochs + train_losses = train_stats[TRAINING]["combined"]["loss"] + regularization_losses.append(train_losses[0]) + + # create a set of losses + regularization_losses_set = set(regularization_losses) + + # ensure all losses obtained with the different methods are different + assert len(regularization_losses) == len(regularization_losses_set) + + +# test cache checksum function +def test_cache_checksum(csv_filename, tmp_path): + # setup for training + input_features = [category_feature(encoder={"vocab_size": 5})] + output_features = [category_feature(decoder={"vocab_size": 2}, top_k=2)] + + source_dataset = os.path.join(tmp_path, csv_filename) + source_dataset = generate_data(input_features, output_features, source_dataset) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + DEFAULTS: {CATEGORY: {PREPROCESSING: {"fill_value": ""}}}, + TRAINER: {EPOCHS: 2, BATCH_SIZE: 128}, + } + + backend = LocalTestBackend() + cache_fname = replace_file_extension(source_dataset, TRAINING_PREPROC_FILE_NAME) + + # conduct initial training + output_directory = os.path.join(tmp_path, "results") + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + first_training_timestamp = os.path.getmtime(cache_fname) + + # conduct second training, should not force recreating hdf5 + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + current_training_timestamp = os.path.getmtime(cache_fname) + + # time stamps should be the same + assert first_training_timestamp == current_training_timestamp + + # force recreating cache file by changing checksum by updating defaults + prior_training_timestamp = current_training_timestamp + config[DEFAULTS][CATEGORY][PREPROCESSING]["fill_value"] = "" + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + current_training_timestamp = os.path.getmtime(cache_fname) + + # timestamp should differ + assert prior_training_timestamp < current_training_timestamp + + # force recreating cache by updating modification time of source dataset + prior_training_timestamp = current_training_timestamp + os.utime(source_dataset) + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + current_training_timestamp = os.path.getmtime(cache_fname) + + # timestamps should be different + assert prior_training_timestamp < current_training_timestamp + + # force change in feature preprocessing + prior_training_timestamp = current_training_timestamp + input_features = config[INPUT_FEATURES].copy() + input_features[0][PREPROCESSING] = {"lowercase": True} + config[INPUT_FEATURES] = input_features + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + current_training_timestamp = os.path.getmtime(cache_fname) + + # timestamps should be different + assert prior_training_timestamp < current_training_timestamp + + # force change in features names (and properties) + prior_training_timestamp = current_training_timestamp + input_features = [category_feature(encoder={"vocab_size": 5}), category_feature()] + source_dataset = generate_data(input_features, output_features, source_dataset) + config[INPUT_FEATURES] = input_features + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + current_training_timestamp = os.path.getmtime(cache_fname) + + # timestamps should be different + assert prior_training_timestamp < current_training_timestamp + + # force change in Ludwig version + prior_training_timestamp = current_training_timestamp + global_vars.LUDWIG_VERSION = "new_version" + model = LudwigModel(config, backend=backend) + model.train(dataset=source_dataset, output_directory=output_directory) + current_training_timestamp = os.path.getmtime(cache_fname) + + # timestamps should be different + assert prior_training_timestamp < current_training_timestamp + + +@pytest.mark.parametrize("transformer_key", list(numeric_transformation_registry.keys())) +def test_numeric_transformer(transformer_key, tmpdir): + Transformer = get_from_registry(transformer_key, numeric_transformation_registry) + transformer_name = Transformer().__class__.__name__ + if transformer_name == "Log1pTransformer": + raw_values = np.random.lognormal(5, 2, size=100) + else: + raw_values = np.random.normal(5, 2, size=100) + + backend = LOCAL_BACKEND + parameters = Transformer.fit_transform_params(raw_values, backend) + if transformer_name in {"Log1pTransformer", "IdentityTransformer"}: + # should be empty + assert not bool(parameters) + else: + # should not be empty + assert bool(parameters) + + # instantiate numeric transformer + numeric_transfomer = Transformer(**parameters) + + # transform values + transformed_values = numeric_transfomer.transform(raw_values) + + # inverse transform the prior transformed values + reconstructed_values = numeric_transfomer.inverse_transform(transformed_values) + + # should now match + assert np.allclose(raw_values, reconstructed_values) + + # now test numeric transformer with output feature + df = pd.DataFrame(np.array([raw_values, raw_values]).T, columns=["x", "y"]) + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number", "preprocessing": {"normalization": transformer_key}}], + "combiner": { + "type": "concat", + }, + TRAINER: { + "epochs": 2, + "batch_size": 16, + }, + } + + args = { + "config": config, + "skip_save_processed_input": True, + "output_directory": os.path.join(tmpdir, "results"), + "logging_level": logging.WARN, + } + + # ensure no exceptions are raised + experiment_cli(dataset=df, **args) diff --git a/tests/integration_tests/test_number_feature.py b/tests/integration_tests/test_number_feature.py new file mode 100644 index 0000000..92719c3 --- /dev/null +++ b/tests/integration_tests/test_number_feature.py @@ -0,0 +1,31 @@ +import pandas as pd + +from ludwig.api import LudwigModel +from tests.integration_tests.utils import generate_data, number_feature + + +def test_number_feature_zscore_normalization_constant(): + """ZScoreTransformer with std=0 should warn and fall back to identity (sigma=1) rather than crash.""" + import warnings + + input_features = [number_feature(name="num_input", preprocessing={"normalization": "zscore"})] + output_features = [number_feature(name="num_output")] + + df = pd.read_csv(generate_data(input_features, output_features)) + + # Override input number feature to have a constant value + df["num_input"] = 1 + + config = { + "input_features": input_features, + "output_features": output_features, + } + + model = LudwigModel(config, backend="local") + + # Should not raise — constant features are gracefully handled with a warning + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + preprocessed = model.preprocess(dataset=df) + + assert preprocessed is not None diff --git a/tests/integration_tests/test_peft.py b/tests/integration_tests/test_peft.py new file mode 100644 index 0000000..034e629 --- /dev/null +++ b/tests/integration_tests/test_peft.py @@ -0,0 +1,44 @@ +import os + +import pytest + +from ludwig.constants import COMBINER, EPOCHS, INPUT_FEATURES, OUTPUT_FEATURES, TRAINER, TYPE +from tests.integration_tests.utils import binary_feature, generate_data, run_test_suite, text_feature + + +@pytest.mark.integration_tests_i +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=[pytest.mark.distributed, pytest.mark.distributed_f]), + ], +) +def test_text_adapter_lora(tmpdir, backend, ray_cluster_2cpu): + input_features = [ + text_feature( + encoder={ + "type": "auto_transformer", + "pretrained_model_name_or_path": "hf-internal-testing/tiny-bert-for-token-classification", + "trainable": True, + "adapter": {"type": "lora"}, + }, + ), + ] + output_features = [binary_feature()] + + data_csv_path = os.path.join(tmpdir, "dataset.csv") + dataset = generate_data(input_features, output_features, data_csv_path) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + COMBINER: {TYPE: "concat", "output_size": 14}, + TRAINER: {EPOCHS: 1}, + } + model = run_test_suite(config, dataset, backend) + + state_dict = model.model.state_dict() + + # check that at least one of the keys contains the word "lora_" denoting a lora parameter + assert any("lora_" in key for key in state_dict.keys()) diff --git a/tests/integration_tests/test_postprocessing.py b/tests/integration_tests/test_postprocessing.py new file mode 100644 index 0000000..b464b6d --- /dev/null +++ b/tests/integration_tests/test_postprocessing.py @@ -0,0 +1,255 @@ +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import os +from functools import partial +from unittest import mock + +import numpy as np +import pandas as pd +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, DECODER, NAME, TRAINER +from ludwig.globals import MODEL_FILE_NAME +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + generate_data, + RAY_BACKEND_CONFIG, + set_feature, + text_feature, +) + + +def random_binary_logits(*args, num_predict_samples, **kwargs): + # Produce an even mix of True and False predictions, as the model may be biased + # towards one direction without training + return torch.tensor(np.random.uniform(low=-1.0, high=1.0, size=(num_predict_samples,)), dtype=torch.float32) + + +def random_set_logits(*args, num_predict_samples, vocab_size, pct_positive, **kwargs): + # Produce a desired mix of predictions based on the pct_positive, as the model may be biased + # towards one direction without training + num_positive = int(num_predict_samples * pct_positive) + num_negative = num_predict_samples - num_positive + negative_logits = np.random.uniform(low=-1.0, high=-0.1, size=(num_negative, vocab_size)) + positive_logits = np.random.uniform(low=0.1, high=1.0, size=(num_positive, vocab_size)) + logits = np.concatenate([negative_logits, positive_logits], axis=0) + return torch.tensor(logits, dtype=torch.float32) # simulate torch model output + + +def _run_binary_predictions(tmpdir, backend, distinct_values, ray_cluster_2cpu): + input_features = [ + category_feature(encoder={"vocab_size": 3}), + ] + + feature = binary_feature() + output_features = [ + feature, + ] + + data_csv_path = generate_data( + input_features, + output_features, + os.path.join(tmpdir, "dataset.csv"), + num_examples=20, + ) + data_df = pd.read_csv(data_csv_path) + + # Optionally convert bool values to strings, e.g., {'Yes', 'No'} + false_value, true_value = distinct_values + data_df[feature[NAME]] = data_df[feature[NAME]].map(lambda x: true_value if x else false_value) + data_df.to_csv(data_csv_path, index=False) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 1, BATCH_SIZE: 128}, + } + + patch_args = ( + "ludwig.features.binary_feature.BinaryOutputFeature.logits", + partial(random_binary_logits, num_predict_samples=len(data_df)), + ) + + preds_df, _ = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args) + cols = set(preds_df.columns) + assert f"{feature[NAME]}_predictions" in cols + assert f"{feature[NAME]}_probabilities_{false_value!s}" in cols + assert f"{feature[NAME]}_probabilities_{true_value!s}" in cols + assert f"{feature[NAME]}_probability" in cols + + for pred, prob_0, prob_1, prob in zip( + preds_df[f"{feature[NAME]}_predictions"], + preds_df[f"{feature[NAME]}_probabilities_{false_value!s}"], + preds_df[f"{feature[NAME]}_probabilities_{true_value!s}"], + preds_df[f"{feature[NAME]}_probability"], + ): + assert pred == false_value or pred == true_value + if pred == true_value: + assert prob_1 == prob + else: + assert prob_0 == prob + assert np.allclose(prob_0, 1 - prob_1) + + +@pytest.mark.parametrize("distinct_values", [(False, True), ("No", "Yes")]) +def test_binary_predictions(tmpdir, distinct_values, ray_cluster_2cpu): + _run_binary_predictions(tmpdir, "local", distinct_values, ray_cluster_2cpu) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.parametrize("distinct_values", [(False, True), ("No", "Yes")]) +def test_binary_predictions_ray(tmpdir, distinct_values, ray_cluster_2cpu): + _run_binary_predictions(tmpdir, "ray", distinct_values, ray_cluster_2cpu) + + +def _run_binary_predictions_with_number_dtype(tmpdir, backend, distinct_values, ray_cluster_2cpu): + input_features = [ + category_feature(encoder={"vocab_size": 3}), + ] + + feature = binary_feature() + output_features = [ + feature, + ] + + data_csv_path = generate_data( + input_features, + output_features, + os.path.join(tmpdir, "dataset.csv"), + num_examples=20, + ) + data_df = pd.read_csv(data_csv_path) + + # Optionally convert bool values to strings, e.g., {'Yes', 'No'} + false_value, true_value = distinct_values + data_df[feature[NAME]] = data_df[feature[NAME]].map(lambda x: true_value if x else false_value) + data_df.to_csv(data_csv_path, index=False) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 1, BATCH_SIZE: 128}, + } + + patch_args = ( + "ludwig.features.binary_feature.BinaryOutputFeature.logits", + partial(random_binary_logits, num_predict_samples=len(data_df)), + ) + + preds_df, _ = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args) + cols = set(preds_df.columns) + assert f"{feature[NAME]}_predictions" in cols + assert f"{feature[NAME]}_probabilities_False" in cols + assert f"{feature[NAME]}_probabilities_True" in cols + assert f"{feature[NAME]}_probability" in cols + + for pred, prob_0, prob_1, prob in zip( + preds_df[f"{feature[NAME]}_predictions"], + preds_df[f"{feature[NAME]}_probabilities_False"], + preds_df[f"{feature[NAME]}_probabilities_True"], + preds_df[f"{feature[NAME]}_probability"], + ): + assert isinstance(pred, bool) + if pred: + assert prob_1 == prob + else: + assert prob_0 == prob + assert np.allclose(prob_0, 1 - prob_1) + + +@pytest.mark.parametrize("distinct_values", [(0.0, 1.0), (0, 1)]) +def test_binary_predictions_with_number_dtype(tmpdir, distinct_values, ray_cluster_2cpu): + _run_binary_predictions_with_number_dtype(tmpdir, "local", distinct_values, ray_cluster_2cpu) + + +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.parametrize("distinct_values", [(0.0, 1.0), (0, 1)]) +def test_binary_predictions_with_number_dtype_ray(tmpdir, distinct_values, ray_cluster_2cpu): + _run_binary_predictions_with_number_dtype(tmpdir, "ray", distinct_values, ray_cluster_2cpu) + + +@pytest.mark.parametrize("pct_positive", [1.0, 0.5, 0.0]) +def test_set_feature_saving(tmpdir, pct_positive): + backend = "local" + input_features = [ + text_feature(encoder={"vocab_size": 3}), + ] + + feature = set_feature(output_feature=True) + output_features = [ + feature, + ] + + data_csv_path = generate_data( + input_features, + output_features, + os.path.join(tmpdir, "dataset.csv"), + num_examples=20, + ) + data_df = pd.read_csv(data_csv_path) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 1, BATCH_SIZE: 128}, + } + + patch_args = ( + "ludwig.features.set_feature.SetOutputFeature.logits", + partial( + random_set_logits, + num_predict_samples=len(data_df), + vocab_size=feature[DECODER]["vocab_size"] + 1, # +1 for UNK + pct_positive=pct_positive, + ), + ) + + preds_df, ludwig_model = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args) + cols = set(preds_df.columns) + assert f"{feature[NAME]}_predictions" in cols + assert f"{feature[NAME]}_probabilities" in cols + + backend = ludwig_model.backend + backend.df_engine.to_parquet(preds_df, os.path.join(tmpdir, "preds.parquet")) # test saving + + +def predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=None): + if backend == "ray": + backend = RAY_BACKEND_CONFIG + backend["processor"]["type"] = "dask" + + ludwig_model = LudwigModel(config, backend=backend) + _, _, output_directory = ludwig_model.train( + dataset=data_csv_path, + output_directory=os.path.join(tmpdir, "output"), + ) + # Check that metadata JSON saves and loads correctly + ludwig_model = LudwigModel.load(os.path.join(output_directory, MODEL_FILE_NAME)) + + if patch_args is not None: + with mock.patch(*patch_args): + preds_df, _ = ludwig_model.predict(dataset=data_csv_path) + else: + preds_df, _ = ludwig_model.predict(dataset=data_csv_path) + + return preds_df, ludwig_model diff --git a/tests/integration_tests/test_preprocessing.py b/tests/integration_tests/test_preprocessing.py new file mode 100644 index 0000000..d107644 --- /dev/null +++ b/tests/integration_tests/test_preprocessing.py @@ -0,0 +1,1194 @@ +import contextlib +import copy +import importlib.util +import logging +import os +import random +import string +from unittest import mock + +import numpy as np +import pandas as pd +import pytest +from PIL import Image +from transformers import AutoTokenizer + +import ludwig +from ludwig.api import LudwigModel +from ludwig.backend import initialize_backend +from ludwig.callbacks import Callback +from ludwig.constants import ( + BASE_MODEL, + BATCH_SIZE, + COLUMN, + DECODER, + EPOCHS, + FULL, + INPUT_FEATURES, + MODEL_ECD, + MODEL_LLM, + MODEL_TYPE, + NAME, + OUTPUT_FEATURES, + PREPROCESSING, + PROC_COLUMN, + PROMPT, + SPLIT, + TRAINER, + TYPE, +) +from ludwig.data.concatenate_datasets import concatenate_df +from ludwig.data.preprocessing import handle_features_with_prompt_config, preprocess_for_prediction +from ludwig.schema.llms.prompt import PromptConfig +from ludwig.schema.model_types.base import ModelConfig +from tests.integration_tests.utils import ( + assert_preprocessed_dataset_shape_and_dtype_for_feature, + audio_feature, + binary_feature, + category_feature, + generate_data, + generate_data_as_dataframe, + image_feature, + LocalTestBackend, + number_feature, + sequence_feature, + text_feature, +) + +NUM_EXAMPLES = 20 + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_sample_ratio(backend, tmpdir, ray_cluster_2cpu): + num_examples = 50 + sample_ratio = 0.5 + + input_features = [sequence_feature(encoder={"reduce_output": "sum"}), audio_feature(folder=tmpdir)] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples + ) + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + EPOCHS: 2, + }, + PREPROCESSING: {"sample_ratio": sample_ratio}, + } + + model = LudwigModel(config, backend=backend) + train_set, val_set, test_set, training_set_metadata = model.preprocess( + data_csv, + skip_save_processed_input=True, + ) + + sample_size = num_examples * sample_ratio + count = len(train_set) + len(val_set) + len(test_set) + assert sample_size == count + + # Check that sample ratio is disabled when doing preprocessing for prediction + dataset, _ = preprocess_for_prediction( + model.config_obj.to_dict(), + dataset=data_csv, + training_set_metadata=training_set_metadata, + split=FULL, + include_outputs=True, + backend=model.backend, + ) + assert "sample_ratio" in model.config_obj.preprocessing.to_dict() + assert len(dataset) == num_examples + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_sample_ratio_deterministic(backend, tmpdir, ray_cluster_2cpu): + """Ensures that the sampled dataset is the same when using a random seed. + + model.preprocess returns a PandasPandasDataset object when using local backend, and returns a RayDataset object when + using the Ray backend. + """ + num_examples = 50 + sample_ratio = 0.5 + + input_features = [binary_feature()] + output_features = [category_feature()] + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples + ) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + PREPROCESSING: {"sample_ratio": sample_ratio}, + } + + model1 = LudwigModel(config, backend=backend) + train_set_1, val_set_1, test_set_1, _ = model1.preprocess( + data_csv, + skip_save_processed_input=True, + ) + + model2 = LudwigModel(config, backend=backend) + train_set_2, val_set_2, test_set_2, _ = model2.preprocess( + data_csv, + skip_save_processed_input=True, + ) + + sample_size = num_examples * sample_ratio + + # Ensure sizes are the same + assert sample_size == len(train_set_1) + len(val_set_1) + len(test_set_1) + assert sample_size == len(train_set_2) + len(val_set_2) + len(test_set_2) + + # Ensure actual rows are the same + if backend == "local": + assert train_set_1.to_df().equals(train_set_2.to_df()) + assert val_set_1.to_df().equals(val_set_2.to_df()) + assert test_set_1.to_df().equals(test_set_2.to_df()) + else: + assert train_set_1.to_df().compute().equals(train_set_2.to_df().compute()) + assert val_set_1.to_df().compute().equals(val_set_2.to_df().compute()) + assert test_set_1.to_df().compute().equals(test_set_2.to_df().compute()) + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_sample_size(backend, tmpdir, ray_cluster_2cpu): + num_examples = 50 + sample_size = 25 + + input_features = [sequence_feature(encoder={"reduce_output": "sum"}), audio_feature(folder=tmpdir)] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples + ) + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + EPOCHS: 2, + }, + PREPROCESSING: {"sample_size": sample_size}, + } + + model = LudwigModel(config, backend=backend) + train_set, val_set, test_set, training_set_metadata = model.preprocess( + data_csv, + skip_save_processed_input=True, + ) + + count = len(train_set) + len(val_set) + len(test_set) + assert sample_size == count + + # Check that sample size is disabled when doing preprocessing for prediction + dataset, _ = preprocess_for_prediction( + model.config_obj.to_dict(), + dataset=data_csv, + training_set_metadata=training_set_metadata, + split=FULL, + include_outputs=True, + backend=model.backend, + ) + assert "sample_size" in model.config_obj.preprocessing.to_dict() + assert len(dataset) == num_examples + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_sample_size_deterministic(backend, tmpdir, ray_cluster_2cpu): + """Ensures that the sampled dataset is the same when using a random seed. + + model.preprocess returns a PandasPandasDataset object when using local backend, and returns a RayDataset object when + using the Ray backend. + """ + num_examples = 50 + sample_size = 25 + + input_features = [binary_feature()] + output_features = [category_feature()] + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=num_examples + ) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + PREPROCESSING: {"sample_size": sample_size}, + } + + model1 = LudwigModel(config, backend=backend) + train_set_1, val_set_1, test_set_1, _ = model1.preprocess( + data_csv, + skip_save_processed_input=True, + ) + + model2 = LudwigModel(config, backend=backend) + train_set_2, val_set_2, test_set_2, _ = model2.preprocess( + data_csv, + skip_save_processed_input=True, + ) + + # Ensure sizes are the same + assert sample_size == len(train_set_1) + len(val_set_1) + len(test_set_1) + assert sample_size == len(train_set_2) + len(val_set_2) + len(test_set_2) + + # Ensure actual rows are the same + if backend == "local": + assert train_set_1.to_df().equals(train_set_2.to_df()) + assert val_set_1.to_df().equals(val_set_2.to_df()) + assert test_set_1.to_df().equals(test_set_2.to_df()) + else: + assert train_set_1.to_df().compute().equals(train_set_2.to_df().compute()) + assert val_set_1.to_df().compute().equals(val_set_2.to_df().compute()) + assert test_set_1.to_df().compute().equals(test_set_2.to_df().compute()) + + +def test_strip_whitespace_category(csv_filename, tmpdir): + data_csv_path = os.path.join(tmpdir, csv_filename) + + input_features = [binary_feature()] + cat_feat = category_feature(decoder={"vocab_size": 3}) + output_features = [cat_feat] + backend = LocalTestBackend() + config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features} + + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + # prefix with whitespace + df[cat_feat[COLUMN]] = df[cat_feat[COLUMN]].apply(lambda s: " " + s) + + # run preprocessing + ludwig_model = LudwigModel(config, backend=backend) + train_ds, _, _, metadata = ludwig_model.preprocess(dataset=df) + + # expect values containing whitespaces to be properly mapped to vocab_size unique values + assert len(np.unique(train_ds.dataset[cat_feat[PROC_COLUMN]])) == cat_feat[DECODER]["vocab_size"] + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_with_split(backend, csv_filename, tmpdir, ray_cluster_2cpu): + num_examples = NUM_EXAMPLES + train_set_size = int(num_examples * 0.8) + val_set_size = int(num_examples * 0.1) + test_set_size = int(num_examples * 0.1) + + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples + ) + data_df = pd.read_csv(data_csv) + data_df[SPLIT] = [0] * train_set_size + [1] * val_set_size + [2] * test_set_size + data_df.to_csv(data_csv, index=False) + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + EPOCHS: 2, + }, + PREPROCESSING: {SPLIT: {TYPE: "fixed", COLUMN: SPLIT}}, + } + + model = LudwigModel(config, backend=backend) + train_set, val_set, test_set, _ = model.preprocess( + data_csv, + skip_save_processed_input=False, + ) + assert len(train_set) == train_set_size + assert len(val_set) == val_set_size + assert len(test_set) == test_set_size + + +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.parametrize("feature_fn", [image_feature, audio_feature]) +def test_dask_known_divisions(feature_fn, csv_filename, tmpdir, ray_cluster_2cpu): + import dask.dataframe as dd + + input_features = [feature_fn(os.path.join(tmpdir, "generated_output"))] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=20) + data_df = dd.from_pandas(pd.read_csv(data_csv), npartitions=2) + assert data_df.known_divisions + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + EPOCHS: 2, + }, + } + + backend = "ray" + model = LudwigModel(config, backend=backend) + train_set, val_set, test_set, _ = model.preprocess( + data_df, + skip_save_processed_input=False, + ) + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_drop_empty_partitions(csv_filename, tmpdir, ray_cluster_2cpu): + import dask.dataframe as dd + + input_features = [image_feature(os.path.join(tmpdir, "generated_output"))] + output_features = [category_feature(vocab_size=5, reduce_input="sum", output_feature=True)] + + # num_examples and npartitions set such that each post-split DataFrame has >1 samples, but empty partitions. + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=25) + data_df = dd.from_pandas(pd.read_csv(data_csv), npartitions=10) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + EPOCHS: 2, + }, + } + + backend = "ray" + model = LudwigModel(config, backend=backend) + train_set, val_set, test_set, _ = model.preprocess( + data_df, + skip_save_processed_input=True, + ) + for dataset in [train_set, val_set, test_set]: + df = dataset.ds.to_dask() + for partition in df.partitions: + assert len(partition) > 0, "empty partitions found in dataset" + + +@pytest.mark.parametrize("generate_images_as_numpy", [False, True]) +def test_read_image_from_path(tmpdir, csv_filename, generate_images_as_numpy): + input_features = [image_feature(os.path.join(tmpdir, "generated_output"), save_as_numpy=generate_images_as_numpy)] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES + ) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {EPOCHS: 2}, + } + + model = LudwigModel(config) + model.preprocess( + data_csv, + skip_save_processed_input=False, + ) + + +def test_read_image_from_numpy_array(tmpdir, csv_filename): + input_features = [image_feature(os.path.join(tmpdir, "generated_output"))] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {EPOCHS: 2, BATCH_SIZE: 128}, + } + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES + ) + + df = pd.read_csv(data_csv) + processed_df_rows = [] + + for _, row in df.iterrows(): + processed_df_rows.append( + { + input_features[0][NAME]: np.array(Image.open(row[input_features[0][NAME]])), + output_features[0][NAME]: row[output_features[0][NAME]], + } + ) + + df_with_images_as_numpy_arrays = pd.DataFrame(processed_df_rows) + + model = LudwigModel(config) + model.preprocess( + df_with_images_as_numpy_arrays, + skip_save_processed_input=False, + ) + + +def test_read_image_failure_default_image(monkeypatch, tmpdir, csv_filename): + """Tests that the default image used when an image cannot be read has the correct properties.""" + + def mock_read_binary_files(self, column, map_fn, file_size): + """Mock read_binary_files to return None (failed image read) to test error handling.""" + return column.map(lambda x: None) + + monkeypatch.setattr(ludwig.backend.base.LocalDataProcessingMixin, "read_binary_files", mock_read_binary_files) + + # mode="eager" forces the eager path so that the monkeypatched read_binary_files is exercised. + # With mode="lazy" (the default), read_binary_files is never called and this test has no meaning. + image_feature_config = image_feature(os.path.join(tmpdir, "generated_output"), preprocessing={"mode": "eager"}) + input_features = [image_feature_config] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {EPOCHS: 2, BATCH_SIZE: 128}, + } + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES, nan_percent=0.2 + ) + + model = LudwigModel(config) + preprocessed_dataset = model.preprocess(data_csv) + training_set_metadata = preprocessed_dataset.training_set_metadata + + preprocessing = training_set_metadata[input_features[0][NAME]][PREPROCESSING] + expected_shape = (preprocessing["num_channels"], preprocessing["height"], preprocessing["width"]) + expected_dtype = np.float32 + + assert_preprocessed_dataset_shape_and_dtype_for_feature( + image_feature_config[NAME], preprocessed_dataset, model.config_obj, expected_dtype, expected_shape + ) + + +def test_number_feature_wrong_dtype(csv_filename, tmpdir): + """Tests that a number feature with all string values is treated as having missing values by default.""" + data_csv_path = os.path.join(tmpdir, csv_filename) + + num_feat = number_feature() + input_features = [num_feat] + output_features = [binary_feature()] + config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features} + + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + # convert numbers to random strings + def random_string(): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for _ in range(10)) + + df[num_feat[COLUMN]] = df[num_feat[COLUMN]].apply(lambda _: random_string()) + + # run preprocessing + backend = LocalTestBackend() + ludwig_model = LudwigModel(config, backend=backend) + train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df) + + concatenated_df = concatenate_df(train_ds.to_df(), val_ds.to_df(), test_ds.to_df(), backend) + + # check that train_ds had invalid values replaced with the missing value + assert len(concatenated_df) == len(df) + assert np.all(concatenated_df[num_feat[PROC_COLUMN]] == 0.0) + + +@pytest.mark.parametrize( + "max_len, sequence_length, max_sequence_length, sequence_length_expected", + [ + # Case 1: infer from the dataset, max_sequence_length is larger than the largest sequence length. + # Expected: max_sequence_length is ignored, and the sequence length is dataset+2 (include start/stop tokens). + (10, None, 15, 12), + # Case 2: infer from the dataset, max_sequence_length is smaller than the largest sequence length. + # Expected: max_sequence_length is used, and the sequence length is max_sequence_length. + (10, None, 8, 8), + # Case 3: infer from the dataset, max_sequence_length is not set. + # Expected: max_sequence_length is ignored, and the sequence length is dataset+2 (include start/stop tokens). + (10, None, None, 12), + # Case 4: set sequence_length explicitly and it is larger than the dataset. + # Expected: sequence_length is used, and the sequence length is sequence_length. + (10, 15, 20, 15), + # Case 5: set sequence_length explicitly and it is smaller than the dataset. + # Expected: sequence_length is used, and the sequence length is sequence_length. + (10, 8, 20, 8), + ], +) +@pytest.mark.parametrize( + "feature_type", + [ + sequence_feature, + ], +) +def test_seq_features_max_sequence_length( + csv_filename, tmpdir, feature_type, max_len, sequence_length, max_sequence_length, sequence_length_expected +): + """Tests that a sequence feature has the correct max_sequence_length in metadata and prepocessed data.""" + feat = feature_type( + encoder={"max_len": max_len}, + preprocessing={"sequence_length": sequence_length, "max_sequence_length": max_sequence_length}, + ) + input_features = [feat] + output_features = [binary_feature()] + config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features} + + data_csv_path = os.path.join(tmpdir, csv_filename) + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + class CheckTrainingSetMetadataCallback(Callback): + def on_preprocess_end(self, proc_training_set, proc_validation_set, proc_test_set, training_set_metadata): + assert training_set_metadata[feat[NAME]]["max_sequence_length"] == sequence_length_expected + + backend = LocalTestBackend() + ludwig_model = LudwigModel(config, backend=backend, callbacks=[CheckTrainingSetMetadataCallback()]) + train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df) + + all_df = concatenate_df(train_ds.to_df(), val_ds.to_df(), test_ds.to_df(), backend) + proc_column_name = feat[PROC_COLUMN] + assert all(len(x) == sequence_length_expected for x in all_df[proc_column_name]) + + +def test_column_feature_type_mismatch_fill(): + """Tests that we are able to fill missing values even in columns where the column dtype and desired feature + dtype do not match.""" + cat_feat = category_feature() + bin_feat = binary_feature() + input_features = [cat_feat] + output_features = [bin_feat] + config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features} + + # Construct dataframe with int-like column representing a categorical feature + df = pd.DataFrame( + { + cat_feat[NAME]: pd.Series(pd.array([None] + [1] * 24, dtype=pd.Int64Dtype())), + bin_feat[NAME]: pd.Series([True] * 25), + } + ) + + # run preprocessing + backend = LocalTestBackend() + ludwig_model = LudwigModel(config, backend=backend) + train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df) + + +@pytest.mark.parametrize("format", ["file", "df"]) +def test_presplit_override(format, tmpdir): + """Tests that provising a pre-split file or dataframe overrides the user's split config.""" + num_feat = number_feature(normalization=None) + input_features = [num_feat, sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=25) + data_df = pd.read_csv(data_csv) + + # Set the feature value equal to an ordinal index so we can ensure the splits are identical before and after + # preprocessing. + data_df[num_feat[COLUMN]] = data_df.index + + train_df = data_df[:15] + val_df = data_df[15:20] + test_df = data_df[20:] + + train_data = train_df + val_data = val_df + test_data = test_df + + if format == "file": + train_data = os.path.join(tmpdir, "train.csv") + val_data = os.path.join(tmpdir, "val.csv") + test_data = os.path.join(tmpdir, "test.csv") + + train_df.to_csv(train_data) + val_df.to_csv(val_data) + test_df.to_csv(test_data) + + data_df.to_csv(data_csv, index=False) + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: { + EPOCHS: 2, + }, + PREPROCESSING: {SPLIT: {TYPE: "random"}}, + } + + model = LudwigModel(config, backend=LocalTestBackend()) + train_set, val_set, test_set, _ = model.preprocess( + training_set=train_data, validation_set=val_data, test_set=test_data + ) + + assert len(train_set) == len(train_df) + assert len(val_set) == len(val_df) + assert len(test_set) == len(test_df) + + assert np.all(train_set.to_df()[num_feat[PROC_COLUMN]].values == train_df[num_feat[COLUMN]].values) + assert np.all(val_set.to_df()[num_feat[PROC_COLUMN]].values == val_df[num_feat[COLUMN]].values) + assert np.all(test_set.to_df()[num_feat[PROC_COLUMN]].values == test_df[num_feat[COLUMN]].values) + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_empty_training_set_error(backend, tmpdir, ray_cluster_2cpu): + """Tests that an error is raised if one or more of the splits is empty after preprocessing.""" + data_csv_path = os.path.join(tmpdir, "data.csv") + + out_feat = binary_feature() + input_features = [number_feature()] + output_features = [out_feat] + config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features} + + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + # Convert all the output features rows to null. Because the default missing value strategy is to drop empty output + # rows, this will result in the dataset being empty after preprocessing. + df[out_feat[COLUMN]] = None + + ludwig_model = LudwigModel(config, backend=backend) + with pytest.raises(ValueError, match="Training data is empty following preprocessing"): + ludwig_model.preprocess(dataset=df) + + +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=pytest.mark.distributed), + ], +) +def test_in_memory_dataset_size(backend, tmpdir, ray_cluster_2cpu): + data_csv_path = os.path.join(tmpdir, "data.csv") + + out_feat = binary_feature() + input_features = [number_feature()] + output_features = [out_feat] + config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features} + + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + ludwig_model = LudwigModel(config, backend=backend) + training_dataset, validation_dataset, test_dataset, _ = ludwig_model.preprocess(dataset=df) + + assert training_dataset.in_memory_size_bytes > 0 + assert validation_dataset.in_memory_size_bytes > 0 + assert test_dataset.in_memory_size_bytes > 0 + + +@pytest.mark.parametrize( + "binary_as_input, expected_preprocessing, missing_value_strategy", + [ + pytest.param( + True, + { + "missing_value_strategy": "fill_with_true", + "fill_value": None, + "computed_fill_value": ">50K", + "fallback_true_label": ">50K", + }, + "fill_with_true", + id="binary_as_input_1", + ), + pytest.param( + True, + { + "missing_value_strategy": "fill_with_false", + "fill_value": None, + "computed_fill_value": "<=50K", + "fallback_true_label": ">50K", + }, + "fill_with_false", + id="binary_as_input_2", + ), + pytest.param( + False, + { + "missing_value_strategy": "drop_row", + "fill_value": None, + "computed_fill_value": None, + "fallback_true_label": ">50K", + }, + "drop_row", + id="binary_as_output", + ), + ], +) +def test_non_conventional_bool_with_fallback(binary_as_input, expected_preprocessing, missing_value_strategy, tmpdir): + # Specify a non-conventional boolean feature with a fallback true label. + bin_feature = binary_feature( + bool2str=["<=50K", ">50K"], + preprocessing={"fallback_true_label": ">50K", "missing_value_strategy": missing_value_strategy}, + ) + + # Generate data with the non-conventional boolean feature. + if binary_as_input: + input_features = [bin_feature] + output_features = [number_feature()] + else: + input_features = [number_feature()] + output_features = [bin_feature] + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {EPOCHS: 2, BATCH_SIZE: 128}, + } + + data_csv_path = os.path.join(tmpdir, "data.csv") + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + # Preprocess the data. + ludwig_model = LudwigModel(config) + _, _, _, training_set_metadata = ludwig_model.preprocess(dataset=df) + + # Check that true/false labels are set correctly. + assert training_set_metadata[bin_feature[NAME]] == { + "str2bool": {"<=50K": False, ">50K": True}, + "bool2str": ["<=50K", ">50K"], + "fallback_true_label": ">50K", + PREPROCESSING: expected_preprocessing, + } + + +@pytest.mark.parametrize( + "binary_as_input", [pytest.param(True, id="binary_as_input"), pytest.param(False, id="binary_as_output")] +) +def test_non_conventional_bool_without_fallback_logs_warning(binary_as_input, caplog, tmpdir): + # Specify a non-conventional boolean feature without a fallback true label. + bin_feature = binary_feature(bool2str=["<=50K", ">50K"], preprocessing={"fallback_true_label": None}) + + # Generate data with the non-conventional boolean feature. + if binary_as_input: + input_features = [bin_feature] + output_features = [number_feature()] + else: + input_features = [number_feature()] + output_features = [bin_feature] + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {EPOCHS: 2, BATCH_SIZE: 128}, + } + + data_csv_path = os.path.join(tmpdir, "data.csv") + training_data_csv_path = generate_data(input_features, output_features, data_csv_path) + df = pd.read_csv(training_data_csv_path) + + # Preprocess the data. + with caplog.at_level(logging.WARN, logger="ludwig.features.binary_feature"): + ludwig_model = LudwigModel(config) + ludwig_model.preprocess(dataset=df) + + # Check that a warning is logged. + assert "unconventional boolean value" in caplog.text + + +@pytest.mark.parametrize("feature_type", ["input_feature", "output_feature"], ids=["input_feature", "output_feature"]) +def test_category_feature_vocab_size_1(feature_type, tmpdir) -> None: + data_csv_path = os.path.join(tmpdir, "data.csv") + + input_feature = [category_feature(encoder={"vocab_size": 1})] + output_feature = [binary_feature()] + + if feature_type == "output_feature": + input_feature = output_feature + output_feature = [category_feature(decoder={"vocab_size": 1})] + + config = {INPUT_FEATURES: input_feature, OUTPUT_FEATURES: output_feature, "training": {EPOCHS: 1}} + + training_data_csv_path = generate_data(config[INPUT_FEATURES], config[OUTPUT_FEATURES], data_csv_path) + + ludwig_model = LudwigModel(config) + with pytest.raises(Exception) if feature_type == "output_feature" else contextlib.nullcontext(): + ludwig_model.train(dataset=training_data_csv_path) + + +@pytest.mark.parametrize("use_pretrained", [False, True], ids=["false", "true"]) +def test_vit_encoder_different_dimension_image(tmpdir, csv_filename, use_pretrained: bool): + input_features = [ + image_feature( + os.path.join(tmpdir, "generated_output"), + preprocessing={"in_memory": True, "height": 224, "width": 206, "num_channels": 3}, + encoder={TYPE: "vit", "model_variant": "b_16", "use_pretrained": use_pretrained}, + ) + ] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES + ) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {"train_steps": 1}, + } + + model = LudwigModel(config) + + # Failure happens post preprocessing but before training during the ECD model creation phase + # so make sure the model can be created properly and training can proceed + model.train(dataset=data_csv) + + +@pytest.mark.skip( + reason=( + "Broken against torch nightly: " + "https://github.com/ludwig-ai/ludwig/actions/runs/4918126111/jobs/8784071603?pr=3388." + ) +) +def test_image_encoder_torchvision_different_num_channels(tmpdir, csv_filename): + input_features = [ + image_feature( + os.path.join(tmpdir, "generated_output"), + preprocessing={"in_memory": True, "height": 224, "width": 206, "num_channels": 1}, + encoder={TYPE: "efficientnet"}, + ) + ] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + data_csv = generate_data( + input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES + ) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + TRAINER: {"train_steps": 1}, + } + + model = LudwigModel(config) + + # Failure happens post preprocessing but before training during the ECD model creation phase + # so make sure the model can be created properly and training can proceed + model.train(dataset=data_csv) + + +@pytest.mark.parametrize( + "df_engine", + [ + pytest.param("pandas", id="pandas"), + pytest.param("dask", id="dask", marks=pytest.mark.distributed), + ], +) +def test_fill_with_mode_different_df_engine(tmpdir, csv_filename, df_engine, ray_cluster_2cpu): + config = { + INPUT_FEATURES: [category_feature(preprocessing={"missing_value_strategy": "fill_with_mode"})], + OUTPUT_FEATURES: [binary_feature()], + } + + training_data_csv_path = generate_data( + config[INPUT_FEATURES], config[OUTPUT_FEATURES], os.path.join(tmpdir, csv_filename) + ) + + df = pd.read_csv(training_data_csv_path) + + if df_engine == "dask": + import dask.dataframe as dd + + df = dd.from_pandas(df, npartitions=1) + + # Only support Dask on Ray backend + config["backend"] = {TYPE: "ray"} + + ludwig_model = LudwigModel(config) + ludwig_model.preprocess(dataset=df) + + +template_task_sample = """ +Instruction: {__task__} +### +Examples: +### +Input: foo bar +Output: true +### +Input: baz quc +Output: false +### +Input: {__sample__} +Output: +""" + +task = "predict the output feature. Return only values in {true, false}" + +template_multi_col = """ +You are a helpful chatbot. USER: {__sample__}: {country}, {year:.2f} ASSISTANT: +""" + +expected_task_sample = """Instruction: predict the output feature. Return only values in {true, false} +### +Examples: +### +Input: foo bar +Output: true +### +Input: baz quc +Output: false +### +Input:""" + + +@pytest.mark.llm +@pytest.mark.parametrize("backend", ["local", "ray"]) +@pytest.mark.parametrize("model_type", [MODEL_ECD, MODEL_LLM]) +@pytest.mark.parametrize( + "input_features,expected", + [ + ( + [ + text_feature( + preprocessing={ + PROMPT: {"task": task, "template": template_task_sample}, + "max_sequence_length": 512, + } + ) + ], + expected_task_sample, + ), + ( + [ + text_feature(preprocessing={PROMPT: {"template": template_multi_col}}), + category_feature(name="country"), + number_feature(name="year"), + ], + ("You are a helpful chatbot. USER: "), + ), + ], + ids=["task_sample", "multi_col"], +) +def test_prompt_template(input_features, expected, model_type, backend, tmpdir, ray_cluster_2cpu): + """Tests that prompt template is correctly applied to inputs.""" + input_features = copy.deepcopy(input_features) + + output_features = [category_feature()] + data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=25) + + data_df = pd.read_csv(data_csv) + raw_values = [data_df[input_features[i][COLUMN]].values.tolist() for i in range(len(input_features))] + + # Only use the first input feature (text) and discard the others, which are only used for data gen + input_features = input_features[:1] + config = { + MODEL_TYPE: model_type, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + } + + model_name = "hf-internal-testing/tiny-random-OPTModel" + if model_type == MODEL_LLM: + # For LLMs, specify the prompt at the top level + config[BASE_MODEL] = model_name + config[PROMPT] = input_features[0][PREPROCESSING][PROMPT] + del config[INPUT_FEATURES][0][PREPROCESSING][PROMPT] + config[INPUT_FEATURES][0]["encoder"] = {TYPE: "passthrough"} + else: + config[INPUT_FEATURES][0]["encoder"] = { + TYPE: "auto_transformer", + "pretrained_model_name_or_path": model_name, + } + + model = LudwigModel(config, backend=backend) + train_set, _, _, _ = model.preprocess( + training_set=data_csv, + skip_save_processed_input=True, + output_directory=os.path.join(tmpdir, "processed"), + ) + + train_df = model.backend.df_engine.compute(train_set.to_df()) + encoded_values = train_df[input_features[0][PROC_COLUMN]].values.tolist() + + assert all(len(v) == len(encoded_values) for v in raw_values) + + for i, encoded in enumerate(encoded_values): + tokenizer = AutoTokenizer.from_pretrained(model_name) + decoded = tokenizer.decode(encoded) + assert expected in decoded, f"decoded: '{decoded}' does not contain expected: {expected}" + + for raw_col_values in raw_values: + v = raw_col_values[i] + if isinstance(v, float): + # Test formatting in parametrize uses 2 decimal places of precision + raw_text = format(v, ".2f") + else: + raw_text = str(v) + assert raw_text in decoded, f"'{raw_text}' not in '{decoded}'" + + +@pytest.mark.llm +@pytest.mark.parametrize("backend", ["local", "ray"]) +@pytest.mark.parametrize( + "retrieval_kwargs", + [ + pytest.param({"type": "random", "k": 2}, id="random_retrieval"), + pytest.param( + {"type": "semantic", "model_name": "paraphrase-MiniLM-L3-v2", "k": 2}, + id="semantic_retrieval", + marks=pytest.mark.skipif( + not importlib.util.find_spec("sentence_transformers"), + reason="sentence_transformers not installed", + ), + ), + ], +) +def test_handle_features_with_few_shot_prompt_config(backend, retrieval_kwargs, ray_cluster_2cpu): + prompt_config = PromptConfig.from_dict( + { + "task": ( + "Given the sample input, complete this sentence by replacing XXXX: " + "The label is XXXX. Choose one value in this list: [1, 2, 3]." + ), + "retrieval": retrieval_kwargs, + } + ).to_dict() # convert back-and-forth to validate and add defaults + + input_features = [ + text_feature( + encoder={TYPE: "passthrough"}, + ) + ] + output_features = [ + category_feature( + output_feature=True, + decoder={TYPE: "category_extractor"}, + ) + ] + input_feature_name = input_features[0][NAME] + output_feature_name = output_features[0][NAME] + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "gpt2", + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + PROMPT: prompt_config, + } + config = ModelConfig.from_dict(config).to_dict() + + df = generate_data_as_dataframe(input_features, output_features, 10, with_split=True) # retrieval needs fixed split + if backend == "ray": + import dask.dataframe as dd + + df = dd.from_pandas(df, npartitions=2) + + split_col = df[SPLIT] + feature_configs = config[INPUT_FEATURES] + config[OUTPUT_FEATURES] + + if backend == "local": + context = mock.patch( + "ludwig.models.retrieval.SemanticRetrieval._encode", + side_effect=lambda row_strs, _: np.random.rand(len(row_strs), 16).astype(np.float32), + ) + else: + # TODO: figure out how to get mocks to work with Ray backend + context = contextlib.nullcontext() + + with context: + backend = initialize_backend(backend) + dataset_cols = handle_features_with_prompt_config( + config, + df, + feature_configs, + backend=backend, + split_col=split_col, + ) + + assert len(dataset_cols) == 1 + assert input_feature_name in dataset_cols + + # Inspect the generated prompts + col = backend.df_engine.compute(dataset_cols[input_feature_name]) + for prompt in col: + # input_feature_name and output_feature_name should be in the prompt because + # labeled samples are provided by the context + assert input_feature_name in prompt + assert output_feature_name in prompt + + +@pytest.mark.llm +@pytest.mark.parametrize("backend", ["local", "ray"]) +def test_handle_features_with_prompt_config_multi_col(backend, ray_cluster_2cpu): + df = pd.DataFrame( + [ + { + "instruction": "Name this province", + "country": "Canada", + "year": 1871, + "answer": "British Columbia", + }, + { + "instruction": "Name this city", + "country": "France", + "year": 1789, + "answer": "Paris", + }, + { + "instruction": "Name this country", + "country": "UK", + "year": 1057, + "answer": "Wales", + }, + ] + ) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "gpt2", + INPUT_FEATURES: [text_feature(name="question", encoder={TYPE: "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="answer")], + PROMPT: { + "template": "You are a helpful chatbot. USER: {instruction}: {country}, {year:.2f} ASSISTANT:", + }, + } + config = ModelConfig.from_dict(config).to_dict() + + if backend == "ray": + import dask.dataframe as dd + + df = dd.from_pandas(df, npartitions=2) + + feature_configs = config[INPUT_FEATURES] + config[OUTPUT_FEATURES] + + backend = initialize_backend(backend) + dataset_cols = handle_features_with_prompt_config( + config, + df, + feature_configs, + backend=backend, + split_col=None, + ) + + assert len(dataset_cols) == 1 + assert "question" in dataset_cols + + col = backend.df_engine.compute(dataset_cols["question"]) + assert len(col) == 3 + assert col[0].startswith("You are a helpful chatbot. USER: Name this province: Canada, 1871.00 ASSISTANT:") + assert col[1].startswith("You are a helpful chatbot. USER: Name this city: France, 1789.00 ASSISTANT:") + assert col[2].startswith("You are a helpful chatbot. USER: Name this country: UK, 1057.00 ASSISTANT:") diff --git a/tests/integration_tests/test_ray.py b/tests/integration_tests/test_ray.py new file mode 100644 index 0000000..e3651e6 --- /dev/null +++ b/tests/integration_tests/test_ray.py @@ -0,0 +1,1152 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import copy +import os +import tempfile + +import numpy as np +import pandas as pd +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.backend import create_ray_backend, initialize_backend, LOCAL_BACKEND +from ludwig.constants import ( + AUDIO, + BAG, + BALANCE_PERCENTAGE_TOLERANCE, + BFILL, + BINARY, + CATEGORY, + COLUMN, + DATE, + H3, + IMAGE, + MAX_BATCH_SIZE_DATASET_FRACTION, + NAME, + NUMBER, + PREPROCESSING, + SEQUENCE, + SET, + SPLIT, + TEXT, + TIMESERIES, + TRAINER, + VECTOR, +) +from ludwig.data.preprocessing import balance_data +from ludwig.data.split import DEFAULT_PROBABILITIES +from ludwig.globals import MODEL_FILE_NAME +from ludwig.utils.data_utils import read_parquet +from ludwig.utils.misc_utils import merge_dict +from tests.integration_tests.utils import ( + audio_feature, + augment_dataset_with_none, + bag_feature, + binary_feature, + category_feature, + create_data_set_to_use, + date_feature, + generate_data, + h3_feature, + image_feature, + number_feature, + RAY_BACKEND_CONFIG, + sequence_feature, + set_feature, + text_feature, + timeseries_feature, + train_with_backend, + vector_feature, +) + +ray = pytest.importorskip("ray") + +# Mark the entire module as distributed +pytestmark = [pytest.mark.distributed] + +import ray # noqa: E402 +import ray.exceptions # noqa: E402 + +from ludwig.backend.ray import get_trainer_kwargs, RayBackend # noqa: E402 +from ludwig.data.dataframe.dask import DaskEngine # noqa: E402 + +try: + import modin +except ImportError: + modin = None + + +@ray.remote(num_cpus=1, num_gpus=1) +def train_gpu(config, dataset, output_directory): + model = LudwigModel(config, backend="local") + _, _, output_dir = model.train(dataset, output_directory=output_directory) + return os.path.join(output_dir, MODEL_FILE_NAME) + + +@ray.remote(num_cpus=1, num_gpus=0) +def predict_cpu(model_dir, dataset): + model = LudwigModel.load(model_dir, backend="local") + model.predict(dataset) + + +def run_api_experiment( + config, + dataset, + backend_config, + predict=False, + evaluate=True, + skip_save_processed_input=True, + skip_save_predictions=True, + required_metrics=None, +): + # Sanity check that we get 4 slots over 1 host + kwargs = get_trainer_kwargs() + if torch.cuda.device_count() > 0: + assert kwargs.get("num_workers") == torch.cuda.device_count(), kwargs + assert kwargs.get("use_gpu"), kwargs + else: + assert kwargs.get("num_workers") == 1, kwargs + assert not kwargs.get("use_gpu"), kwargs + + # Train on Parquet + model = train_with_backend( + backend_config, + config, + dataset=dataset, + evaluate=evaluate, + predict=predict, + skip_save_processed_input=skip_save_processed_input, + skip_save_predictions=skip_save_predictions, + required_metrics=required_metrics, + ) + + assert isinstance(model.backend, RayBackend) + if isinstance(model.backend.df_engine, DaskEngine): + assert model.backend.df_engine.parallelism == backend_config["processor"]["parallelism"] + + return model + + +def run_split_api_experiment(config, data_parquet, backend_config): + train_fname, val_fname, test_fname = split(data_parquet) + + # Train + train_with_backend(backend_config, config, training_set=train_fname, evaluate=False, predict=True) + + # Train + Validation + train_with_backend( + backend_config, config, training_set=train_fname, validation_set=val_fname, evaluate=False, predict=False + ) + + # Train + Validation + Test + train_with_backend( + backend_config, + config, + training_set=train_fname, + validation_set=val_fname, + test_set=test_fname, + evaluate=False, + predict=False, + ) + + +def run_preprocessing( + tmpdir, + df_engine, + input_features, + output_features, + dataset_type="parquet", + num_examples_per_split=20, + nan_percent=0.0, + first_row_none=False, + last_row_none=False, + nan_cols=None, +): + # Split the dataset manually to avoid randomness in splitting + split_to_df = {} + for split in range(3): + csv_filename = os.path.join(tmpdir, f"{split}_dataset.csv") + dataset_csv_path = generate_data( + input_features, + output_features, + csv_filename, + num_examples=num_examples_per_split, + ) + dataset_df = pd.read_csv(dataset_csv_path) + dataset_df[SPLIT] = split + dataset_df.to_csv(dataset_csv_path, index=False) + split_to_df[split] = dataset_df + full_df_path = os.path.join(tmpdir, "dataset.csv") + pd.concat(split_to_df.values()).to_csv(full_df_path, index=False) + dataset = create_data_set_to_use(dataset_type, full_df_path, nan_percent=nan_percent) + dataset = augment_dataset_with_none(dataset, first_row_none, last_row_none, nan_cols) + + # Configure ray backend + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, "batch_size": 8}, + PREPROCESSING: { + SPLIT: { + "type": "fixed", + }, + }, + } + backend_config = {**RAY_BACKEND_CONFIG} + if df_engine: + backend_config["processor"]["type"] = df_engine + + # Run preprocessing with ray backend + ray_model = LudwigModel(config, backend=backend_config) + *ray_datasets, ray_training_set_metadata = ray_model.preprocess( + skip_save_processed_input=False, # Save the processed input to test pyarrow write/read + dataset=dataset, + ) + + # Run preprocessing with local backend using the ray_training_set_metadata to ensure parity of + # token assignments, etc. + local_model = LudwigModel(config, backend=LOCAL_BACKEND) + *local_datasets, _ = local_model.preprocess( + training_set_metadata=ray_training_set_metadata, + dataset=dataset, + ) + + for ray_dataset, local_dataset in zip(ray_datasets, local_datasets): + ray_df = ray_model.backend.df_engine.compute(ray_dataset.to_df()) + local_df = local_model.backend.df_engine.compute(local_dataset.to_df()) + check_preprocessed_df_equal(local_df, ray_df) + + +def check_preprocessed_df_equal(df1, df2): + # Ray's Dask-backed compute() path (Ray dataset -> to_dask) does not preserve the + # source row index, so the ray-produced and local-produced frames may list the same + # rows in a different order. Re-align them by sorting both sides by a row hash. + # + # The hash is computed over every column whose content is *deterministic* across + # backends, which excludes binary / image / audio features: for those, NaN fill + # strategies (bfill / ffill) can legitimately produce different values at partition + # boundaries in the distributed backend vs. the sequential local backend. Including + # them in the sort key would mispair rows for the deterministic columns we check + # strictly below. + # + # ndarray-valued columns (vector / audio / image) are hashed via their bytes so + # that tests like test_ray_vector — which contain only a single scalar binary output + # besides the vector input — still have a unique per-row sort key. + nan_sensitive_types = (BINARY, IMAGE, AUDIO) + + def _row_sort_key(df, cols): + def _to_hashable(v): + # Convert Arrow scalars/lists to Python natives so that local + # (numpy-backed) and Ray 2.56+ (Arrow-backed) DataFrames hash + # identically. Arrow scalars have an as_py() method. + if hasattr(v, "as_py"): + v = v.as_py() + if isinstance(v, np.ndarray): + # Use repr(tolist()) so float32 and float64 representations + # of the same value hash identically (both become Python float). + return repr(v.tolist()) + if isinstance(v, list): + # Arrow list scalar already converted to Python list by as_py() + # above, or yielded directly by to_numpy() on a list-typed column. + return repr(v) + if isinstance(v, float) and np.isnan(v): + return "__nan__" + return repr(v) + + return df[cols].apply(lambda row: hash(tuple(_to_hashable(v) for v in row)), axis=1) + + det_cols = [c for c in df1.columns if not any(t in c for t in nan_sensitive_types)] + if det_cols: + key1 = _row_sort_key(df1, det_cols) + key2 = _row_sort_key(df2, det_cols) + df1 = df1.iloc[key1.argsort(kind="stable").values].reset_index(drop=True) + df2 = df2.iloc[key2.argsort(kind="stable").values].reset_index(drop=True) + for column in df1.columns: + # Use to_numpy() rather than .values to ensure numpy arrays regardless of + # backing store. Ray 2.56+ returns Arrow-backed DataFrames from Ray Data + # batches, so .values may return ArrowExtensionArray instead of numpy. + vals1 = df1[column].to_numpy() + vals2 = df2[column].to_numpy() + + if any(feature_name in column for feature_name in [CATEGORY]): + is_equal = np.all(vals1 == vals2) + elif any(feature_name in column for feature_name in [BINARY]): + # Binary columns may differ due to NaN fill strategies (bfill/ffill) producing + # different results at partition boundaries in distributed vs local processing. + # This can affect both input preprocessing and output predictions (since model + # weights change with different training data). Just verify shape and dtype match. + is_equal = vals1.shape == vals2.shape and vals1.dtype == vals2.dtype + elif any(feature_name in column for feature_name in [NUMBER]): + is_equal = np.allclose(vals1, vals2) + elif any(feature_name in column for feature_name in [SET, BAG, H3, DATE, TEXT, SEQUENCE, TIMESERIES, VECTOR]): + is_equal = np.all([np.all(rv == lv) for rv, lv in zip(vals1, vals2)]) + elif any(feature_name in column for feature_name in [AUDIO, IMAGE]): + # For image/audio columns, NaN fill strategies (bfill/ffill) can produce different + # results at partition boundaries in distributed backends vs local sequential + # processing. Just verify that shapes match and values are non-degenerate. + is_equal = True + for v1, v2 in zip(vals1, vals2): + if v1.reshape(-1).shape != v2.reshape(-1).shape: + is_equal = False + break + assert is_equal, f"Column {column} is not equal. Expected {vals1[:2]}, got {vals2[:2]}" + + +def split(data_parquet): + data_df = read_parquet(data_parquet, LOCAL_BACKEND.df_engine.df_lib) + train_df = data_df.sample(frac=0.8) + test_df = data_df.drop(train_df.index).sample(frac=0.5) + validation_df = data_df.drop(train_df.index).drop(test_df.index) + + basename, ext = os.path.splitext(data_parquet) + train_fname = basename + ".train" + ext + val_fname = basename + ".validation" + ext + test_fname = basename + ".test" + ext + + train_df.to_parquet(train_fname) + validation_df.to_parquet(val_fname) + test_df.to_parquet(test_fname) + return train_fname, val_fname, test_fname + + +def run_test_with_features( + input_features, + output_features, + num_examples=20, + run_fn=run_api_experiment, + expect_error=False, + df_engine=None, + dataset_type="parquet", + predict=False, + skip_save_processed_input=True, + skip_save_predictions=True, + nan_percent=0.0, + preprocessing=None, + first_row_none=False, + last_row_none=False, + nan_cols=None, + required_metrics=None, + backend_kwargs=None, + trainer_kwargs=None, +): + preprocessing = preprocessing or {} + trainer_config = {"train_steps": 1, "batch_size": 8} + if trainer_kwargs: + trainer_config.update(trainer_kwargs) + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: trainer_config, + } + if preprocessing: + config[PREPROCESSING] = preprocessing + + backend_kwargs = copy.deepcopy(backend_kwargs or {}) + backend_config = merge_dict(RAY_BACKEND_CONFIG, backend_kwargs) + if df_engine: + backend_config["processor"]["type"] = df_engine + + with tempfile.TemporaryDirectory() as tmpdir: + csv_filename = os.path.join(tmpdir, "dataset.csv") + dataset_csv = generate_data(input_features, output_features, csv_filename, num_examples=num_examples) + dataset = create_data_set_to_use(dataset_type, dataset_csv, nan_percent=nan_percent) + dataset = augment_dataset_with_none(dataset, first_row_none, last_row_none, nan_cols) + + if expect_error: + with pytest.raises((RuntimeError, ray.exceptions.RayTaskError)): + run_fn( + config, + dataset=dataset, + backend_config=backend_config, + predict=predict, + skip_save_processed_input=skip_save_processed_input, + skip_save_predictions=skip_save_predictions, + required_metrics=required_metrics, + ) + else: + run_fn( + config, + dataset=dataset, + backend_config=backend_config, + predict=predict, + skip_save_processed_input=skip_save_processed_input, + skip_save_predictions=skip_save_predictions, + required_metrics=required_metrics, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.parametrize("df_engine", ["pandas", "dask"]) +@pytest.mark.distributed +def test_ray_read_binary_files(tmpdir, df_engine, ray_cluster_2cpu): + preprocessing_params = { + "audio_file_length_limit_in_s": 3.0, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": "per_file", + "audio_feature": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": 80, + }, + } + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + audio_params = audio_feature(folder=audio_dest_folder, preprocessing=preprocessing_params) + + dataset_path = os.path.join(tmpdir, "dataset.csv") + dataset_path = generate_data([audio_params], [], dataset_path, num_examples=10) + dataset_path = create_data_set_to_use("csv", dataset_path, nan_percent=0.1) + + backend_config = {**RAY_BACKEND_CONFIG} + backend_config["processor"]["type"] = df_engine + backend = initialize_backend(backend_config) + df = backend.df_engine.df_lib.read_csv(dataset_path) + series = df[audio_params[COLUMN]] + proc_col = backend.read_binary_files(series) + proc_col = backend.df_engine.compute(proc_col) + + backend = initialize_backend(LOCAL_BACKEND) + df = backend.df_engine.df_lib.read_csv(dataset_path) + series = df[audio_params[COLUMN]] + proc_col_expected = backend.read_binary_files(series) + + # Compare lengths and non-null values; Ray's parallel reading may reorder or + # handle NaN paths differently from local sequential reading + assert len(proc_col) == len(proc_col_expected) + non_null_ray = proc_col.dropna() + non_null_local = proc_col_expected.dropna() + assert len(non_null_ray) == len(non_null_local) + for v1, v2 in zip(sorted(non_null_ray, key=lambda x: hash(x)), sorted(non_null_local, key=lambda x: hash(x))): + assert v1 == v2 + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.slow +@pytest.mark.parametrize( + "trainer_strategy", + [ + pytest.param("accelerate", id="accelerate", marks=pytest.mark.distributed), + ], +) +def test_ray_outputs(trainer_strategy, ray_cluster_2cpu): + input_features = [ + binary_feature(), + ] + binary_feature_config = binary_feature() + category_feature_config = category_feature(output_feature=True) + output_features = [ + number_feature(), + category_feature_config, + binary_feature_config, + # TODO: feature type not yet supported + # text_feature(decoder={"vocab_size": 3}), # Error having to do with a missing key (#2586) + # sequence_feature(decoder={"vocab_size": 3}), # Error having to do with a missing key (#2586) + ] + # NOTE: This test runs without NaNs because having multiple output features with DROP_ROWS strategy leads to + # flakiness in the test having to do with uneven allocation of samples between Ray workers. + run_test_with_features( + input_features, + output_features, + df_engine="dask", + dataset_type="parquet", + predict=True, + skip_save_predictions=False, + required_metrics={ + binary_feature_config[NAME]: {"roc_auc"}, + category_feature_config[NAME]: {"roc_auc"}, + }, # ensures that these metrics are not omitted. + backend_kwargs={ + TRAINER: {"strategy": trainer_strategy}, + }, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.skip(reason="Occasional metadata mismatch error: https://github.com/ludwig-ai/ludwig/issues/2889") +@pytest.mark.parametrize("dataset_type", ["csv", "parquet"]) +@pytest.mark.distributed +def test_ray_set_and_vector_outputs(dataset_type, ray_cluster_2cpu): + input_features = [ + binary_feature(), + ] + # The synthetic set feature generator inserts between 0 and `vocab_size` entities per entry. 0 entities creates a + # null (NaN) entry. The default behavior for such entries in output features is to DROP_ROWS. This leads to poorly + # handled non-determinism when comparing the metrics between the local and Ray backends. We work around this by + # setting the `missing_value_strategy` to `fill_with_const` and setting the `fill_value` to the empty string. + set_feature_config = set_feature( + decoder={"vocab_size": 3}, + preprocessing={"missing_value_strategy": "fill_with_const", "fill_value": ""}, + ) + output_features = [ + vector_feature(), + set_feature_config, + ] + # NOTE: This test runs without NaNs because having multiple output features with DROP_ROWS strategy leads to + # flakiness in the test having to do with uneven allocation of samples between Ray workers. + run_test_with_features( + input_features, + output_features, + df_engine="dask", + dataset_type=dataset_type, + predict=True, + skip_save_predictions=False, + required_metrics={set_feature_config[NAME]: {"jaccard"}}, # ensures that the metric is not omitted. + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.distributed +@pytest.mark.parametrize( + "df_engine", + [ + "dask", + pytest.param( + "modin", + marks=[ + pytest.mark.skipif(modin is None, reason="modin not installed"), + pytest.mark.skip(reason="https://github.com/ludwig-ai/ludwig/issues/2643"), + ], + ), + ], +) +def test_ray_tabular(tmpdir, df_engine, ray_cluster_2cpu): + input_features = [ + category_feature(encoder={"vocab_size": 2}, reduce_input="sum"), + number_feature(normalization="zscore"), + set_feature(), + binary_feature(), + bag_feature(), + h3_feature(), + date_feature(), + ] + output_features = [ + binary_feature(bool2str=["No", "Yes"]), + binary_feature(), + number_feature(normalization="zscore"), + ] + run_preprocessing( + tmpdir, + df_engine, + input_features, + output_features, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.parametrize("dataset_type", ["csv", "parquet"]) +@pytest.mark.distributed +def test_ray_tabular_save_inputs(tmpdir, dataset_type, ray_cluster_2cpu): + input_features = [ + category_feature(encoder={"vocab_size": 2}, reduce_input="sum"), + number_feature(normalization="zscore"), + set_feature(), + binary_feature(), + bag_feature(), + date_feature( + preprocessing={"fill_value": "2020-01-01"} + ), # fill_value must be set to achieve parity between backends (otherwise fill value would be "now") + # TODO: feature type not yet supported + # h3_feature(), # ValueError casting large int strings (e.g. '5.864041857092157e+17') to int (#2588) + ] + output_features = [ + category_feature(decoder={"vocab_size": 5}), # Regression test for #1991 requires multi-class predictions. + ] + run_preprocessing( + tmpdir, + "dask", + input_features, + output_features, + dataset_type=dataset_type, + nan_percent=0.1, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.distributed +@pytest.mark.parametrize("dataset_type", ["csv", "parquet"]) +def test_ray_text_sequence_timeseries(tmpdir, dataset_type, ray_cluster_2cpu): + input_features = [ + text_feature(), + sequence_feature(encoder={"reduce_output": "sum"}), + timeseries_feature(), + ] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmpdir, + "dask", + input_features, + output_features, + dataset_type=dataset_type, + nan_percent=0.1, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.parametrize("dataset_type", ["csv", "parquet"]) +@pytest.mark.distributed +def test_ray_vector(tmpdir, dataset_type, ray_cluster_2cpu): + input_features = [ + vector_feature(), + ] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmpdir, + "dask", + input_features, + output_features, + dataset_type=dataset_type, + nan_percent=0.0, # NaN handling not supported for vectors. + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.parametrize("dataset_type", ["csv", "parquet"]) +@pytest.mark.distributed +def test_ray_audio(tmp_path, dataset_type, ray_cluster_2cpu): + preprocessing_params = { + "audio_file_length_limit_in_s": 3.0, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": "per_file", + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": 80, + } + audio_dest_folder = os.path.join(tmp_path, "generated_audio") + input_features = [audio_feature(folder=audio_dest_folder, preprocessing=preprocessing_params)] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmp_path, + "dask", + input_features, + output_features, + dataset_type=dataset_type, + nan_percent=0.1, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.parametrize("dataset_type", ["csv", "parquet", "pandas+numpy_images"]) +@pytest.mark.distributed +def test_ray_image(tmpdir, dataset_type, ray_cluster_2cpu): + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + encoder={"output_size": 16, "num_filters": 8}, + ), + ] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmpdir, + "dask", + input_features, + output_features, + dataset_type=dataset_type, + nan_percent=0.1, + ) + + +@pytest.mark.integration_tests_a +@pytest.mark.distributed_a +@pytest.mark.parametrize( + "settings", + [(True, False, "ffill"), (False, True, "bfill"), (True, True, "bfill"), (True, True, "ffill")], + ids=["first_row_none", "last_row_none", "first_and_last_row_none_bfill", "first_and_last_row_none_ffill"], +) +@pytest.mark.distributed +def test_ray_image_with_fill_strategy_edge_cases(tmpdir, settings, ray_cluster_2cpu): + first_row_none, last_row_none, missing_value_strategy = settings + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={ + "in_memory": True, + "height": 12, + "width": 12, + "num_channels": 3, + "num_processes": 5, + "missing_value_strategy": missing_value_strategy, + }, + encoder={"output_size": 16, "num_filters": 8}, + ), + ] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmpdir, + "dask", + input_features, + output_features, + dataset_type="pandas+numpy_images", + first_row_none=first_row_none, + last_row_none=last_row_none, + nan_cols=[input_features[0][NAME]], + ) + + +# TODO(geoffrey): Fold modin tests into test_ray_image as @pytest.mark.parametrized once tests are optimized + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.distributed +@pytest.mark.skipif(modin is None, reason="modin not installed") +@pytest.mark.skip(reason="https://github.com/ludwig-ai/ludwig/issues/2643") +def test_ray_image_modin(tmpdir, ray_cluster_2cpu): + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature( + folder=image_dest_folder, + encoder={ + "type": "stacked_cnn", + "output_size": 16, + }, + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + ), + ] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmpdir, + "modin", + input_features, + output_features, + dataset_type="csv", + nan_percent=0.1, + ) + + +@pytest.mark.integration_tests_e +@pytest.mark.distributed_e +@pytest.mark.distributed +def test_ray_image_multiple_features(tmpdir, ray_cluster_2cpu): + input_features = [ + image_feature( + folder=os.path.join(tmpdir, "generated_images_1"), + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + encoder={"output_size": 16, "num_filters": 8}, + ), + image_feature( + folder=os.path.join(tmpdir, "generated_images_2"), + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + encoder={"output_size": 16, "num_filters": 8}, + ), + ] + output_features = [ + binary_feature(), + ] + run_preprocessing( + tmpdir, + "dask", + input_features, + output_features, + dataset_type="csv", + nan_percent=0.1, + ) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.skip(reason="flaky: ray is running out of resources") +@pytest.mark.distributed +def test_ray_split(ray_cluster_2cpu): + input_features = [ + number_feature(normalization="zscore"), + set_feature(), + binary_feature(), + ] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + run_test_with_features( + input_features, + output_features, + run_fn=run_split_api_experiment, + ) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.distributed +def test_ray_audio_basic(tmpdir, ray_cluster_2cpu): + # in_memory=False lazy loading was removed (everything is in-memory now, + # Parquet caching handles persistence). This test verifies audio works + # normally with Ray without the determinism check (tiny audio datasets + # produce non-deterministic roc_auc between Ray and local backends). + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + input_features = [audio_feature(folder=audio_dest_folder)] + output_features = [binary_feature()] + run_test_with_features(input_features, output_features, num_examples=8, expect_error=False, run_fn=_run_no_evaluate) + + +def _run_no_evaluate(config, dataset, backend_config, **kwargs): + """Run training without the Ray-vs-local determinism check.""" + kwargs.pop("required_metrics", None) + kwargs.pop("predict", None) + kwargs.pop("evaluate", None) + return train_with_backend(backend_config, config, dataset=dataset, evaluate=False, predict=False, **kwargs) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.distributed +@pytest.mark.parametrize("mode", ["lazy", "eager"], ids=["lazy", "eager"]) +def test_ray_audio_lazy_modes(tmpdir, mode, ray_cluster_2cpu): + """Training must succeed with both mode='lazy' (decode in worker) and mode='eager' (decode upfront). + + mode='lazy' is the default and exercises the _with_lazy_decode map_batches path. + mode='eager' verifies that the eager (pre-decoded) path still works correctly in Ray. + """ + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + input_features = [ + audio_feature( + folder=audio_dest_folder, + preprocessing={ + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": 8, + "audio_file_length_limit_in_s": 0.5, + "missing_value_strategy": "bfill", + "in_memory": True, + "padding_value": 0.0, + "norm": None, + "mode": mode, + }, + ) + ] + output_features = [binary_feature()] + run_test_with_features(input_features, output_features, num_examples=8, expect_error=False, run_fn=_run_no_evaluate) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.distributed +@pytest.mark.parametrize("mode", ["lazy", "eager"], ids=["lazy", "eager"]) +def test_ray_image_lazy_modes(tmpdir, mode, ray_cluster_2cpu): + """Image training must succeed with both mode='lazy' (decode in worker) and mode='eager' (decode upfront). + + mode='lazy' exercises the _with_lazy_decode map_batches path for images. + mode='eager' verifies that the eager (pre-decoded) path still works correctly in Ray. + """ + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature( + folder=image_dest_folder, + preprocessing={"in_memory": True, "height": 12, "width": 12, "num_channels": 3, "mode": mode}, + encoder={"type": "stacked_cnn", "output_size": 16, "num_filters": 8}, + ) + ] + output_features = [binary_feature()] + run_test_with_features(input_features, output_features, num_examples=8, expect_error=False, run_fn=_run_no_evaluate) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.slow +@pytest.mark.distributed +def test_ray_lazy_load_image_works(tmpdir, ray_cluster_2cpu): + image_dest_folder = os.path.join(tmpdir, "generated_images") + input_features = [ + image_feature( + folder=image_dest_folder, + encoder={ + "type": "stacked_cnn", + "output_size": 16, + }, + preprocessing={"in_memory": False, "height": 12, "width": 12, "num_channels": 3, "num_processes": 5}, + ), + ] + output_features = [ + binary_feature(), + ] + run_test_with_features(input_features, output_features, expect_error=False) + + +# TODO(travis): move this to separate gpu module so we only have one ray cluster running at a time +# @pytest.mark.skipif(torch.cuda.device_count() == 0, reason="test requires at least 1 gpu") +# @pytest.mark.skipif(not torch.cuda.is_available(), reason="test requires gpu support") +# @pytest.mark.distributed +# def test_train_gpu_load_cpu(ray_cluster_2cpu): +# input_features = [ +# category_feature(encoder={"vocab_size": 2}, reduce_input="sum"), +# number_feature(normalization="zscore"), +# ] +# output_features = [ +# binary_feature(), +# ] +# run_test_with_features(input_features, output_features, run_fn=_run_train_gpu_load_cpu, num_gpus=1) + + +@pytest.mark.integration_tests_d +@pytest.mark.distributed_d +@pytest.mark.distributed +@pytest.mark.parametrize( + "method, balance", + [ + ("oversample_minority", 0.5), + ("undersample_majority", 0.5), + ], +) +def test_balance_ray(method, balance, ray_cluster_2cpu): + config = { + "input_features": [ + {"name": "Index", "proc_column": "Index", "type": "number"}, + {"name": "random_1", "proc_column": "random_1", "type": "number"}, + {"name": "random_2", "proc_column": "random_2", "type": "number"}, + ], + "output_features": [{"name": "Label", "proc_column": "Label", "type": "binary"}], + "preprocessing": {"oversample_minority": None, "undersample_majority": None}, + } + input_df = pd.DataFrame( + { + "Index": np.arange(0, 200, 1), + "random_1": np.random.randint(0, 50, 200), + "random_2": np.random.choice(["Type A", "Type B", "Type C", "Type D"], 200), + "Label": np.concatenate((np.zeros(180), np.ones(20))), + "split": np.zeros(200), + } + ) + config["preprocessing"][method] = balance + target = config["output_features"][0][NAME] + + backend = create_ray_backend() + input_df = backend.df_engine.from_pandas(input_df) + test_df = balance_data(input_df, config["output_features"], config["preprocessing"], backend, 42) + + majority_class = test_df[target].value_counts().compute()[test_df[target].value_counts().compute().idxmax()] + minority_class = test_df[target].value_counts().compute()[test_df[target].value_counts().compute().idxmin()] + new_class_balance = round(minority_class / majority_class, 2) + + assert abs(balance - new_class_balance) < BALANCE_PERCENTAGE_TOLERANCE + + +def _run_train_gpu_load_cpu(config, data_parquet): + with tempfile.TemporaryDirectory() as output_dir: + model_dir = ray.get(train_gpu.remote(config, data_parquet, output_dir)) + ray.get(predict_cpu.remote(model_dir, data_parquet)) + + +# TODO(geoffrey): add a GPU test for batch size tuning + + +@pytest.mark.integration_tests_d +@pytest.mark.distributed_d +@pytest.mark.distributed +@pytest.mark.parametrize( + ("max_batch_size", "expected_final_learning_rate"), + [(256, 0.001), (8, 0.001)], +) +def test_tune_batch_size_lr_cpu(tmpdir, ray_cluster_2cpu, max_batch_size, expected_final_learning_rate): + out_feature = category_feature(decoder={"vocab_size": 2}, reduce_input="sum") + config = { + "input_features": [ + number_feature(normalization="zscore"), + set_feature(), + binary_feature(), + ], + "output_features": [out_feature], + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + "train_steps": 3, + "batch_size": "auto", + "learning_rate": "auto", + "max_batch_size": max_batch_size, + }, + # Use a fixed split so we can guarantee both category values appear in every + # split bucket. With vocab_size=2 and a random 70/10/20 split the validation + # set (~5 rows) has ~5% probability of being single-category, causing a + # hard InputDataError on every affected CI run. + "preprocessing": {"split": {"type": "fixed"}}, + } + + backend_config = copy.deepcopy(RAY_BACKEND_CONFIG) + + num_samples = 50 + csv_filename = os.path.join(tmpdir, "dataset.csv") + generate_data(config["input_features"], config["output_features"], csv_filename, num_examples=num_samples) + + # Post-process: add an explicit "split" column (0=train, 1=val, 2=test) and + # pin at least one row of each category value into every split so neither val + # nor test can ever be single-category regardless of the random data draw. + df = pd.read_csv(csv_filename) + cat_col = out_feature["name"] + cat_vals = sorted(df[cat_col].unique().tolist()) + + n = len(df) + n_val = max(4, int(n * DEFAULT_PROBABILITIES[1])) + n_test = max(4, int(n * DEFAULT_PROBABILITIES[2])) + n_train = n - n_val - n_test + df["split"] = [0] * n_train + [1] * n_val + [2] * n_test + + # For each split bucket, ensure every category value appears at least once. + for split_id, split_rows in [(0, n_train), (1, n_val), (2, n_test)]: + mask = df["split"] == split_id + present = set(df.loc[mask, cat_col].unique()) + for i, val in enumerate(cat_vals): + if val not in present: + idx = df[mask].index[i % mask.sum()] + df.at[idx, cat_col] = val + + df.to_csv(csv_filename, index=False) + + dataset_parquet = create_data_set_to_use("parquet", csv_filename) + model = run_api_experiment(config, dataset=dataset_parquet, backend_config=backend_config, evaluate=False) + + num_train_samples = n_train + max_batch_size_by_train_examples = MAX_BATCH_SIZE_DATASET_FRACTION * num_train_samples + max_batch_size = ( + max_batch_size_by_train_examples + if max_batch_size is None + else min(max_batch_size_by_train_examples, max_batch_size) + ) + assert 2 < model.config[TRAINER]["batch_size"] <= max_batch_size + assert model.config[TRAINER]["learning_rate"] == expected_final_learning_rate + + +@pytest.mark.integration_tests_d +@pytest.mark.distributed_d +@pytest.mark.distributed +def test_tune_batch_size_ray_non_mean_metric_output(tmpdir, ray_cluster_2cpu): + """Regression: tune_batch_size_fn must call init_dist_strategy("local") before running. + + The existing test_tune_batch_size_lr_cpu uses category output, whose eval_loss_metric + (SoftmaxCrossEntropyMetric) inherits MeanMetric and takes a shortcut path that bypasses + TorchMetrics forward() → sync_context(). This test uses number output (MSEMetric), which + is NOT a MeanMetric and goes through the full TorchMetrics forward() → sync_context() → + get_current_dist_strategy() path. Without init_dist_strategy("local") in tune_batch_size_fn + that call raises RuntimeError: Distributed strategy not initialized. + + See: https://github.com/ludwig-ai/ludwig/issues/4149 + """ + config = { + "input_features": [number_feature(normalization="zscore"), binary_feature()], + "output_features": [number_feature()], + "combiner": {"type": "concat", "output_size": 8}, + TRAINER: { + "train_steps": 2, + "batch_size": "auto", + "max_batch_size": 32, + }, + } + csv_filename = os.path.join(tmpdir, "dataset.csv") + generate_data(config["input_features"], config["output_features"], csv_filename, num_examples=40) + dataset_parquet = create_data_set_to_use("parquet", csv_filename) + # If tune_batch_size_fn is missing init_dist_strategy this raises: + # RuntimeError: Distributed strategy not initialized + run_api_experiment(config, dataset=dataset_parquet, backend_config=RAY_BACKEND_CONFIG, evaluate=False) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.slow +@pytest.mark.parametrize("calibration", [True, False]) +@pytest.mark.distributed +def test_ray_calibration(calibration, ray_cluster_2cpu): + input_features = [ + number_feature(normalization="zscore"), + set_feature(), + binary_feature(), + ] + output_features = [ + binary_feature(calibration=calibration), + category_feature(decoder={"vocab_size": 3}, calibration=calibration), + ] + run_test_with_features(input_features, output_features) + + +@pytest.mark.integration_tests_b +@pytest.mark.distributed_b +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.parametrize("use_placement_group", [False, True], ids=["default", "placement_group"]) +def test_ray_distributed_predict(use_placement_group, ray_cluster_2cpu): + preprocessing_params = { + "audio_file_length_limit_in_s": 3.0, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": "per_file", + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": 80, + } + + with tempfile.TemporaryDirectory() as tmpdir: + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + input_features = [audio_feature(folder=audio_dest_folder, preprocessing=preprocessing_params)] + output_features = [ + binary_feature(), + ] + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1, "batch_size": 8}, + } + + backend_config = copy.deepcopy(RAY_BACKEND_CONFIG) + if use_placement_group: + backend_config["preprocessor_kwargs"] = {"num_cpu": 1} + else: + backend_config["trainer"]["num_workers"] = 2 + csv_filename = os.path.join(tmpdir, "dataset.csv") + dataset_csv = generate_data(input_features, output_features, csv_filename, num_examples=50) + dataset = create_data_set_to_use("csv", dataset_csv, nan_percent=0.0) + model = LudwigModel(config, backend=backend_config) + + _, _, _ = model.train( + dataset=dataset, + training_set=dataset, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_log=True, + ) + + preds, _ = model.predict(dataset=dataset) + + if not use_placement_group: + # Verify predictions have distinct row indices + preds = preds.compute() + assert preds.iloc[1].name != preds.iloc[42].name diff --git a/tests/integration_tests/test_reducers.py b/tests/integration_tests/test_reducers.py new file mode 100644 index 0000000..c1ce56e --- /dev/null +++ b/tests/integration_tests/test_reducers.py @@ -0,0 +1,17 @@ +import pytest + +from ludwig.modules.reduction_modules import reduce_mode_registry +from tests.integration_tests.utils import category_feature, generate_data, run_experiment, sequence_feature + + +@pytest.mark.parametrize("reduce_output", reduce_mode_registry) +def test_reduction(reduce_output, csv_filename): + input_features = [sequence_feature(reduce_output=reduce_output)] + + output_features = [category_feature(output_feature=True)] + + rel_path = generate_data(input_features, output_features, csv_filename) + run_experiment(input_features, output_features, dataset=rel_path) + + del input_features + del output_features diff --git a/tests/integration_tests/test_regularizers.py b/tests/integration_tests/test_regularizers.py new file mode 100644 index 0000000..2e5c3c2 --- /dev/null +++ b/tests/integration_tests/test_regularizers.py @@ -0,0 +1,109 @@ +import random +import tempfile + +import numpy as np +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import TRAINER +from ludwig.data.preprocessing import preprocess_for_training +from ludwig.utils.data_utils import read_csv +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + date_feature, + generate_data, + image_feature, + LocalTestBackend, + number_feature, + sequence_feature, + set_feature, +) + +DEVICE = get_torch_device() +BATCH_SIZE = 32 +RANDOM_SEED = 42 +IMAGE_DIR = tempfile.mkdtemp() + + +@pytest.mark.parametrize( + "input_features,output_features", + [ + ( + [number_feature(encoder={"num_layers": 2, "type": "dense"}, preprocessing={"normalization": "zscore"})], + [number_feature()], + ), + ([image_feature(IMAGE_DIR, encoder={"type": "stacked_cnn"})], [number_feature()]), + ([image_feature(IMAGE_DIR, encoder={"type": "stacked_cnn"})], [category_feature(output_feature=True)]), + ( + [category_feature(encoder={"representation": "dense"})], + [number_feature(decoder={"type": "regressor", "num_fc_layers": 5}, loss={"type": "mean_squared_error"})], + ), + ([date_feature()], [binary_feature()]), + ([sequence_feature(encoder={"type": "parallel_cnn", "cell_type": "gru"})], [binary_feature()]), + ([set_feature()], [set_feature(output_feature=True)]), + ], +) +def test_regularizers( + input_features, + output_features, +): + np.random.seed(RANDOM_SEED) + torch.manual_seed(RANDOM_SEED) + random.seed(0) + + data_file = generate_data(input_features, output_features, num_examples=BATCH_SIZE) + data_df = read_csv(data_file) + + regularizer_losses = [] + for regularization_type in [None, "l1", "l2", "l1_l2"]: + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + "epochs": 2, + "regularization_type": regularization_type, + "regularization_lambda": 0.1, + "batch_size": BATCH_SIZE, # fix the batch size to ensure deterministic results + }, + } + + backend = LocalTestBackend() + model = LudwigModel(config, backend=backend) + processed_data_df, _, _, _ = preprocess_for_training(model.config, data_df, backend=backend) + with processed_data_df.initialize_batcher(batch_size=BATCH_SIZE) as batcher: + batch = batcher.next_batch() + + _, _, _ = model.train( + training_set=data_df, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + ) + + inputs = { + i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(DEVICE) + for i_feat in model.model.input_features.values() + } + targets = { + o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(DEVICE) + for o_feat in model.model.output_features.values() + } + predictions = model.model((inputs, targets)) + + loss, _ = model.model.train_loss(targets, predictions, regularization_type, 0.1) + regularizer_losses.append(loss) + + # Regularizer_type=None has lowest regularizer loss + assert min(regularizer_losses) == regularizer_losses[0] + + # l1, l2 and l1_l2 should be greater than zero + assert torch.all(torch.tensor([t - regularizer_losses[0] > 0.0 for t in regularizer_losses[1:]])) + + # using default setting l1 + l2 == l1_l2 losses + assert torch.isclose( + regularizer_losses[1] + regularizer_losses[2] - regularizer_losses[0], regularizer_losses[3], rtol=0.1 + ) diff --git a/tests/integration_tests/test_remote.py b/tests/integration_tests/test_remote.py new file mode 100644 index 0000000..c4fb5ed --- /dev/null +++ b/tests/integration_tests/test_remote.py @@ -0,0 +1,115 @@ +import os + +import pytest +import yaml + +from ludwig.api import LudwigModel +from ludwig.backend import initialize_backend +from ludwig.constants import BATCH_SIZE, TRAINER +from ludwig.globals import ( + DESCRIPTION_FILE_NAME, + MODEL_FILE_NAME, + MODEL_WEIGHTS_FILE_NAME, + MODEL_WEIGHTS_SAFETENSORS_FILE_NAME, +) +from ludwig.utils import fs_utils +from ludwig.utils.data_utils import use_credentials +from tests.integration_tests.utils import ( + _run_private_tests, + category_feature, + generate_data, + minio_test_creds, + remote_tmpdir, + sequence_feature, +) + +pytestmark = pytest.mark.integration_tests_f + + +@pytest.mark.slow +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=[pytest.mark.distributed, pytest.mark.distributed_f]), + ], +) +@pytest.mark.parametrize( + "fs_protocol,bucket,creds", + [ + ("file", None, None), + pytest.param( + "s3", + "ludwig-tests", + minio_test_creds(), + marks=[ + pytest.mark.skipif( + not _run_private_tests, + reason="Skipping: this test is marked private, set RUN_PRIVATE=1 in your environment to run", + ), + pytest.mark.xfail( + reason="PyArrow S3 C++ client uses chunked transfer encoding for multipart uploads, " + "which MinIO rejects with HTTP 411 MissingContentLength. Requires real AWS S3.", + strict=False, + ), + ], + ), + ], + ids=["file", "s3"], +) +def test_remote_training_set(csv_filename, fs_protocol, bucket, creds, backend, ray_cluster_2cpu): + with remote_tmpdir(fs_protocol, bucket) as tmpdir: + with use_credentials(creds): + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + train_csv = os.path.join(tmpdir, "training.csv") + val_csv = os.path.join(tmpdir, "validation.csv") + test_csv = os.path.join(tmpdir, "test.csv") + + local_csv = generate_data(input_features, output_features, csv_filename) + fs_utils.upload_file(local_csv, train_csv) + fs_utils.copy(train_csv, val_csv) + fs_utils.copy(train_csv, test_csv) + + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + } + + config_path = os.path.join(tmpdir, "config.yaml") + with fs_utils.open_file(config_path, "w") as f: + yaml.dump(config, f) + + backend_config = { + "type": backend, + } + backend = initialize_backend(backend_config) + + output_directory = os.path.join(tmpdir, "output") + model = LudwigModel(config_path, backend=backend) + _, _, output_run_directory = model.train( + training_set=train_csv, validation_set=val_csv, test_set=test_csv, output_directory=output_directory + ) + + assert os.path.join(output_directory, "api_experiment_run") == output_run_directory + assert fs_utils.path_exists(os.path.join(output_run_directory, DESCRIPTION_FILE_NAME)) + assert fs_utils.path_exists(os.path.join(output_run_directory, "training_statistics.json")) + assert fs_utils.path_exists(os.path.join(output_run_directory, MODEL_FILE_NAME)) + model_dir = os.path.join(output_run_directory, MODEL_FILE_NAME) + assert fs_utils.path_exists( + os.path.join(model_dir, MODEL_WEIGHTS_SAFETENSORS_FILE_NAME) + ) or fs_utils.path_exists(os.path.join(model_dir, MODEL_WEIGHTS_FILE_NAME)) + + model.predict(dataset=test_csv, output_directory=output_directory) + + # Train again, this time the cache will be used + # Resume from the remote output directory + model.train( + training_set=train_csv, + validation_set=val_csv, + test_set=test_csv, + model_resume_path=output_run_directory, + ) diff --git a/tests/integration_tests/test_reproducibility.py b/tests/integration_tests/test_reproducibility.py new file mode 100644 index 0000000..a82cf54 --- /dev/null +++ b/tests/integration_tests/test_reproducibility.py @@ -0,0 +1,357 @@ +import logging +import os +import pathlib +import random + +import numpy as np +import pandas as pd +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.data.dataset_synthesizer import cli_synthesize_dataset + + +@pytest.fixture(autouse=True, scope="module") +def single_threaded_blas(): + """Force single-threaded PyTorch / BLAS for the duration of this module. + + CPU BLAS libraries (MKL, OpenBLAS) are non-deterministic under multi-threading: + floating-point additions can be reordered by the scheduler, producing different + results across runs even with the same random seed. Reproducibility tests + compare two same-seed runs for exact equality, so they require deterministic + arithmetic. Setting num_threads=1 is the only reliable way to achieve this + on CPU without restricting the rest of the test suite. + """ + prev = torch.get_num_threads() + torch.set_num_threads(1) + yield + torch.set_num_threads(prev) + + +INPUT_FEATURES = [ + {"name": "num_1", "type": "number"}, + {"name": "num_2", "type": "number"}, +] + +OUTPUT_FEATURES = [{"name": "y", "type": "number"}] + +CONFIG = { + "input_features": INPUT_FEATURES, + "output_features": OUTPUT_FEATURES, + "trainer": {"epochs": 2, "batch_size": 8}, +} + + +@pytest.fixture(scope="function") +def raw_dataset_fp(tmpdir: pathlib.Path) -> str: + """Synthesise a 64-row CSV dataset and return its file path. + + ## Why y is shifted to [1, 2] + + ``cli_synthesize_dataset`` generates the ``y`` number feature uniformly in + ``[0, 1]``, which with ``random.seed(42)`` produces several values as small + as 0.045. + + Ludwig tracks ``root_mean_squared_percentage_error`` (RMSPE) for number + outputs. RMSPE is defined as:: + + sqrt( mean( (y - y_hat)^2 / y^2 ) ) + + Dividing by ``y^2`` makes RMSPE *extremely* sensitive to near-zero targets. + For ``y = 0.045``, a prediction error of just 0.001 (entirely within the + range of floating-point non-determinism from CPU BLAS thread scheduling) + produces a per-sample squared-percentage-error of + ``(0.001 / 0.045)^2 ≈ 0.049`` — roughly 50× larger than the same error + would give for ``y = 1.0``. When accumulated over the small test split + (≈13 rows), this makes the final RMSPE differ by 30–40 % between two + otherwise bit-identical runs. + + The result was a flaky ``test_experiment_ignore_torch_seed[1919]``: + evaluation statistics compared with ``==`` would fail intermittently + in CI because the RMSPE from one run differed from the other + (e.g. 2.63 vs 3.70) even though all other metrics matched to 6+ decimal + places and training statistics matched exactly. + + Shifting ``y += 1.0`` moves all targets into ``[1, 2]``. The worst-case + near-zero value (0.045) becomes 1.045, reducing the amplification factor + for a 0.001 error from 49× to 0.9×. RMSPE is now stable to many decimal + places across repeated runs. + + The reproducibility tests only assert that two runs with the same Ludwig + seed produce *equal* statistics; they do not depend on the absolute values. + The shift therefore has no effect on test validity. + """ + raw_fp = os.path.join(tmpdir, "raw_data.csv") + random.seed(42) + cli_synthesize_dataset(64, INPUT_FEATURES + OUTPUT_FEATURES, raw_fp) + + # Shift y into [1, 2] to prevent RMSPE instability — see fixture docstring. + df = pd.read_csv(raw_fp) + df["y"] = df["y"] + 1.0 + df.to_csv(raw_fp, index=False) + + yield raw_fp + + +@pytest.mark.parametrize("second_seed_offset", [0, 1]) +@pytest.mark.parametrize("random_seed", [1919, 31]) +def test_preprocess(raw_dataset_fp: str, random_seed: int, second_seed_offset: int) -> None: + """Test reproducibility of train/validation/test splits. + + Args: + raw_dataset_fp (str): file path for data to be used as part of this test + random_seed(int): random seed integer to use for test + second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different + seed for the second run. + + Returns: None + """ + # define Ludwig model + model1 = LudwigModel(config=CONFIG) + + # preprocess the raw data set, specify seed + preprocessed_data1 = model1.preprocess(raw_dataset_fp, random_seed=random_seed) + + # perform second preprocess operation + model2 = LudwigModel(config=CONFIG) + # preprocess same raw data set with same seed + preprocessed_data2 = model2.preprocess(raw_dataset_fp, random_seed=random_seed + second_seed_offset) + + # confirm data splits are reproducible + for i in range(3): + for k in preprocessed_data1[i].dataset: + if second_seed_offset == 0: + # same seeds should result in same output + assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert not np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + + +@pytest.mark.parametrize("random_seed", [1919, 31]) +def test_preprocess_ignore_torch_seed(raw_dataset_fp: str, random_seed: int) -> None: + """Test reproducibility of train/validation/test splits when an unrelated torch random operation is performed + between the Ludwig operations. + + Args: + raw_dataset_fp (str): file path for data to be used as part of this test + random_seed(int): random seed integer to use for test + + Returns: None + """ + # define Ludwig model + model1 = LudwigModel(config=CONFIG) + + # preprocess the raw data set, specify seed + preprocessed_data1 = model1.preprocess(raw_dataset_fp, random_seed=random_seed) + + # invoke torch random functions with unrelated seed to + # see if it affects Ludwig reproducibility + torch.manual_seed(random_seed + 5) + torch.rand((5,)) + + # define Ludwig model + model2 = LudwigModel(config=CONFIG) + # preprocess same raw data set with same seed + preprocessed_data2 = model2.preprocess(raw_dataset_fp, random_seed=random_seed) + + # confirm data splits are reproducible + for i in range(3): + for k in preprocessed_data1[i].dataset: + # same seeds should result in same output + assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + + +@pytest.mark.parametrize("second_seed_offset", [0, 1]) +@pytest.mark.parametrize("random_seed", [1919, 31]) +def test_train(raw_dataset_fp: str, random_seed: int, second_seed_offset: int) -> None: + """Test reproducibility of training API. + + Args: + raw_dataset_fp (str): file path for data to be used as part of this test + random_seed(int): random seed integer to use for test + second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different + seed for the second run. + + Returns: None + """ + # perform first model training run + model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + training_statistics1, preprocessed_data1, _ = model1.train( + dataset=raw_dataset_fp, random_seed=random_seed, skip_save_progress=True, skip_save_processed_input=True + ) + + # perform second model training run + model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + training_statistics2, preprocessed_data2, _ = model2.train( + dataset=raw_dataset_fp, + random_seed=random_seed + second_seed_offset, + skip_save_progress=True, + skip_save_processed_input=True, + ) + + # confirm data splits are reproducible + for i in range(3): + for k in preprocessed_data1[i].dataset: + if second_seed_offset == 0: + # same seeds should result in same output + assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert not np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + + # confirm reproducibility/non-reproducibility of results + if second_seed_offset == 0: + # same seeds should result in same output + assert training_statistics1 == training_statistics2 + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert training_statistics1 != training_statistics2 + + +@pytest.mark.parametrize("random_seed", [1919, 31]) +def test_train_ignore_torch_seed(raw_dataset_fp: str, random_seed: int) -> None: + """Test reproducibility of training API when an unrelated torch random operation is performed between the + Ludwig operations. + + Args: + raw_dataset_fp (str): file path for data to be used as part of this test + random_seed(int): random seed integer to use for test + + Returns: None + """ + # define Ludwig model + model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + training_statistics1, preprocessed_data1, _ = model1.train( + dataset=raw_dataset_fp, random_seed=random_seed, skip_save_progress=True, skip_save_processed_input=True + ) + + # invoke torch random functions with unrelated seed to + # see if it affects Ludwig reproducibility + torch.manual_seed(random_seed + 5) + torch.rand((5,)) + + model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + training_statistics2, preprocessed_data2, _ = model2.train( + dataset=raw_dataset_fp, + random_seed=random_seed, + skip_save_progress=True, + skip_save_processed_input=True, + ) + + # confirm data splits are reproducible + for i in range(3): + for k in preprocessed_data1[i].dataset: + # same seeds should result in same output + assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + + # confirm reproducibility/non-reproducibility of results + assert training_statistics1 == training_statistics2 + + +@pytest.mark.parametrize("second_seed_offset", [0, 1]) +@pytest.mark.parametrize("random_seed", [1919, 31]) +def test_experiment(raw_dataset_fp: str, random_seed: int, second_seed_offset: int) -> None: + """Test reproducibility of experiment API. + + Args: + raw_dataset_fp (str): file path for data to be used as part of this test + random_seed(int): random seed integer to use for test + second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different + seed for the second run. + + Returns: None + """ + # perform first model experiment + model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + evaluation_statistics1, training_statistics1, preprocessed_data1, _ = model1.experiment( + dataset=raw_dataset_fp, random_seed=random_seed, skip_save_processed_input=True + ) + + # perform second model experiment + model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + evaluation_statistics2, training_statistics2, preprocessed_data2, _ = model2.experiment( + dataset=raw_dataset_fp, random_seed=random_seed + second_seed_offset, skip_save_processed_input=True + ) + + # confirm data splits are reproducible + for i in range(3): + for k in preprocessed_data1[i].dataset: + if second_seed_offset == 0: + # same seeds should result in same output + assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert not np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + + # confirm results reproducibility/non-reproducibility of results + if second_seed_offset == 0: + # same seeds should result in same output + assert training_statistics1 == training_statistics2 + assert evaluation_statistics1 == evaluation_statistics2 + else: + # non-zero second_seed_offset uses different seeds and should result in different output + assert training_statistics1 != training_statistics2 + assert evaluation_statistics1 != evaluation_statistics2 + + +@pytest.mark.parametrize("random_seed", [1919, 31]) +def test_experiment_ignore_torch_seed(raw_dataset_fp: str, random_seed: int) -> None: + """Ludwig's seeding must isolate a run from any unrelated torch RNG state. + + This test verifies that calling ``torch.manual_seed()`` and ``torch.rand()`` + *between* two Ludwig experiment runs does not affect the second run's results. + Ludwig reseeds its own RNG at the start of each run, so the global torch + state set between runs should be invisible to it. + + ## Flakiness history and why the dataset fixture matters + + An earlier version of the ``raw_dataset_fp`` fixture produced ``y`` values + in ``[0, 1]``. The ``root_mean_squared_percentage_error`` (RMSPE) metric + divides by ``y^2``, making it wildly sensitive to near-zero targets: with + ``y ≈ 0.045`` a sub-millionth prediction difference (within the noise of + CPU BLAS thread scheduling) produced RMSPE values that differed by >30 % + between runs. This caused the ``assert evaluation_statistics1 == + evaluation_statistics2`` line below to fail intermittently in CI even + though training statistics matched exactly and all other eval metrics + agreed to 6+ decimal places. + + The fixture now shifts ``y`` into ``[1, 2]`` so RMSPE is well-conditioned. + See the ``raw_dataset_fp`` fixture docstring for the full analysis. + + ## What is and is not being tested + + - ``training_statistics`` are computed *during* training under Ludwig's + seeded RNG, so they are bit-exact between runs. + - ``evaluation_statistics`` are computed in a final ``evaluate()`` call + *after* training completes. That pass is also deterministic once the + metric is numerically stable (i.e. targets are away from zero). + - The unrelated ``torch.manual_seed`` / ``torch.rand`` calls between the + two runs should have zero effect on model2 — that is the core assertion. + """ + # Run 1: train and evaluate with the specified Ludwig seed. + model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + evaluation_statistics1, training_statistics1, preprocessed_data1, _ = model1.experiment( + dataset=raw_dataset_fp, random_seed=random_seed, skip_save_processed_input=True + ) + + # Simulate an unrelated torch RNG operation between the two Ludwig runs. + # If Ludwig's seeding is correct, model2 must be unaffected by this. + torch.manual_seed(random_seed + 5) + torch.rand((5,)) + + # Run 2: same config and same Ludwig seed — must produce identical results. + model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN) + evaluation_statistics2, training_statistics2, preprocessed_data2, _ = model2.experiment( + dataset=raw_dataset_fp, random_seed=random_seed, skip_save_processed_input=True + ) + + # Same seed → same train/val/test split. + for i in range(3): + for k in preprocessed_data1[i].dataset: + assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k]) + + # Same seed → identical training curve and final evaluation metrics. + assert training_statistics1 == training_statistics2 + assert evaluation_statistics1 == evaluation_statistics2 diff --git a/tests/integration_tests/test_sequence_decoders.py b/tests/integration_tests/test_sequence_decoders.py new file mode 100644 index 0000000..2cdcf63 --- /dev/null +++ b/tests/integration_tests/test_sequence_decoders.py @@ -0,0 +1,54 @@ +import os + +import pytest + +from ludwig.constants import ( + BATCH_SIZE, + DECODER, + ENCODER, + INPUT_FEATURES, + OUTPUT_FEATURES, + SEQUENCE, + TEXT, + TRAINER, + TYPE, +) +from tests.integration_tests.utils import ( + create_data_set_to_use, + generate_data, + RAY_BACKEND_CONFIG, + sequence_feature, + text_feature, + train_with_backend, +) + +pytestmark = pytest.mark.integration_tests_g + + +@pytest.mark.slow +@pytest.mark.parametrize("feature_type,feature_gen", [(TEXT, text_feature), (SEQUENCE, sequence_feature)]) +@pytest.mark.parametrize("decoder_type", ["generator", "tagger"]) +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_sequence_decoder_predictions(tmpdir, csv_filename, ray_cluster_2cpu, feature_type, feature_gen, decoder_type): + """Test that sequence decoders return the correct successfully predict.""" + input_feature = feature_gen() + output_feature = feature_gen(output_feature=True) + + input_feature[ENCODER] = {TYPE: "embed", "reduce_output": None} + output_feature[DECODER] = {TYPE: decoder_type} + + dataset_path = generate_data( + input_features=[input_feature], + output_features=[output_feature], + filename=os.path.join(tmpdir, csv_filename), + ) + dataset_path = create_data_set_to_use("csv", dataset_path) + + config = {INPUT_FEATURES: [input_feature], TRAINER: {"train_steps": 1, BATCH_SIZE: 4}} + + # Ensure that the decoder outputs the correct predictions through both the default and feature-specific configs. + config[OUTPUT_FEATURES] = [output_feature] + + # Test with decoder in output feature config + train_with_backend(RAY_BACKEND_CONFIG, config=config, dataset=dataset_path) diff --git a/tests/integration_tests/test_sequence_encoders.py b/tests/integration_tests/test_sequence_encoders.py new file mode 100644 index 0000000..edd776b --- /dev/null +++ b/tests/integration_tests/test_sequence_encoders.py @@ -0,0 +1,182 @@ +import logging + +import numpy as np +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, SEQUENCE +from ludwig.encoders.registry import get_encoder_cls +from tests.integration_tests.utils import ENCODERS + +logger = logging.getLogger(__name__) + +TEST_VOCAB_SIZE = 132 +TEST_HIDDEN_SIZE = 32 +TEST_STATE_SIZE = 16 +TEST_EMBEDDING_SIZE = 64 +TEST_NUM_FILTERS = 24 +BATCH_SIZE = 2 +SEQ_SIZE = 10 +PARALLEL_CNN_LAYERS = 4 + +# encoder parameters combinations tested +encoder_parameters = { + "vocab": [str(i) for i in range(TEST_VOCAB_SIZE)], + "embedding_size": TEST_EMBEDDING_SIZE, + "hidden_size": TEST_HIDDEN_SIZE, + "num_filters": TEST_NUM_FILTERS, + "num_layers": 1, + "max_sequence_length": SEQ_SIZE, + "state_size": TEST_STATE_SIZE, + "cell_type": "rnn", + "should_embed": True, + "dropout": 0.0, + "norm": None, + "reduce_output": None, +} + + +@pytest.fixture(scope="module") +def input_sequence() -> torch.Tensor: + # generates a realistic looking synthetic sequence tensor, i.e. + # each sequence will have non-zero tokens at the beginning with + # trailing zero tokens, including a max length token with a single + # zero token at the end. Example: + # [ + # [3, 5, 6, 0, 0, 0], + # [10, 11, 12, 13, 14, 0], # max length sequence + # [32, 0, 0, 0, 0, 0] # minimum length sequence + # ] + input_tensor = torch.zeros([BATCH_SIZE, SEQ_SIZE], dtype=torch.int32) + sequence_lengths = np.random.randint(1, SEQ_SIZE, size=BATCH_SIZE) + for i in range(input_tensor.shape[0]): + input_tensor[i, : sequence_lengths[i]] = torch.tensor( + np.random.randint(2, TEST_VOCAB_SIZE, size=sequence_lengths[i]) + ) + + if torch.cuda.is_available(): + input_tensor = input_tensor.cuda() + + return input_tensor + + +@pytest.mark.parametrize("enc_reduce_output", [None, "sum"]) +@pytest.mark.parametrize("enc_norm", [None, "batch", "layer"]) +@pytest.mark.parametrize("enc_num_layers", [1, 2]) +@pytest.mark.parametrize("enc_dropout", [0, 0.2]) +@pytest.mark.parametrize("enc_cell_type", ["rnn", "gru", "lstm"]) +@pytest.mark.parametrize("enc_encoder", ENCODERS + ["passthrough"]) +def test_sequence_encoders( + enc_encoder: str, + enc_cell_type: str, + enc_dropout: float, + enc_num_layers: int, + enc_norm: None | str, + enc_reduce_output: None | str, + input_sequence: torch.Tensor, +): + # update encoder parameters for specific unit test case + encoder_parameters["cell_type"] = enc_cell_type + encoder_parameters["dropout"] = enc_dropout + encoder_parameters["num_layers"] = enc_num_layers + encoder_parameters["norm"] = enc_norm + encoder_parameters["reduce_output"] = enc_reduce_output + + # retrieve encoder to test + encoder_obj = get_encoder_cls(SEQUENCE, enc_encoder)(**encoder_parameters) + if torch.cuda.is_available(): + encoder_obj = encoder_obj.cuda() + + encoder_out = encoder_obj(input_sequence) + + assert ENCODER_OUTPUT in encoder_out + assert isinstance(encoder_out[ENCODER_OUTPUT], torch.Tensor) + + if enc_encoder == "parallel_cnn" or enc_encoder == "stacked_parallel_cnn": + number_parallel_cnn_layers = PARALLEL_CNN_LAYERS + output_dimension = encoder_parameters["num_filters"] * number_parallel_cnn_layers + assert ( + encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, output_dimension) + if enc_reduce_output is None + else (BATCH_SIZE, output_dimension) + ) + + elif enc_encoder == "rnn": + assert ( + encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, TEST_STATE_SIZE) + if enc_reduce_output is None + else (BATCH_SIZE, TEST_STATE_SIZE) + ) + + assert ENCODER_OUTPUT_STATE in encoder_out + if enc_cell_type == "lstm": + assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], tuple) + assert isinstance(encoder_out[ENCODER_OUTPUT_STATE][0], torch.Tensor) + assert isinstance(encoder_out[ENCODER_OUTPUT_STATE][1], torch.Tensor) + assert encoder_out[ENCODER_OUTPUT_STATE][0].shape == (BATCH_SIZE, TEST_STATE_SIZE) + assert encoder_out[ENCODER_OUTPUT_STATE][1].shape == (BATCH_SIZE, TEST_STATE_SIZE) + else: + assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], torch.Tensor) + assert encoder_out[ENCODER_OUTPUT_STATE].shape == (BATCH_SIZE, TEST_STATE_SIZE) + + elif enc_encoder == "cnnrnn": + assert encoder_out[ENCODER_OUTPUT].shape[1:] == encoder_obj.output_shape + assert ENCODER_OUTPUT_STATE in encoder_out + + if enc_cell_type == "lstm": + assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], tuple) + assert encoder_out[ENCODER_OUTPUT_STATE][0].shape == (BATCH_SIZE, TEST_STATE_SIZE) + assert encoder_out[ENCODER_OUTPUT_STATE][1].shape == (BATCH_SIZE, TEST_STATE_SIZE) + else: + assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], torch.Tensor) + assert encoder_out[ENCODER_OUTPUT_STATE].shape == (BATCH_SIZE, TEST_STATE_SIZE) + + elif enc_encoder == "stacked_cnn": + assert encoder_out[ENCODER_OUTPUT].shape[1:] == encoder_obj.output_shape + + elif enc_encoder == "embed": + assert ( + encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, TEST_EMBEDDING_SIZE) + if enc_reduce_output is None + else (BATCH_SIZE, TEST_EMBEDDING_SIZE) + ) + + elif enc_encoder == "transformer": + assert encoder_out[ENCODER_OUTPUT].shape[1:] == encoder_obj.output_shape + + elif enc_encoder == "passthrough": + assert ( + encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, 1) + if enc_reduce_output is None + else (BATCH_SIZE, 1) + ) + + else: + raise ValueError(f"{enc_encoder} is an invalid encoder specification") + + +@pytest.mark.parametrize("enc_reduce_output", [None, "sum", "last", "mean", "max", "concat"]) +def test_passthrough_encoder(enc_reduce_output, input_sequence): + encoder_parameters = {"reduce_output": enc_reduce_output} + + # retrieve encoder to test + encoder_obj = get_encoder_cls(SEQUENCE, "passthrough")(**encoder_parameters) + + encoder_out = encoder_obj(input_sequence) + + assert ENCODER_OUTPUT in encoder_out + assert ( + encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, 1) if enc_reduce_output is None else (BATCH_SIZE, 1) + ) + + +# test to ensure correct handling of vocab_size and embedding_size specifications +@pytest.mark.parametrize("enc_embedding_size", [TEST_VOCAB_SIZE - 8, TEST_VOCAB_SIZE, TEST_VOCAB_SIZE + 8]) +def test_sequence_embed_encoder(enc_embedding_size: int, input_sequence: torch.Tensor) -> None: + encoder_parameters["embedding_size"] = enc_embedding_size + + encoder_obj = get_encoder_cls(SEQUENCE, "embed")(**encoder_parameters) + + encoder_out = encoder_obj(input_sequence) + + assert encoder_out[ENCODER_OUTPUT].size()[1:] == encoder_obj.output_shape diff --git a/tests/integration_tests/test_sequence_features.py b/tests/integration_tests/test_sequence_features.py new file mode 100644 index 0000000..a477af0 --- /dev/null +++ b/tests/integration_tests/test_sequence_features.py @@ -0,0 +1,171 @@ +import contextlib +import copy +from io import StringIO + +import pandas as pd +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import DECODER, ENCODER_OUTPUT_STATE, LOGITS +from ludwig.data.dataset_synthesizer import build_synthetic_dataset +from ludwig.data.preprocessing import preprocess_for_training +from ludwig.features.feature_registries import update_config_with_metadata +from tests.integration_tests.utils import generate_data, run_experiment, sequence_feature + +# +# this test is focused on testing input sequence features with all encoders +# and output sequence feature with Generator decoder. Except for specified +# configuration parameters all other parameters assume default values. +# + +TEST_VOCAB_SIZE = 132 +TEST_HIDDEN_SIZE = 32 +TEST_STATE_SIZE = 8 +TEST_EMBEDDING_SIZE = 64 +TEST_NUM_FILTERS = 24 + + +# generates dataset that can be used for rest of test +@pytest.fixture(scope="module") +def generate_sequence_training_data(): + input_features = [ + sequence_feature( + encoder={ + "vocab_size": TEST_VOCAB_SIZE, + "embedding_size": TEST_EMBEDDING_SIZE, + "state_size": TEST_STATE_SIZE, + "hidden_size": TEST_HIDDEN_SIZE, + "num_filters": TEST_NUM_FILTERS, + "min_len": 5, + "max_len": 10, + "type": "rnn", + "cell_type": "lstm", + } + ) + ] + + output_features = [ + sequence_feature( + decoder={"type": "generator", "min_len": 5, "max_len": 10, "cell_type": "lstm", "attention": "bahdanau"} + ) + ] + + # generate synthetic data set testing + dataset = build_synthetic_dataset(150, copy.deepcopy(input_features) + copy.deepcopy(output_features)) + raw_data = "\n".join([r[0] + "," + r[1] for r in dataset]) + df = pd.read_csv(StringIO(raw_data)) + + return df, input_features, output_features + + +# setups up minimal number of data structures required to support initialized +# input and output features. The function returns initialized LudwigModel +# and batcher for training dataset +@contextlib.contextmanager +def setup_model_scaffolding(raw_df, input_features, output_features): + # setup input feature for testing + config = {"input_features": input_features, "output_features": output_features} + + # setup model scaffolding to for testing + model = LudwigModel(config) + training_set, _, _, training_set_metadata = preprocess_for_training( + model.config, training_set=raw_df, skip_save_processed_input=True + ) + model.training_set_metadata = training_set_metadata + update_config_with_metadata(model.config_obj, training_set_metadata) + model.model = model.create_model(model.config_obj) + + # setup batcher to go through synthetic data + with training_set.initialize_batcher() as batcher: + yield model, batcher + + +# TODO(#1333): Refactor this test once torch sequence generator work is complete. +# - Tests may be covered by other smaller scoped unit tests. +# +# tests output feature sequence with `Generator` decoder +# pytest parameters +# dec_cell_type: decoder cell type +# combiner_output_shapes: is a 2-tuple specifies the possible types of +# tensors that the combiner may generate for sequences. +# combiner_output_shapes[0]: specifies shape for hidden key +# combiner_output_shapes[1]: is either None or 1 or 2-tuple representing +# the encoder_output_state key. None: no encoder_output_state key, +# 1-tuple: generate tf.Tensor, 2-tuple: generate list with 2 tf.Tensors +# TODO(Justin): Move these to test_sequence_generator unit tests, and reintroduce decoder attention, beam_width, and +# num_layers when these are reimplemented. +@pytest.mark.parametrize( + "dec_cell_type,combiner_output_shapes", + [ + ("lstm", ((128, 10, TEST_STATE_SIZE), None)), + ("rnn", ((128, 10, TEST_STATE_SIZE), ((128, TEST_STATE_SIZE), (128, TEST_STATE_SIZE)))), + ("gru", ((128, 10, TEST_STATE_SIZE), ((128, TEST_STATE_SIZE),))), + ], + ids=["lstm_no_state", "rnn_dual_state", "gru_single_state"], +) +def test_sequence_decoders( + dec_cell_type, + combiner_output_shapes, + generate_sequence_training_data, +): + # retrieve pre-computed dataset and features + raw_df = generate_sequence_training_data[0] + input_features = generate_sequence_training_data[1] + output_features = generate_sequence_training_data[2] + output_feature_name = output_features[0]["name"] + output_features[0][DECODER]["cell_type"] = dec_cell_type + + with setup_model_scaffolding(raw_df, input_features, output_features) as (model, _): + # generate synthetic encoder_output tensors and make it look like + # it came out of the combiner + encoder_output = torch.randn(combiner_output_shapes[0]) + combiner_outputs = {"hidden": encoder_output} + + if combiner_output_shapes[1] is not None: + if len(combiner_output_shapes[1]) > 1: + encoder_output_state = ( + torch.randn(combiner_output_shapes[1][0]), + torch.randn(combiner_output_shapes[1][1]), + ) + else: + encoder_output_state = torch.randn(combiner_output_shapes[1][0]) + + combiner_outputs[ENCODER_OUTPUT_STATE] = encoder_output_state + + decoder = model.model.output_features.get(output_feature_name).decoder_obj + decoder_out = decoder(combiner_outputs) + + # gather expected components of the shape + batch_size = combiner_outputs["hidden"].shape[0] + seq_size = output_features[0][DECODER]["max_len"] + 2 # For start and stop symbols. + vocab_size = model.config_obj.output_features.to_list()[0][DECODER]["vocab_size"] + + # confirm shape and format of decoder output + assert list(decoder_out[LOGITS].size()) == [batch_size, seq_size, vocab_size] + + +# final sanity test. Checks a subset of sequence parameters +@pytest.mark.parametrize( + "enc_encoder,enc_cell_type,dec_cell_type", + [ + ("embed", "lstm", "lstm"), + ("rnn", "rnn", "gru"), + ("rnn", "gru", "rnn"), + ], + ids=["embed_lstm", "rnn_gru", "gru_rnn"], +) +def test_sequence_generator(enc_encoder, enc_cell_type, dec_cell_type, csv_filename): + # Define input and output features + input_features = [ + sequence_feature(encoder={"type": enc_encoder, "min_len": 5, "max_len": 10, "cell_type": enc_cell_type}) + ] + output_features = [ + sequence_feature(decoder={"type": "generator", "min_len": 5, "max_len": 10, "cell_type": dec_cell_type}) + ] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + # run the experiment + run_experiment(input_features, output_features, dataset=rel_path) diff --git a/tests/integration_tests/test_server.py b/tests/integration_tests/test_server.py new file mode 100644 index 0000000..942e420 --- /dev/null +++ b/tests/integration_tests/test_server.py @@ -0,0 +1,322 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import json +import logging +import os +import sys + +import numpy as np +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, DECODER, TRAINER +from ludwig.serve import server +from ludwig.utils.data_utils import read_csv +from tests.integration_tests.utils import ( + audio_feature, + category_feature, + generate_data, + image_feature, + LocalTestBackend, + number_feature, + text_feature, +) + +logger = logging.getLogger(__name__) + +ALL_FEATURES_PRESENT_ERROR = "Data received does not contain all input features" + +try: + from starlette.testclient import TestClient +except ImportError: + logger.error( + " fastapi and other serving dependencies are not installed. " + "In order to install all serving dependencies run " + "pip install ludwig[serve]" + ) + sys.exit(-1) + + +def train_and_predict_model(input_features, output_features, data_csv, output_directory): + """Helper method to avoid code repetition for training a model and using it for prediction. + + :param input_features: input schema + :param output_features: output schema + :param data_csv: path to data + :param output_directory: model output directory + :return: None + """ + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + } + model = LudwigModel(config, backend=LocalTestBackend()) + model.train( + dataset=data_csv, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + output_directory=output_directory, + ) + model.predict(dataset=data_csv, output_directory=output_directory) + return model + + +def train_and_predict_model_with_stratified_split(input_features, output_features, data_csv, output_directory): + """Same as above, but with stratified split.""" + print(f'output_features[0]["column"]: {output_features[0]["column"]}') + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1, BATCH_SIZE: 128}, + "preprocessing": { + "split": {"column": output_features[0]["column"], "probabilities": [0.7, 0.1, 0.2], "type": "stratify"}, + }, + } + model = LudwigModel(config, backend=LocalTestBackend()) + model.train( + dataset=data_csv, + skip_save_processed_input=True, + skip_save_progress=True, + skip_save_unprocessed_output=True, + output_directory=output_directory, + ) + model.predict(dataset=data_csv, output_directory=output_directory) + return model + + +def output_keys_for(output_features): + keys = [] + for feature in output_features: + name = feature["name"] + if feature["type"] == "category": + keys.append(f"{name}_predictions") + keys.append(f"{name}_probability") + keys.append(f"{name}_probabilities") + for category in feature[DECODER]["idx2str"]: + keys.append(f"{name}_probabilities_{category}") + + elif feature["type"] == "number": + keys.append(f"{name}_predictions") + else: + raise NotImplementedError + return keys + + +def convert_to_form(entry): + data = {} + files = [] + for k, v in entry.items(): + if isinstance(v, str) and os.path.exists(v): + file = open(v, "rb") + files.append((k, (v, file.read(), "application/octet-stream"))) + else: + data[k] = v + return data, files + + +def convert_to_batch_form(data_df): + data = data_df.to_dict(orient="split") + files = { + "dataset": (None, json.dumps(data), "application/json"), + } + for row in data["data"]: + for v in row: + if isinstance(v, str) and os.path.exists(v) and v not in files: + files[v] = (v, open(v, "rb"), "application/octet-stream") + return files + + +def test_server_integration_with_images(tmpdir): + # Image Inputs + image_dest_folder = os.path.join(tmpdir, "generated_images") + + # Resnet encoder + input_features = [ + image_feature( + folder=image_dest_folder, + encoder={"output_size": 16, "num_filters": 8}, + preprocessing={"in_memory": True, "height": 32, "width": 32, "num_channels": 3}, + ), + text_feature(encoder={"type": "embed", "min_len": 1}), + number_feature(normalization="zscore"), + ] + output_features = [category_feature(decoder={"vocab_size": 4}), number_feature()] + + np.random.seed(123) # reproducible synthetic data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + model = train_and_predict_model(input_features, output_features, data_csv=rel_path, output_directory=tmpdir) + + app = server(model) + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + + response = client.post("/predict") + # expect the HTTP 400 error code for this situation + assert response.status_code == 400 + assert ALL_FEATURES_PRESENT_ERROR in str(response.json()) + + data_df = read_csv(rel_path) + + # One-off prediction + first_entry = data_df.T.to_dict()[0] + data, files = convert_to_form(first_entry) + server_response = client.post("/predict", data=data, files=files) + assert server_response.status_code == 200 + server_response = server_response.json() + + server_response_keys = sorted(list(server_response.keys())) + assert server_response_keys == sorted(output_keys_for(output_features)) + + model_output, _ = model.predict(dataset=[first_entry], data_format=dict) + model_output = model_output.to_dict("records")[0] + assert model_output == server_response + + # Batch prediction + assert len(data_df) > 1 + files = convert_to_batch_form(data_df) + server_response = client.post("/batch_predict", files=files) + assert server_response.status_code == 200 + server_response = server_response.json() + + server_response_keys = sorted(server_response["columns"]) + assert server_response_keys == sorted(output_keys_for(output_features)) + assert len(data_df) == len(server_response["data"]) + + model_output, _ = model.predict(dataset=data_df) + model_output = model_output.to_dict("split") + assert model_output == server_response + + +def test_server_integration_with_stratified_split(tmpdir): + input_features = [ + text_feature(encoder={"type": "embed", "min_len": 1}), + number_feature(normalization="zscore"), + ] + output_features = [category_feature(decoder={"vocab_size": 4})] + + np.random.seed(123) # reproducible synthetic data + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv"), num_examples=50) + + model = train_and_predict_model_with_stratified_split( + input_features, output_features, data_csv=rel_path, output_directory=tmpdir + ) + + app = server(model) + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + + response = client.post("/predict") + # expect the HTTP 400 error code for this situation + assert response.status_code == 400 + assert ALL_FEATURES_PRESENT_ERROR in str(response.json()) + + data_df = read_csv(rel_path) + + # One-off prediction + first_entry = data_df.T.to_dict()[0] + data, files = convert_to_form(first_entry) + server_response = client.post("/predict", data=data, files=files) + assert server_response.status_code == 200 + server_response = server_response.json() + + server_response_keys = sorted(list(server_response.keys())) + assert server_response_keys == sorted(output_keys_for(output_features)) + + model_output, _ = model.predict(dataset=[first_entry], data_format=dict) + model_output = model_output.to_dict("records")[0] + assert model_output == server_response + + # Batch prediction + assert len(data_df) > 1 + files = convert_to_batch_form(data_df) + server_response = client.post("/batch_predict", files=files) + assert server_response.status_code == 200 + server_response = server_response.json() + + server_response_keys = sorted(server_response["columns"]) + assert server_response_keys == sorted(output_keys_for(output_features)) + assert len(data_df) == len(server_response["data"]) + + model_output, _ = model.predict(dataset=data_df) + model_output = model_output.to_dict("split") + assert model_output == server_response + + +@pytest.mark.parametrize("single_record", [False, True]) +def test_server_integration_with_audio(single_record, tmpdir): + # Audio Inputs + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + + # Resnet encoder + input_features = [ + audio_feature( + folder=audio_dest_folder, + ), + text_feature(encoder={"type": "embed", "min_len": 1}), + number_feature(normalization="zscore"), + ] + output_features = [category_feature(decoder={"vocab_size": 4}), number_feature()] + + rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + + model = train_and_predict_model(input_features, output_features, data_csv=rel_path, output_directory=tmpdir) + + app = server(model) + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + + response = client.post("/predict") + # expect the HTTP 400 error code for this situation + assert response.status_code == 400 + assert ALL_FEATURES_PRESENT_ERROR in str(response.json()) + + data_df = read_csv(rel_path) + + if single_record: + # Single record prediction + first_entry = data_df.T.to_dict()[0] + data, files = convert_to_form(first_entry) + server_response = client.post("/predict", data=data, files=files) + assert server_response.status_code == 200 + server_response = server_response.json() + + server_response_keys = sorted(list(server_response.keys())) + assert server_response_keys == sorted(output_keys_for(output_features)) + + model_output, _ = model.predict(dataset=[first_entry], data_format=dict) + model_output = model_output.to_dict("records")[0] + assert model_output == server_response + else: + # Batch prediction + assert len(data_df) > 1 + files = convert_to_batch_form(data_df) + server_response = client.post("/batch_predict", files=files) + assert server_response.status_code == 200 + server_response = server_response.json() + + server_response_keys = sorted(server_response["columns"]) + assert server_response_keys == sorted(output_keys_for(output_features)) + assert len(data_df) == len(server_response["data"]) + + model_output, _ = model.predict(dataset=data_df) + model_output = model_output.to_dict("split") + assert model_output == server_response diff --git a/tests/integration_tests/test_simple_features.py b/tests/integration_tests/test_simple_features.py new file mode 100644 index 0000000..96ae55f --- /dev/null +++ b/tests/integration_tests/test_simple_features.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import logging +import os + +import pandas as pd +import pytest + +from ludwig.constants import NAME +from tests.integration_tests.utils import ( + bag_feature, + binary_feature, + category_feature, + generate_data, + number_feature, + run_experiment, + sequence_feature, + set_feature, + text_feature, + vector_feature, +) + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + + +@pytest.mark.parametrize( + "input_test_feature, output_test_feature, output_loss_parameter", + [ + # number features + (number_feature(), number_feature(), None), + (number_feature(normalization="minmax"), number_feature(), {"loss": {"type": "mean_squared_error"}}), + (number_feature(normalization="zscore"), number_feature(), {"loss": {"type": "mean_absolute_error"}}), + # binary feature + (binary_feature(), binary_feature(), None), + # Categorical feature + (category_feature(), category_feature(output_feature=True), None), + (category_feature(), category_feature(output_feature=True), {"loss": {"type": "softmax_cross_entropy"}}), + ], +) +def test_feature(input_test_feature, output_test_feature, output_loss_parameter, csv_filename): + input_features = [input_test_feature] + + of_test_feature = output_test_feature + if output_loss_parameter is not None: + of_test_feature.update(output_loss_parameter) + output_features = [of_test_feature] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename, 100) + + run_experiment(input_features, output_features, dataset=rel_path) + + +@pytest.mark.parametrize( + "input_test_feature, output_test_feature", + [ + ([category_feature()], [binary_feature(), binary_feature()]), + ( + [category_feature()], + [category_feature(decoder={"vocab_size": 5}), category_feature(decoder={"vocab_size": 7})], + ), + ([category_feature()], [number_feature(), number_feature()]), + ( + [category_feature()], + [sequence_feature(decoder={"vocab_size": 5}), sequence_feature(decoder={"vocab_size": 7})], + ), + ( + [set_feature(encoder={"vocab_size": 5})], + [set_feature(decoder={"vocab_size": 5}), set_feature(decoder={"vocab_size": 7})], + ), + ([category_feature()], [text_feature(decoder={"vocab_size": 5}), text_feature(decoder={"vocab_size": 7})]), + ([category_feature()], [vector_feature(), vector_feature()]), + ([vector_feature()], [vector_feature(), vector_feature()]), + ([bag_feature()], [vector_feature(), vector_feature()]), + ], +) +def test_feature_multiple_outputs(input_test_feature, output_test_feature, csv_filename): + # Generate test data + rel_path = generate_data(input_test_feature, output_test_feature, csv_filename, 100) + + run_experiment(input_test_feature, output_test_feature, dataset=rel_path) + + +def test_category_int_dtype(tmpdir): + feature = category_feature() + input_features = [feature] + output_features = [binary_feature()] + + csv_fname = generate_data(input_features, output_features, os.path.join(tmpdir, "dataset.csv")) + df = pd.read_csv(csv_fname) + + distinct_values = df[feature[NAME]].drop_duplicates().values + value_map = {v: idx for idx, v in enumerate(distinct_values)} + df[feature[NAME]] = df[feature[NAME]].map(lambda x: value_map[x]) + + run_experiment(input_features, output_features, dataset=df) diff --git a/tests/integration_tests/test_timeseries_feature.py b/tests/integration_tests/test_timeseries_feature.py new file mode 100644 index 0000000..d8d2bb4 --- /dev/null +++ b/tests/integration_tests/test_timeseries_feature.py @@ -0,0 +1,85 @@ +import numpy as np +import pandas as pd +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import COLUMN, ENCODER_OUTPUT, INPUT_FEATURES, OUTPUT_FEATURES +from ludwig.features.timeseries_feature import TimeseriesInputFeature +from ludwig.schema.features.timeseries_feature import TimeseriesInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from tests.integration_tests.utils import number_feature, timeseries_feature + +BATCH_SIZE = 2 +SEQ_SIZE = 10 +DEFAULT_OUTPUT_SIZE = 4 + + +@pytest.mark.parametrize("enc_encoder", ["stacked_cnn", "rnn", "passthrough"]) +def test_timeseries_feature(enc_encoder): + # synthetic time series tensor + timeseries_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE], dtype=torch.float32) + + # generate feature config + timeseries_feature_config = timeseries_feature( + encoder={ + "type": enc_encoder, + "max_len": SEQ_SIZE, + "fc_layers": [{"output_size": DEFAULT_OUTPUT_SIZE}], + # simulated parameters determined by pre-processing + "max_sequence_length": SEQ_SIZE, + } + ) + + # instantiate input feature object + timeseries_feature_config, _ = load_config_with_kwargs(TimeseriesInputFeatureConfig, timeseries_feature_config) + timeseries_input_feature = TimeseriesInputFeature(timeseries_feature_config) + + # pass synthetic tensor through input feature + encoder_output = timeseries_input_feature(timeseries_tensor) + + # confirm correctness of the encoder output + assert isinstance(encoder_output, dict) + assert ENCODER_OUTPUT in encoder_output + assert isinstance(encoder_output[ENCODER_OUTPUT], torch.Tensor) + if enc_encoder == "passthrough": + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, 1) + else: + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, DEFAULT_OUTPUT_SIZE) + + +def test_timeseries_preprocessing_with_nan(): + config = { + "input_features": [timeseries_feature(preprocessing={"padding_value": 42})], + "output_features": [number_feature()], + } + + # generate synthetic data + data = { + config[INPUT_FEATURES][0][COLUMN]: [ + "1.53 2.3 NaN 6.4 3 ", + "1.53 2.3 2 ", + "1.53 NaN 3 2 ", + ], + config[OUTPUT_FEATURES][0][COLUMN]: [1.0, 2.0, 3.0], + } + df = pd.DataFrame(data) + + model = LudwigModel(config) + ds = model.preprocess(df) + out_df = ds.training_set.to_df() + + assert len(out_df.columns) == len(df.columns) + + expected_df = pd.DataFrame( + [ + [np.array([1.53, 2.3, 42.0, 6.4, 3.0]), 1.0], + [np.array([1.53, 2.3, 2.0, 42.0, 42.0]), 2.0], + [np.array([1.53, 42.0, 3.0, 2.0, 42.0]), 3.0], + ], + columns=out_df.columns.to_list(), + ) + + for row1, row2 in zip(out_df.values, expected_df.values): + assert np.allclose(row1[0], row2[0]) + assert row1[1] == row2[1] diff --git a/tests/integration_tests/test_trainer.py b/tests/integration_tests/test_trainer.py new file mode 100644 index 0000000..34bf8ca --- /dev/null +++ b/tests/integration_tests/test_trainer.py @@ -0,0 +1,294 @@ +import logging +import os +import shutil +from unittest import mock + +import pytest +import torch +from packaging.version import parse as parse_version + +from ludwig.api import LudwigModel +from ludwig.constants import ( + BATCH_SIZE, + EFFECTIVE_BATCH_SIZE, + EPOCHS, + EVAL_BATCH_SIZE, + INPUT_FEATURES, + MAX_BATCH_SIZE_DATASET_FRACTION, + OUTPUT_FEATURES, + TRAINER, +) +from ludwig.globals import MODEL_FILE_NAME +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + generate_data, + LocalTestBackend, + number_feature, + sequence_feature, + text_feature, + vector_feature, +) + + +def test_tune_learning_rate(tmpdir): + config = { + INPUT_FEATURES: [text_feature(), binary_feature()], + OUTPUT_FEATURES: [binary_feature()], + TRAINER: { + "train_steps": 1, + BATCH_SIZE: 128, + "learning_rate": "auto", + }, + } + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(config[INPUT_FEATURES], config[OUTPUT_FEATURES], csv_filename) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO) + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir) + + assert model.config_obj.trainer.learning_rate == 0.0001 + + +@pytest.mark.parametrize( + "is_cpu,effective_batch_size,eval_batch_size", + [ + (True, "auto", "auto"), + (False, 256, 128), + (True, "auto", None), + ], + ids=["cpu_auto", "gpu_fixed", "cpu_no_eval_bs"], +) +def test_ecd_tune_batch_size_and_lr(tmpdir, eval_batch_size, effective_batch_size, is_cpu): + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + number_feature(), + binary_feature(), + vector_feature(), + ] + + num_samples = 30 + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename, num_examples=num_samples) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + trainer = { + EPOCHS: 2, + EFFECTIVE_BATCH_SIZE: effective_batch_size, + BATCH_SIZE: "auto", + "gradient_accumulation_steps": "auto", + "learning_rate": "auto", + } + + if eval_batch_size: + trainer[EVAL_BATCH_SIZE] = eval_batch_size + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: trainer, + } + + model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO) + + # check preconditions + assert model.config_obj.trainer.effective_batch_size == effective_batch_size + assert model.config_obj.trainer.batch_size == "auto" + assert model.config_obj.trainer.gradient_accumulation_steps == "auto" + assert model.config_obj.trainer.eval_batch_size == eval_batch_size + assert model.config_obj.trainer.learning_rate == "auto" + + with mock.patch("ludwig.trainers.trainer.Trainer.is_cpu_training") as mock_fn: + mock_fn.return_value = is_cpu + _, _, output_directory = model.train( + training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir + ) + + def check_postconditions(model): + # check batch size + assert model.config_obj.trainer.effective_batch_size == effective_batch_size + assert model.config_obj.trainer.batch_size != "auto" + assert model.config_obj.trainer.batch_size > 1 + + # check gradient accumulation + assert model.config_obj.trainer.gradient_accumulation_steps != "auto" + if effective_batch_size == "auto": + assert model.config_obj.trainer.gradient_accumulation_steps == 1 + else: + batch_size = model.config_obj.trainer.batch_size + assert model.config_obj.trainer.gradient_accumulation_steps == effective_batch_size // batch_size + + # 4 is the largest possible batch size for this dataset (20% of dataset size) + assert model.config_obj.trainer.batch_size <= MAX_BATCH_SIZE_DATASET_FRACTION * num_samples + + assert model.config_obj.trainer.eval_batch_size != "auto" + assert model.config_obj.trainer.eval_batch_size > 1 + + if eval_batch_size in ("auto", None): + assert model.config_obj.trainer.batch_size == model.config_obj.trainer.eval_batch_size + else: + assert model.config_obj.trainer.eval_batch_size == eval_batch_size + + # check learning rate + assert model.config_obj.trainer.learning_rate == 0.0001 # has sequence feature + + check_postconditions(model) + + model = LudwigModel.load(os.path.join(output_directory, MODEL_FILE_NAME)) + + # loaded model should retain the tuned params + check_postconditions(model) + + +def test_changing_parameters_on_plateau(tmpdir): + input_features = [sequence_feature(encoder={"reduce_output": "sum"})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + EPOCHS: 2, + BATCH_SIZE: 128, + "learning_rate": 1.0, + "reduce_learning_rate_on_plateau": 1, + "increase_batch_size_on_plateau": 1, + }, + } + model = LudwigModel(config, backend=LocalTestBackend()) + + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir) + + +@pytest.mark.skipif(torch.cuda.device_count() == 0, reason="test requires at least 1 gpu") +@pytest.mark.skipif(not torch.cuda.is_available(), reason="test requires gpu support") +def test_mixed_precision(tmpdir): + input_features = [text_feature()] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + trainer = { + EPOCHS: 2, + "use_mixed_precision": True, + } + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: trainer, + } + + # Just test that training completes without error. + # TODO(travis): We may want to expand upon this in the future to include some checks on model + # convergence like gradient magnitudes, etc. Should also add distributed tests. + model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO) + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir) + + +@pytest.mark.skipif( + parse_version(torch.__version__) < parse_version("2.0"), reason="Model compilation requires PyTorch >= 2.0" +) +def test_compile(tmpdir): + input_features = [text_feature()] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + trainer = { + EPOCHS: 2, + "compile": True, + } + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: trainer, + } + + # Just test that training completes without error. + # TODO(travis): We may want to expand upon this in the future to include some checks on model + # convergence like gradient magnitudes, etc. Should also add distributed tests. + model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO) + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir) + + +@pytest.mark.parametrize("gradient_accumulation_steps", [1, 2]) +def test_gradient_accumulation(gradient_accumulation_steps: int, tmpdir): + input_features = [text_feature()] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename, num_examples=64) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + trainer = { + EPOCHS: 2, + BATCH_SIZE: 8, + "gradient_accumulation_steps": gradient_accumulation_steps, + } + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: trainer, + } + + # Just test that training completes without error. + # TODO(travis): We may want to expand upon this in the future to include some checks on model + # convergence like gradient magnitudes, etc. Should also add distributed tests. + model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO) + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir) + + +def test_enable_gradient_checkpointing(tmpdir, caplog): + """Test that gradient checkpointing is enabled when specified in the config and that it does not cause an error + when the model does not have support for gradient checkpointing.""" + input_features = [text_feature()] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + csv_filename = os.path.join(tmpdir, "training.csv") + data_csv = generate_data(input_features, output_features, csv_filename) + val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "validation.csv")) + test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, "test.csv")) + + config = { + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: { + "train_steps": 2, + BATCH_SIZE: 8, + "enable_gradient_checkpointing": True, + }, + } + + model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO) + assert model.config_obj.trainer.enable_gradient_checkpointing + + model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir) + + # Check that the warning is emitted when the model does not support gradient checkpointing + # but does not prevent training from starting. + assert "Gradient checkpointing is currently only supported for model_type: llm. Skipping..." in caplog.text diff --git a/tests/integration_tests/test_triton_configs/transformer_combiner_with_attention_reduce.yaml b/tests/integration_tests/test_triton_configs/transformer_combiner_with_attention_reduce.yaml new file mode 100644 index 0000000..f6e21f0 --- /dev/null +++ b/tests/integration_tests/test_triton_configs/transformer_combiner_with_attention_reduce.yaml @@ -0,0 +1,19 @@ +input_features: + - name: founded_on_timestamp + type: number + - name: first_equity_timestamp + type: number + - name: founded_first_equity_diff + type: number +output_features: + - name: assigned_label + type: number +combiner: + type: transformer + hidden_size: 16 + output_size: 64 + num_fc_layers: 0 + reduce_output: attention + transformer_output_size: 56 +trainer: + train_steps: 1 diff --git a/tests/integration_tests/test_visualization.py b/tests/integration_tests/test_visualization.py new file mode 100644 index 0000000..7f51f98 --- /dev/null +++ b/tests/integration_tests/test_visualization.py @@ -0,0 +1,1639 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Integration tests for the visualization commands. +# +# Author: Ivaylo Stefanov +# email: ivaylo.stefanov82@gmail.com +# github: https://github.com/istefano82 +# ============================================================================== +import glob +import json +import os +import random +import subprocess +import sys + +import numpy as np +import pytest + +from ludwig.constants import BATCH_SIZE, ENCODER, TRAINER, TYPE +from ludwig.experiment import experiment_cli +from ludwig.globals import DESCRIPTION_FILE_NAME, PREDICTIONS_PARQUET_FILE_NAME, TEST_STATISTICS_FILE_NAME +from ludwig.utils.data_utils import get_split_path +from ludwig.visualize import _extract_ground_truth_values +from tests.integration_tests.test_visualization_api import obtain_df_splits +from tests.integration_tests.utils import ( + bag_feature, + binary_feature, + category_feature, + generate_data, + number_feature, + sequence_feature, + set_feature, + text_feature, +) + +pytestmark = pytest.mark.integration_tests_g + + +def run_experiment_with_visualization(input_features, output_features, dataset): + """Helper method to run an experiment with visualization enabled. + + Does not garbage collect. + """ + output_directory = os.path.dirname(dataset) + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + args = { + "config": config, + "skip_save_processed_input": False, + "skip_save_progress": False, + "skip_save_unprocessed_output": False, + "skip_save_eval_stats": False, + "dataset": dataset, + "output_directory": output_directory, + } + + _, _, _, _, experiment_dir = experiment_cli(**args) + + return experiment_dir + + +def get_output_feature_name(experiment_dir, output_feature=0): + """Helper function to extract specified output feature name. + + :param experiment_dir: Path to the experiment directory + :param output_feature: position of the output feature the description.json + :return output_feature_name: name of the first output feature name from the experiment + """ + description_file = os.path.join(experiment_dir, DESCRIPTION_FILE_NAME) + with open(description_file, "rb") as f: + content = json.load(f) + output_feature_name = content["config"]["output_features"][output_feature]["name"] + return output_feature_name + + +def test_visualization_learning_curves_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [text_feature(encoder={"type": "parallel_cnn"})] + output_features = [category_feature(output_feature=True)] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "parallel_cnn" + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + train_stats = os.path.join(exp_dir_name, "training_statistics.json") + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "learning_curves", + "--training_statistics", + train_stats, + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run( + command, + ) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 4 + + +def test_visualization_confusion_matrix_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [text_feature(encoder={"type": "parallel_cnn"})] + output_features = [category_feature(output_feature=True)] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "parallel_cnn" + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth_metadata = experiment_source_data_name + ".meta.json" + test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME) + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confusion_matrix", + "--test_statistics", + test_stats, + "--ground_truth_metadata", + ground_truth_metadata, + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 2 + + +def test_visualization_compare_performance_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Compare performance between two models. To reduce test complexity one model is compared to it self. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [text_feature(encoder={"type": "parallel_cnn"})] + output_features = [category_feature(output_feature=True)] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "parallel_cnn" + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME) + + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_performance", + "--test_statistics", + test_stats, + test_stats, + "-m", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command, capture_output=True) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_from_prob_csv_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Probabilities are loaded from csv file. + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = get_split_path(csv_filename) + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_performance_from_prob", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_from_prob_npy_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Probabilities are loaded from npy file. + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_performance_from_prob", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_from_pred_npy_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Predictions are loaded from npy file. + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + ground_truth_metadata = experiment_source_data_name + ".meta.json" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_performance_from_pred", + "--ground_truth_metadata", + ground_truth_metadata, + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--predictions", + prediction, + prediction, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_from_pred_csv_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Predictions are loaded from csv file. + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + ground_truth_metadata = experiment_source_data_name + ".meta.json" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_performance_from_pred", + "--ground_truth_metadata", + ground_truth_metadata, + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--predictions", + prediction, + prediction, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_subset_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_performance_subset", + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "--ground_truth", + ground_truth, + "--top_n_classes", + "6", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_changing_k_output_pdf(csv_filename): + """It should be possible to save figures as pdf in the specified directory.""" + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + ground_truth_metadata = exp_dir_name + "/model/training_set_metadata.json" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_performance_changing_k", + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + ground_truth_metadata, + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "--ground_truth", + ground_truth, + "--top_n_classes", + "6", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_multiclass_multimetric_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth_metadata = experiment_source_data_name + ".meta.json" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_multiclass_multimetric", + "--output_feature_name", + output_feature_name, + "--test_statistics", + test_stats, + test_stats, + "--ground_truth_metadata", + ground_truth_metadata, + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 4 + + +def test_visualization_compare_classifiers_predictions_npy_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Predictions are loaded form npy file. + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_predictions", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--predictions", + prediction, + prediction, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_compare_classifiers_predictions_csv_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + Predictions are loaded form csv file. + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_predictions", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--predictions", + prediction, + prediction, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_cmp_classifiers_predictions_distribution_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "compare_classifiers_predictions_distribution", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--predictions", + prediction, + prediction, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_cconfidence_thresholding_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confidence_thresholding", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_confidence_thresholding_data_vs_acc_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confidence_thresholding_data_vs_acc", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_confidence_thresholding_data_vs_acc_subset_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confidence_thresholding_data_vs_acc_subset", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "--top_n_classes", + "3", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_vis_confidence_thresholding_data_vs_acc_subset_per_class_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 5}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confidence_thresholding_data_vs_acc_subset_per_class", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "--top_n_classes", + "3", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + # 3 figures should be saved because experiment setting top_n_classes = 3 + # hence one figure per class + assert len(figure_cnt) == 3 + + +def test_vis_confidence_thresholding_2thresholds_2d_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [ + text_feature(encoder={"vocab_size": 10, "min_len": 1, "type": "stacked_cnn"}), + number_feature(), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + output_features = [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + ] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "parallel_cnn" + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + threshold_output_feature_name1 = get_output_feature_name(exp_dir_name) + threshold_output_feature_name2 = get_output_feature_name(exp_dir_name, output_feature=1) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confidence_thresholding_2thresholds_2d", + "--ground_truth", + ground_truth, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + "--threshold_output_feature_names", + threshold_output_feature_name1, + threshold_output_feature_name2, + "--model_names", + "Model1", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run( + command, + ) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 3 + + +def test_vis_confidence_thresholding_2thresholds_3d_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [ + text_feature(encoder={"vocab_size": 10, "min_len": 1, "type": "stacked_cnn"}), + number_feature(), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + output_features = [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + ] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "parallel_cnn" + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + threshold_output_feature_name1 = get_output_feature_name(exp_dir_name) + threshold_output_feature_name2 = get_output_feature_name(exp_dir_name, output_feature=1) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "confidence_thresholding_2thresholds_3d", + "--ground_truth", + ground_truth, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + "--threshold_output_feature_names", + threshold_output_feature_name1, + threshold_output_feature_name2, + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run( + command, + ) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +@pytest.mark.parametrize("binary_output_type", [True, False]) +def test_visualization_binary_threshold_vs_metric_output_saved(csv_filename, binary_output_type): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [ + text_feature(encoder={"vocab_size": 10, "min_len": 1, "type": "stacked_cnn"}), + number_feature(), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + if binary_output_type: + output_features = [binary_feature()] + else: + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + random.seed(1919) + rel_path = generate_data(input_features, output_features, csv_filename) + input_features[0][ENCODER][TYPE] = "parallel_cnn" + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "binary_threshold_vs_metric", + "--positive_label", + "1", + "--metrics", + "accuracy", + "precision", + "recall", + "f1", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 4 + + +@pytest.mark.parametrize("binary_output_type", [True, False]) +def test_visualization_precision_recall_curves_output_saved(csv_filename, binary_output_type): + """Ensure pdf and png figures for precision recall curves from the experiments can be saved.""" + input_features = [category_feature(encoder={"vocab_size": 10})] + if binary_output_type: + output_features = [binary_feature()] + else: + output_features = [category_feature(decoder={"vocab_size": 3}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=20) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "precision_recall_curves", + "--positive_label", + "1", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_precision_recall_curves_from_test_statistics_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [binary_feature(), bag_feature()] + output_features = [binary_feature()] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename, num_examples=20) + + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME) + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "precision_recall_curves_from_test_statistics", + "--output_feature_name", + output_feature_name, + "--test_statistics", + test_stats, + "--model_names", + "Model1", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +@pytest.mark.parametrize("binary_output_type", [True, False]) +def test_visualization_roc_curves_output_saved(csv_filename, binary_output_type): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + if binary_output_type: + output_features = [binary_feature()] + else: + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "roc_curves", + "--positive_label", + "1", + "--metrics", + "accuracy", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_roc_curves_from_test_statistics_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [binary_feature(), bag_feature()] + output_features = [binary_feature()] + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME) + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "roc_curves_from_test_statistics", + "--output_feature_name", + output_feature_name, + "--test_statistics", + test_stats, + "--model_names", + "Model1", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 1 + + +def test_visualization_calibration_1_vs_all_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "calibration_1_vs_all", + "--metrics", + "accuracy", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "--top_k", + "6", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 5 + + +def test_visualization_calibration_multiclass_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "calibration_multiclass", + "--ground_truth", + ground_truth, + "--output_feature_name", + output_feature_name, + "--split_file", + split_file, + "--ground_truth_metadata", + exp_dir_name + "/model/training_set_metadata.json", + "--probabilities", + probability, + probability, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 2 + + +def test_visualization_frequency_vs_f1_output_saved(csv_filename): + """Ensure pdf and png figures from the experiments can be saved. + + :param csv_filename: csv fixture from tests.conftest.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + vis_output_pattern_pdf = os.path.join(exp_dir_name, "*.pdf") + vis_output_pattern_png = os.path.join(exp_dir_name, "*.png") + output_feature_name = get_output_feature_name(exp_dir_name) + test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth_metadata = experiment_source_data_name + ".meta.json" + test_cmd_pdf = [ + sys.executable, + "-m", + "ludwig.visualize", + "--visualization", + "frequency_vs_f1", + "--ground_truth_metadata", + ground_truth_metadata, + "--output_feature_name", + output_feature_name, + "--test_statistics", + test_stats, + test_stats, + "--model_names", + "Model1", + "Model2", + "-od", + exp_dir_name, + ] + test_cmd_png = test_cmd_pdf.copy() + ["-ff", "png"] + + commands = [test_cmd_pdf, test_cmd_png] + vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png] + + for command, viz_pattern in zip(commands, vis_patterns): + result = subprocess.run(command) + figure_cnt = glob.glob(viz_pattern) + + assert result.returncode == 0 + assert len(figure_cnt) == 2 + + +def test_load_ground_truth_split_from_df(csv_filename): + import pandas as pd + + ground_truth = pd.DataFrame( + { + "PassengerId": [1], + "Survived": [0], + "Pclass": [3], + "Name": ["Braund, Mr. Owen Harris"], + "Sex": ["male"], + "Age": [22.0], + "SibSp": [1], + "Parch": [0], + "Ticket": ["A/5 21171"], + "Fare": ["7.25"], + "Cabin": [None], + "Embarked": ["S"], + "split": [0], + } + ) + output_feature = "Survived" + ground_truth_train_split = _extract_ground_truth_values(ground_truth, output_feature, 0) + ground_truth_val_split = _extract_ground_truth_values(ground_truth, output_feature, 1) + ground_truth_test_split = _extract_ground_truth_values(ground_truth, output_feature, 2) + + assert ground_truth_train_split.equals(pd.Series([0])) + assert ground_truth_val_split.empty + assert ground_truth_test_split.empty + + +def test_load_ground_truth_split_from_file(csv_filename): + """Ensure correct ground truth split is loaded when ground_truth_split is given. + + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [category_feature(encoder={"vocab_size": 10})] + output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + + # Generate test data + rel_path = generate_data(input_features, output_features, csv_filename) + exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path) + output_feature_name = get_output_feature_name(exp_dir_name) + experiment_source_data_name = csv_filename.split(".")[0] + ground_truth = experiment_source_data_name + ".csv" + split_file = experiment_source_data_name + ".split.parquet" + + # retrieve ground truth from source data set + ground_truth_train_split = _extract_ground_truth_values(ground_truth, output_feature_name, 0, split_file) + ground_truth_val_split = _extract_ground_truth_values(ground_truth, output_feature_name, 1, split_file) + ground_truth_test_split = _extract_ground_truth_values(ground_truth, output_feature_name, 2, split_file) + + test_df, train_df, val_df = obtain_df_splits(csv_filename) + target_predictions_from_train = train_df[output_feature_name] + target_predictions_from_val = val_df[output_feature_name] + target_predictions_from_test = test_df[output_feature_name] + + assert np.all(ground_truth_train_split.eq(target_predictions_from_train)) + assert np.all(ground_truth_val_split.eq(target_predictions_from_val)) + assert np.all(ground_truth_test_split.eq(target_predictions_from_test)) diff --git a/tests/integration_tests/test_visualization_api.py b/tests/integration_tests/test_visualization_api.py new file mode 100644 index 0000000..9f1ab77 --- /dev/null +++ b/tests/integration_tests/test_visualization_api.py @@ -0,0 +1,936 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import glob +import logging +import os +from tempfile import TemporaryDirectory + +import numpy as np +import pytest + +from ludwig import visualize +from ludwig.api import LudwigModel, TrainingStats +from ludwig.constants import BATCH_SIZE, ENCODER, NAME, PREDICTIONS, PROBABILITIES, PROBABILITY, TRAINER, TYPE +from ludwig.data.split import get_splitter +from ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME +from ludwig.utils.data_utils import read_csv +from tests.integration_tests.utils import ( + bag_feature, + binary_feature, + category_feature, + generate_data, + LocalTestBackend, + number_feature, + sequence_feature, + set_feature, + text_feature, +) + +pytestmark = pytest.mark.integration_tests_g + + +def run_api_experiment(input_features, output_features): + """Helper method to avoid code repetition in running an experiment. + + :param input_features: input schema + :param output_features: output schema + :return: None + """ + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + model = LudwigModel(config) + return model + + +@pytest.fixture(scope="module") +def experiment_to_use(): + with TemporaryDirectory() as tmpdir: + experiment = Experiment("data_for_test.csv", tmpdir) + return experiment + + +class Experiment: + """Helper class to create model test data, setup and run experiment. + + Contain the needed model experiment statistics as class attributes. + """ + + def __init__(self, csv_filename, tmpdir): + self.tmpdir = tmpdir + self.csv_file = os.path.join(tmpdir, csv_filename) + self.input_features = [category_feature(encoder={"vocab_size": 10})] + self.output_features = [category_feature(decoder={"vocab_size": 2}, reduce_input="sum")] + data_csv = generate_data(self.input_features, self.output_features, self.csv_file) + self.model = self._create_model() + test_df, train_df, val_df = obtain_df_splits(data_csv) + self.train_stats, self.preprocessed_data, self.output_dir = self.model.train( + training_set=train_df, validation_set=val_df, output_directory=os.path.join(tmpdir, "results") + ) + self.test_stats_full, predictions, self.output_dir = self.model.evaluate( + dataset=test_df, + collect_overall_stats=True, + collect_predictions=True, + output_directory=self.output_dir, + return_type="dict", + ) + self.output_feature_name = self.output_features[0][NAME] + self.ground_truth_metadata = self.preprocessed_data[3] + self.ground_truth = test_df[self.output_feature_name] + # probabilities need to be list of lists containing each row data + # from the probability columns + # ref: https://ludwig-ai.github.io/ludwig-docs/latest/user_guide/api/LudwigModel#evaluate - Return + self.probability = predictions[self.output_feature_name][PROBABILITY] + self.probabilities = predictions[self.output_feature_name][PROBABILITIES] + self.predictions = predictions[self.output_feature_name][PREDICTIONS] + + # numeric encoded values required for some visualizations + of_metadata = self.ground_truth_metadata[self.output_feature_name] + self.predictions_num = [of_metadata["str2idx"][x] for x in self.predictions] + + def _create_model(self): + """Configure and setup test model.""" + config = { + "input_features": self.input_features, + "output_features": self.output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + return LudwigModel(config, logging_level=logging.WARN) + + +def obtain_df_splits(data_csv): + """Split input data csv file in to train, validation and test dataframes. + + :param data_csv: Input data CSV file. :return test_df, train_df, val_df: Train, validation and test dataframe splits + """ + data_df = read_csv(data_csv) + # Obtain data split array mapping data rows to split type + # 0-train, 1-validation, 2-test + splitter = get_splitter("random") + train_df, val_df, test_df = splitter.split(data_df, LocalTestBackend()) + return test_df, train_df, val_df + + +@pytest.mark.parametrize("training_only", [True, False]) +def test_learning_curves_vis_api(experiment_to_use, training_only): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + viz_outputs = ("pdf", "png") + train_stats = experiment.train_stats + if training_only: + # ensure plot works with only training metrics + # Handle situation in Issue #1875 + train_stats = TrainingStats(train_stats.training, {}, {}) + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.learning_curves( + [train_stats], output_feature_name=None, output_directory=tmpvizdir, file_format=viz_output + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 3 + + +def test_compare_performance_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + # extract test stats only + test_stats = experiment.test_stats_full + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_performance( + [test_stats, test_stats], + output_feature_name=None, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_compare_classifier_performance_from_prob_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probability = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_performance_from_prob( + [probability, probability], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[0], + labels_limit=0, + model_namess=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_compare_classifier_performance_from_pred_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + prediction = experiment.predictions + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_performance_from_pred( + [prediction, prediction], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + labels_limit=0, + model_namess=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_compare_classifiers_performance_subset_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_performance_subset( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[6], + labels_limit=0, + subset="ground_truth", + model_namess=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_compare_classifiers_performance_changing_k_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_performance_changing_k( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_k=3, + labels_limit=0, + model_namess=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_compare_classifiers_multiclass_multimetric_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + # extract test stats only + test_stats = experiment.test_stats_full + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_multiclass_multimetric( + [test_stats, test_stats], + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[6], + model_namess=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 4 + + +def test_compare_classifiers_predictions_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + predictions = experiment.predictions + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_predictions( + [predictions, predictions], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + labels_limit=0, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_compare_classifiers_predictions_distribution_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + predictions = experiment.predictions_num + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.compare_classifiers_predictions_distribution( + [predictions, predictions], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + labels_limit=0, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_confidence_thresholding_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.confidence_thresholding( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + labels_limit=0, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_confidence_thresholding_data_vs_acc_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.confidence_thresholding_data_vs_acc( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + labels_limit=0, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_confidence_thresholding_data_vs_acc_subset_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.confidence_thresholding_data_vs_acc_subset( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[3], + labels_limit=0, + subset="ground_truth", + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_confidence_thresholding_data_vs_acc_subset_per_class_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.confidence_thresholding_data_vs_acc_subset_per_class( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[2], + labels_limit=0, + subset="ground_truth", + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + # 3 figures should be saved because experiment setting top_n_classes = 3 + # hence one figure per class + assert len(figure_cnt) == 2 + + +def test_confidence_thresholding_2thresholds_2d_vis_api(csv_filename): + """Ensure pdf and png figures can be saved via visualization API call. + + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [ + text_feature(encoder={"vocab_size": 10, "min_len": 1, "type": "stacked_cnn"}), + number_feature(), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + output_features = [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + ] + encoder = "parallel_cnn" + with TemporaryDirectory() as tmpvizdir: + # Generate test data + data_csv = generate_data(input_features, output_features, os.path.join(tmpvizdir, csv_filename)) + input_features[0][ENCODER][TYPE] = encoder + model = run_api_experiment(input_features, output_features) + test_df, train_df, val_df = obtain_df_splits(data_csv) + _, _, output_dir = model.train( + training_set=train_df, validation_set=val_df, output_directory=os.path.join(tmpvizdir, "results") + ) + test_stats, predictions, _ = model.evaluate(dataset=test_df, collect_predictions=True, output_dir=output_dir) + + output_feature_name1 = output_features[0]["name"] + output_feature_name2 = output_features[1]["name"] + + ground_truth_metadata = model.training_set_metadata + feature1_cols = [ + f"{output_feature_name1}_probabilities_{label}" + for label in ground_truth_metadata[output_feature_name1]["idx2str"] + ] + feature2_cols = [ + f"{output_feature_name2}_probabilities_{label}" + for label in ground_truth_metadata[output_feature_name2]["idx2str"] + ] + + # probabilities need to be list of lists containing each row data from the + # probability columns ref: https://ludwig-ai.github.io/ludwig-docs/latest/user_guide/api/LudwigModel#evaluate + probability1 = predictions.loc[:, feature1_cols].values + probability2 = predictions.loc[:, feature2_cols].values + + target_predictions1 = test_df[output_feature_name1] + target_predictions2 = test_df[output_feature_name2] + ground_truth1 = np.asarray( + [ground_truth_metadata[output_feature_name1]["str2idx"][prediction] for prediction in target_predictions1] + ) + ground_truth2 = np.asarray( + [ground_truth_metadata[output_feature_name2]["str2idx"][prediction] for prediction in target_predictions2] + ) + viz_outputs = ("pdf", "png") + for viz_output in viz_outputs: + vis_output_pattern_pdf = os.path.join(output_dir, "*.{}").format(viz_output) + visualize.confidence_thresholding_2thresholds_2d( + [probability1, probability2], + [ground_truth1, ground_truth2], + model.training_set_metadata, + [output_feature_name1, output_feature_name2], + labels_limit=0, + model_names=["Model1"], + output_directory=output_dir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 3 + + +def test_confidence_thresholding_2thresholds_3d_vis_api(csv_filename): + """Ensure pdf and png figures can be saved via visualization API call. + + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [ + text_feature(encoder={"vocab_size": 10, "min_len": 1, "type": "stacked_cnn"}), + number_feature(), + category_feature(encoder={"vocab_size": 10, "embedding_size": 5}), + set_feature(), + sequence_feature(encoder={"vocab_size": 10, "max_len": 10, "type": "embed"}), + ] + output_features = [ + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + category_feature(decoder={"vocab_size": 2}, reduce_input="sum"), + ] + encoder = "parallel_cnn" + with TemporaryDirectory() as tmpvizdir: + # Generate test data + data_csv = generate_data(input_features, output_features, os.path.join(tmpvizdir, csv_filename)) + input_features[0][ENCODER][TYPE] = encoder + model = run_api_experiment(input_features, output_features) + test_df, train_df, val_df = obtain_df_splits(data_csv) + _, _, output_dir = model.train( + training_set=train_df, validation_set=val_df, output_directory=os.path.join(tmpvizdir, "results") + ) + test_stats, predictions, _ = model.evaluate( + dataset=test_df, collect_predictions=True, output_directory=output_dir + ) + + output_feature_name1 = output_features[0]["name"] + output_feature_name2 = output_features[1]["name"] + + ground_truth_metadata = model.training_set_metadata + feature1_cols = [ + f"{output_feature_name1}_probabilities_{label}" + for label in ground_truth_metadata[output_feature_name1]["idx2str"] + ] + feature2_cols = [ + f"{output_feature_name2}_probabilities_{label}" + for label in ground_truth_metadata[output_feature_name2]["idx2str"] + ] + + # probabilities need to be list of lists containing each row data from the + # probability columns ref: https://ludwig-ai.github.io/ludwig-docs/latest/user_guide/api/LudwigModel#evaluate + probability1 = predictions.loc[:, feature1_cols].values + probability2 = predictions.loc[:, feature2_cols].values + + target_predictions1 = test_df[output_feature_name1] + target_predictions2 = test_df[output_feature_name2] + ground_truth1 = np.asarray( + [ground_truth_metadata[output_feature_name1]["str2idx"][prediction] for prediction in target_predictions1] + ) + ground_truth2 = np.asarray( + [ground_truth_metadata[output_feature_name2]["str2idx"][prediction] for prediction in target_predictions2] + ) + viz_outputs = ("pdf", "png") + for viz_output in viz_outputs: + vis_output_pattern_pdf = os.path.join(output_dir, f"*.{viz_output}") + visualize.confidence_thresholding_2thresholds_3d( + [probability1, probability2], + [ground_truth1, ground_truth2], + model.training_set_metadata, + [output_feature_name1, output_feature_name2], + labels_limit=0, + output_directory=output_dir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_binary_threshold_vs_metric_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + metrics = ["accuracy"] + positive_label = 1 + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.binary_threshold_vs_metric( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + metrics, + positive_label, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_precision_recall_curves_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + positive_label = 1 + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.precision_recall_curves( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + positive_label, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_precision_recall_curves_from_test_statistics_vis_api(csv_filename): + """Ensure pdf and png figures can be saved via visualization API call. + + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [binary_feature(), bag_feature()] + output_features = [binary_feature()] + + with TemporaryDirectory() as tmpvizdir: + # Generate test data + data_csv = generate_data( + input_features, output_features, os.path.join(tmpvizdir, csv_filename), num_examples=20 + ) + output_feature_name = output_features[0]["name"] + + model = run_api_experiment(input_features, output_features) + data_df = read_csv(data_csv) + _, _, output_dir = model.train(dataset=data_df, output_directory=os.path.join(tmpvizdir, "results")) + test_stats, _, _ = model.evaluate(dataset=data_df, collect_overall_stats=True, output_directory=output_dir) + viz_outputs = ("pdf", "png") + for viz_output in viz_outputs: + vis_output_pattern_pdf = os.path.join(output_dir, f"*.{viz_output}") + visualize.precision_recall_curves_from_test_statistics( + [test_stats, test_stats], + output_feature_name, + model_names=["Model1", "Model2"], + output_directory=output_dir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_roc_curves_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + positive_label = 1 + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.roc_curves( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + positive_label, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_roc_curves_from_test_statistics_vis_api(csv_filename): + """Ensure pdf and png figures can be saved via visualization API call. + + :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename + :return: None + """ + input_features = [binary_feature(), bag_feature()] + output_features = [binary_feature()] + + with TemporaryDirectory() as tmpvizdir: + # Generate test data + data_csv = generate_data(input_features, output_features, os.path.join(tmpvizdir, csv_filename)) + output_feature_name = output_features[0]["name"] + + model = run_api_experiment(input_features, output_features) + data_df = read_csv(data_csv) + _, _, output_dir = model.train(dataset=data_df, output_directory=os.path.join(tmpvizdir, "results")) + # extract test metrics + test_stats, _, _ = model.evaluate(dataset=data_df, collect_overall_stats=True, output_directory=output_dir) + test_stats = test_stats + viz_outputs = ("pdf", "png") + for viz_output in viz_outputs: + vis_output_pattern_pdf = os.path.join(output_dir, f"*.{viz_output}") + visualize.roc_curves_from_test_statistics( + [test_stats, test_stats], + output_feature_name, + model_names=["Model1", "Model2"], + output_directory=output_dir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 1 + + +def test_calibration_1_vs_all_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = os.path.join(tmpvizdir, f"*.{viz_output}") + visualize.calibration_1_vs_all( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[6], + labels_limit=0, + model_namess=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 5 + + +def test_calibration_multiclass_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + probabilities = experiment.probabilities + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.calibration_multiclass( + [probabilities, probabilities], + experiment.ground_truth, + experiment.ground_truth_metadata, + experiment.output_feature_name, + labels_limit=0, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 2 + + +def test_confusion_matrix_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + # extract test stats only + test_stats = experiment.test_stats_full + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.confusion_matrix( + [test_stats, test_stats], + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[0], + normalize=False, + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 4 + + +def test_frequency_vs_f1_vis_api(experiment_to_use): + """Ensure pdf and png figures can be saved via visualization API call. + + :param experiment_to_use: Object containing trained model and results to test visualization + :return: None + """ + experiment = experiment_to_use + # extract test stats + test_stats = experiment.test_stats_full + viz_outputs = ("pdf", "png") + with TemporaryDirectory() as tmpvizdir: + for viz_output in viz_outputs: + vis_output_pattern_pdf = tmpvizdir + f"/*.{viz_output}" + visualize.frequency_vs_f1( + [test_stats, test_stats], + experiment.ground_truth_metadata, + experiment.output_feature_name, + top_n_classes=[0], + model_names=["Model1", "Model2"], + output_directory=tmpvizdir, + file_format=viz_output, + ) + figure_cnt = glob.glob(vis_output_pattern_pdf) + assert len(figure_cnt) == 2 + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_hyperopt_report_vis_api(hyperopt_results_multiple_parameters, tmpdir): + vis_dir = os.path.join(tmpdir, "visualizations") + + # Ensure visualizations directory is empty before creating plots + if os.path.exists(vis_dir): + for f in os.listdir(vis_dir): + os.remove(os.path.join(vis_dir, f)) + + visualize.hyperopt_report( + os.path.join(hyperopt_results_multiple_parameters, HYPEROPT_STATISTICS_FILE_NAME), output_directory=vis_dir + ) + + # test for creation of output directory + assert os.path.isdir(vis_dir) + + figure_cnt = glob.glob(os.path.join(vis_dir, "*")) + assert len(figure_cnt) == 4 + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_hyperopt_hiplot_vis_api(hyperopt_results_multiple_parameters, tmpdir): + vis_dir = os.path.join(tmpdir, "visualizations") + + # Ensure visualizations directory is empty before creating plots + if os.path.exists(vis_dir): + for f in os.listdir(vis_dir): + os.remove(os.path.join(vis_dir, f)) + + visualize.hyperopt_hiplot( + os.path.join(hyperopt_results_multiple_parameters, HYPEROPT_STATISTICS_FILE_NAME), output_directory=vis_dir + ) + + # test for creation of output directory + assert os.path.isdir(vis_dir) + + # test for generatated html page + assert os.path.isfile(os.path.join(vis_dir, "hyperopt_hiplot.html")) + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_hyperopt_report_vis_api_no_pairplot(hyperopt_results_single_parameter, tmpdir): + vis_dir = os.path.join(tmpdir, "visualizations") + + # Ensure visualizations directory is empty before creating plots + if os.path.exists(vis_dir): + for f in os.listdir(vis_dir): + os.remove(os.path.join(vis_dir, f)) + + visualize.hyperopt_report( + os.path.join(hyperopt_results_single_parameter, HYPEROPT_STATISTICS_FILE_NAME), output_directory=vis_dir + ) + + figure_cnt = glob.glob(os.path.join(vis_dir, "*")) + + # Only create plot for single parameter and skip pairplot creation + assert len(figure_cnt) == 1 diff --git a/tests/integration_tests/utils.py b/tests/integration_tests/utils.py new file mode 100644 index 0000000..a72afcd --- /dev/null +++ b/tests/integration_tests/utils.py @@ -0,0 +1,1185 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import contextlib +import logging +import multiprocessing +import os +import random +import shutil +import sys +import tempfile +import traceback +import uuid + + +def strtobool(val): + val = str(val).strip().lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + raise ValueError(f"invalid truth value {val!r}") + + +from typing import Any, TYPE_CHECKING # noqa: E402 + +import cloudpickle # noqa: E402 +import numpy as np # noqa: E402 +import pandas as pd # noqa: E402 +import pytest # noqa: E402 +import torch # noqa: E402 +from PIL import Image # noqa: E402 + +from ludwig.api import LudwigModel # noqa: E402 +from ludwig.backend import LocalBackend # noqa: E402 +from ludwig.constants import ( # noqa: E402 + AUDIO, + BAG, + BATCH_SIZE, + BINARY, + CATEGORY, + CATEGORY_DISTRIBUTION, + COLUMN, + DATE, + DECODER, + ENCODER, + H3, + IMAGE, + MODEL_ECD, + NAME, + NUMBER, + PROC_COLUMN, + SEQUENCE, + SET, + SPLIT, + TEXT, + TIMESERIES, + TRAINER, + VECTOR, +) +from ludwig.data.dataset_synthesizer import build_synthetic_dataset, DATETIME_FORMATS # noqa: E402 +from ludwig.experiment import experiment_cli # noqa: E402 +from ludwig.features.feature_utils import compute_feature_hash # noqa: E402 +from ludwig.globals import MODEL_FILE_NAME, PREDICTIONS_PARQUET_FILE_NAME # noqa: E402 +from ludwig.schema.encoders.text_encoders import HFEncoderConfig # noqa: E402 +from ludwig.schema.encoders.utils import get_encoder_classes # noqa: E402 +from ludwig.trainers.trainer import Trainer # noqa: E402 +from ludwig.utils import fs_utils # noqa: E402 +from ludwig.utils.data_utils import read_csv, replace_file_extension, use_credentials # noqa: E402 + +if TYPE_CHECKING: + from ludwig.data.dataset.base import Dataset + from ludwig.schema.model_types.base import ModelConfig + +logger = logging.getLogger(__name__) + +# Used in sequence-related unit tests (encoders, features) as well as end-to-end integration tests. +# Missing: passthrough encoder. +ENCODERS = ["embed", "rnn", "parallel_cnn", "cnnrnn", "stacked_parallel_cnn", "stacked_cnn", "transformer"] +TEXT_ENCODERS = ENCODERS + ["tf_idf"] + +HF_ENCODERS_SHORT = ["distilbert"] + +HF_ENCODERS = [name for name, cls in get_encoder_classes(MODEL_ECD, TEXT).items() if issubclass(cls, HFEncoderConfig)] + +RAY_BACKEND_CONFIG = { + "type": "ray", + "processor": { + "parallelism": 2, + }, + "trainer": { + "use_gpu": False, + "num_workers": 1, + "resources_per_worker": { + "CPU": 0.1, + "GPU": 0, + }, + }, +} + + +class LocalTestBackend(LocalBackend): + @property + def supports_multiprocessing(self): + return False + + +# Simulates running training on a separate node from the driver process +class FakeRemoteBackend(LocalBackend): + def create_trainer(self, **kwargs) -> "BaseTrainer": + return FakeRemoteTrainer(**kwargs) + + @property + def supports_multiprocessing(self): + return False + + +class FakeRemoteTrainer(Trainer): + def train(self, *args, save_path=MODEL_FILE_NAME, **kwargs): + with tempfile.TemporaryDirectory() as tmpdir: + return super().train(*args, save_path=tmpdir, **kwargs) + + +def parse_flag_from_env(key, default=False): + try: + value = os.environ[key] + except KeyError: + # KEY isn't set, default to `default`. + _value = default + else: + # KEY is set, convert it to True or False. + try: + if isinstance(value, bool): + return 1 if value else 0 + _value = strtobool(value) + except ValueError: + # More values are supported, but let's keep the message simple. + raise ValueError(f"If set, {key} must be yes or no.") + return _value + + +_run_private_tests = parse_flag_from_env("RUN_PRIVATE", default=False) + + +private_test = pytest.mark.skipif( + not _run_private_tests, + reason="Skipping: this test is marked private, set RUN_PRIVATE=1 in your environment to run", +) + + +def private_param(param): + """Wrap param to mark it as private, meaning it requires credentials to run. + + Private tests are skipped by default. Set the RUN_PRIVATE environment variable to a truth value to run them. + """ + return pytest.param( + *param, + marks=pytest.mark.skipif( + not _run_private_tests, + reason="Skipping: this test is marked private, set RUN_PRIVATE=1 in your environment to run", + ), + ) + + +def generate_data( + input_features, + output_features, + filename="test_csv.csv", + num_examples=25, + nan_percent=0.0, + with_split=False, +): + """Helper method to generate synthetic data based on input, output feature specs. + + :param num_examples: number of examples to generate + :param input_features: schema + :param output_features: schema + :param filename: path to the file where data is stored + :param nan_percent: percent of values in a feature to be NaN + :param with_split: If True, then new column "split" is created, containing integer values as follows: + 0 -- for training set; + 1 -- for validation set; + 2 -- for test set. + + :return: + """ + df = generate_data_as_dataframe(input_features, output_features, num_examples, nan_percent, with_split=with_split) + df.to_csv(filename, index=False) + return filename + + +def generate_data_as_dataframe( + input_features, + output_features, + num_examples=25, + nan_percent=0.0, + with_split=False, +) -> pd.DataFrame: + """Helper method to generate synthetic data based on input, output feature specs. + + Args: + input_features: schema + output_features: schema + num_examples: number of examples to generate + nan_percent: percent of values in a feature to be NaN + with_split: If True, then new column "split" is created, containing integer values as follows: + 0 -- for training set; + 1 -- for validation set; + 2 -- for test set. + + Returns: + A pandas DataFrame + """ + features = input_features + output_features + df = build_synthetic_dataset(num_examples, features) + data = [next(df) for _ in range(num_examples + 1)] + + df = pd.DataFrame(data[1:], columns=data[0]) + + # Add "split" column to DataFrame + if with_split: + num_val_examples = max(2, int(num_examples * 0.1)) + num_test_examples = max(2, int(num_examples * 0.1)) + num_train_examples = num_examples - num_val_examples - num_test_examples + df["split"] = [0] * num_train_examples + [1] * num_val_examples + [2] * num_test_examples + + return df + + +def recursive_update(dictionary, values): + for k, v in values.items(): + if isinstance(v, dict): + dictionary[k] = recursive_update(dictionary.get(k, {}), v) + else: + dictionary[k] = v + return dictionary + + +def random_string(length=5): + return uuid.uuid4().hex[:length].upper() + + +def number_feature(normalization=None, **kwargs): + feature = { + "name": f"{NUMBER}_{random_string()}", + "type": NUMBER, + "preprocessing": {"normalization": normalization}, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def category_feature(output_feature=False, **kwargs): + if DECODER in kwargs: + output_feature = True + feature = { + "name": f"{CATEGORY}_{random_string()}", + "type": CATEGORY, + } + if output_feature: + feature.update( + { + DECODER: {"type": "classifier", "vocab_size": 10}, + } + ) + else: + feature.update( + { + ENCODER: {"vocab_size": 10, "embedding_size": 5}, + } + ) + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def text_feature(output_feature: bool = False, name: str = None, **kwargs): + if DECODER in kwargs: + output_feature = True + if name is not None: + feature_name = name + else: + feature_name = f"{TEXT}_{random_string()}" + feature = { + "name": feature_name, + "type": TEXT, + } + if output_feature: + feature.update( + { + DECODER: {"type": "generator", "vocab_size": 5, "max_len": 7}, + } + ) + else: + feature.update( + { + ENCODER: { + "type": "parallel_cnn", + "vocab_size": 5, + "min_len": 7, + "max_len": 7, + "embedding_size": 8, + "state_size": 8, + }, + } + ) + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def set_feature(output_feature=False, **kwargs): + if DECODER in kwargs: + output_feature = True + feature = { + "name": f"{SET}_{random_string()}", + "type": SET, + } + if output_feature: + feature.update( + { + DECODER: {"type": "classifier", "vocab_size": 10, "max_len": 5}, + } + ) + else: + feature.update( + { + ENCODER: {"type": "embed", "vocab_size": 10, "max_len": 5, "embedding_size": 5}, + } + ) + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def sequence_feature(output_feature=False, **kwargs): + if DECODER in kwargs: + output_feature = True + feature = { + "name": f"{SEQUENCE}_{random_string()}", + "type": SEQUENCE, + } + if output_feature: + feature.update( + { + DECODER: { + "type": "generator", + "vocab_size": 10, + "max_len": 7, + } + } + ) + else: + feature.update( + { + ENCODER: { + "type": "embed", + "vocab_size": 10, + "max_len": 7, + "embedding_size": 8, + "output_size": 8, + "state_size": 8, + "num_filters": 8, + "hidden_size": 8, + }, + } + ) + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def image_feature(folder, **kwargs): + feature = { + "name": f"{IMAGE}_{random_string()}", + "type": IMAGE, + "preprocessing": {"in_memory": True, "height": 12, "width": 12, "num_channels": 3}, + ENCODER: { + "type": "stacked_cnn", + }, + "destination_folder": folder, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def audio_feature(folder, **kwargs): + # Default params are intentionally small for fast test execution. + # With 0.5s audio and window_shift=0.02s → ~23 frames; filter_size=8 fits safely. + # Tests that need specific preprocessing (e.g. fbank-80, 3s files) pass their own overrides. + feature = { + "name": f"{AUDIO}_{random_string()}", + "type": AUDIO, + "preprocessing": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": 8, + "audio_file_length_limit_in_s": 0.5, + "missing_value_strategy": "bfill", + "in_memory": True, + "padding_value": 0.0, + "norm": None, + }, + ENCODER: { + "type": "stacked_cnn", + "should_embed": False, + "conv_layers": [ + {"filter_size": 8, "pool_size": 2, "num_filters": 8}, + ], + "output_size": 8, + }, + "destination_folder": folder, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def timeseries_feature(**kwargs): + feature = { + "name": f"{TIMESERIES}_{random_string()}", + "type": TIMESERIES, + } + + output_feature = DECODER in kwargs + if output_feature: + feature.update( + { + DECODER: {"type": "projector"}, + } + ) + else: + feature.update( + { + ENCODER: {"type": "parallel_cnn", "max_len": 7}, + } + ) + + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def binary_feature(**kwargs): + feature = { + "name": f"{BINARY}_{random_string()}", + "type": BINARY, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def bag_feature(**kwargs): + feature = { + "name": f"{BAG}_{random_string()}", + "type": BAG, + ENCODER: {"type": "embed", "max_len": 5, "vocab_size": 10, "embedding_size": 5}, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def date_feature(**kwargs): + feature = { + "name": f"{DATE}_{random_string()}", + "type": DATE, + "preprocessing": { + "datetime_format": random.choice(list(DATETIME_FORMATS.keys())), + }, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def h3_feature(**kwargs): + feature = { + "name": f"{H3}_{random_string()}", + "type": H3, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def vector_feature(**kwargs): + feature = { + "name": f"{VECTOR}_{random_string()}", + "type": VECTOR, + "preprocessing": { + "vector_size": 5, + }, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def category_distribution_feature(**kwargs): + feature = { + "name": f"{CATEGORY_DISTRIBUTION}_{random_string()}", + "type": CATEGORY_DISTRIBUTION, + "preprocessing": { + "vocab": ["a", "b", "c"], + }, + DECODER: {"type": "classifier"}, + } + recursive_update(feature, kwargs) + feature[COLUMN] = feature[NAME] + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def run_experiment( + input_features=None, output_features=None, config=None, skip_save_processed_input=True, backend=None, **kwargs +): + """Helper method to avoid code repetition in running an experiment. Deletes the data saved to disk related to + running an experiment. + + :param input_features: list of input feature dictionaries + :param output_features: list of output feature dictionaries + :param config: A dictionary containing the Ludwig model configuration + :param skip_save_processed_input: (bool, default: `False`) if input + dataset is provided it is preprocessed and cached by saving an HDF5 + and JSON files to avoid running the preprocessing again. If this + parameter is `False`, the HDF5 and JSON file are not saved. + :param backend: (Union[Backend, str]) `Backend` or string name + **kwargs you may also pass extra parameters to the experiment as keyword + arguments + :return: None + """ + if input_features is None and output_features is None and config is None: + raise ValueError("Cannot run test experiment without features nor config.") + + if config is None: + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + with tempfile.TemporaryDirectory() as tmpdir: + args = { + "config": config, + "backend": backend or LocalTestBackend(), + "skip_save_training_description": True, + "skip_save_training_statistics": True, + "skip_save_processed_input": skip_save_processed_input, + "skip_save_progress": True, + "skip_save_unprocessed_output": True, + "skip_save_model": True, + "skip_save_predictions": True, + "skip_save_eval_stats": True, + "skip_collect_predictions": True, + "skip_collect_overall_stats": True, + "skip_save_log": True, + "output_directory": tmpdir, + } + args.update(kwargs) + + return experiment_cli(**args) + + +def generate_output_features_with_dependencies(main_feature, dependencies): + """Generates multiple output features specifications with dependencies. + + Example usage: + generate_output_features_with_dependencies('sequence_feature', ['category_feature', 'number_feature']) + + Args: + main_feature: feature identifier, valid values 'category_feature', 'sequence_feature', 'number_feature' + dependencies: list of dependencies for 'main_feature', do not li + """ + + output_features = [ + category_feature(decoder={"type": "classifier", "vocab_size": 2}, reduce_input="sum", output_feature=True), + sequence_feature(decoder={"type": "generator", "vocab_size": 10, "max_len": 5}, output_feature=True), + number_feature(), + ] + + # value portion of dictionary is a tuple: (position, feature_name) + # position: location of output feature in the above output_features list + # feature_name: Ludwig generated feature name + feature_names = { + "category_feature": (0, output_features[0]["name"]), + "sequence_feature": (1, output_features[1]["name"]), + "number_feature": (2, output_features[2]["name"]), + } + + # generate list of dependencies with real feature names + generated_dependencies = [feature_names[feat_name][1] for feat_name in dependencies] + + # specify dependencies for the main_feature + output_features[feature_names[main_feature][0]]["dependencies"] = generated_dependencies + + return output_features + + +def generate_output_features_with_dependencies_complex(): + """Generates multiple output features specifications with dependencies.""" + + tf = text_feature(decoder={"vocab_size": 4, "max_len": 5, "type": "generator"}) + sf = sequence_feature(decoder={"vocab_size": 4, "max_len": 5, "type": "generator"}, dependencies=[tf["name"]]) + nf = number_feature(dependencies=[tf["name"]]) + vf = vector_feature(dependencies=[sf["name"], nf["name"]]) + set_f = set_feature(decoder={"type": "classifier", "vocab_size": 4}, dependencies=[tf["name"], vf["name"]]) + cf = category_feature( + decoder={"type": "classifier", "vocab_size": 4}, dependencies=[sf["name"], nf["name"], set_f["name"]] + ) + + # The correct order ids[tf, sf, nf, vf, set_f, cf] + # shuffling it to test the robustness of the topological sort + output_features = [nf, tf, set_f, vf, cf, sf] + + return output_features + + +def _subproc_wrapper(fn, queue, *args, **kwargs): + fn = cloudpickle.loads(fn) + try: + results = fn(*args, **kwargs) + except Exception as e: + traceback.print_exc(file=sys.stderr) + results = e + queue.put(results) + + +def spawn(fn): + def wrapped_fn(*args, **kwargs): + ctx = multiprocessing.get_context("spawn") + queue = ctx.Queue() + + p = ctx.Process(target=_subproc_wrapper, args=(cloudpickle.dumps(fn), queue, *args), kwargs=kwargs) + + p.start() + p.join() + results = queue.get() + if isinstance(results, Exception): + raise RuntimeError( + f"Spawned subprocess raised {type(results).__name__}, check log output above for stack trace." + ) + return results + + return wrapped_fn + + +def get_weights(model: torch.nn.Module) -> list[torch.Tensor]: + return [param.data for param in model.parameters()] + + +def has_no_grad( + val: np.ndarray | torch.Tensor | str | list, +): + """Checks if two values are close to each other.""" + if isinstance(val, list): + return all(has_no_grad(v) for v in val) + if isinstance(val, torch.Tensor): + return not val.requires_grad + return True + + +def is_all_close( + val1: np.ndarray | torch.Tensor | str | list, + val2: np.ndarray | torch.Tensor | str | list, + tolerance=1e-4, +): + """Checks if two values are close to each other.""" + if isinstance(val1, list): + return all(is_all_close(v1, v2, tolerance) for v1, v2 in zip(val1, val2)) + if isinstance(val1, str): + return val1 == val2 + if isinstance(val1, torch.Tensor): + val1 = val1.cpu().detach().numpy() + if isinstance(val2, torch.Tensor): + val2 = val2.cpu().detach().numpy() + return val1.shape == val2.shape and np.allclose(val1, val2, atol=tolerance) + + +def is_all_tensors_cuda(val: np.ndarray | torch.Tensor | str | list) -> bool: + if isinstance(val, list): + return all(is_all_tensors_cuda(v) for v in val) + + if isinstance(val, torch.Tensor): + return val.is_cuda + return True + + +def run_api_experiment(input_features, output_features, data_csv): + """Helper method to avoid code repetition in running an experiment. + + :param input_features: input schema + :param output_features: output schema + :param data_csv: path to data + :return: None + """ + config = { + "input_features": input_features, + "output_features": output_features, + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 2, BATCH_SIZE: 128}, + } + + model = LudwigModel(config) + output_dir = None + + try: + # Training with csv + _, _, output_dir = model.train( + dataset=data_csv, skip_save_processed_input=True, skip_save_progress=True, skip_save_unprocessed_output=True + ) + model.predict(dataset=data_csv) + + model_dir = os.path.join(output_dir, MODEL_FILE_NAME) + loaded_model = LudwigModel.load(model_dir) + + # Necessary before call to get_weights() to materialize the weights + loaded_model.predict(dataset=data_csv) + + model_weights = get_weights(model.model) + loaded_weights = get_weights(loaded_model.model) + for model_weight, loaded_weight in zip(model_weights, loaded_weights): + assert torch.allclose(model_weight, loaded_weight) + finally: + # Remove results/intermediate data saved to disk + shutil.rmtree(output_dir, ignore_errors=True) + + try: + # Training with dataframe + data_df = read_csv(data_csv) + _, _, output_dir = model.train( + dataset=data_df, skip_save_processed_input=True, skip_save_progress=True, skip_save_unprocessed_output=True + ) + model.predict(dataset=data_df) + finally: + shutil.rmtree(output_dir, ignore_errors=True) + + +def add_nans_to_df_in_place(df: pd.DataFrame, nan_percent: float): + """Adds nans to a pandas dataframe in-place.""" + if nan_percent == 0: + # No-op if nan_percent is 0 + return None + if nan_percent < 0 or nan_percent > 1: + raise ValueError("nan_percent must be between 0 and 1") + + num_rows = len(df) + num_nans_per_col = int(round(nan_percent * num_rows)) + for col in df.columns: + if col == SPLIT: # do not add NaNs to the split column + continue + col_idx = df.columns.get_loc(col) + for row_idx in random.sample(range(num_rows), num_nans_per_col): + df.iloc[row_idx, col_idx] = np.nan + return None + + +def read_csv_with_nan(path, nan_percent=0.0): + """Converts `nan_percent` of samples in each row of the CSV at `path` to NaNs.""" + df = pd.read_csv(path) + add_nans_to_df_in_place(df, nan_percent) + return df + + +def create_data_set_to_use(data_format, raw_data, nan_percent=0.0): + # helper function for generating training and test data with specified format + # handles all data formats except for hdf5 + # assumes raw_data is a csv dataset generated by + # tests.integration_tests.utils.generate_data() function + + # support for writing to a fwf dataset based on this stackoverflow posting: + # https://stackoverflow.com/questions/16490261/python-pandas-write-dataframe-to-fixed-width-file-to-fwf + from tabulate import tabulate + + def to_fwf(df: pd.DataFrame, fname: str): + content = tabulate(df.values.tolist(), list(df.columns), tablefmt="plain") + open(fname, "w").write(content) + + pd.DataFrame.to_fwf = to_fwf + + dataset_to_use = None + + if data_format == "csv": + # Replace the original CSV with a CSV with NaNs + dataset_to_use = raw_data + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_csv(dataset_to_use, index=False) + + elif data_format in {"df", "dict"}: + dataset_to_use = read_csv_with_nan(raw_data, nan_percent=nan_percent) + if data_format == "dict": + dataset_to_use = dataset_to_use.to_dict(orient="list") + + elif data_format == "excel": + dataset_to_use = replace_file_extension(raw_data, "xlsx") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_excel(dataset_to_use, index=False) + + elif data_format == "excel_xls": + dataset_to_use = replace_file_extension(raw_data, "xls") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_excel(dataset_to_use, index=False) + + elif data_format == "feather": + dataset_to_use = replace_file_extension(raw_data, "feather") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_feather(dataset_to_use) + + elif data_format == "fwf": + dataset_to_use = replace_file_extension(raw_data, "fwf") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_fwf(dataset_to_use) + + elif data_format == "html": + dataset_to_use = replace_file_extension(raw_data, "html") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_html(dataset_to_use, index=False) + + elif data_format == "json": + dataset_to_use = replace_file_extension(raw_data, "json") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_json(dataset_to_use, orient="records") + + elif data_format == "jsonl": + dataset_to_use = replace_file_extension(raw_data, "jsonl") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_json(dataset_to_use, orient="records", lines=True) + + elif data_format == "parquet": + dataset_to_use = replace_file_extension(raw_data, "parquet") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_parquet(dataset_to_use, index=False) + + elif data_format == "pickle": + dataset_to_use = replace_file_extension(raw_data, "pickle") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_pickle(dataset_to_use) + + elif data_format == "stata": + dataset_to_use = replace_file_extension(raw_data, "stata") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_stata(dataset_to_use) + + elif data_format == "tsv": + dataset_to_use = replace_file_extension(raw_data, "tsv") + read_csv_with_nan(raw_data, nan_percent=nan_percent).to_csv(dataset_to_use, sep="\t", index=False) + + elif data_format == "pandas+numpy_images": + df = read_csv_with_nan(raw_data, nan_percent=nan_percent) + processed_df_rows = [] + for _, row in df.iterrows(): + processed_df_row = {} + for feature_name, raw_feature in row.items(): + if "image" in feature_name and not (isinstance(raw_feature, float) and np.isnan(raw_feature)): + feature = np.array(Image.open(raw_feature)) + else: + feature = raw_feature + processed_df_row[feature_name] = feature + processed_df_rows.append(processed_df_row) + dataset_to_use = pd.DataFrame(processed_df_rows) + + else: + ValueError(f"'{data_format}' is an unrecognized data format") + + return dataset_to_use + + +def augment_dataset_with_none( + df: pd.DataFrame, first_row_none: bool = False, last_row_none: bool = False, nan_cols: list | None = None +) -> pd.DataFrame: + """Optionally sets the first and last rows of nan_cols of the given dataframe to nan. + + :param df: dataframe containg input features/output features + :type df: pd.DataFrame + :param first_row_none: indicates whether to set the first rowin the dataframe to np.nan + :type first_row_none: bool + :param last_row_none: indicates whether to set the last row in the dataframe to np.nan + :type last_row_none: bool + :param nan_cols: a list of columns in the dataframe to explicitly set the first or last rows to np.nan + :type nan_cols: list + """ + nan_cols = nan_cols if nan_cols is not None else [] + + if first_row_none: + for col in nan_cols: + df.iloc[0, df.columns.get_loc(col)] = np.nan + if last_row_none: + for col in nan_cols: + df.iloc[-1, df.columns.get_loc(col)] = np.nan + return df + + +def train_with_backend( + backend, + config, + dataset=None, + training_set=None, + validation_set=None, + test_set=None, + predict=True, + evaluate=True, + callbacks=None, + skip_save_processed_input=True, + skip_save_predictions=True, + required_metrics=None, +): + model = LudwigModel(config, backend=backend, callbacks=callbacks) + with tempfile.TemporaryDirectory() as output_directory: + _, _, _ = model.train( + dataset=dataset, + training_set=training_set, + validation_set=validation_set, + test_set=test_set, + skip_save_processed_input=skip_save_processed_input, + skip_save_progress=True, + skip_save_unprocessed_output=True, + skip_save_log=True, + output_directory=output_directory, + ) + + if dataset is None: + dataset = training_set + + if predict: + preds, _ = model.predict( + dataset=dataset, skip_save_predictions=skip_save_predictions, output_directory=output_directory + ) + assert preds is not None + + if not skip_save_predictions: + read_preds = model.backend.df_engine.read_predictions( + os.path.join(output_directory, PREDICTIONS_PARQUET_FILE_NAME) + ) + # call compute to ensure preds materialize correctly + read_preds = read_preds.compute() + assert read_preds is not None + + if evaluate: + eval_stats, eval_preds, _ = model.evaluate( + dataset=dataset, collect_overall_stats=False, collect_predictions=True + ) + assert eval_preds is not None + assert_all_required_metrics_exist(eval_stats, required_metrics) + + # Test that eval_stats are approx equal when using local backend + with tempfile.TemporaryDirectory() as tmpdir: + model.save(tmpdir) + local_model = LudwigModel.load(tmpdir, backend=LocalTestBackend()) + local_eval_stats, _, _ = local_model.evaluate( + dataset=dataset, collect_overall_stats=False, collect_predictions=False + ) + + # Filter out metrics that are not being aggregated correctly for now + # TODO(travis): https://github.com/ludwig-ai/ludwig/issues/1956 + # Filter out next_token_perplexity since it is only relevant for LLMs + def filter(stats): + return { + k: { + metric_name: value + for metric_name, value in v.items() + if metric_name + not in { + "loss", + "root_mean_squared_percentage_error", + "jaccard", + "token_accuracy", + "next_token_perplexity", + } + } + for k, v in stats.items() + } + + for (feature_name_from_eval, metrics_dict_from_eval), ( + feature_name_from_local, + metrics_dict_from_local, + ) in zip(filter(eval_stats).items(), filter(local_eval_stats).items()): + for (metric_name_from_eval, metric_value_from_eval), ( + metric_name_from_local, + metric_value_from_local, + ) in zip(metrics_dict_from_eval.items(), metrics_dict_from_local.items()): + assert metric_name_from_eval == metric_name_from_local, ( + f"Metric mismatch between eval and local. Metrics from eval: " + f"{metrics_dict_from_eval.keys()}. Metrics from local: {metrics_dict_from_local.keys()}" + ) + if ( + metric_value_from_eval == metric_value_from_eval + and feature_name_from_eval == feature_name_from_eval + ): + # Check for equality if the values are non-nans. + assert np.isclose( + metric_value_from_eval, metric_value_from_local, rtol=1e-03, atol=1e-04 + ), ( + f"Metric {metric_name_from_eval} for feature {feature_name_from_eval}: " + f"{metric_value_from_eval} != {metric_value_from_local}" + ) + + return model + + +def assert_all_required_metrics_exist( + feature_to_metrics_dict: dict[str, dict[str, Any]], required_metrics: dict[str, set] | None = None +): + """Checks that all `required_metrics` exist in the dictionary returned during Ludwig model evaluation. + + `feature_to_metrics_dict` is a dict where the feature name is a key and the value is a dictionary of metrics: + + { + "binary_1234": { + "accuracy": 0.5, + "loss": 0.5, + }, + "numerical_1234": { + "mean_squared_error": 0.5, + "loss": 0.5, + } + } + + `required_metrics` is a dict where the feature name is a key and the value is a set of metric names: + + { + "binary_1234": {"accuracy"}, + "numerical_1234": {"mean_squared_error"}, + } + + Args: + feature_to_metrics_dict: dictionary of output feature to a dictionary of metrics + required_metrics: optional dictionary of output feature to a set of metrics names. If None, then function + returns True immediately. + Returns: + None. Raises an AssertionError if any required metrics are missing. + """ + if required_metrics is None: + return + + for feature_name, metrics_dict in feature_to_metrics_dict.items(): + if feature_name in required_metrics: + required_metric_names = set(required_metrics[feature_name]) + metric_names = set(metrics_dict.keys()) + assert required_metric_names.issubset(metric_names), ( + f"required metrics {required_metric_names} not in metrics {metric_names} for feature {feature_name}" + ) + + +def assert_preprocessed_dataset_shape_and_dtype_for_feature( + feature_name: str, + preprocessed_dataset: "Dataset", + config_obj: "ModelConfig", + expected_dtype: np.dtype, + expected_shape: tuple, +): + """Asserts that the preprocessed dataset has the correct shape and dtype for a given feature type. + + Args: + feature_name: the name of the feature to check + preprocessed_dataset: the preprocessed dataset + config_obj: the model config object + expected_dtype: the expected dtype + expected_shape: the expected shape + Returns: + None. + Raises: + AssertionError if the preprocessed dataset does not have the correct shape and dtype for the given feature type. + """ + if_configs = [if_config for if_config in config_obj.input_features if if_config.name == feature_name] + # fail fast if given `feature_name`` is not found or is not unique + if len(if_configs) != 1: + raise ValueError(f"feature_name {feature_name} found {len(if_configs)} times in config_obj") + if_config = if_configs[0] + + if_config_proc_column = if_config.proc_column + for result in [ + preprocessed_dataset.training_set, + preprocessed_dataset.validation_set, + preprocessed_dataset.test_set, + ]: + result_df = result.to_df() + result_df_proc_col = result_df[if_config_proc_column] + + # Check that the proc col is of the correct dtype + result_df_proc_col_dtypes = set(result_df_proc_col.map(lambda x: x.dtype)) + assert all([expected_dtype == dtype for dtype in result_df_proc_col_dtypes]), ( + f"proc dtype should be {expected_dtype}, got the following set of values: {result_df_proc_col_dtypes}" + ) + + # Check that the proc col is of the right dimensions + result_df_proc_col_shapes = set(result_df_proc_col.map(lambda x: x.shape)) + assert all(expected_shape == shape for shape in result_df_proc_col_shapes), ( + f"proc shape should be {expected_shape}, got the following set of values: {result_df_proc_col_shapes}" + ) + + +@contextlib.contextmanager +def remote_tmpdir(fs_protocol, bucket): + if bucket is None: + with tempfile.TemporaryDirectory() as tmpdir: + yield f"{fs_protocol}://{tmpdir}" + return + + prefix = f"tmp_{uuid.uuid4().hex}" + tmpdir = f"{fs_protocol}://{bucket}/{prefix}" + try: + with use_credentials(minio_test_creds()): + fs_utils.makedirs(f"{fs_protocol}://{bucket}", exist_ok=True) + yield tmpdir + finally: + try: + with use_credentials(minio_test_creds()): + fs_utils.delete(tmpdir, recursive=True) + except Exception as e: + logger.info(f"failed to delete remote tempdir: {e!s}") + + +def minio_test_creds(): + return { + "s3": { + "client_kwargs": { + "endpoint_url": os.environ.get("LUDWIG_MINIO_ENDPOINT", "http://localhost:9000"), + "aws_access_key_id": os.environ.get("LUDWIG_MINIO_ACCESS_KEY", "minio"), + "aws_secret_access_key": os.environ.get("LUDWIG_MINIO_SECRET_KEY", "minio123"), + } + } + } + + +def clear_huggingface_cache(): + cache_path = os.environ.get("TRANSFORMERS_CACHE") + + if cache_path is None: + try: + from huggingface_hub.constants import HF_HUB_CACHE + + cache_path = HF_HUB_CACHE.rstrip("/") + except ImportError: + cache_path = os.path.expanduser("~/.cache/huggingface") + while not cache_path.endswith("huggingface") and cache_path: + cache_path = "/".join(cache_path.split("/")[:-1]) + + du = shutil.disk_usage(cache_path) + + logger.info(f"Current disk usage {du} ({100 * du.free / du.total}% usage)") + + # only clean up cache if less than 25% of disk space is used. + if du.free / du.total > 0.25: + return + + logger.info( + f"Clearing HuggingFace cache under path: `{cache_path}`. " + f"Free disk space is {100 * du.free / du.total}% of total disk space." + ) + for root, dirs, files in os.walk(cache_path): + for f in files: + os.unlink(os.path.join(root, f)) + for d in dirs: + shutil.rmtree(os.path.join(root, d)) + + +def run_test_suite(config, dataset, backend): + with tempfile.TemporaryDirectory() as tmpdir: + model = LudwigModel(config, backend=backend) + _, _, output_dir = model.train(dataset=dataset, output_directory=tmpdir) + + model_dir = os.path.join(output_dir, MODEL_FILE_NAME) + loaded_model = LudwigModel.load(model_dir, backend=backend) + loaded_model.predict(dataset=dataset) + return loaded_model diff --git a/tests/ludwig/__init__.py b/tests/ludwig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/accounting/test_used_tokens.py b/tests/ludwig/accounting/test_used_tokens.py new file mode 100644 index 0000000..434efbf --- /dev/null +++ b/tests/ludwig/accounting/test_used_tokens.py @@ -0,0 +1,27 @@ +import torch + +from ludwig.accounting.used_tokens import get_used_tokens_for_ecd, get_used_tokens_for_llm + + +def test_get_used_tokens_for_ecd(): + inputs = {"input1": torch.tensor([[1, 2], [3, 4]]), "input2": torch.tensor([5, 6])} + targets = {"output": torch.tensor([7, 8, 9])} + + assert get_used_tokens_for_ecd(inputs, targets) == 9 + + +def test_get_used_tokens_for_ecd_no_targets(): + inputs = {"input1": torch.tensor([[1, 2], [3, 4]]), "input2": torch.tensor([5, 6])} + targets = None + + assert get_used_tokens_for_ecd(inputs, targets) == 6 + + +def test_get_used_tokens_for_llm(): + class MockTokenizer: + pad_token_id = 0 + + tokenizer = MockTokenizer() + model_inputs = torch.tensor([1, 2, 3, 0, 0]) + + assert get_used_tokens_for_llm(model_inputs, tokenizer) == 3 diff --git a/tests/ludwig/augmentation/test_augmentation_pipeline.py b/tests/ludwig/augmentation/test_augmentation_pipeline.py new file mode 100644 index 0000000..3395c92 --- /dev/null +++ b/tests/ludwig/augmentation/test_augmentation_pipeline.py @@ -0,0 +1,342 @@ +import copy +import logging +import os +import tempfile + +import pytest +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import IMAGENET1K +from ludwig.data.dataset_synthesizer import cli_synthesize_dataset +from ludwig.error import ConfigValidationError +from ludwig.features.image_feature import ImageAugmentation +from ludwig.schema.features.image_feature import ImageInputFeatureConfig + + +# define fixture for test image augmentation +@pytest.fixture(scope="module") +def test_image(): + # return random normal batch of images of size 2x3x32x32 [batch_size, channels, height, width] + return torch.randn(2, 3, 32, 32) + + +# create training data for model training with augmentation pipeline +@pytest.fixture(scope="module") +def train_data_rgb(): + with tempfile.TemporaryDirectory() as tmp_dir: + # setup basic data description for training + output_features = [ + { + "name": "binary_output_feature", + "type": "binary", + }, + ] + input_features = [ + { + "name": "my_image", + "type": "image", + }, + ] + + # add parameters to generate images + input_features[0].update( + { + "destination_folder": os.path.join(tmp_dir, "images"), + "preprocessing": {"height": 350, "width": 350, "num_channels": 3}, + } + ) + feature_list = input_features + output_features + + # create synthetic data + data_dir = os.path.join(tmp_dir, "data") + os.makedirs(data_dir, exist_ok=True) + train_fp = os.path.join(tmp_dir, "train.csv") + + cli_synthesize_dataset(16, feature_list, train_fp) + + # remove unneeded data generation parameters + input_features[0].pop("destination_folder") + + # return training data for testing + yield train_fp, input_features, output_features + + +# create training data for model training with augmentation pipeline +@pytest.fixture(scope="module") +def train_data_gray_scale(): + with tempfile.TemporaryDirectory() as tmp_dir: + # setup basic data description for training + output_features = [ + { + "name": "binary_output_feature", + "type": "binary", + }, + ] + input_features = [ + { + "name": "my_image", + "type": "image", + }, + ] + + # add parameters to generate images + input_features[0].update( + { + "destination_folder": os.path.join(tmp_dir, "images"), + "preprocessing": {"height": 350, "width": 350, "num_channels": 1}, + } + ) + feature_list = input_features + output_features + + # create synthetic data + data_dir = os.path.join(tmp_dir, "data") + os.makedirs(data_dir, exist_ok=True) + train_fp = os.path.join(tmp_dir, "train.csv") + + cli_synthesize_dataset(16, feature_list, train_fp) + + # remove unneeded data generation parameters + input_features[0].pop("destination_folder") + + # return training data for testing + yield train_fp, input_features, output_features + + +# common function to run model training with augmentation pipeline +def run_augmentation_training( + train_data: str = "", + backend: str = "local", + encoder: dict = None, + preprocessing: dict = None, + augmentation_pipeline_ops: list[dict] = None, +): + # unpack training data + train_fp, input_features, output_features = train_data + + # add encoder and preprocessing specification to input feature + input_features[0].update( + { + "encoder": encoder, + "preprocessing": preprocessing, + } + ) + + # add augmentation pipeline to input feature if specified + test_input_features = copy.deepcopy(input_features) + test_input_features[0].update({"augmentation": augmentation_pipeline_ops}) + + config = { + "input_features": test_input_features, + "output_features": output_features, + "trainer": { + "epochs": 2, + "batch_size": 8, + }, + "backend": { + "type": backend, + }, + } + + with tempfile.TemporaryDirectory() as tmpdir: + model = LudwigModel(config, logging_level=logging.INFO) + model.experiment( + dataset=train_fp, + skip_save_processed_input=True, + skip_save_model=True, + output_directory=os.path.join(tmpdir, "output"), + ) + return model + + +@pytest.mark.parametrize( + "augmentation_pipeline_ops", + [ + [{"type": "random_horizontal_flip"}], + [ + {"type": "random_vertical_flip"}, + {"type": "random_rotate", "degree": 45}, + ], + [ + {"type": "random_horizontal_flip"}, + {"type": "random_vertical_flip"}, + {"type": "random_rotate", "degree": 45}, + {"type": "random_brightness"}, + {"type": "random_blur", "kernel_size": 9}, + {"type": "random_contrast"}, + ], + ], +) +# test image augmentation pipeline +def test_image_augmentation(test_image, augmentation_pipeline_ops): + # define augmentation pipeline + feature = ImageInputFeatureConfig.from_dict( + {"name": "foo", "type": "image", "augmentation": augmentation_pipeline_ops} + ) + augmentation_pipeline = ImageAugmentation(feature.augmentation) + # apply augmentation pipeline to batch of test images + augmentation_pipeline(test_image) + + +AUGMENTATION_PIPELINE_OPS = [ + False, + True, + [{"type": "random_blur"}, {"type": "random_rotate"}], +] + +IMAGE_ENCODER = [ + {"type": "stacked_cnn"}, + {"type": "alexnet", "use_pretrained": False, "model_cache_dir": os.path.join(os.getcwd(), "tv_cache")}, +] + +IMAGE_PREPROCESSING = [ + { + "standardize_image": None, + "width": 300, + "height": 300, + }, + { + "standardize_image": IMAGENET1K, + "width": 300, + "height": 300, + }, +] + + +@pytest.mark.parametrize("augmentation_pipeline_ops", AUGMENTATION_PIPELINE_OPS) +@pytest.mark.parametrize("encoder", IMAGE_ENCODER) +@pytest.mark.parametrize("preprocessing", IMAGE_PREPROCESSING) +def test_local_model_training_with_augmentation_pipeline( + train_data_rgb, + encoder, + preprocessing, + augmentation_pipeline_ops, +): + model = run_augmentation_training( + train_data=train_data_rgb, + backend="local", + encoder=encoder, # Ludwig encoder + preprocessing=preprocessing, # Ludwig image preprocessing + augmentation_pipeline_ops=augmentation_pipeline_ops, # Ludwig image augmentation + ) + + if augmentation_pipeline_ops is not False: + assert model.config_obj.input_features[0].has_augmentation() + else: + assert not model.config_obj.input_features[0].has_augmentation() + + +# due to the time it takes to run the tests, run only a subset of the tests +# and focus on interaction of Ludwig encoder with image preprocessing and augmentation +@pytest.mark.slow +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.parametrize("augmentation_pipeline_ops", AUGMENTATION_PIPELINE_OPS) +@pytest.mark.parametrize("preprocessing", IMAGE_PREPROCESSING) +def test_ray_model_training_with_augmentation_pipeline( + train_data_rgb, + preprocessing, + augmentation_pipeline_ops, + ray_cluster_2cpu, +): + model = run_augmentation_training( + train_data=train_data_rgb, + backend="ray", + encoder={"type": "stacked_cnn"}, + preprocessing=preprocessing, + augmentation_pipeline_ops=augmentation_pipeline_ops, + ) + + if augmentation_pipeline_ops is not False: + assert model.config_obj.input_features[0].has_augmentation() + else: + assert not model.config_obj.input_features[0].has_augmentation() + + +# this test gray-scale image augmentation pipeline +@pytest.mark.parametrize( + "augmentation_pipeline_ops", + [ + False, + True, + [ + {"type": "auto_augmentation"}, + {"type": "random_horizontal_flip"}, + {"type": "random_vertical_flip"}, + {"type": "random_rotate"}, + {"type": "random_brightness"}, + {"type": "random_blur"}, + {"type": "random_contrast"}, + ], + ], +) +def test_ludwig_encoder_gray_scale_image_augmentation_pipeline( + train_data_gray_scale, + augmentation_pipeline_ops, +): + run_augmentation_training( + train_data=train_data_gray_scale, + backend="local", + encoder={"type": "stacked_cnn", "num_filters": 1}, + preprocessing={}, + augmentation_pipeline_ops=augmentation_pipeline_ops, + ) + + +# this test invalid augmentation pipeline specification +@pytest.mark.parametrize( + "augmentation_pipeline_ops", + [ + None, + [{"type": "invalid_string"}], + ["random_horizontal_flip"], + "random_horizontal_flip", + [ + {"type": "random_rotate", "degree": "45"}, + ], + ], +) +def test_invalid_augmentation_parameters( + train_data_rgb, + augmentation_pipeline_ops, +): + with pytest.raises(ConfigValidationError): + run_augmentation_training( + train_data=train_data_rgb, + backend="local", + encoder={"type": "alexnet", "model_cache_dir": os.path.join(os.getcwd(), "tv_cache")}, + preprocessing={}, + augmentation_pipeline_ops=augmentation_pipeline_ops, + ) + + +# tests saving and loading a model with augmentation pipeline +def test_load_model_with_augmentation_pipeline( + train_data_rgb, +): + augmentation_pipeline_ops = [ + {"type": "random_blur"}, + {"type": "random_rotate"}, + ] + preprocessing = { + "standardize_image": None, + "width": 300, + "height": 300, + } + encoder = { + "type": "alexnet", + "use_pretrained": False, + "model_cache_dir": os.path.join(os.getcwd(), "tv_cache"), + } + + model = run_augmentation_training( + train_data=train_data_rgb, + backend="local", + encoder=encoder, # Ludwig encoder + preprocessing=preprocessing, # Ludwig image preprocessing + augmentation_pipeline_ops=augmentation_pipeline_ops, # Ludwig image augmentation + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + model.save(tmp_dir) + LudwigModel.load(tmp_dir) diff --git a/tests/ludwig/augmentation/test_auto_augmentation.py b/tests/ludwig/augmentation/test_auto_augmentation.py new file mode 100644 index 0000000..81367f8 --- /dev/null +++ b/tests/ludwig/augmentation/test_auto_augmentation.py @@ -0,0 +1,27 @@ +import pytest +import torch + +from ludwig.constants import IMAGE +from ludwig.features.image_feature import get_augmentation_op +from ludwig.schema.features.augmentation.utils import get_augmentation_cls + + +@pytest.fixture(scope="module") +def test_image(): + return torch.randn(5, 3, 256, 256) + + +@pytest.mark.parametrize( + "augmentation_type, augmentation_params", + [ + ("auto_augmentation", {"method": "trivial_augment"}), + ("auto_augmentation", {"method": "auto_augment"}), + ("auto_augmentation", {"method": "rand_augment"}), + ], +) +def test_auto_augmentation(test_image, augmentation_type, augmentation_params): + aug_config = get_augmentation_cls(IMAGE, augmentation_type).from_dict(augmentation_params) + augmentation_op_cls = get_augmentation_op(IMAGE, augmentation_type) + augmentation_op = augmentation_op_cls(aug_config) + augmented_image = augmentation_op(test_image) + assert augmented_image.shape == (5, 3, 256, 256) diff --git a/tests/ludwig/augmentation/test_image_augmentation.py b/tests/ludwig/augmentation/test_image_augmentation.py new file mode 100644 index 0000000..07baab9 --- /dev/null +++ b/tests/ludwig/augmentation/test_image_augmentation.py @@ -0,0 +1,31 @@ +import pytest +import torch + +from ludwig.constants import IMAGE +from ludwig.features.image_feature import get_augmentation_op +from ludwig.schema.features.augmentation.utils import get_augmentation_cls + + +@pytest.fixture(scope="module") +def test_image(): + # return random normal image of size 2x3x32x32 [batch_size, channels, height, width] + return torch.randn(2, 3, 32, 32) + + +@pytest.mark.parametrize( + "augmentation_type, augmentation_params", + [ + ("random_horizontal_flip", {}), + ("random_vertical_flip", {}), + ("random_rotate", {"degree": 45}), + ("random_blur", {"kernel_size": 9}), + ("random_blur", {"kernel_size": 15}), + ("random_contrast", {"min": 0.5, "max": 1.5}), + ("random_brightness", {"min": 0.5, "max": 1.5}), + ], +) +def test_image_augmentation(test_image, augmentation_type, augmentation_params): + aug_config = get_augmentation_cls(IMAGE, augmentation_type).from_dict(augmentation_params) + augmentation_op_cls = get_augmentation_op(IMAGE, augmentation_type) + augmentation_op = augmentation_op_cls(aug_config) + augmentation_op(test_image) diff --git a/tests/ludwig/automl/__init__.py b/tests/ludwig/automl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/automl/test_base_config.py b/tests/ludwig/automl/test_base_config.py new file mode 100644 index 0000000..8565b16 --- /dev/null +++ b/tests/ludwig/automl/test_base_config.py @@ -0,0 +1,186 @@ +import os +from decimal import Decimal + +import dask +import numpy as np +import pandas as pd +import pytest +import yaml + +ray = pytest.importorskip("ray") + +# Prevent Dask from converting object-dtype columns to PyArrow strings. +dask.config.set({"dataframe.convert-string": False}) + +from ludwig.automl.base_config import ( # noqa + get_dataset_info, + get_dataset_info_from_source, + get_field_metadata, + get_reference_configs, + is_field_boolean, +) +from ludwig.data.dataframe.dask import DaskEngine # noqa +from ludwig.data.dataframe.pandas import PandasEngine # noqa +from ludwig.schema.model_types.base import ModelConfig # noqa +from ludwig.utils.automl.data_source import DataframeSource, wrap_data_source # noqa + +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_c] + + +@pytest.fixture(scope="module") +def dummy_df(): + data = { + "title": { + 0: " Donald Trump Sends ...Disturbing", + 1: " Drunk Bragging Trum...estigation", + 2: " Sheriff David Clark...n The Eye", + 3: " Trump Is So Obsesse...e (IMAGES)", + 4: " Pope Francis Just C...mas Speech", + }, + "text": { + 0: "Donald Trump just co...ty Images.", + 1: "House Intelligence C...ty Images.", + 2: "On Friday, it was re...ty Images.", + 3: "On Christmas day, Do...ty Images.", + 4: "Pope Francis used hi...ty Images.", + }, + "subject": {0: "News", 1: "News", 2: "News", 3: "News", 4: "News"}, + "date": { + 0: "December 31, 2017", + 1: "December 31, 2017", + 2: "December 30, 2017", + 3: "December 29, 2017", + 4: "December 25, 2017", + }, + "label": {0: "Fake", 1: "Fake", 2: "Fake", 3: "Fake", 4: "Fake"}, + } + + return pd.DataFrame.from_dict(data) + + +@pytest.mark.parametrize( + ("df_engine",), + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=pytest.mark.distributed), + ], +) +def test_is_field_boolean(df_engine, dummy_df): + assert np.array_equal(dummy_df.dtypes, ["object", "object", "object", "object", "object"]) + + if isinstance(df_engine, DaskEngine): + dummy_df = df_engine.df_lib.from_pandas(dummy_df, npartitions=1) + + source = wrap_data_source(dummy_df) + + for field in dummy_df.columns: + assert not is_field_boolean(source, field) + + +@pytest.mark.parametrize( + "df_engine", + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=pytest.mark.distributed), + ], +) +def test_dataset_info(df_engine, dummy_df): + assert np.array_equal(dummy_df.dtypes, ["object", "object", "object", "object", "object"]) + + if isinstance(df_engine, DaskEngine): + dummy_df = df_engine.df_lib.from_pandas(dummy_df, npartitions=1) + + ds_info = get_dataset_info(dummy_df) + + assert [f.dtype for f in ds_info.fields] == ["object", "object", "object", "object", "object"] + + +@pytest.mark.parametrize( + "col,expected_dtype", + [ + (["a", "b", "c", "d", "e", "a", "b", "b"], "object"), + (["a", "b", "a", "b", np.nan], "object"), + (["a", "b", "a", "b", None], "object"), + ([True, False, True, True, ""], "object"), + ([True, False, True, False, np.nan], "bool"), + ], +) +def test_object_and_bool_type_inference(col, expected_dtype): + df = pd.DataFrame({"col1": col}) + info = get_dataset_info(df) + assert info.fields[0].dtype == expected_dtype + + +def test_reference_configs(): + ref_configs = get_reference_configs() + for dataset in ref_configs["datasets"]: + config = dataset["config"] + + # Ensure config is valid with the latest Ludwig schema + ModelConfig.from_dict(config) + + +def repeat(df, n): + """Repeat a dataframe n times.""" + return pd.concat([df] * n, ignore_index=True) + + +def test_infer_parquet_types(tmpdir): + """Test type inference works properly for a parquet file with unconventional types types.""" + # Create a temporary directory to store the parquet file + tmpdir = str(tmpdir) + + # Create a dataframe with all the types + df = pd.DataFrame( + { + "int": [1, 2, 3], + "float": [1.1, 2.2, 3.3], + "string": ["a", "b", "c"], + "datetime": pd.date_range("20130101", periods=3), + "category": pd.Series(["a", "b", "c"], dtype="category"), + "bool": [True, False, True], + } + ) + df = repeat(df, 10) + df["float"] = df["float"].apply(Decimal) + df["date"] = df["datetime"].apply(str) + + # Write the dataframe to parquet and read it back + dataset_path = os.path.join(tmpdir, "dataset.parquet") + df.to_parquet(dataset_path) + df = pd.read_parquet(dataset_path) + + # Test type inference + ds = DataframeSource(df) + ds_info = get_dataset_info_from_source(ds) + metas = get_field_metadata(ds_info.fields, ds_info.row_count, targets=["bool"]) + + config = yaml.safe_load(""" + input_features: + - name: int + type: category + - name: float + type: number + - name: string + type: category + - name: datetime + type: date + - name: category + type: category + - name: date + type: date + output_features: + - name: bool + type: binary + combiner: + type: concat + output_size: 14 + trainer: + epochs: 2 + batch_size: 8 + """) + + meta_dict = {meta.config.name: meta for meta in metas} + for feature in config["input_features"] + config["output_features"]: + meta = meta_dict[feature["name"]] + assert feature["type"] == meta.config.type, f"{feature['name']}: {feature['type']} != {meta.config.type}" diff --git a/tests/ludwig/automl/test_config_enumerator.py b/tests/ludwig/automl/test_config_enumerator.py new file mode 100644 index 0000000..12b0494 --- /dev/null +++ b/tests/ludwig/automl/test_config_enumerator.py @@ -0,0 +1,164 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Unit tests for ludwig.automl.config_enumerator.""" + +from ludwig.automl.config_enumerator import ( + ConfigSpec, + enumerate_config_specs, + FeatureSpec, + get_valid_combiners, + get_valid_decoders, + get_valid_encoders, +) + +# --------------------------------------------------------------------------- +# get_valid_encoders +# --------------------------------------------------------------------------- + + +def test_get_valid_encoders_binary(): + encoders = get_valid_encoders("binary") + assert set(encoders) == {"passthrough", "dense"} + + +def test_get_valid_encoders_text(): + encoders = get_valid_encoders("text") + assert len(encoders) >= 10 + assert "bert" in encoders + assert "distilbert" in encoders + + +def test_get_valid_encoders_unknown(): + encoders = get_valid_encoders("anomaly_type_that_does_not_exist") + assert encoders == [] + + +# --------------------------------------------------------------------------- +# get_valid_decoders +# --------------------------------------------------------------------------- + + +def test_get_valid_decoders_binary(): + decoders = get_valid_decoders("binary") + assert set(decoders) == {"mlp_classifier", "regressor"} + + +def test_get_valid_decoders_number(): + decoders = get_valid_decoders("number") + assert decoders == ["regressor"] + + +# --------------------------------------------------------------------------- +# get_valid_combiners +# --------------------------------------------------------------------------- + + +def test_get_valid_combiners_all_tabular(): + features = [ + FeatureSpec(name="a", type="binary"), + FeatureSpec(name="b", type="category"), + FeatureSpec(name="c", type="number"), + ] + combiners = get_valid_combiners(features) + assert "tabnet" in combiners + + +def test_get_valid_combiners_with_text(): + features = [ + FeatureSpec(name="a", type="binary"), + FeatureSpec(name="b", type="text"), + ] + combiners = get_valid_combiners(features) + # tabnet requires all-tabular; text is not tabular + assert "tabnet" not in combiners + + +def test_get_valid_combiners_comparator(): + # Exactly 2 inputs → comparator allowed + two_features = [ + FeatureSpec(name="a", type="binary"), + FeatureSpec(name="b", type="category"), + ] + combiners_two = get_valid_combiners(two_features) + assert "comparator" in combiners_two + + # 3 inputs → comparator NOT allowed + three_features = [ + FeatureSpec(name="a", type="binary"), + FeatureSpec(name="b", type="category"), + FeatureSpec(name="c", type="number"), + ] + combiners_three = get_valid_combiners(three_features) + assert "comparator" not in combiners_three + + +def test_get_valid_combiners_sequence(): + # text input → sequence combiner allowed + text_features = [ + FeatureSpec(name="a", type="text"), + FeatureSpec(name="b", type="binary"), + ] + combiners = get_valid_combiners(text_features) + assert "sequence" in combiners + + # binary-only → sequence combiner NOT allowed + binary_only = [ + FeatureSpec(name="a", type="binary"), + FeatureSpec(name="b", type="number"), + ] + combiners_no_seq = get_valid_combiners(binary_only) + assert "sequence" not in combiners_no_seq + + +# --------------------------------------------------------------------------- +# enumerate_config_specs +# --------------------------------------------------------------------------- + + +def test_enumerate_config_specs_basic(): + inputs = [FeatureSpec(name="x", type="binary")] + output = FeatureSpec(name="y", type="binary") + specs = enumerate_config_specs(inputs, output) + assert len(specs) > 0 + + +def test_enumerate_config_specs_max_configs(): + inputs = [FeatureSpec(name="x", type="category"), FeatureSpec(name="z", type="number")] + output = FeatureSpec(name="y", type="binary") + specs = enumerate_config_specs(inputs, output, max_configs=5) + assert len(specs) == 5 + + +def test_enumerate_config_specs_no_valid_decoder(): + inputs = [FeatureSpec(name="x", type="binary")] + # "anomaly" has no decoder registry entry + output = FeatureSpec(name="y", type="anomaly") + specs = enumerate_config_specs(inputs, output) + assert specs == [] + + +def test_config_spec_has_correct_fields(): + inputs = [FeatureSpec(name="x", type="binary")] + output = FeatureSpec(name="y", type="binary") + specs = enumerate_config_specs(inputs, output, max_configs=1) + assert len(specs) == 1 + spec = specs[0] + assert isinstance(spec, ConfigSpec) + assert hasattr(spec, "input_encoders") + assert hasattr(spec, "combiner") + assert hasattr(spec, "output_decoder") + assert hasattr(spec, "output_type") + assert isinstance(spec.input_encoders, dict) + assert "x" in spec.input_encoders diff --git a/tests/ludwig/automl/test_config_sampler.py b/tests/ludwig/automl/test_config_sampler.py new file mode 100644 index 0000000..1ea8d49 --- /dev/null +++ b/tests/ludwig/automl/test_config_sampler.py @@ -0,0 +1,117 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Unit tests for ludwig.automl.config_sampler.""" + +from ludwig.automl.config_enumerator import FeatureSpec +from ludwig.automl.config_sampler import sample_configs +from ludwig.constants import COMBINER, INPUT_FEATURES, OUTPUT_FEATURES, TRAINER, TYPE + +# --------------------------------------------------------------------------- +# Fixtures / helpers +# --------------------------------------------------------------------------- + +_TABULAR_INPUTS = [ + FeatureSpec(name="age", type="number"), + FeatureSpec(name="income", type="number"), + FeatureSpec(name="category_col", type="category"), + FeatureSpec(name="flag", type="binary"), +] +_BINARY_OUTPUT = FeatureSpec(name="label", type="binary") + +_TEXT_INPUTS = [FeatureSpec(name="sentence", type="text")] + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_sample_configs_returns_n(): + results = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=10, seed=0) + assert len(results) <= 10 + assert len(results) > 0 + + +def test_sample_configs_deduplication(): + results = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=5, seed=7) + hashes = [r.config_hash for r in results] + assert len(hashes) == len(set(hashes)), "Duplicate config hashes found" + + +def test_sample_configs_combiner_diversity(): + results = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=50, seed=42) + combiners_seen = {r.spec.combiner for r in results} + assert len(combiners_seen) >= 3, f"Expected ≥3 distinct combiners, got {combiners_seen}" + + +def test_sample_configs_reproducible(): + results_a = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=10, seed=99) + results_b = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=10, seed=99) + hashes_a = [r.config_hash for r in results_a] + hashes_b = [r.config_hash for r in results_b] + assert hashes_a == hashes_b, "Same seed should produce identical configs" + + +def test_sample_configs_different_seeds(): + results_a = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=20, seed=1) + results_b = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=20, seed=2) + hashes_a = {r.config_hash for r in results_a} + hashes_b = {r.config_hash for r in results_b} + # They may share some configs, but should not be identical sets for a large sample + assert hashes_a != hashes_b, "Different seeds should not produce identical config sets" + + +def test_sampled_config_has_valid_structure(): + results = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=3, seed=0) + assert len(results) > 0 + cfg = results[0].config_dict + assert INPUT_FEATURES in cfg + assert OUTPUT_FEATURES in cfg + assert COMBINER in cfg + assert TRAINER in cfg + + +def test_sample_configs_text_schema(): + results = sample_configs(_TEXT_INPUTS, _BINARY_OUTPUT, n=5, seed=0) + assert len(results) > 0 + + +def test_sample_configs_empty_schema(): + # sample_configs with empty input features still generates configs (combiner/decoder axes are non-empty); + # the result is a non-empty list with configs that have empty input_features. + results = sample_configs([], _BINARY_OUTPUT, n=10, seed=0) + # Each config should declare an empty input_features list + for r in results: + assert r.config_dict[INPUT_FEATURES] == [] + + +def test_build_config_dict_structure(): + results = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=3, seed=0) + assert len(results) > 0 + input_features = results[0].config_dict[INPUT_FEATURES] + for feat_dict in input_features: + assert "name" in feat_dict + assert TYPE in feat_dict + assert "encoder" in feat_dict + + +def test_combiner_params_in_config(): + # Force a tabnet config by running enough samples + results = sample_configs(_TABULAR_INPUTS, _BINARY_OUTPUT, n=50, seed=42) + tabnet_results = [r for r in results if r.spec.combiner == "tabnet"] + assert len(tabnet_results) > 0, "Expected at least one tabnet config in 50 samples" + combiner_dict = tabnet_results[0].config_dict[COMBINER] + assert "size" in combiner_dict + assert "num_steps" in combiner_dict diff --git a/tests/ludwig/automl/test_config_validator.py b/tests/ludwig/automl/test_config_validator.py new file mode 100644 index 0000000..8250f4c --- /dev/null +++ b/tests/ludwig/automl/test_config_validator.py @@ -0,0 +1,183 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Unit tests for ludwig.automl.config_validator.""" + +import numpy as np +import pandas as pd + +from ludwig.automl.config_validator import validate_config_for_dataset, ValidationResult + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_binary_df(n: int = 100) -> pd.DataFrame: + rng = np.random.default_rng(0) + return pd.DataFrame( + { + "feature_a": rng.integers(0, 10, size=n).astype(float), + "feature_b": rng.standard_normal(n), + "label": rng.choice([0, 1], size=n), + } + ) + + +def _simple_config(batch_size: int | None = None) -> dict: + cfg = { + "input_features": [ + {"name": "feature_a", "type": "number"}, + {"name": "feature_b", "type": "number"}, + ], + "output_features": [{"name": "label", "type": "binary"}], + "combiner": {"type": "concat"}, + } + if batch_size is not None: + cfg["trainer"] = {"batch_size": batch_size} + return cfg + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_valid_config_passes(): + df = _make_binary_df(100) + config = _simple_config() + result = validate_config_for_dataset(config, df) + assert result.is_valid + + +def test_missing_input_column(): + df = _make_binary_df(100) + config = { + "input_features": [{"name": "does_not_exist", "type": "number"}], + "output_features": [{"name": "label", "type": "binary"}], + "combiner": {"type": "concat"}, + } + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert len(result.failures) > 0 + + +def test_missing_output_column(): + df = _make_binary_df(100) + config = { + "input_features": [{"name": "feature_a", "type": "number"}], + "output_features": [{"name": "missing_label", "type": "binary"}], + "combiner": {"type": "concat"}, + } + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert len(result.failures) > 0 + + +def test_invalid_encoder_type(): + df = _make_binary_df(100) + config = { + "input_features": [ + {"name": "feature_a", "type": "binary", "encoder": {"type": "nonexistent_enc"}}, + ], + "output_features": [{"name": "label", "type": "binary"}], + "combiner": {"type": "concat"}, + } + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert any("nonexistent_enc" in f for f in result.failures) + + +def test_invalid_decoder_type(): + df = _make_binary_df(100) + config = { + "input_features": [{"name": "feature_a", "type": "number"}], + "output_features": [{"name": "label", "type": "binary", "decoder": {"type": "nonexistent_dec"}}], + "combiner": {"type": "concat"}, + } + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert any("nonexistent_dec" in f for f in result.failures) + + +def test_invalid_combiner_for_schema(): + # tabnet is only valid for all-tabular inputs; text is not tabular + df = pd.DataFrame( + { + "sentence": ["hello world"] * 100, + "label": [0, 1] * 50, + } + ) + config = { + "input_features": [{"name": "sentence", "type": "text"}], + "output_features": [{"name": "label", "type": "binary"}], + "combiner": {"type": "tabnet"}, + } + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert any("tabnet" in f for f in result.failures) + + +def test_batch_size_too_large(): + df = _make_binary_df(50) + config = _simple_config(batch_size=10000) + result = validate_config_for_dataset(config, df) + # batch_size >> dataset size → warning + assert len(result.warnings) > 0 + + +def test_output_in_input(): + df = _make_binary_df(100) + config = { + "input_features": [ + {"name": "feature_a", "type": "number"}, + {"name": "label", "type": "binary"}, # label is also an output + ], + "output_features": [{"name": "label", "type": "binary"}], + "combiner": {"type": "concat"}, + } + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert any("label" in f for f in result.failures) + + +def test_single_class_output(): + df = _make_binary_df(100) + df["label"] = 0 # all same value + config = _simple_config() + result = validate_config_for_dataset(config, df) + assert not result.is_valid + assert any("distinct" in f.lower() or "1" in f for f in result.failures) + + +def test_strict_mode(): + # A config with no combiner type generates a warning; strict=True should fail, strict=False pass + df = _make_binary_df(100) + config = { + "input_features": [{"name": "feature_a", "type": "number"}], + "output_features": [{"name": "label", "type": "binary"}], + # combiner intentionally omitted → warning about missing combiner type + } + result_strict = validate_config_for_dataset(config, df, strict=True) + result_lenient = validate_config_for_dataset(config, df, strict=False) + + # strict mode: warnings become failures + assert not result_strict.is_valid + # lenient mode: only hard failures matter + assert result_lenient.is_valid + + +def test_validation_result_bool(): + assert bool(ValidationResult(is_valid=True)) is True + assert bool(ValidationResult(is_valid=False)) is False diff --git a/tests/ludwig/automl/test_data_source.py b/tests/ludwig/automl/test_data_source.py new file mode 100644 index 0000000..3095a91 --- /dev/null +++ b/tests/ludwig/automl/test_data_source.py @@ -0,0 +1,39 @@ +import tempfile + +import pytest + +from ludwig.constants import TEXT +from ludwig.utils.data_utils import read_csv + +try: + import dask.dataframe as dd + + from ludwig.automl import create_auto_config +except ImportError: + pass + + +CSV_CONTENT = """ +name,gender,lives_in_sf +Jessica,f, +Jim,m,FALSE +""" + + +def get_test_df(): + temp = tempfile.NamedTemporaryFile(mode="w+") + temp.write(CSV_CONTENT) + temp.seek(0) + ds = read_csv(temp.name, dtype=None) + df = dd.from_pandas(ds, npartitions=1) + return df + + +@pytest.mark.distributed +@pytest.mark.distributed_c +def test_mixed_csv_data_source(ray_cluster_2cpu): + config = create_auto_config(dataset=get_test_df(), target=[], time_limit_s=3600) + + assert len(config["input_features"]) == 2 + assert config["input_features"][0]["type"] == TEXT + assert config["input_features"][1]["type"] == TEXT diff --git a/tests/ludwig/automl/test_target_detection.py b/tests/ludwig/automl/test_target_detection.py new file mode 100644 index 0000000..ce19f8d --- /dev/null +++ b/tests/ludwig/automl/test_target_detection.py @@ -0,0 +1,168 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Unit tests for ludwig.automl.target_detection.""" + +import numpy as np +import pandas as pd +import pytest + +from ludwig.automl.target_detection import ( + detect_all_target_candidates, + detect_target_column, + infer_task_type, + TaskType, +) + +# --------------------------------------------------------------------------- +# detect_target_column — name-based heuristics +# --------------------------------------------------------------------------- + + +def test_detect_by_name_target(): + df = pd.DataFrame( + { + "feature_a": range(100), + "feature_b": range(100), + "target": [0, 1] * 50, + } + ) + result = detect_target_column(df) + assert result.column == "target" + assert result.confidence >= 0.90 + + +def test_detect_by_name_label(): + df = pd.DataFrame( + { + "x": range(100), + "label": ["cat", "dog"] * 50, + } + ) + result = detect_target_column(df) + assert result.column == "label" + + +def test_detect_by_name_class(): + df = pd.DataFrame( + { + "x": range(50), + "class": ["a", "b"] * 25, + } + ) + result = detect_target_column(df) + assert result.column == "class" + + +# --------------------------------------------------------------------------- +# detect_target_column — positional heuristics +# --------------------------------------------------------------------------- + + +def test_detect_last_column(): + # No hint name; last column should be returned with confidence ~0.6 + df = pd.DataFrame( + { + "feature_a": range(100), + "feature_b": range(100), + "outcome_col": [0, 1] * 50, + } + ) + result = detect_target_column(df) + assert result.column == "outcome_col" + assert abs(result.confidence - 0.60) < 0.05 + + +# --------------------------------------------------------------------------- +# detect_target_column — binary balanced heuristic +# --------------------------------------------------------------------------- + + +def test_detect_binary_balanced(): + rng = np.random.default_rng(42) + df = pd.DataFrame( + { + "feature_a": rng.standard_normal(200), + "flag": rng.choice([0, 1], size=200, p=[0.5, 0.5]), + } + ) + # flag is the last column, so it wins on the last-column heuristic at 0.6 + result = detect_target_column(df) + assert result.column == "flag" + + +# --------------------------------------------------------------------------- +# infer_task_type +# --------------------------------------------------------------------------- + + +def test_infer_task_type_binary(): + series = pd.Series([0, 1, 0, 1, 1, 0]) + assert infer_task_type(series) == TaskType.BINARY + + +def test_infer_task_type_multiclass(): + series = pd.Series(["cat", "dog", "bird", "fish", "rabbit", "cat", "dog"]) + assert infer_task_type(series) == TaskType.MULTICLASS + + +def test_infer_task_type_regression(): + rng = np.random.default_rng(7) + # >20 distinct continuous float values → REGRESSION + series = pd.Series(rng.standard_normal(500)) + assert infer_task_type(series) == TaskType.REGRESSION + + +# --------------------------------------------------------------------------- +# detect_all_target_candidates +# --------------------------------------------------------------------------- + + +def test_all_candidates_sorted(): + df = pd.DataFrame( + { + "feature_a": range(100), + "feature_b": range(100), + "target": [0, 1] * 50, # should score highest (name hint) + } + ) + candidates = detect_all_target_candidates(df) + assert len(candidates) > 0 + confidences = [c.confidence for c in candidates] + assert confidences == sorted(confidences, reverse=True), "Candidates must be sorted by confidence descending" + + +# --------------------------------------------------------------------------- +# Edge cases +# --------------------------------------------------------------------------- + + +def test_empty_df_raises(): + df = pd.DataFrame() + with pytest.raises(ValueError): + detect_target_column(df) + + +def test_id_column_excluded(): + # Sequential 0..N integer column should not be the best candidate when a + # better heuristic column (e.g. "target") is also present. + df = pd.DataFrame( + { + "id": range(100), # sequential int — ID column + "feature": np.random.default_rng(0).standard_normal(100), + "target": [0, 1] * 50, + } + ) + result = detect_target_column(df) + assert result.column != "id", "Sequential ID column should not be chosen as the target" diff --git a/tests/ludwig/automl/test_tune_config.py b/tests/ludwig/automl/test_tune_config.py new file mode 100644 index 0000000..3fbd9ea --- /dev/null +++ b/tests/ludwig/automl/test_tune_config.py @@ -0,0 +1,64 @@ +import pytest + +try: + from ludwig.automl.auto_tune_config import reduce_text_feature_max_length +except ImportError: + pass + + +@pytest.mark.distributed +@pytest.mark.distributed_c +def test_reduce_text_model_mem_99ptile(): + config = {"input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}]} + training_set_metadata = {"description": {"max_sequence_length_99ptile": 117.0}} + config_upd = { + "input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}], + "preprocessing": {"text": {"max_sequence_length": 117}}, + } + reduce_text_feature_max_length(config, training_set_metadata) + assert config == config_upd + + +@pytest.mark.distributed +@pytest.mark.distributed_c +def test_reduce_text_model_mem_128(): + config = {"input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}]} + training_set_metadata = {"description": {"max_sequence_length_99ptile": 512.0}} + config_upd = { + "input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}], + "preprocessing": {"text": {"max_sequence_length": 128}}, + } + reduce_text_feature_max_length(config, training_set_metadata) + assert config == config_upd + + +@pytest.mark.distributed +@pytest.mark.distributed_c +def test_reduce_text_model_mem_override(): + config = { + "input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}], + "preprocessing": {"text": {"max_sequence_length": 256}}, + } + training_set_metadata = {"description": {"max_sequence_length_99ptile": 117.0}} + config_upd = { + "input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}], + "preprocessing": {"text": {"max_sequence_length": 117}}, + } + reduce_text_feature_max_length(config, training_set_metadata) + assert config == config_upd + + +@pytest.mark.distributed +@pytest.mark.distributed_c +def test_reduce_text_model_mem_respect(): + config = { + "input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}], + "preprocessing": {"text": {"max_sequence_length": 56}}, + } + training_set_metadata = {"description": {"max_sequence_length_99ptile": 117.0}} + config_upd = { + "input_features": [{"name": "description", "column": "description", "type": "text", "encoder": "bert"}], + "preprocessing": {"text": {"max_sequence_length": 56}}, + } + reduce_text_feature_max_length(config, training_set_metadata) + assert config == config_upd diff --git a/tests/ludwig/automl/test_utils.py b/tests/ludwig/automl/test_utils.py new file mode 100644 index 0000000..e930458 --- /dev/null +++ b/tests/ludwig/automl/test_utils.py @@ -0,0 +1,27 @@ +import pytest + +ray = pytest.importorskip("ray") + +from ludwig.utils.automl.utils import get_model_type # noqa + +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_c] + + +def _features(*in_types, out): + return { + "input_features": [{"name": f"in_{i}", "type": dtype} for i, dtype in enumerate(in_types)], + "output_features": [{"name": "out_0", "type": out}], + } + + +@pytest.mark.parametrize( + "config,expected", + [ + ({**_features("text", out="number")}, "text"), + ({**_features("text", "text", out="number")}, "concat"), + ({**_features("text", "text", out="number"), "combiner": {"type": "tabnet"}}, "tabnet"), + ], +) +def test_get_model_type(config, expected): + actual = get_model_type(config) + assert actual == expected diff --git a/tests/ludwig/backend/__init__.py b/tests/ludwig/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/backend/test_batch_infer_model.py b/tests/ludwig/backend/test_batch_infer_model.py new file mode 100644 index 0000000..2ba8a55 --- /dev/null +++ b/tests/ludwig/backend/test_batch_infer_model.py @@ -0,0 +1,148 @@ +"""Unit tests for the BatchInferModel inner class in ludwig.backend.ray. + +BatchInferModel is a dynamically-generated class returned by +RayPredictor.get_batch_infer_model(). These tests exercise: + - _prepare_batch: numpy conversion, stacking of non-scalar columns, reshape +without requiring a live Ray cluster, GPU, or real model weights. +""" + +from unittest.mock import MagicMock, patch + +import numpy as np +import pandas as pd + +from ludwig.constants import NAME, PROC_COLUMN, TYPE + +# BINARY/CATEGORY/NUMBER are scalar; other types (e.g. IMAGE) are non-scalar. +SCALAR_TYPE = "binary" +NON_SCALAR_TYPE = "image" + + +def _build_features(*col_defs): + """Build (features, training_set_metadata) dicts from (col, type, reshape?) tuples.""" + features = {} + metadata = {} + for col_type_reshape in col_defs: + col, col_type = col_type_reshape[:2] + reshape = col_type_reshape[2] if len(col_type_reshape) > 2 else None + features[col] = {TYPE: col_type, NAME: col, PROC_COLUMN: col} + metadata[col] = {"reshape": reshape} + return features, metadata + + +def _get_batch_infer_class(features, training_set_metadata, num_gpus=0): + """Return the BatchInferModel class with all heavy dependencies mocked.""" + mock_model = MagicMock() + mock_model.type.return_value = "ecd" + mock_model.to.return_value = mock_model + + mock_predictor = MagicMock() + + with patch("ludwig.backend.ray.ray") as mock_ray: + mock_ray.put.return_value = "obj_ref" + mock_ray.get.return_value = mock_model + + with patch("ludwig.backend.ray.get_predictor_cls") as mock_get_cls: + mock_get_cls.return_value = lambda **kw: mock_predictor + + with patch("ludwig.backend.ray.get_torch_device", return_value="cpu"): + from ludwig.backend.ray import RayPredictor + + predictor = MagicMock(spec=RayPredictor) + predictor.get_resources_per_worker = MagicMock(return_value=(1, num_gpus)) + + BatchInferModel = RayPredictor.get_batch_infer_model( + predictor, + model=mock_model, + predictor_kwargs={}, + output_columns=list(features.keys()), + features=features, + training_set_metadata=training_set_metadata, + ) + + return BatchInferModel, mock_model, mock_predictor + + +def _instantiate(BatchInferModel, mock_model, mock_predictor): + """Instantiate BatchInferModel with mocked Ray and predictor.""" + with patch("ludwig.backend.ray.ray") as mock_ray: + mock_ray.get.return_value = mock_model + + with patch("ludwig.backend.ray.get_predictor_cls") as mock_get_cls: + mock_get_cls.return_value = lambda **kw: mock_predictor + + with patch("ludwig.backend.ray.get_torch_device", return_value="cpu"): + return BatchInferModel() + + +class TestPrepareBatch: + def test_scalar_column_converted_to_numpy(self): + features, meta = _build_features(("label", SCALAR_TYPE)) + Cls, model, predictor = _get_batch_infer_class(features, meta) + inst = _instantiate(Cls, model, predictor) + + df = pd.DataFrame({"label": [0, 1, 0, 1]}) + result = inst._prepare_batch(df) + + assert isinstance(result["label"], np.ndarray) + np.testing.assert_array_equal(result["label"], [0, 1, 0, 1]) + + def test_non_scalar_column_is_stacked(self): + features, meta = _build_features(("image", NON_SCALAR_TYPE)) + Cls, model, predictor = _get_batch_infer_class(features, meta) + inst = _instantiate(Cls, model, predictor) + + rows = [np.zeros((3, 4)), np.ones((3, 4)), np.full((3, 4), 2.0)] + df = pd.DataFrame({"image": rows}) + result = inst._prepare_batch(df) + + assert isinstance(result["image"], np.ndarray) + assert result["image"].shape == (3, 3, 4) + + def test_reshape_applied(self): + features, meta = _build_features(("flat", NON_SCALAR_TYPE, (2, 5))) + Cls, model, predictor = _get_batch_infer_class(features, meta) + inst = _instantiate(Cls, model, predictor) + + rows = [np.zeros(10), np.ones(10)] + df = pd.DataFrame({"flat": rows}) + result = inst._prepare_batch(df) + + assert result["flat"].shape == (2, 2, 5), result["flat"].shape + + def test_no_reshape_when_none(self): + features, meta = _build_features(("x", NON_SCALAR_TYPE, None)) + Cls, model, predictor = _get_batch_infer_class(features, meta) + inst = _instantiate(Cls, model, predictor) + + rows = [np.zeros((4,)), np.ones((4,))] + df = pd.DataFrame({"x": rows}) + result = inst._prepare_batch(df) + + assert result["x"].shape == (2, 4) + + def test_mixed_scalar_and_non_scalar(self): + features, meta = _build_features(("cat", SCALAR_TYPE), ("img", NON_SCALAR_TYPE)) + Cls, model, predictor = _get_batch_infer_class(features, meta) + inst = _instantiate(Cls, model, predictor) + + df = pd.DataFrame( + { + "cat": [0, 1, 2], + "img": [np.zeros((2,)), np.ones((2,)), np.full((2,), 3.0)], + } + ) + result = inst._prepare_batch(df) + + assert result["cat"].shape == (3,) + assert result["img"].shape == (3, 2) + + def test_empty_dataframe_returns_empty_arrays(self): + features, meta = _build_features(("label", SCALAR_TYPE)) + Cls, model, predictor = _get_batch_infer_class(features, meta) + inst = _instantiate(Cls, model, predictor) + + df = pd.DataFrame({"label": pd.Series([], dtype=float)}) + result = inst._prepare_batch(df) + + assert result["label"].shape == (0,) diff --git a/tests/ludwig/backend/test_ray.py b/tests/ludwig/backend/test_ray.py new file mode 100644 index 0000000..559f40c --- /dev/null +++ b/tests/ludwig/backend/test_ray.py @@ -0,0 +1,115 @@ +import copy +from unittest.mock import patch + +import pytest + +# Skip these tests if Ray is not installed +ray = pytest.importorskip("ray") + +from ray.train.torch import TorchConfig # noqa + +from ludwig.backend import initialize_backend # noqa +from ludwig.backend.ray import get_trainer_kwargs # noqa +from ludwig.constants import AUTO, EXECUTOR, MAX_CONCURRENT_TRIALS, RAY # noqa + +# Mark the entire module as distributed +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_d] + + +@pytest.mark.parametrize( + "trainer_config,cluster_resources,num_nodes,expected_kwargs", + [ + # Prioritize using the GPU when available over multi-node + pytest.param( + {}, + {"CPU": 4, "GPU": 1}, + 2, + dict( + backend=TorchConfig(), + num_workers=1, + use_gpu=True, + resources_per_worker={ + "CPU": 0, + "GPU": 1, + }, + ), + id="accelerate", + marks=pytest.mark.distributed, + ), + ], +) +def test_get_trainer_kwargs(trainer_config, cluster_resources, num_nodes, expected_kwargs): + with patch("ludwig.backend.ray.ray.cluster_resources", return_value=cluster_resources): + with patch("ludwig.backend.ray._num_nodes", return_value=num_nodes): + trainer_config_copy = copy.deepcopy(trainer_config) + actual_kwargs = get_trainer_kwargs(**trainer_config_copy) + + # Function should not modify the original input + assert trainer_config_copy == trainer_config + + actual_backend = actual_kwargs.pop("backend") + expected_backend = expected_kwargs.pop("backend") + + assert type(actual_backend) is type(expected_backend) + assert actual_kwargs == expected_kwargs + + +@pytest.mark.distributed +@pytest.mark.parametrize( + "hyperopt_config_old, hyperopt_config_expected", + [ + ( # If max_concurrent_trials is none, it should not be set in the updated config + { + "parameters": {"trainer.learning_rate": {"space": "choice", "values": [0.001, 0.01, 0.1]}}, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": None}, + }, + { + "parameters": {"trainer.learning_rate": {"space": "choice", "values": [0.001, 0.01, 0.1]}}, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": None}, + }, + ), + ( # If max_concurrent_trials is auto, set to cpus // cpus_per_trial + { + "parameters": {"trainer.learning_rate": {"space": "choice", "values": [0.001, 0.01, 0.1]}}, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": "auto"}, + }, + { + "parameters": {"trainer.learning_rate": {"space": "choice", "values": [0.001, 0.01, 0.1]}}, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": 4}, + }, + ), + ( # Even though num_samples is set to 4, this will actually result in 9 trials. + # With 4 CPUs and 1 CPU/trial, max_concurrent_trials = 4 + { + "parameters": { + "trainer.learning_rate": {"space": "grid_search", "values": [0.001, 0.01, 0.1]}, + "combiner.num_fc_layers": {"space": "grid_search", "values": [1, 2, 3]}, + }, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": "auto"}, + }, + { + "parameters": { + "trainer.learning_rate": {"space": "grid_search", "values": [0.001, 0.01, 0.1]}, + "combiner.num_fc_layers": {"space": "grid_search", "values": [1, 2, 3]}, + }, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": 4}, + }, + ), + ( # Ensure user config value (1) is respected if it is passed in + { + "parameters": {"trainer.learning_rate": {"space": "choice", "values": [0.001, 0.01, 0.1]}}, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": 1}, + }, + { + "parameters": {"trainer.learning_rate": {"space": "choice", "values": [0.001, 0.01, 0.1]}}, + "executor": {"num_samples": 4, "cpu_resources_per_trial": 1, "max_concurrent_trials": 1}, + }, + ), + ], + ids=["none", "auto", "auto_with_large_num_trials", "1"], +) +def test_set_max_concurrent_trials(hyperopt_config_old, hyperopt_config_expected, ray_cluster_4cpu): + backend = initialize_backend(RAY) + if hyperopt_config_old[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO: + hyperopt_config_old[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config_old) + assert hyperopt_config_old == hyperopt_config_expected diff --git a/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_1.yaml b/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_1.yaml new file mode 100644 index 0000000..c91dbda --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_1.yaml @@ -0,0 +1,19 @@ +# This benchmarking config is missing because the global experiment name is missing. +process_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py +hyperopt: false +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +profiler: + enable: false + use_torch_profiler: false + logging_interval: 0.1 +experiments: + - dataset_name: ames_housing + experiment_name: large_learning_rate + config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml + - dataset_name: protein + config_path: tests/regression_tests/benchmark/configs/protein.yaml + - dataset_name: mercedes_benz_greener + experiment_name: zscore_normalization + config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml diff --git a/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_2.yaml b/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_2.yaml new file mode 100644 index 0000000..c1bcd82 --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_2.yaml @@ -0,0 +1,17 @@ +# This benchmarking config is invalid beacuse it's missing the export section. +experiment_name: github_action +process_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py +hyperopt: false +profiler: + enable: false + use_torch_profiler: false + logging_interval: 0.1 +experiments: + - dataset_name: ames_housing + experiment_name: large_learning_rate + config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml + - dataset_name: protein + config_path: tests/regression_tests/benchmark/configs/protein.yaml + - dataset_name: mercedes_benz_greener + experiment_name: zscore_normalization + config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml diff --git a/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_3.yaml b/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_3.yaml new file mode 100644 index 0000000..514e7ed --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_3.yaml @@ -0,0 +1,18 @@ +# This benchmarking config is invalid because some of the dataset names aren't specified. +experiment_name: github_action +process_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py +hyperopt: false +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +profiler: + enable: false + use_torch_profiler: false + logging_interval: 0.1 +experiments: + - experiment_name: large_learning_rate + config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml + - config_path: tests/regression_tests/benchmark/configs/protein.yaml + - dataset_name: mercedes_benz_greener + experiment_name: zscore_normalization + config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml diff --git a/tests/ludwig/benchmarking/example_files/process_config.py b/tests/ludwig/benchmarking/example_files/process_config.py new file mode 100644 index 0000000..cd28f8d --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/process_config.py @@ -0,0 +1,29 @@ +def process_config(ludwig_config: dict, experiment_dict: dict) -> dict: + """Modify a Ludwig config. + + :param ludwig_config: a Ludwig config. + :param experiment_dict: a benchmarking config experiment dictionary. + + returns: a modified Ludwig config. + """ + + # Only keep input_features and output_features for the ames_housing dataset. + if experiment_dict["dataset_name"] == "ames_housing": + main_config_keys = list(ludwig_config.keys()) + for key in main_config_keys: + if key not in ["input_features", "output_features"]: + del ludwig_config[key] + + # Set the early_stop criteria to stop training after 7 epochs of no score improvement. + ludwig_config["trainer"] = {"early_stop": 7} + + # use sparse encoder for categorical features to mimic logreg + ludwig_config["combiner"] = {"type": "concat"} + for i, feature in enumerate(ludwig_config["input_features"]): + if feature["type"] == "category": + ludwig_config["input_features"][i]["encoder"] = "sparse" + for i, feature in enumerate(ludwig_config["output_features"]): + if feature["type"] == "category": + ludwig_config["output_features"][i]["encoder"] = "sparse" + + return ludwig_config diff --git a/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_1.yaml b/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_1.yaml new file mode 100644 index 0000000..49dbb19 --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_1.yaml @@ -0,0 +1,24 @@ +# You can specify any of the global parameters locally to any experiment. This will override the global behavior. +experiment_name: github_action +process_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +profiler: + enable: false + use_torch_profiler: false + logging_interval: 0.1 +experiments: + - dataset_name: ames_housing + experiment_name: large_learning_rate + config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml + hyperopt: true + - dataset_name: protein + config_path: tests/regression_tests/benchmark/configs/protein.yaml + profiler: + enable: true + use_torch_profiler: true + logging_interval: 0.1 + - dataset_name: mercedes_benz_greener + experiment_name: zscore_normalization + config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml diff --git a/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_2.yaml b/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_2.yaml new file mode 100644 index 0000000..b8aaa2f --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_2.yaml @@ -0,0 +1,13 @@ +# This is a minimal example of a valid benchmarking config. the hyperopt section of the benchmarking config +# will default to false. The profiler section will also default to false. +experiment_name: github_action +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +experiments: + - dataset_name: ames_housing + config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml + - dataset_name: protein + config_path: tests/regression_tests/benchmark/configs/protein.yaml + - dataset_name: mercedes_benz_greener + config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml diff --git a/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_3.yaml b/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_3.yaml new file mode 100644 index 0000000..6bf1a99 --- /dev/null +++ b/tests/ludwig/benchmarking/example_files/valid/benchmarking_config_3.yaml @@ -0,0 +1,19 @@ +# We can skip specifying a global experiment name if it's specified for each experiment. +process_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py +export: + export_artifacts: true + export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/ # include the slash at the end. +profiler: + enable: true + use_torch_profiler: false + logging_interval: 0.1 +experiments: + - dataset_name: ames_housing + experiment_name: large_learning_rate + config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml + - dataset_name: protein + experiment_name: decay_rate_0.8 + config_path: tests/regression_tests/benchmark/configs/protein.yaml + - dataset_name: mercedes_benz_greener + experiment_name: zscore_normalization + config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml diff --git a/tests/ludwig/benchmarking/test_benchmarking.py b/tests/ludwig/benchmarking/test_benchmarking.py new file mode 100644 index 0000000..41ebb83 --- /dev/null +++ b/tests/ludwig/benchmarking/test_benchmarking.py @@ -0,0 +1,31 @@ +import os +from contextlib import nullcontext as does_not_raise + +import pytest + +from ludwig.benchmarking.utils import validate_benchmarking_config +from ludwig.utils.data_utils import load_yaml + + +def get_benchamrking_configs(validity): + local_dir = "/".join(__file__.split("/")[:-1]) + return [ + os.path.join(local_dir, "example_files", validity, config_fp) + for config_fp in os.listdir(os.path.join(local_dir, "example_files", validity)) + ] + + +@pytest.mark.parametrize("benchmarking_config_fp", get_benchamrking_configs("valid")) +def test_valid_benchmarking_configs_valid(benchmarking_config_fp): + benchmarking_config = load_yaml(benchmarking_config_fp) + + with does_not_raise(): + validate_benchmarking_config(benchmarking_config) + + +@pytest.mark.parametrize("benchmarking_config_fp", get_benchamrking_configs("invalid")) +def test_invalid_benchmarking_configs_valid(benchmarking_config_fp): + benchmarking_config = load_yaml(benchmarking_config_fp) + + with pytest.raises(ValueError): + validate_benchmarking_config(benchmarking_config) diff --git a/tests/ludwig/benchmarking/test_profiler.py b/tests/ludwig/benchmarking/test_profiler.py new file mode 100644 index 0000000..098ad3a --- /dev/null +++ b/tests/ludwig/benchmarking/test_profiler.py @@ -0,0 +1,70 @@ +import os +import time + +import numpy as np +import pandas as pd +import pytest +import torch +from packaging.version import parse as parse_version + +if parse_version(pd.__version__) > parse_version("1.5.3"): + pytest.skip(allow_module_level=True) + + +from ludwig.api import LudwigModel +from ludwig.benchmarking.profiler import LudwigProfiler +from ludwig.constants import BATCH_SIZE, TRAINER + + +@pytest.mark.skipif( + parse_version(pd.__version__) > parse_version("1.5.3"), + reason="experiment_impact_tracker package is incompatible with pandas 2.0", +) +def test_ludwig_profiler(tmpdir): + @LudwigProfiler(tag="test_function", output_dir=tmpdir, use_torch_profiler=False, logging_interval=0.1) + def func(duration): + time.sleep(duration) + x = torch.Tensor(2, 3) + y = torch.rand(2, 3) + torch.add(x, y) + + train_df = pd.DataFrame(np.random.normal(0, 1, size=(100, 3)), columns=["input_1", "input_2", "output_1"]) + eval_df = pd.DataFrame(np.random.normal(0, 1, size=(20, 3)), columns=["input_1", "input_2", "output_1"]) + + config = { + "input_features": [{"name": "input_1", "type": "number"}, {"name": "input_2", "type": "number"}], + "output_features": [{"name": "output_1", "type": "number"}], + "combiner": {"type": "concat", "output_size": 14}, + TRAINER: {"epochs": 1, BATCH_SIZE: 128}, + } + + model = LudwigModel(config=config, backend="local") + + with LudwigProfiler(tag="profile_1", output_dir=tmpdir, use_torch_profiler=False, logging_interval=0.1): + model.train( + dataset=train_df, + output_directory=tmpdir, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + + assert os.path.exists(os.path.join(tmpdir, "system_resource_usage", "profile_1", "run_0.json")) + + with LudwigProfiler(tag="profile_2", output_dir=tmpdir, use_torch_profiler=True, logging_interval=0.1): + model.evaluate(dataset=eval_df) + func(0.1) + + assert os.path.exists(os.path.join(tmpdir, "system_resource_usage", "profile_2", "run_0.json")) + assert os.path.exists(os.path.join(tmpdir, "torch_ops_resource_usage", "profile_2", "run_0.json")) + + func(0.25) + func(0.5) + assert set(os.listdir(os.path.join(tmpdir, "system_resource_usage", "test_function"))) == { + "run_0.json", + "run_1.json", + "run_2.json", + } diff --git a/tests/ludwig/combiners/test_combiners.py b/tests/ludwig/combiners/test_combiners.py new file mode 100644 index 0000000..5c1018a --- /dev/null +++ b/tests/ludwig/combiners/test_combiners.py @@ -0,0 +1,831 @@ +import logging +from collections import OrderedDict + +import numpy as np +import pytest +import torch + +from ludwig.combiners.combiners import ( + ComparatorCombiner, + ConcatCombiner, + ProjectAggregateCombiner, + SequenceCombiner, + SequenceConcatCombiner, + TabNetCombiner, + TabTransformerCombiner, + TransformerCombiner, +) +from ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, TYPE +from ludwig.schema.combiners.comparator import ComparatorCombinerConfig +from ludwig.schema.combiners.concat import ConcatCombinerConfig +from ludwig.schema.combiners.project_aggregate import ProjectAggregateCombinerConfig +from ludwig.schema.combiners.sequence import SequenceCombinerConfig +from ludwig.schema.combiners.sequence_concat import SequenceConcatCombinerConfig +from ludwig.schema.combiners.tab_transformer import TabTransformerCombinerConfig +from ludwig.schema.combiners.tabnet import TabNetCombinerConfig +from ludwig.schema.combiners.transformer import TransformerCombinerConfig +from ludwig.schema.utils import load_config +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logging.getLogger("ludwig").setLevel(logging.INFO) + +DEVICE = get_torch_device() +BATCH_SIZE = 16 +SEQ_SIZE = 12 +HIDDEN_SIZE = 24 +OTHER_HIDDEN_SIZE = 32 +OUTPUT_SIZE = 8 +BASE_OUTPUT_SIZE = 16 +NUM_FILTERS = 20 +RANDOM_SEED = 1919 + + +# emulate Input Feature class. Need to provide output_shape property to +# mimic what happens during ECD.forward() processing. +class PseudoInputFeature: + def __init__(self, feature_name, output_shape, feature_type=None): + self.name = feature_name + self._output_shape = output_shape + self.feature_type = feature_type + + def type(self): + return self.feature_type + + @property + def output_shape(self): + return torch.Size(self._output_shape[1:]) + + +# helper function to test correctness of combiner output +def check_combiner_output(combiner, combiner_output, batch_size): + # check for required attributes + assert hasattr(combiner, "input_dtype") + assert hasattr(combiner, "output_shape") + + # check for correct data type + assert isinstance(combiner_output, dict) + + # required key present + assert "combiner_output" in combiner_output + + # check for correct output shape + assert combiner_output["combiner_output"].shape == (batch_size, *combiner.output_shape) + + +# generates encoder outputs and minimal input feature objects for testing +@pytest.fixture +def features_to_test(feature_list: list[tuple[str, list]]) -> tuple[dict, dict]: + # feature_list: list of tuples that define the output_shape and type + # of input features to generate. tuple[0] is input feature type, + # tuple[1] is expected encoder output shape for the input feature + + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_outputs = {} + input_features = {} + for i in range(len(feature_list)): + feature_name = f"feature_{i:02d}" + encoder_outputs[feature_name] = { + ENCODER_OUTPUT: torch.randn(feature_list[i][1], dtype=torch.float32, device=DEVICE) + } + input_features[feature_name] = PseudoInputFeature(feature_name, feature_list[i][1], feature_list[i][0]) + + return encoder_outputs, input_features + + +# set up simulated encoder outputs +@pytest.fixture +def encoder_outputs(): + # generates simulated encoder outputs dictionary: + # feature_1: shape [b, h1] tensor + # feature_2: shape [b, h2] tensor + # feature_3: shape [b, s, h1] tensor + # feature_4: shape [b, sh, h2] tensor + + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup synthetic encoder output for testing + encoder_outputs = {} + input_features = OrderedDict() + shapes_list = [ + [BATCH_SIZE, HIDDEN_SIZE], + [BATCH_SIZE, OTHER_HIDDEN_SIZE], + [BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], + [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE], + ] + feature_names = ["feature_" + str(i + 1) for i in range(len(shapes_list))] + + for feature_name, batch_shape in zip(feature_names, shapes_list): + encoder_outputs[feature_name] = {ENCODER_OUTPUT: torch.randn(batch_shape, dtype=torch.float32, device=DEVICE)} + if len(batch_shape) > 2: + encoder_outputs[feature_name][ENCODER_OUTPUT_STATE] = torch.randn( + [batch_shape[0], batch_shape[2]], dtype=torch.float32, device=DEVICE + ) + + # create pseudo input feature object + input_features[feature_name] = PseudoInputFeature(feature_name, batch_shape) + + return encoder_outputs, input_features + + +# setup encoder outputs for ComparatorCombiner +@pytest.fixture +def encoder_comparator_outputs(): + # generates simulated encoder outputs dictionary: + # feature_1: shape [b, h1] tensor + # feature_2: shape [b, h2] tensor + # feature_3: shape [b, s, h1] tensor + # feature_4: shape [b, sh, h2] tensor + + encoder_outputs = {} + input_features = {} + shapes_list = [ + [BATCH_SIZE, HIDDEN_SIZE], + [BATCH_SIZE, OTHER_HIDDEN_SIZE], + [BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], + [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE], + ] + text_feature_names = ["text_feature_" + str(i + 1) for i in range(len(shapes_list))] + image_feature_names = ["image_feature_" + str(i + 1) for i in range(len(shapes_list))] + for i, (feature_name, batch_shape) in enumerate(zip(text_feature_names, shapes_list)): + # is there a better way to do this? + if i == 0 or i == 3: + dot_product_shape = [batch_shape[0], BASE_OUTPUT_SIZE] + encoder_outputs[feature_name] = { + ENCODER_OUTPUT: torch.randn(dot_product_shape, dtype=torch.float32, device=DEVICE) + } + input_features[feature_name] = PseudoInputFeature(feature_name, dot_product_shape) + else: + encoder_outputs[feature_name] = { + ENCODER_OUTPUT: torch.randn(batch_shape, dtype=torch.float32, device=DEVICE) + } + input_features[feature_name] = PseudoInputFeature(feature_name, batch_shape) + + for i, (feature_name, batch_shape) in enumerate(zip(image_feature_names, shapes_list)): + if i == 0 or i == 3: + dot_product_shape = [batch_shape[0], BASE_OUTPUT_SIZE] + encoder_outputs[feature_name] = { + ENCODER_OUTPUT: torch.randn(dot_product_shape, dtype=torch.float32, device=DEVICE) + } + input_features[feature_name] = PseudoInputFeature(feature_name, dot_product_shape) + else: + encoder_outputs[feature_name] = { + ENCODER_OUTPUT: torch.randn(batch_shape, dtype=torch.float32, device=DEVICE) + } + input_features[feature_name] = PseudoInputFeature(feature_name, batch_shape) + + return encoder_outputs, input_features + + +# test for simple concatenation combiner +@pytest.mark.parametrize("norm", [None, "batch", "layer"]) +@pytest.mark.parametrize("number_inputs", [None, 1]) +@pytest.mark.parametrize("flatten_inputs", [True, False]) +@pytest.mark.parametrize("fc_layer", [None, [{"output_size": OUTPUT_SIZE}, {"output_size": OUTPUT_SIZE}]]) +def test_concat_combiner( + encoder_outputs: tuple, + fc_layer: list[dict] | None, + flatten_inputs: bool, + number_inputs: int | None, + norm: str, +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_outputs_dict, input_features_dict = encoder_outputs + + # setup encoder inputs to combiner based on test case + if not flatten_inputs: + # clean out rank-3 encoder outputs + for feature in ["feature_3", "feature_4"]: + del encoder_outputs_dict[feature] + del input_features_dict[feature] + if number_inputs == 1: + # need only one encoder output for the test + del encoder_outputs_dict["feature_2"] + del input_features_dict["feature_2"] + elif number_inputs == 1: + # require only one rank-3 encoder output for testing + for feature in ["feature_1", "feature_2", "feature_3"]: + del encoder_outputs_dict[feature] + del input_features_dict[feature] + + # setup combiner to test with pseudo input features + combiner = ConcatCombiner( + input_features_dict, + config=load_config(ConcatCombinerConfig, fc_layers=fc_layer, flatten_inputs=flatten_inputs, norm=norm), + ).to(DEVICE) + + # confirm correctness of input_shape property + assert isinstance(combiner.input_shape, dict) + for k in encoder_outputs_dict: + assert k in combiner.input_shape + assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k] + + # combine encoder outputs + combiner_output = combiner(encoder_outputs_dict) + + # check for correctness of combiner output + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + if fc_layer is not None: + # check for parameter updating if fully connected layer is present + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +# test for sequence concatenation combiner +@pytest.mark.parametrize("reduce_output", [None, "sum"]) +@pytest.mark.parametrize("main_sequence_feature", [None, "feature_3"]) +def test_sequence_concat_combiner( + encoder_outputs: tuple, main_sequence_feature: str | None, reduce_output: str | None +) -> None: + # extract encoder outputs and input feature dictionaries + encoder_outputs_dict, input_feature_dict = encoder_outputs + + # setup combiner for testing + combiner = SequenceConcatCombiner( + input_feature_dict, + config=load_config( + SequenceConcatCombinerConfig, main_sequence_feature=main_sequence_feature, reduce_output=reduce_output + ), + ).to(DEVICE) + + # confirm correctness of input_shape property + assert isinstance(combiner.input_shape, dict) + for k in encoder_outputs_dict: + assert k in combiner.input_shape + assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k] + + # calculate expected hidden size for concatenated tensors + hidden_size = 0 + for k in encoder_outputs_dict: + hidden_size += encoder_outputs_dict[k][ENCODER_OUTPUT].shape[-1] + + # confirm correctness of concatenated_shape + assert combiner.concatenated_shape[-1] == hidden_size + + # combine encoder outputs + combiner_output = combiner(encoder_outputs_dict) + + # check for correctness of combiner output + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # This combiner does not contain any learnable parameters, bypassing parameter update testing + + +# test for sequence combiner +@pytest.mark.parametrize("reduce_output", [None, "sum"]) +@pytest.mark.parametrize("encoder", ["rnn", "transformer"]) +@pytest.mark.parametrize("main_sequence_feature", [None, "feature_3"]) +def test_sequence_combiner( + encoder_outputs: tuple, main_sequence_feature: str | None, encoder: str, reduce_output: str | None +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_outputs_dict, input_features_dict = encoder_outputs + + combiner = SequenceCombiner( + input_features_dict, + config=load_config( + SequenceCombinerConfig, + main_sequence_feature=main_sequence_feature, + encoder={TYPE: encoder}, + reduce_output=reduce_output, + ), + # following emulates encoder parameters passed in from config file + output_size=OUTPUT_SIZE, + num_fc_layers=3, + ).to(DEVICE) + + # confirm correctness of input_shape property + assert isinstance(combiner.input_shape, dict) + for k in encoder_outputs_dict: + assert k in combiner.input_shape + assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k] + + # calculate expected hidden size for concatenated tensors + hidden_size = 0 + for k in encoder_outputs_dict: + hidden_size += encoder_outputs_dict[k][ENCODER_OUTPUT].shape[-1] + + # confirm correctness of concatenated_shape + assert combiner.concatenated_shape[-1] == hidden_size + + # combine encoder outputs + combiner_output = combiner(encoder_outputs_dict) + + # check for correctness of combiner output + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize( + "feature_list", # defines parameter for fixture features_to_test() + [ + [ # only numeric features + ("binary", [BATCH_SIZE, 1]), # passthrough encoder + ("number", [BATCH_SIZE, 1]), # passthrough encoder + ], + [ # only numeric features + ("binary", [BATCH_SIZE, 1]), # passthrough encoder + ("number", [BATCH_SIZE, 1]), # passthrough encoder + ("number", [BATCH_SIZE, 1]), # passthrough encoder + ], + [ # numeric and categorical features + ("binary", [BATCH_SIZE, 1]), # passthrough encoder + ("number", [BATCH_SIZE, 12]), # dense encoder + ("category", [BATCH_SIZE, 8]), # dense encoder + ], + ], +) +@pytest.mark.parametrize("size", [4, 8]) +@pytest.mark.parametrize("output_size", [6, 10]) +def test_tabnet_combiner(features_to_test: dict, size: int, output_size: int) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_outputs, input_features = features_to_test + + # setup combiner to test + combiner = TabNetCombiner( + input_features, + config=load_config( + TabNetCombinerConfig, + size=size, + output_size=output_size, + num_steps=3, + num_total_blocks=4, + num_shared_blocks=2, + dropout=0.1, + ), + ).to(DEVICE) + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs) + + # required key present + assert "combiner_output" in combiner_output + assert "attention_masks" in combiner_output + assert "aggregated_attention_masks" in combiner_output + + assert isinstance(combiner_output["combiner_output"], torch.Tensor) + assert combiner_output["combiner_output"].shape == (BATCH_SIZE, output_size) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize("fc_layer", [None, [{"output_size": 64}, {"output_size": 32}]]) +@pytest.mark.parametrize("entity_1", [["text_feature_1", "text_feature_4"]]) +@pytest.mark.parametrize("entity_2", [["image_feature_1", "image_feature_2"]]) +def test_comparator_combiner( + encoder_comparator_outputs: tuple, fc_layer: list[dict] | None, entity_1: str, entity_2: str +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_comparator_outputs_dict, input_features_dict = encoder_comparator_outputs + # clean out unneeded encoder outputs since we only have 2 layers + del encoder_comparator_outputs_dict["text_feature_2"] + del encoder_comparator_outputs_dict["image_feature_3"] + del encoder_comparator_outputs_dict["text_feature_3"] + del encoder_comparator_outputs_dict["image_feature_4"] + + # setup combiner to test set to 256 for case when none as it's the default size + output_size = fc_layer[0]["output_size"] if fc_layer else 256 + combiner = ComparatorCombiner( + input_features_dict, + config=load_config( + ComparatorCombinerConfig, entity_1=entity_1, entity_2=entity_2, fc_layers=fc_layer, output_size=output_size + ), + ).to(DEVICE) + + # concatenate encoder outputs + combiner_output = combiner(encoder_comparator_outputs_dict) + + # check for correctness of combiner output + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_comparator_outputs_dict,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize("output_size", [8, 16]) +@pytest.mark.parametrize("transformer_output_size", [4, 12]) +def test_transformer_combiner(encoder_outputs: tuple, transformer_output_size: int, output_size: int) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_outputs_dict, input_feature_dict = encoder_outputs + + # setup combiner to test + combiner = TransformerCombiner(input_features=input_feature_dict, config=load_config(TransformerCombinerConfig)).to( + DEVICE + ) + + # confirm correctness of input_shape property + assert isinstance(combiner.input_shape, dict) + for k in encoder_outputs_dict: + assert k in combiner.input_shape + assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k] + + # calculate expected hidden size for concatenated tensors + hidden_size = 0 + for k in encoder_outputs_dict: + hidden_size += np.prod(encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:]) + + # confirm correctness of effective_input_shape + assert combiner.concatenated_shape[-1] == hidden_size + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs_dict) + + # check for correctness of combiner output + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize("projection_size", [8, 16]) +@pytest.mark.parametrize("output_size", [8, 16]) +def test_project_aggregate_combiner(encoder_outputs: tuple, projection_size: int, output_size: int) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + encoder_outputs_dict, input_feature_dict = encoder_outputs + + # setup combiner to test + combiner = ProjectAggregateCombiner( + input_features=input_feature_dict, + config=load_config( + ProjectAggregateCombinerConfig, + projection_size=projection_size, + output_size=output_size, + ), + ).to(DEVICE) + + # confirm correctness of input_shape property + assert isinstance(combiner.input_shape, dict) + for k in encoder_outputs_dict: + assert k in combiner.input_shape + assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k] + + # calculate expected hidden size for concatenated tensors + hidden_size = 0 + for k in encoder_outputs_dict: + hidden_size += np.prod(encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:]) + + # confirm correctness of effective_input_shape + assert combiner.concatenated_shape[-1] == hidden_size + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs_dict) + + # check for correctness of combiner output + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +# Magic values for the TabTransformerCombiner test +PARAMETERS_IN_SELF_ATTENTION = 4 +PARAMETERS_IN_TRANSFORMER_BLOCK = 16 +UNEMBEDDABLE_LAYER_NORM_PARAMETERS = 2 + + +@pytest.mark.parametrize( + "feature_list", # defines parameter for fixture features_to_test() + [ + [ + ("binary", [BATCH_SIZE, 1]), # passthrough encoder + ("number", [BATCH_SIZE, 1]), # passthrough encoder + ], + [ + ("number", [BATCH_SIZE, 1]), + ("binary", [BATCH_SIZE, 1]), + ("number", [BATCH_SIZE, 1]), + ], + [ + ("binary", [BATCH_SIZE, 1]), + ("number", [BATCH_SIZE, 1]), + ("binary", [BATCH_SIZE, 1]), + ], + ], +) +@pytest.mark.parametrize( + "num_layers,reduce_output,fc_layers,embed_input_feature_name", + [ + (1, "concat", None, None), + (2, "sum", [{"output_size": 256}], 64), + (1, "sum", None, "add"), + ], + ids=["simple", "full", "add_embed"], +) +def test_tabtransformer_combiner_binary_and_number_without_category( + features_to_test: tuple, + embed_input_feature_name: int | str | None, + fc_layers: list | None, + reduce_output: str, + num_layers: int, +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + # retrieve simulated encoder outputs and input features for the test + encoder_outputs, input_features = features_to_test + + # setup combiner to test + combiner = TabTransformerCombiner( + input_features=input_features, + config=load_config( + TabTransformerCombinerConfig, + embed_input_feature_name=embed_input_feature_name, + # emulates parameters passed from combiner def + num_layers=num_layers, # number of transformer layers + fc_layers=fc_layers, # fully_connected layer definition + reduce_output=reduce_output, # sequence reducer + ), + ).to(DEVICE) + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs) + + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + combiner, + (encoder_outputs,), + target, + ) + + # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed + # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the + # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to + # instantiate the TabTransformerCombiner object. + + # The entire transformer stack is by-passed because there is no categorical input features. Subtract the + # number for parameters in the transformer stack to account for this situation. + + assert upc == ( + tpc - num_layers * PARAMETERS_IN_TRANSFORMER_BLOCK - (1 if embed_input_feature_name is not None else 0) + ), f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize( + "feature_list", # defines parameter for fixture features_to_test() + [ + [ + ("number", [BATCH_SIZE, 1]), # passthrough encoder + ("category", [BATCH_SIZE, 64]), + ("binary", [BATCH_SIZE, 1]), # passthrough encoder + ], + [ + ("binary", [BATCH_SIZE, 1]), # passthrough encoder + ("category", [BATCH_SIZE, 16]), + ("number", [BATCH_SIZE, 1]), # passthrough encoder + ("category", [BATCH_SIZE, 48]), + ("number", [BATCH_SIZE, 32]), + ("binary", [BATCH_SIZE, 1]), + ], + ], +) +@pytest.mark.parametrize( + "num_layers,reduce_output,fc_layers,embed_input_feature_name", + [ + (1, "concat", None, None), + (2, "sum", [{"output_size": 256}], 64), + (1, "sum", None, "add"), + ], + ids=["simple", "full", "add_embed"], +) +def test_tabtransformer_combiner_number_and_binary_with_category( + features_to_test: tuple, + embed_input_feature_name: int | str | None, + fc_layers: list | None, + reduce_output: str, + num_layers: int, +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + # retrieve simulated encoder outputs and input features for the test + encoder_outputs, input_features = features_to_test + + # setup combiner to test + combiner = TabTransformerCombiner( + input_features=input_features, + config=load_config( + TabTransformerCombinerConfig, + embed_input_feature_name=embed_input_feature_name, + # emulates parameters passed from combiner def + num_layers=num_layers, # number of transformer layers + fc_layers=fc_layers, # fully_connected layer definition + reduce_output=reduce_output, # sequence reducer + ), + ).to(DEVICE) + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs) + + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + combiner, + (encoder_outputs,), + target, + ) + + # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed + # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the + # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to + # instantiate the TabTransformerCombiner object. + + # With F.scaled_dot_product_attention, all parameters receive gradients even with a single category feature. + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize( + "feature_list", # defines parameter for fixture features_to_test() + [ + [ + ("binary", [BATCH_SIZE, 1]), + ("binary", [BATCH_SIZE, 1]), + ], + [ + ("number", [BATCH_SIZE, 1]), + ("number", [BATCH_SIZE, 1]), + ], + [ + ("number", [BATCH_SIZE, 1]), + ("binary", [BATCH_SIZE, 1]), + ], + ], +) +@pytest.mark.parametrize( + "num_layers,reduce_output,fc_layers,embed_input_feature_name", + [ + (1, "concat", None, None), + (2, "sum", [{"output_size": 256}], 64), + (1, "sum", None, "add"), + ], + ids=["simple", "full", "add_embed"], +) +def test_tabtransformer_combiner_number_or_binary_without_category( + features_to_test: tuple, + embed_input_feature_name: int | str | None, + fc_layers: list | None, + reduce_output: str, + num_layers: int, +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + # retrieve simulated encoder outputs and input features for the test + encoder_outputs, input_features = features_to_test + + # setup combiner to test + combiner = TabTransformerCombiner( + input_features=input_features, + config=load_config( + TabTransformerCombinerConfig, + embed_input_feature_name=embed_input_feature_name, + # emulates parameters passed from combiner def + num_layers=num_layers, # number of transformer layers + fc_layers=fc_layers, # fully_connected layer definition + reduce_output=reduce_output, # sequence reducer + ), + ).to(DEVICE) + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs) + + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + combiner, + (encoder_outputs,), + target, + ) + + # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed + # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the + # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to + # instantiate the TabTransformerCombiner object. + + # The entire transformer stack is by-passed because there is no categorical input features. Subtract the + # number for parameters in the transformer stack to account for this situation. + + assert upc == ( + tpc - num_layers * PARAMETERS_IN_TRANSFORMER_BLOCK - (1 if embed_input_feature_name is not None else 0) + ), f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize( + "feature_list", # defines parameter for fixture features_to_test() + [ + [ + ("binary", [BATCH_SIZE, 1]), + ("category", [BATCH_SIZE, 16]), + ("binary", [BATCH_SIZE, 1]), + ("category", [BATCH_SIZE, 32]), + ], + [ + ("number", [BATCH_SIZE, 1]), + ("category", [BATCH_SIZE, 16]), + ("number", [BATCH_SIZE, 1]), + ("category", [BATCH_SIZE, 32]), + ], + [ + ("number", [BATCH_SIZE, 1]), + ("category", [BATCH_SIZE, 16]), + ("binary", [BATCH_SIZE, 1]), + ("category", [BATCH_SIZE, 32]), + ], + ], +) +@pytest.mark.parametrize( + "num_layers,reduce_output,fc_layers,embed_input_feature_name", + [ + (1, "concat", None, None), + (2, "sum", [{"output_size": 256}], 64), + (1, "sum", None, "add"), + ], + ids=["simple", "full", "add_embed"], +) +def test_tabtransformer_combiner_number_or_binary_with_category( + features_to_test: tuple, + embed_input_feature_name: int | str | None, + fc_layers: list | None, + reduce_output: str, + num_layers: int, +) -> None: + # make repeatable + set_random_seed(RANDOM_SEED) + + # retrieve simulated encoder outputs and input features for the test + encoder_outputs, input_features = features_to_test + + # setup combiner to test + combiner = TabTransformerCombiner( + input_features=input_features, + config=load_config( + TabTransformerCombinerConfig, + embed_input_feature_name=embed_input_feature_name, + # emulates parameters passed from combiner def + num_layers=num_layers, # number of transformer layers + fc_layers=fc_layers, # fully_connected layer definition + reduce_output=reduce_output, # sequence reducer + ), + ).to(DEVICE) + + # concatenate encoder outputs + combiner_output = combiner(encoder_outputs) + + check_combiner_output(combiner, combiner_output, BATCH_SIZE) + + # check for parameter updating + target = torch.randn(combiner_output["combiner_output"].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + combiner, + (encoder_outputs,), + target, + ) + + # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed + # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the + # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to + # instantiate the TabTransformerCombiner object. + + # This test does not explicity test for a single categorical input feature + # in this situation of a one categorical input feature, the query and key parameters are not updated + + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/config_sampling/static_schema.json b/tests/ludwig/config_sampling/static_schema.json new file mode 100644 index 0000000..2a6c633 --- /dev/null +++ b/tests/ludwig/config_sampling/static_schema.json @@ -0,0 +1,146652 @@ +{ + "description": "Settings for Ludwig configuration", + "properties": { + "backend": { + "additionalProperties": {}, + "default": null, + "description": "", + "parameter_metadata": null, + "title": "backend", + "type": [ + "object", + "null" + ] + }, + "combiner": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "comparator" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "entity_1": { + "default": null, + "description": "The list of input feature names `[feature_1, feature_2, ...]` constituting the first entity to compare. *Required*.", + "items": { + "title": "entity_1", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://ludwig.ai/0.6/configuration/combiner/#comparator-combiner" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Entity 1" + }, + "title": "entity_1", + "type": [ + "array", + "null" + ] + }, + "entity_2": { + "default": null, + "description": "The list of input feature names `[feature_1, feature_2, ...]` constituting the second entity to compare. *Required*.", + "items": { + "title": "entity_2", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://ludwig.ai/0.6/configuration/combiner/#comparator-combiner" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Entity 2" + }, + "title": "entity_2", + "type": [ + "array", + "null" + ] + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Output size of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "15 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "concat" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "flatten_inputs": { + "default": false, + "description": "Whether to flatten input tensors to a vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "flatten_inputs", + "type": "boolean" + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Output size of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "16 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "residual": { + "default": false, + "description": "Whether to add a residual connection to each fully connected layer block. Requires all fully connected layers to have the same `output_size`.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "residual", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "project_aggregate" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Activation to apply to each fully connected layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias of the projection and for the fully connected layers.", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate to apply to each fully connected layer.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "Full specification of the fully connected layers after the aggregation. It should be a list of dict, each dict representing one layer of the fully connected layer stack. ", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": "layer", + "description": "Normalization to apply to each projection and fully connected layer.", + "enum": [ + "batch", + "layer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": "string" + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters of the normalization to apply to each projection and fully connected layer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 2, + "description": "Number of fully connected layers after aggregation.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 128, + "description": "Output size of each layer of the stack of fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "17 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "projection_size": { + "default": 128, + "description": "All combiner inputs are projected to this size before being aggregated.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "projection_size", + "type": "integer" + }, + "residual": { + "default": true, + "description": "Whether to add residual skip connection between the fully connected layers in the stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "residual", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layers use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights of the projection and for the fully connected layers.", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "sequence" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "transformer", + "cnnrnn", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "rnn" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h` tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps each element of the sequence to an embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "main_sequence_feature": { + "default": null, + "description": "\nName of a sequence, text, or time series feature to concatenate the outputs\nof the other features to. If no `main_sequence_feature` is specified, the combiner will look through all the features in\nthe order they are defined in the configuration and will look for a feature with a rank 3 tensor output (sequence, text\nor time series). If it cannot find one it will raise an exception, otherwise the output of that feature will be used for\nconcatenating the other features along the sequence `s` dimension. If there are other input features with a rank 3\noutput tensor, the combiner will concatenate them alongside the `s` dimension. All sequence-like input features must\nhave identical `s` dimension, otherwise an error will be thrown.\n", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "main_sequence_feature", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": null, + "description": "Strategy to use to aggregate the embeddings of the items of the set.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "sequence_concat" + } + } + }, + "then": { + "properties": { + "main_sequence_feature": { + "default": null, + "description": "\nName of a sequence, text, or time series feature to concatenate the outputs\nof the other features to. If no `main_sequence_feature` is specified, the combiner will look through all the features in\nthe order they are defined in the configuration and will look for a feature with a rank 3 tensor output (sequence, text\nor time series). If it cannot find one it will raise an exception, otherwise the output of that feature will be used for\nconcatenating the other features along the sequence `s` dimension. If there are other input features with a rank 3\noutput tensor, the combiner will concatenate them alongside the `s` dimension. All sequence-like input features must\nhave identical `s` dimension, otherwise an error will be thrown.\n", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "main_sequence_feature", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": null, + "description": "Strategy to use to aggregate the embeddings of the items of the set.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tabnet" + } + } + }, + "then": { + "properties": { + "bn_epsilon": { + "default": 0.001, + "description": "Epsilon to be added to the batch norm denominator.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value found in popular ML packages like Keras and Tensorflow.", + "description_implications": "An epsilon is added to the denominator of the batch normalization operation so that the function converges. Setting the epsilon to 0 is inadvisable.", + "example_value": [ + 1e-05 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Keras example](https://keras.io/api/layers/normalization_layers/batch_normalization/)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1e-3-1e-9", + "suggested_values_reasoning": "Common epsilon choices", + "ui_display_name": "Batch Normalization Epsilon" + }, + "title": "bn_epsilon", + "type": "number" + }, + "bn_momentum": { + "default": 0.05, + "description": "Momentum of the batch norm. 1 - `m_B` from the TabNet paper.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Higher values result in faster updates, but more sensitivity to noise in the dataset. Lower values result in slower updates.\n\nIf momentum is set to 0, moving statistics will not be updated during training. This is likely to cause variance between train and test performance, and is not recommended.", + "example_value": [ + 0.05 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "TabNet Paper: https://arxiv.org/abs/1908.07442", + "Torch Batch Norm: https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html" + ], + "long_description": "", + "other_information": "`bn_momentum` is only used if `norm`: `batch`. For other values of `norm` it has no effect.\n\n`bn_momentum` is different from optimizer momentum. Batch norm moving estimate statistics are updated according to the rule:\nx_hat = (1 - momentum) * x_hat + momentum * x_t,\nwhere x_hat is the estimated statistic and x_t is the new observed value.", + "related_parameters": null, + "short_description": "", + "suggested_values": "0.01-0.2", + "suggested_values_reasoning": null, + "ui_display_name": "Batch Norm Momentum" + }, + "title": "bn_momentum", + "type": "number" + }, + "bn_virtual_bs": { + "default": 1024, + "description": "Size of the virtual batch size used by ghost batch norm. If null, regular batch norm is used instead. `B_v` from the TabNet paper.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Paper default.", + "description_implications": "Virtual Batch Normalization is a normalization method that extends batch normalization. Regular batch normalization causes the output of a neural network for an input example to be highly dependent on several other inputs in the same minibatch. To avoid this problem in virtual batch normalization (VBN), each example is normalized based on the statistics collected on a reference batch of examples that are chosen once and fixed at the start of training, and on itself. The reference batch is normalized using only its own statistics. VBN is computationally expensive because it requires running forward propagation on two minibatches of data, so the authors use it only in the generator network. A higher virtual batch size could improve normalization, but it also causes training to run slower since each batch will be sampled multiple times.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://paperswithcode.com/method/virtual-batch-normalization" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Ghost Normalization: Virtual batch size" + }, + "title": "bn_virtual_bs", + "type": [ + "integer", + "null" + ] + }, + "dropout": { + "default": 0.05, + "description": "Dropout rate for the transformer block.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from published literature (https://arxiv.org/abs/1908.07442).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "entmax_alpha": { + "default": 1.5, + "description": "Must be a number between 1.0 and 2.0. If entmax_mode is `adaptive`, `entmax_alpha` is used as the initial value for the learnable parameter. 1 corresponds to softmax, 2 is sparsemax.", + "format": "float", + "maximum": 2, + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "entmax_alpha", + "type": "number" + }, + "entmax_mode": { + "default": "sparsemax", + "description": "Entmax is a sparse family of probability mapping which generalizes softmax and sparsemax. `entmax_mode` controls the sparsity", + "enum": [ + "entmax15", + "sparsemax", + "constant", + "adaptive" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "entmax_mode", + "type": "string" + }, + "num_shared_blocks": { + "default": 2, + "description": "Number of shared feature transformer blocks across the steps.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_shared_blocks", + "type": "integer" + }, + "num_steps": { + "default": 3, + "description": "Number of steps / repetitions of the the attentive transformer and feature transformer computations. `N_steps` in (Arik and Pfister, 2019).", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_steps", + "type": "integer" + }, + "num_total_blocks": { + "default": 4, + "description": "Total number of feature transformer blocks at each step.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_total_blocks", + "type": "integer" + }, + "output_size": { + "default": 128, + "description": "Output size of a fully connected layer. `N_d` in (Arik and Pfister, 2019).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "18 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "relaxation_factor": { + "default": 1.5, + "description": "Factor that influences how many times a feature should be used across the steps of computation. a value of 1 implies it each feature should be use once, a higher value allows for multiple usages. `gamma` in (Arik and Pfister, 2019).", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "relaxation_factor", + "type": "number" + }, + "size": { + "default": 32, + "description": "Size of the hidden layers. `N_a` in (Arik and Pfister, 2019).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "size", + "type": "integer" + }, + "sparsity": { + "default": 0.0001, + "description": "Multiplier of the sparsity inducing loss. `lambda_sparse` in (Arik and Pfister, 2019).", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sparsity", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tabtransformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "Dropout rate for the transformer block.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from published literature (https://arxiv.org/abs/1706.03762).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embed_input_feature_name": { + "description": "Valid options for embedding (or not embedding) input feature names.", + "oneOf": [ + { + "default": "add", + "description": "MISSING", + "enum": [ + "add" + ], + "title": "embed_string_option", + "type": "string" + }, + { + "description": "MISSING", + "title": "embed_integer_option", + "type": "integer" + }, + { + "description": "MISSING", + "title": "embed_null_option", + "type": "null" + } + ], + "title": "embed_input_feature_name" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "activation, activation_function, conv_activation, recurrent_activation" + ], + "short_description": "", + "suggested_values": "relu, alternatively leakyRelu or elu", + "suggested_values_reasoning": "The default value will work well in the majority of the cases", + "ui_display_name": "FC Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_residual": { + "default": false, + "description": "Whether to add a residual connection to each fully connected layer block. Requires all fully connected layers to have the same `output_size`.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_residual", + "type": "boolean" + }, + "hidden_size": { + "default": 256, + "description": "The number of hidden units of the TransformerStack as well as the dimension that each incoming input feature is projected to before feeding to the TransformerStack.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers (only applies if `reduce_output` is not null).", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of heads of the self attention in the transformer block.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Output size of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "21 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "reduce_output": { + "default": "concat", + "description": "Strategy to use to aggregate the output of the transformer.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as `hidden_size` and `embedding_size`.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "22 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "Dropout rate for the transformer block.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from published literature (https://arxiv.org/abs/1706.03762).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "activation, activation_function, conv_activation, recurrent_activation" + ], + "short_description": "", + "suggested_values": "relu, alternatively leakyRelu or elu", + "suggested_values_reasoning": "The default value will work well in the majority of the cases", + "ui_display_name": "FC Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_residual": { + "default": false, + "description": "Whether to add a residual connection to each fully connected layer block. Requires all fully connected layers to have the same `output_size`.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_residual", + "type": "boolean" + }, + "hidden_size": { + "default": 256, + "description": "The number of hidden units of the TransformerStack as well as the dimension that each incoming input feature is projected to before feeding to the TransformerStack.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers (only applies if `reduce_output` is not null).", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of heads of the self attention in the transformer block.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Output size of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "21 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "reduce_output": { + "default": "mean", + "description": "Strategy to use to aggregate the output of the transformer.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as `hidden_size` and `embedding_size`.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "22 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "concat", + "description": "Select the combiner type.", + "enum": [ + "comparator", + "concat", + "project_aggregate", + "sequence", + "sequence_concat", + "tabnet", + "tabtransformer", + "transformer" + ], + "enumDescriptions": { + "comparator": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The comparator combiner compares the hidden representation of two entities defined by lists of features. It assumes all outputs from encoders are tensors of size `b x h` where `b` is the batch size and `h` is the hidden dimension, which can be different for each input. If the input tensors have a different shape, it automatically flattens them. It then concatenates the representations of each entity and projects them both to vectors of size `output_size`. Finally, it compares the two entity representations by dot product, element-wise multiplication, absolute difference and bilinear product. It returns the final `b x h` tensor where `h` is the size of the concatenation of the four comparisons.", + "other_information": null, + "related_parameters": null, + "short_description": "Used for recommendation problems, features associated with distinct entities, output depends on entity-level comparison.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "concat": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The concat combiner assumes all outputs from encoders are tensors of size `b x h` where `b` is the batch size and `h` is the hidden dimension, which can differ for each input. It concatenates along the `h` dimension, and then (optionally) passes the concatenated tensor through a stack of fully connected layers. It returns the final `b x h` tensor where `h` is the size of the last fully connected layer or the sum of the sizes of the `h` of all inputs in the case there are no fully connected layers. If there is only a single input feature and no fully connected layers, the output of the input feature encoder is passed through the combiner unchanged.", + "other_information": null, + "related_parameters": null, + "short_description": "Concatenates outputs of all encoders and passes concatenated representation through stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "project_aggregate": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The project aggregate combiner projects the input vectors to a common size and then aggregates them by taking the average across all the vectors.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects the encoder outputs to a common size then takes the average.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "sequence": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The sequence combiner stacks a sequence concat combiner with a sequence encoder. All the considerations about input tensor ranks described for the sequence concat combiner apply also in this case, but the main difference is that this combiner uses the `b x s x h` output of the sequence concat combiner, where `b` is the batch size, `s` is the sequence length and `h` is the sum of the hidden dimensions of all input features, as input for any of the sequence encoders described in the sequence features encoders section. All considerations on the shape of the outputs for the sequence encoders also apply to the sequence combiner.", + "other_information": null, + "related_parameters": null, + "short_description": "Stacks a sequence concat combiner with a sequence encoder.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "sequence_concat": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The sequence_concat combiner assumes at least one output from the encoders is a tensor of size `b x s x h` where `b` is the batch size, `s` is the length of the sequence and `h` is the hidden dimension. A sequence-like (sequence, text or time series) input feature can be specified with the `main_sequence_feature` parameter which takes the name of sequence-like input feature as its value. If no `main_sequence_feature` is specified, the combiner will look through all the features in the order they are defined in the configuration and will look for a feature with a rank 3 tensor output (sequence, text or time series). If it cannot find one it will raise an exception, otherwise the output of that feature will be used for concatenating the other features along the sequence `s` dimension.\nIf there are other input features with a rank 3 output tensor, the combiner will concatenate them alongside the s dimension, which means that all of them must have identical s dimension, otherwise a dimension mismatch error will be returned thrown during training when a datapoint with two sequential features of different lengths are provided.\nOther features that have a b x h rank 2 tensor output will be replicated s times and concatenated to the s dimension. The final output is a b x s x h' tensor where h' is the size of the concatenation of the h dimensions of all input features.", + "other_information": null, + "related_parameters": null, + "short_description": "Concatenates the outputs of multiple sequence features.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "tabnet": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1908.07442" + ], + "long_description": "The tabnet combiner implements the TabNet model, which uses attention and sparsity to achieve high performance on tabular data. It assumes all outputs from encoders are tensors of size b x h where b is the batch size and h is the hidden dimension, which can be different for each input. If the input tensors have a different shape, it automatically flattens them. It returns the final b x h' tensor where h' is the user-specified output size.", + "other_information": null, + "related_parameters": null, + "short_description": "Tabnet is specifically tailored for high performance on tabular data.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "tabtransformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/2012.06678" + ], + "long_description": "The tabtransformer combiner combines input features in the following sequence of operations. Except for binary and number features, the combiner projects features to an embedding size. These features are concatenated as if they were a sequence and passed through a transformer. After the transformer, the number and binary features are concatenated (which are of size 1) and then concatenated with the output of the transformer and is passed to a stack of fully connected layers (from TabTransformer Tabular Data Modeling Using Contextual Embeddings). It assumes all outputs from encoders are tensors of size `b x h` where `b` is the batch size and `h` is the hidden dimension, which can be different for each input. If the input tensors have a different shape, it automatically flattens them. It then projects each input tensor to the same hidden / embedding size and encodes them with a stack of Transformer layers. Finally, the transformer combiner applies a reduction to the outputs of the Transformer stack, followed by the above concatenation and optional fully connected layers. The output is a `b x h` tensor where `h` is the size of the last fully connected layer or the hidden / embedding size, or a `b x n x h` where `n` is the number of input features and `h` is the hidden / embedding size if no reduction is applied.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects and concatenates features, then passes them through a transformer.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer combiner combines input features using a stack of Transformer blocks (from Attention Is All You Need). It assumes all outputs from encoders are tensors of size `b x h` where `b` is the batch size and `h` is the hidden dimension, which can be different for each input. If the input tensors have a different shape, it automatically flattens them. It then projects each input tensor to the same hidden / embedding size and encodes them with a stack of Transformer layers. Finally, the transformer combiner applies a reduction to the outputs of the Transformer stack, followed by optional fully connected layers. The output is a `b x h` tensor where `h` is the size of the last fully connected layer or the hidden / embedding size, or a `b x n x h` where `n` is the number of input features and `h` is the hidden / embedding size if no reduction is applied.", + "other_information": null, + "related_parameters": null, + "short_description": "The transformer combiner combines input features using a stack of Transformer blocks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "parameter_metadata": { + "commonly_used": true, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Combiner Type" + }, + "title": "combiner_options", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "defaults": { + "additionalProperties": true, + "properties": { + "audio": { + "additionalProperties": false, + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "cnnrnn", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "rnn" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "audio_file_length_limit_in_s": { + "default": 7.5, + "description": "Float value that defines the maximum limit of the audio file in seconds. All files longer than this limit are cut off. All files shorter than this limit are padded with padding_value", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "audio_file_length_limit_in_s", + "type": "number" + }, + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": [ + "number", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": [ + "number", + "null" + ] + }, + "in_memory": { + "default": true, + "description": "Defines whether the audio dataset will reside in memory during the training process or will be dynamically fetched from disk (useful for large datasets). In the latter case a training batch of input audio will be fetched from disk each training iteration.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "in_memory", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "bfill", + "description": "What strategy to follow when there's a missing value in an audio column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "norm": { + "default": null, + "description": "Normalization strategy for the audio files. If None, no normalization is performed. If per_file, z-norm is applied on a 'per file' level", + "enum": [ + "per_file", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "num_fft_points": { + "default": null, + "description": "Defines the number of fft points used for the short time Fourier transformation", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_fft_points", + "type": [ + "number", + "null" + ] + }, + "num_filter_bands": { + "default": 80, + "description": "Defines the number of filters used in the filterbank. Only needed if audio_feature_type is 'fbank'", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "window_length_in_s", + "type", + "window_shift_in_s" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Type" + }, + "title": "num_filter_bands", + "type": "integer" + }, + "padding_value": { + "default": 0.0, + "description": "Float value that is used for padding.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_value", + "type": "number" + }, + "type": { + "default": "fbank", + "description": "Defines the type of audio feature to be used.", + "enum": [ + "fbank", + "group_delay", + "raw", + "stft", + "stft_phase" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default type fbank is set based on values that we have tested and determined to be a good starting point for audio feature preprocessing. This is not to say that it is the best way to process every audio feature, it is just a good starting place that performs well in general.", + "description_implications": "The different type of audio you select hear will determine how your audio feature is preprocessed and transformed into trainable data for the model.", + "example_value": [ + "stft" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": "Audio feature preprocessing depends heavily on the type of audio data you are dealing with. The type of audio preprocessing you will want to use will be dictated by the audio data you are dealing with.", + "related_parameters": [ + "audio_file_length_limit_in_s", + "norm", + "padding_value", + "in_memory" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Type" + }, + "title": "type", + "type": "string" + }, + "window_length_in_s": { + "default": 0.04, + "description": "Defines the window length used for the short time Fourier transformation. This is only needed if the audio_feature_type is 'raw'.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "window_shift_in_s", + "type", + "num_filter_bands" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Window Length in Seconds" + }, + "title": "window_length_in_s", + "type": "number" + }, + "window_shift_in_s": { + "default": 0.02, + "description": "Defines the window shift used for the short time Fourier transformation (also called hop_length). This is only needed if the audio_feature_type is 'raw'. ", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "window_length_in_s", + "type", + "num_filter_bands" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Window Shift in Seconds" + }, + "title": "window_shift_in_s", + "type": "number" + }, + "window_type": { + "default": "hamming", + "description": "Defines the type window the signal is weighted before the short time Fourier transformation.", + "enum": [ + "bartlett", + "blackman", + "hamming", + "hann" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "window_type", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "bag": { + "additionalProperties": false, + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 50, + "description": "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size is the number of different strings appearing in the training set in the input column (plus 1 for the unknown token placeholder ).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If true embeddings are trained during the training process, if false embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding fine tuning them. This parameter has effect only when representation is dense as sparse one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "force_embedding_size": { + "default": false, + "description": "Force the embedding size to be equal to the vocabulary size. This parameter has effect only if representation is dense.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is not often the case that the user has a strict need for using an embedding size that should be larger than the vocabulary size.", + "description_implications": "Should only be True if the user has a strict need for using an embedding size that should be larger than the vocabulary size. For example, there may be size requirements across multiple features imposed by downstream modules like the ComparatorCombiner.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "True for advanced usage only.", + "ui_display_name": "Force Embedding Size" + }, + "title": "force_embedding_size", + "type": "boolean" + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "This is the number of stacked fully connected layers that the input to the feature passes through. Their output is projected in the feature's output space.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "By default dense embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the GloVe format. When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if representation is dense.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "The representation of the embedding. Either dense or sparse.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary of the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from published [literature](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "embed" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed weighted encoder first transforms the element frequency vector to sparse integer lists, which are then mapped to either dense or sparse embeddings (one-hot encodings). Lastly, embeddings are aggregated as a weighted sum where each embedding is multiplied by its respective element's frequency. Inputs are of size b while outputs are of size b x h where b is the batch size and h is the dimensionality of the embeddings.", + "other_information": null, + "related_parameters": null, + "short_description": "Transforms feature to vector, maps to sparse or dense embeddings, then aggregates.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a set column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to transform the raw text content of the dataset column to a set of elements. The default value space splits the string on spaces. Common options include: underscore (splits on underscore), comma (splits on comma), json (decodes the string into a set or a list through a JSON parser).", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "binary": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "regressor" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "regressor", + "enum": [ + "regressor" + ], + "enumDescriptions": { + "regressor": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The regressor decoder is a (potentially empty) stack of fully connected layers, followed by a projection to a single number.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a single number.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "passthrough", + "enum": [ + "passthrough", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "binary_weighted_cross_entropy" + } + } + }, + "then": { + "properties": { + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "positive_class_weight": { + "default": null, + "description": "Weight of the positive class.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "positive_class_weight", + "type": [ + "number", + "null" + ] + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "binary_weighted_cross_entropy", + "enum": [ + "binary_weighted_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "oneOf": [ + { + "default": 1.0, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "computed_fill_value_float_option", + "type": "number" + }, + { + "default": "Y", + "description": "", + "enum": [ + "-1", + "-1.0", + "0", + "0.0", + "1", + "1.0", + "F", + "FALSE", + "False", + "N", + "NO", + "No", + "T", + "TRUE", + "True", + "Y", + "YES", + "Yes", + "f", + "false", + "n", + "no", + "t", + "true", + "y", + "yes" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "computed_fill_value_string_option", + "type": "string" + }, + { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "computed_fill_value_boolean_option", + "type": "boolean" + }, + { + "description": "Disable this parameter.", + "title": "null_option", + "type": "null" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value" + }, + "fallback_true_label": { + "default": null, + "description": "The label to interpret as 1 (True) when the binary feature doesn't have a conventional boolean value", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Modeling performance should not be affected, but the semantics of some binary metrics may change like for \"false positives\", \"false negatives\", etc. if the true label is pinned to the other value.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fallback True Label" + }, + "title": "fallback_true_label", + "type": [ + "string", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "oneOf": [ + { + "default": null, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "fill_value_float_option", + "type": [ + "number", + "null" + ] + }, + { + "default": "Y", + "description": "", + "enum": [ + "-1", + "-1.0", + "0", + "0.0", + "1", + "1.0", + "F", + "FALSE", + "False", + "N", + "NO", + "No", + "T", + "TRUE", + "True", + "Y", + "YES", + "Yes", + "f", + "false", + "n", + "no", + "t", + "true", + "y", + "yes" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "fill_value_string_option", + "type": "string" + }, + { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "fill_value_boolean_option", + "type": "boolean" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value" + }, + "missing_value_strategy": { + "default": "fill_with_false", + "description": "What strategy to follow when there's a missing value in a binary column", + "enum": [ + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_false", + "fill_with_true" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "category": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "classifier" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_classes": { + "default": null, + "description": "Number of classes to predict.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "classifier", + "enum": [ + "classifier" + ], + "enumDescriptions": { + "classifier": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a vector of size of the number of available classes, followed by a sigmoid.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a vector the size of the number of available classes.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_initializer": { + "default": null, + "description": "Initializer for the embedding matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "description_implications": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "example_value": [ + "kaiming" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1711.09160" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "kaiming", + "suggested_values_reasoning": "https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326", + "ui_display_name": "Embedding Initialization" + }, + "title": "embedding_initializer", + "type": [ + "string", + "null" + ] + }, + "embedding_size": { + "default": 50, + "description": "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size is the number of different strings appearing in the training set in the column the feature is named after (plus 1 for ).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "sparse" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_initializer": { + "default": null, + "description": "Initializer for the embedding matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "description_implications": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "example_value": [ + "kaiming" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1711.09160" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "kaiming", + "suggested_values_reasoning": "https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326", + "ui_display_name": "Embedding Initialization" + }, + "title": "embedding_initializer", + "type": [ + "string", + "null" + ] + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": false, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "onehot" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "dense", + "enum": [ + "passthrough", + "onehot", + "sparse", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder maps to a dense embedding and is returned as outputs of size `b x h`, where `b` is the batch size and `h` is the dimensionality of the embeddings.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps the categorical feature to a dense embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "softmax_cross_entropy", + "enum": [ + "softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "For fixed encoders, compute encoder embeddings in preprocessing to avoid this step at train time. Can speed up the time taken per step during training, but will invalidate the preprocessed data if the encoder type is changed. Some model types (GBM) require caching encoder embeddings to use embedding features, and those models will override this value to `true` automatically.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "Whether the string has to be lowercased before being handled by the tokenizer.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a category column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. if the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "date": { + "additionalProperties": false, + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "wave" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "The number of stacked fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "wave", + "embed" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Embed encoder passes the year through a fully connected layer of one neuron and embeds all other elements for the date, concatenates them and passes the concatenated representation through fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Embeds the date elements passes them through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "wave": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Wave encoder passes the year through a fully connected layer of one neuron and represents all other elements for the date by taking the cosine of their value with a different period (12 for months, 31 for days, etc.), concatenates them and passes the concatenated representation through fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Embeds the date elements by taking the cosine of their value before passing through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "datetime_format": { + "default": null, + "description": "This parameter can either be a datetime format string, or null, in which case the datetime format will be inferred automatically.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Ludwig will try to infer the date format automatically, but a specific format can be provided. The date string spec is the same as the one described in python's datetime.", + "description_implications": "If Ludwig has trouble parsing dates, it could be useful to specify an explicit format that Ludwig should parse date feature values as. This could also serve as a form of normalization, for example, if not all datetimes have the same granularity (some have days, some have times), then the common format (i.e. %d %m %Y) serves as a truncator.", + "example_value": [ + "%d %b %Y" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Have Ludwig figure out the date format automatically.", + "ui_display_name": "Datetime format" + }, + "title": "datetime_format", + "type": [ + "string", + "null" + ] + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a date column", + "enum": [ + "fill_with_const", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "h3": { + "additionalProperties": false, + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "weighted_sum" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "should_softmax": { + "default": false, + "description": "Determines if the weights of the weighted sum should be passed though a softmax layer before being used.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "should_softmax", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The activation function to use", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "For short sequences, it is reasonable to use a vanilla RNN.", + "description_implications": "Setting bidirectional to True may increase the compute and memory requirements of the model, but may also increase model performance on long sequences.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://devopedia.org/bidirectional-rnn#:~:text=RNN%20has%20the%20limitation%20that,forward%20and%20reverse%20time%20order." + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "cell_type, activation, recurrent_activation, use_bias" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": "RNNs can sometimes suffer from catastrophic forgetting (source: https://en.wikipedia.org/wiki/Catastrophic_interference ) on long sequences. Allowing the RNN to read from both the beginning and end of the sequence can improve its representation at each timestep.", + "ui_display_name": "Bidirectional" + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `lstm_block`, `lstm`, `ln`, `lstm_cudnn`, `gru`, `gru_block`, `gru_cudnn`. For reference about the differences between the cells please refer to PyTorch's documentation. We suggest to use the `block` variants on CPU and the `cudnn` variants on GPU because of their increased speed. ", + "enum": [ + "rnn", + "lstm", + "lstm_block", + "ln", + "lstm_cudnn", + "gru", + "gru_block", + "gru_cudnn" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The LSTM cell has proven to be the most performant of the three cells.", + "description_implications": "There are two reasons to consider other cell types: (1) compute costs and (2) catastrophic forgetting (source: https://en.wikipedia.org/wiki/Catastrophic_interference ). RNNs have marginally less compute costs, but are prone to catastrophic forgetting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "bidirectional\nactivation\nrecurrent_activation\nuse_bias" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cell Type" + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "The dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "recurrent_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "hidden_size": { + "default": 10, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "H3 values numbers, so a small RNN dimensionality is likely sufficient.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If you have multiple input features, varying the number of layers in the combiner or output feature decoder will have more impact.", + "related_parameters": [ + "layers" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "embed", + "weighted_sum", + "rnn" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Embed encoder encodes each component of the H3 representation (mode, edge, resolution, base cell and children cells) with embeddings. Children cells with value 0 will be masked out. After the embedding, all embeddings are summed and optionally passed through a stack of fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Encodes each H3 component with embeddings then takes a sum and passes them through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The RNN encoder encodes each component of the H3 representation (mode, edge, resolution, base cell and children cells) with embeddings. Children cells with value 0 will be masked out. After the embedding, all embeddings are passed through an RNN encoder. The intuition behind this is that, starting from the base cell, the sequence of children cells can be seen as a sequence encoding the path in the tree of all H3 hexes.", + "other_information": null, + "related_parameters": null, + "short_description": "Encodes each H3 component with embeddings then passes them through an RNN encoder.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "weighted_sum": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Weighted Sum encoder encodes each component of the H3 representation (mode, edge, resolution, base cell and children cells) with embeddings. Children cells with value 0 will be masked out. After the embedding, all embeddings are summed with a weighted sum (with learned weights) and optionally passed through a stack of fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Encodes each H3 component with embeddings then takes a weighted sum.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": 576495936675512319, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "integer" + }, + "fill_value": { + "default": 576495936675512319, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "integer" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in an h3 column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "augmentation": { + "oneOf": [ + { + "items": { + "additionalProperties": true, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "random_horizontal_flip" + } + } + }, + "then": { + "properties": {} + } + }, + { + "if": { + "properties": { + "type": { + "const": "auto_augmentation" + } + } + }, + "then": { + "properties": { + "method": { + "default": "trivial_augment", + "description": "Specifies the method for applying automatic data augmentation.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trivial augment is computationally more efficient than the other options.", + "description_implications": "The type of auto-augment method to apply to a batch of images.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": "Trivial-augment: https://arxiv.org/abs/2103.10158. Rand-augment: https://arxiv.org/abs/1909.13719. Auto-augment: https://arxiv.org/pdf/1805.09501.pdf.", + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Automatic Augmentation Method" + }, + "title": "method", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_vertical_flip" + } + } + }, + "then": { + "properties": {} + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_rotate" + } + } + }, + "then": { + "properties": { + "degree": { + "default": 15, + "description": "Range of angle for random rotation, i.e., [-degree, +degree].", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 15 means that the image will be randomly rotated between -15 to +15 degrees.", + "description_implications": "The degree of rotation to apply to the image.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Rotation Degree" + }, + "title": "degree", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_blur" + } + } + }, + "then": { + "properties": { + "kernel_size": { + "default": 3, + "description": "Kernel size for random blur.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 3.", + "description_implications": "The kernel size is the size of the filter matrix. A larger kernel size will result in a blurrier image, while a smaller kernel size will result in less blurring.", + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + 3, + 5, + 7 + ], + "suggested_values_reasoning": "The default value is 3, which is a common value for image processing", + "ui_display_name": "Kernel Size" + }, + "title": "kernel_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_brightness" + } + } + }, + "then": { + "properties": { + "max": { + "default": 2.0, + "description": "Maximum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 3.0.", + "description_implications": "The maximum factor by which the brightness of the image will be randomly changed.", + "example_value": [ + 3.9 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Brightness" + }, + "title": "max", + "type": "number" + }, + "min": { + "default": 0.5, + "description": "Minimum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 0.1.", + "description_implications": "The minimum brightness factor to apply to the image.", + "example_value": [ + 0.5 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Minimum Brightness" + }, + "title": "min", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_contrast" + } + } + }, + "then": { + "properties": { + "max": { + "default": 2.0, + "description": "Maximum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 3.0", + "description_implications": "The maximum factor by which the contrast of the image will be randomly changed.", + "example_value": [ + 3.0 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Contrast" + }, + "title": "max", + "type": "number" + }, + "min": { + "default": 0.5, + "description": "Minimum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 0.1.", + "description_implications": "The minimum contrast factor to apply to the image.", + "example_value": [ + 0.1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Minimum contrast" + }, + "title": "min", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "description": "Type of augmentation to apply.", + "enum": [ + "auto_augmentation", + "random_blur", + "random_brightness", + "random_contrast", + "random_horizontal_flip", + "random_rotate", + "random_vertical_flip" + ], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "augmentation", + "type": "object" + }, + "type": "array" + }, + { + "description": "Apply standard augmentation pipeline.", + "type": "boolean" + } + ] + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "conv_activation": { + "default": "relu", + "description": "If an activation is not already specified in conv_layers this is the default activation that will be used for each layer. It indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Activation" + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "Dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "List of convolutional layers to use in the encoder. ", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "conv_norm": { + "default": null, + "description": "If a norm is not already specified in conv_layers this is the default norm that will be used for each layer. It indicates the normalization applied to the activations and can be null, batch or layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Normalization" + }, + "title": "conv_norm", + "type": [ + "string", + "null" + ] + }, + "conv_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if conv_norm is either batch or layer. ", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Normalization Parameters" + }, + "title": "conv_norm_params", + "type": [ + "object", + "null" + ] + }, + "conv_use_bias": { + "default": true, + "description": "If bias not already specified in conv_layers, specifies if the 2D convolutional kernel should have a bias term.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "conv_use_bias", + "type": "boolean" + }, + "dilation": { + "default": 1, + "description": "An int or pair of ints specifying the dilation rate to use for dilated convolution. If dilation is not already specified in conv_layers, specifies the default dilation of the 2D convolutional kernel that will be used for each layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "dilation_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "dilation_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Dilation" + }, + "title": "dilation" + }, + "fc_activation": { + "default": "relu", + "description": "If an activation is not already specified in fc_layers this is the default activation that will be used for each layer. It indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "activation, activation_function, conv_activation, recurrent_activation" + ], + "short_description": "", + "suggested_values": "relu, alternatively leakyRelu or elu", + "suggested_values_reasoning": "The default value will work well in the majority of the cases", + "ui_display_name": "FC Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "fc_bias_initializer", + "type": "string" + }, + "fc_dropout": { + "default": 0.0, + "description": "Dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "FC Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "A list of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and weights_initializer. If any of those values is missing from the dictionary, the default one specified as a parameter of the encoder will be used instead. ", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "If a norm is not already specified in fc_layers this is the default norm that will be used for each layer. It indicates the norm of the output and can be null, batch or layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate. See Torch's documentation on batch normalization or for layer see Torch's documentation on layer normalization.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fc_norm_params" + ], + "short_description": "", + "suggested_values": "batch", + "suggested_values_reasoning": null, + "ui_display_name": "Fully Connected Normalization" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either batch or layer. For information on parameters used with batch see Torch's documentation on batch normalization or for layer see Torch's documentation on layer normalization.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fc_norm" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": null, + "ui_display_name": "Fully Connected Normalization Parameters" + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "FC Use Bias" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weights matrix.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "FC Weights Initializer" + }, + "title": "fc_weights_initializer", + "type": "string" + }, + "groups": { + "default": 1, + "description": "Groups controls the connectivity between convolution inputs and outputs. When groups = 1, each output channel depends on every input channel. When groups > 1, input and output channels are divided into groups separate groups, where each output channel depends only on the inputs in its respective input channel group. in_channels and out_channels must both be divisible by groups.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Groups" + }, + "title": "groups", + "type": "integer" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed internally, automatically, based on image data preprocessing.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "NOT DISPLAYED" + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "kernel_size": { + "default": 3, + "description": "An integer or pair of integers specifying the kernel size. A single integer specifies a square kernel, while a pair of integers specifies the height and width of the kernel in that order (h, w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for each layer.", + "oneOf": [ + { + "default": 3, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "kernel_size_integer_option", + "type": "integer" + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Kernel Size" + }, + "title": "kernel_size" + }, + "num_channels": { + "default": null, + "description": "Number of channels to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed internally, automatically, based on image data preprocessing.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "NOT DISPLAYED" + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "Number of convolutional layers to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "The number of stacked fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "out_channels": { + "default": 32, + "description": "Indicates the number of filters, and by consequence the output channels of the 2d convolution. If out_channels is not already specified in conv_layers this is the default out_channels that will be used for each layer. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Output Channels" + }, + "title": "out_channels", + "type": "integer" + }, + "output_size": { + "default": 128, + "description": "If output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "valid", + "description": "An int, pair of ints (h, w), or one of ['valid', 'same'] specifying the padding used forconvolution kernels.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 0, + "parameter_metadata": null, + "title": "padding_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "padding_list_option", + "type": "array" + }, + { + "default": "valid", + "description": "", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "padding_string_option", + "type": "string" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "When padding is set to 'valid' like in the default case, no padding is added. As a default value putting in the raw image is the goal here.", + "description_implications": "By increasing the amount of padding, you can increase the accuracy of the image analysis for certain circumstances.", + "example_value": [ + "'same'" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://www.geeksforgeeks.org/cnn-introduction-to-padding/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "padding_mode,\nresize method" + ], + "short_description": "", + "suggested_values": "Same' padding if images are of different dimensions. \nSpecific [h, w] entries can be valuable on a per dataset basis.", + "suggested_values_reasoning": "If your images already have padding, there is no need to add padding, so the default is fine. If your images come in different dimensions, then 'same' padding can help pad the images to standardized dimensions. For certain images, adding padding to the edges can help the CNN process the images better which can improve model performance. This depends on the images however.", + "ui_display_name": "Padding" + }, + "title": "padding" + }, + "padding_mode": { + "default": "zeros", + "description": "If padding_mode is not already specified in conv_layers, specifies the default padding_mode of the 2D convolutional kernel that will be used for each layer.", + "enum": [ + "zeros", + "reflect", + "replicate", + "circular" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Padding Mode" + }, + "title": "padding_mode", + "type": "string" + }, + "pool_dilation": { + "default": 1, + "description": "An integer or pair of ints specifying pooling dilation rate (h, w).", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "pool_dilation_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_dilation_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Dilation" + }, + "title": "pool_dilation" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use.", + "enum": [ + "max", + "average", + "avg", + "mean" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": "string" + }, + "pool_kernel_size": { + "default": 2, + "description": "An integer or pair of integers specifying the pooling size. If pool_kernel_size is not specified in conv_layers this is the default value that will be used for each layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "pool_kernel_size_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Kernel Size" + }, + "title": "pool_kernel_size" + }, + "pool_padding": { + "default": 0, + "description": "An integer or pair of ints specifying pooling padding (h, w).", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 0, + "parameter_metadata": null, + "title": "pool_padding_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_padding_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Padding" + }, + "title": "pool_padding" + }, + "pool_stride": { + "default": null, + "description": "An integer or pair of integers specifying the pooling stride, which is the factor by which the pooling layer downsamples the feature map. Defaults to pool_kernel_size.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "pool_stride_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Stride" + }, + "title": "pool_stride" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stride": { + "default": 1, + "description": "An integer or pair of integers specifying the stride of the convolution along the height and width. If a stride is not already specified in conv_layers, specifies the default stride of the 2D convolutional kernel that will be used for each layer.", + "oneOf": [ + { + "default": 1, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "stride_integer_option", + "type": "integer" + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Stride" + }, + "title": "stride" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed internally, automatically, based on image data preprocessing.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "NOT DISPLAYED" + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "_resnet_legacy" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "if an activation is not already specified in fc_layers this is the default activation that will be used for each layer. It indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "batch_norm_epsilon": { + "default": 0.001, + "description": "Epsilon of the batch norm.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "batch_norm_epsilon", + "type": "number" + }, + "batch_norm_momentum": { + "default": 0.9, + "description": "Momentum of the batch norm running statistics.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "batch_norm_momentum", + "type": "number" + }, + "bias_initializer": { + "default": "zeros", + "description": "initializer for the bias vector.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "conv_stride": { + "default": 1, + "description": "An integer or pair of integers specifying the stride of the initial convolutional layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "conv_stride_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "conv_stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_stride" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "A list of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and weights_initializer. If any of those values is missing from the dictionary, the default one specified as a parameter of the encoder will be used instead. ", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "first_pool_kernel_size": { + "default": null, + "description": "Pool size to be used for the first pooling layer. If none, the first pooling layer is skipped.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "first_pool_kernel_size_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "first_pool_kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "first_pool_kernel_size" + }, + "first_pool_stride": { + "default": null, + "description": "Stride for first pooling layer. If null, defaults to first_pool_kernel_size.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "first_pool_stride_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "first_pool_stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "first_pool_stride" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "kernel_size": { + "default": 3, + "description": "An integer or pair of integers specifying the kernel size. A single integer specifies a square kernel, while a pair of integers specifies the height and width of the kernel in that order (h, w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for each layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "kernel_size_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "kernel_size" + }, + "norm": { + "default": null, + "description": "if a norm is not already specified in fc_layers this is the default norm that will be used for each layer. It indicates the norm of the output and can be null, batch or layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "parameters used if norm is either batch or layer. For information on parameters used with batch see Torch's documentation on batch normalization or for layer see Torch's documentation on layer normalization.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_channels": { + "default": null, + "description": "Number of channels to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "The number of stacked fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "out_channels": { + "default": 32, + "description": "Indicates the number of filters, and by consequence the output channels of the 2d convolution. If out_channels is not already specified in conv_layers this is the default out_channels that will be used for each layer. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "out_channels", + "type": "integer" + }, + "output_size": { + "default": 128, + "description": "if output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "resnet_size": { + "default": 50, + "description": "The size of the ResNet model to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "resnet_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weights matrix.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mlp_mixer" + } + } + }, + "then": { + "properties": { + "avg_pool": { + "default": true, + "description": "If true, pools output over patch dimension, outputs a vector of shape (embed_size). If false, the output tensor is of shape (n_patches, embed_size), where n_patches is img_height x img_width / patch_size\u00b2.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "avg_pool", + "type": "boolean" + }, + "channel_dim": { + "default": 256, + "description": "Number of channels in hidden layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "channel_dim", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embed_size": { + "default": 512, + "description": "The patch embedding size, the output size of the mixer if avg_pool is true.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embed_size", + "type": "integer" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "num_channels": { + "default": null, + "description": "Number of channels to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_layers": { + "default": 8, + "description": "The depth of the network (the number of Mixer blocks).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the size and complexity of the input images. The default value is used in the paper and tested on several image datasets.", + "description_implications": "Increasing the number of layers may improve model performance for larger images or more complex image tasks.", + "example_value": [ + 8 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "MLP-Mixer: An all-MLP Architecture for Vision\nhttps://arxiv.org/abs/2105.01601" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "4 - 32", + "suggested_values_reasoning": "Values from 8 - 32 are tested in the paper. It is possible that fewer layers will be sufficient for some tasks.", + "ui_display_name": "Number of Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "patch_size": { + "default": 16, + "description": "The image patch size. Each patch is patch_size\u00b2 pixels. Must evenly divide the image width and height.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from MLP-Mixer paper.", + "description_implications": "The implications of the image patch size for this layer depend on other factors, such as the true resolution of the incoming image dataset. If the patch size is kept consistent but a higher resolution image is used as input, then the resulting chunked sequence of tokens will be longer than it would have been if the input resolution was lower. \n\nThe original MLP-Mixer paper also notes that there is a tradeoff with respect to the projection units learned by a model. In their findings, a 32x32 patch size model learned very structured low frequency projection units, while the equivalent 16x16 model learned high frequencies and showed no clear structure.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[MLP Mixer paper](https://arxiv.org/pdf/2105.01601.pdf)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + 16, + 32 + ], + "suggested_values_reasoning": "16 and 32 are the values used in the original MLP Mixer paper", + "ui_display_name": "Patch Size" + }, + "title": "patch_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "token_size": { + "default": 2048, + "description": "The per-patch embedding size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "token_size", + "type": "integer" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "_vit_legacy" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout rate for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/2010.11929).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob,\nattention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Attention Dropout" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "gradient_checkpointing": { + "default": false, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gradient_checkpointing", + "type": "boolean" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "hidden_act": { + "default": "gelu", + "description": "Hidden layer activation, one of gelu, relu, selu or gelu_new.", + "enum": [ + "relu", + "gelu", + "selu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[Huggingface docs for ViT config](https://huggingface.co/docs/transformers/model_doc/vit#transformers.ViTConfig.hidden_act)\n\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout rate for all fully connected layers in the embeddings, encoder, and pooling.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/2010.11929).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob,\nattention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Hidden Dropout" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooling layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 3072, + "description": "Dimensionality of the intermediate (i.e., feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads in each attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "patch_size": { + "default": 16, + "description": "The image patch size. Each patch is patch_size\u00b2 pixels. Must evenly divide the image width and height.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from ViT paper.", + "description_implications": "The implications of the image patch size for this layer depend on other factors, such as the true resolution of the incoming image dataset. If the patch size is kept consistent but a higher resolution image is used as input, then the resulting chunked sequence of tokens will be longer than it would have been if the input resolution was lower. \n\nThe ViT paper notes that decreasing the patch size in this way led to robust improvements without introducing other parameters.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[Huggingface docs](https://huggingface.co/docs/transformers/model_doc/vit)\n\n[ViT paper](https://arxiv.org/abs/2010.11929)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + 16, + 32 + ], + "suggested_values_reasoning": "16 and 32 are the values used in the original ViT paper.", + "ui_display_name": "Patch Size" + }, + "title": "patch_size", + "type": "integer" + }, + "pretrained_model": { + "default": "google/vit-base-patch16-224", + "description": "The name of the pre-trained model to use.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default model is the canonical model for this model architecture, and is therefore a good starting point for most use cases.", + "description_implications": "There are two factors to consider when choosing a pre-trained model: (1) size, and (2) task similarity. \n\nThe larger the model, the more subtle its comprehension of inputs can become. However, larger models are also more compute and memory-intensive to train.\n\nModels pretrained on highly-related source tasks are more likely to be successful on the target task. Consider searching the HuggingFace model repository for models trained on similar tasks.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/2010.11929" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, trainable, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": "google/vit-large-patch16-224", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, increasing model capacity can potentially provide a richer representation than the default. The suggested value upsizes the model while maintaining the same model architecture.\n\nModel trained on internet-scale datasets typically generalize well. Consider deviating from the default only if the images in the dataset originate from another domain (e.g. medical images, geospatial data).", + "ui_display_name": "Pretrained model name" + }, + "title": "pretrained_model", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Use pre-trained model weights from Hugging Face.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "alexnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "convnext" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "tiny", + "small", + "base", + "large" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "densenet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": 121, + "description": "Pretrained model variant to use.", + "enum": [ + 121, + 161, + 169, + 201 + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "efficientnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "b0", + "description": "Pretrained model variant to use.", + "enum": [ + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "v2_", + "v2_m", + "v2_l" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "googlenet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "inceptionv3" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "maxvit" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "t", + "description": "Pretrained model variant to use.", + "enum": [ + "t" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mnasnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "0_5", + "description": "Pretrained model variant to use.", + "enum": [ + "0_5", + "0_75", + "1_0", + "1_3" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mobilenetv2" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mobilenetv3" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "small", + "description": "Pretrained model variant to use.", + "enum": [ + "small", + "large" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "regnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "x_1_6gf", + "description": "Pretrained model variant to use.", + "enum": [ + "x_1_6gf", + "x_16gf", + "x_32gf", + "x_3_2gf", + "x_400mf", + "x_800mf", + "x_8gf", + "y_128gf", + "y_16gf", + "y_1_6gf", + "y_32gf", + "y_3_2gf", + "y_400mf", + "y_800mf", + "y_8gf" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "resnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": 50, + "description": "Pretrained model variant to use.", + "enum": [ + 18, + 34, + 50, + 101, + 152 + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "resnext" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "50_32x4d", + "description": "Pretrained model variant to use.", + "enum": [ + "50_32x4d", + "101_32x8d", + "101_64x4d" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "shufflenet_v2" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "x0_5", + "description": "Pretrained model variant to use.", + "enum": [ + "x0_5", + "x1_0", + "x1_5", + "x2_0" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "squeezenet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "1_0", + "description": "Pretrained model variant to use.", + "enum": [ + "1_0", + "1_1" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "swin_transformer" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "t", + "description": "Pretrained model variant to use.", + "enum": [ + "t", + "s", + "b" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "vit" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "b_16", + "description": "Pretrained model variant to use.", + "enum": [ + "b_16", + "b_32", + "l_16", + "l_32", + "h_14" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "vgg" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": 11, + "description": "Pretrained model variant to use.", + "oneOf": [ + { + "default": 11, + "description": "", + "enum": [ + 11, + 13, + 16, + 19 + ], + "enumNames": [], + "parameter_metadata": null, + "title": "model_variant_integer_option", + "type": "integer" + }, + { + "default": "11_bn", + "description": "", + "enum": [ + "11_bn", + "13_bn", + "16_bn", + "19_bn" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "model_variant_string_option", + "type": "string" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "wide_resnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "50_2", + "description": "Pretrained model variant to use.", + "enum": [ + "50_2", + "101_2" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + } + ], + "properties": { + "type": { + "default": "stacked_cnn", + "enum": [ + "densenet", + "mnasnet", + "regnet", + "swin_transformer", + "convnext", + "vit", + "squeezenet", + "efficientnet", + "resnext", + "maxvit", + "wide_resnet", + "_resnet_legacy", + "mobilenetv3", + "shufflenet_v2", + "resnet", + "inceptionv3", + "mlp_mixer", + "googlenet", + "mobilenetv2", + "alexnet", + "_vit_legacy", + "vgg", + "stacked_cnn" + ], + "enumDescriptions": { + "_resnet_legacy": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "ResNet - short for residual network is part of a family of extremely deep architectures showing compelling accuracy and nice convergence behaviors for computer vision applications. It is a type of CNN architecture designed to support hundreds or thousands of convolutional layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Residual network achieving very high performance on computer vision tasks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "_vit_legacy": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "ViT, short for Vision Transformer, divides the image into equal-sized patches, uses a linear transformation to encode each flattened patch, then applies a deep transformer architecture to the sequence of encoded patches.", + "other_information": null, + "related_parameters": null, + "short_description": "ViT encoder divides images into patches, performs a linear transformation, and then applies a transformer.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "mlp_mixer": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "MLP-Mixer divides the image into equal-sized patches, applying fully connected layers to each patch to compute per-patch representations (tokens) and combining the representations with fully-connected mixer layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Image encoder which applies fully connected layers to different patches of the image.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "Stack of 2D convolutional layers with optional normalization, dropout, and down-sampling pooling layers, followed by an optional stack of fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of 2D convolutional layers followed by an optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": [ + "number", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": [ + "number", + "null" + ] + }, + "height": { + "default": null, + "description": "The image height in pixels. If this parameter is set, images will be resized to the specified height using the resize_method parameter. If None, images will be resized to the size of the first image in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "in_memory": { + "default": true, + "description": "Defines whether image dataset will reside in memory during the training process or will be dynamically fetched from disk (useful for large datasets). In the latter case a training batch of input images will be fetched from disk each training iteration.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "in_memory", + "type": "boolean" + }, + "infer_image_dimensions": { + "default": true, + "description": "If true, then the height and width of images in the dataset will be inferred from a sample of the first image in the dataset. Each image that doesn't conform to these dimensions will be resized according to resize_method. If set to false, then the height and width of images in the dataset will be specified by the user.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_dimensions", + "type": "boolean" + }, + "infer_image_max_height": { + "default": 256, + "description": "If infer_image_dimensions is set, this is used as the maximum height of the images in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_max_height", + "type": "integer" + }, + "infer_image_max_width": { + "default": 256, + "description": "If infer_image_dimensions is set, this is used as the maximum width of the images in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_max_width", + "type": "integer" + }, + "infer_image_num_channels": { + "default": true, + "description": "If true, then the number of channels in the dataset is inferred from a sample of the first image in the dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_num_channels", + "type": "boolean" + }, + "infer_image_sample_size": { + "default": 100, + "description": "The sample size used for inferring dimensions of images in infer_image_dimensions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_sample_size", + "type": "integer" + }, + "missing_value_strategy": { + "default": "bfill", + "description": "What strategy to follow when there's a missing value in an image column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "num_channels": { + "default": null, + "description": "Number of channels in the images. If specified, images will be read in the mode specified by the number of channels. If not specified, the number of channels will be inferred from the image format of the first valid image in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_processes": { + "default": 1, + "description": "Specifies the number of processes to run for preprocessing images.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_processes", + "type": "integer" + }, + "requires_equal_dimensions": { + "default": false, + "description": "If true, then width and height must be equal.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "requires_equal_dimensions", + "type": "boolean" + }, + "resize_method": { + "default": "interpolate", + "description": "The method to use for resizing images.", + "enum": [ + "crop_or_pad", + "interpolate" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Interpolation may stretch or squish the image, but it does not remove content or change the statistical distribution of image values so it is more appropriate for most tasks.", + "description_implications": "interpolation will not change the content of the image, but it will change the aspect ratio.\n\ncrop_or_pad will preserve the aspect ratio of the image, but may remove some content (in the case of cropping).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "height, width" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Resize Method" + }, + "title": "resize_method", + "type": "string" + }, + "standardize_image": { + "default": null, + "description": "Standardize image by per channel mean centering and standard deviation scaling .", + "enum": [ + "imagenet1k", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "standardize_image", + "type": [ + "string", + "null" + ] + }, + "width": { + "default": null, + "description": "The image width in pixels. If this parameter is set, images will be resized to the specified width using the resize_method parameter. If None, images will be resized to the size of the first image in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "number": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "regressor" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "regressor", + "enum": [ + "regressor" + ], + "enumDescriptions": { + "regressor": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The regressor decoder is a (potentially empty) stack of fully connected layers, followed by a projection to a single number.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a single number.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "passthrough", + "enum": [ + "passthrough", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "root_mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "root_mean_squared_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "huber" + } + } + }, + "then": { + "properties": { + "delta": { + "default": 1.0, + "description": "Threshold at which to change between delta-scaled L1 and L2 loss.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "delta", + "type": "number" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "mean_squared_error", + "enum": [ + "mean_squared_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "root_mean_squared_error", + "root_mean_squared_percentage_error", + "huber" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": 0.0, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "number" + }, + "computed_outlier_fill_value": { + "default": 0.0, + "description": "The internally computed fill value to replace outliers with in case the outlier_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_outlier_fill_value", + "type": "number" + }, + "fill_value": { + "default": 0.0, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "number" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a number column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_mean" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "normalization": { + "default": "zscore", + "description": "Normalization strategy to use for this number feature. If the value is `null` no normalization is performed.", + "enum": [ + "zscore", + "minmax", + "log1p", + "iq", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Z-score normalization helps improve the training stability and convergence of neural networks by rescaling the numeric input features to have a mean of 0 and a standard deviation of 1, reducing the variability and distribution of the data. This improves neural network training.", + "description_implications": "The goal of normalization is to transform features to be on a similar scale. Normalization can be a form of feature smoothing that improves the performance and training stability of the model. Normalizations may result in different effects on the semantics of your number features. The best normalization technique is one that empirically works well, so try new ideas if you think they'll work well on your feature distribution.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/data-prep/transform/normalization" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "z-score", + "suggested_values_reasoning": "Z-score is a variation of scaling that represents the number of standard deviations away from the mean. You would use z-score to ensure your feature distributions have mean = 0 and std = 1. It\u2019s useful when there are a few outliers, but not so extreme that you need clipping.", + "ui_display_name": "Normalization" + }, + "title": "normalization", + "type": [ + "string", + "null" + ] + }, + "outlier_strategy": { + "default": null, + "description": "Determines how outliers will be handled in the dataset. In most cases, replacing outliers with the column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers is to leave them in the data, which is the behavior when this parameter is left as `null`.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_mean", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Outlier definitions and how to handle them are very task-specific, so we leave this feature disabled by default and ask the user to choose the strategy that works best for them.", + "description_implications": "Determines how outliers will be handled in the dataset. In most cases replacing outliers with the column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers is to leave them in the data, which is the behavior when this parameter is left as `null`.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "outlier_threshold" + ], + "short_description": "", + "suggested_values": "fill_with_mean", + "suggested_values_reasoning": null, + "ui_display_name": "Outlier Strategy" + }, + "title": "outlier_strategy", + "type": [ + "string", + "null" + ] + }, + "outlier_threshold": { + "default": 3.0, + "description": "Standard deviations from the mean past which a value is considered an outlier. The 3-sigma rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations of the mean (see: [68\u201395\u201399.7 rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule)). As such anything farther away than that is highly likely to be an outlier, and may distort the learning process by disproportionately affecting the model.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The definition of an outlier is often dataset and task dependent, but 2 or 3 standard deviations from the mean is a common heuristic.", + "description_implications": "Determines the threshold past which a number will be considered an outlier in the dataset. The 3-sigma rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations of the mean (see: 68\u201395\u201399.7 rule). As such anything farther away than that is highly likely to be an outlier, and may distort the learning process by disproportionately affecting the model.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "outlier_strategy" + ], + "short_description": "", + "suggested_values": "2 - 3", + "suggested_values_reasoning": null, + "ui_display_name": "Outlier Threshold" + }, + "title": "outlier_threshold", + "type": "number" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "sequence": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "generator" + } + } + }, + "then": { + "properties": { + "cell_type": { + "default": "gru", + "description": "Type of recurrent cell to use.", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data and task. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "sum", + "mean", + "avg", + "max", + "concat", + "last" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "\u201clast\u201d: Reduces tensor by taking the last non-zero element per sequence in the sequence dimension.\n\u201csum\u201d: Reduces tensor by summing across the sequence dimension.\n\u201cmean\u201d: Reduces tensor by taking the mean of the sequence dimension.\n\u201cavg\u201d: synonym for \u201cmean\u201d.\n\u201cmax\u201d: Reduces tensor by taking the maximum value of the last dimension across the sequence dimension.\n\u201cconcat\u201d: Reduces tensor by concatenating the second and last dimension.\n\u201cattention\u201d: Reduces tensor by summing across the sequence dimension after applying feedforward attention.\n\u201cnone\u201d: no reduction.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Combiner Reduce Mode" + }, + "title": "reduce_input", + "type": "string" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tagger" + } + } + }, + "then": { + "properties": { + "attention_embedding_size": { + "default": 256, + "description": "The embedding size of the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "Increasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "128 - 2048", + "suggested_values_reasoning": "Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Attention Embedding Size" + }, + "title": "attention_embedding_size", + "type": "integer" + }, + "attention_num_heads": { + "default": 8, + "description": "The number of attention heads in the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attention_num_heads", + "type": "integer" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_attention": { + "default": false, + "description": "Whether to apply a multi-head self attention layer before prediction.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_attention", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "generator", + "enum": [ + "generator", + "tagger" + ], + "enumDescriptions": { + "generator": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The generator decoder is a (potentially empty) stack of fully connected layers, followed by an RNN that generates outputs feeding on its own previous predictions and generates a tensor of size `b x s' x c`, where `b` is the batch size, `s'` is the length of the generated sequence and `c` is the number of classes, followed by a softmax_cross_entropy. During training teacher forcing is adopted, meaning the list of targets is provided as both inputs and outputs (shifted by 1), while at evaluation time greedy decoding (generating one token at a time and feeding it as input for the next step) is performed by beam search, using a beam of 1 by default. In general a generator expects a `b x h` shaped input tensor, where `h` is a hidden dimension. The `h` vectors are (after an optional stack of fully connected layers) fed into the rnn generator. One exception is when the generator uses attention, as in that case the expected size of the input tensor is `b x s x h`, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. If a `b x h` input is provided to a generator decoder using an RNN with attention instead, an error will be raised during model building.", + "other_information": null, + "related_parameters": null, + "short_description": "Generates a sequence by sampling from the model.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "tagger": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The tagger decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of size `b x s x c`, where `b` is the batch size, `s` is the length of the sequence and `c` is the number of classes, followed by a `softmax_cross_entropy`.\nThis decoder requires its input to be shaped as `b x s x h`, where `h` is a hidden dimension, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. This can be done by ensuring that at least one of the sequence, text or time series input feature's encoders has `reduce_output` set to `None`. This will prevent a `b x h` input from being provided to this decoder and an error from being raised during model building.\nThe tagger decoder also requires the `reduce_input` parameter of the output feature to be set to `None`. If this is not set, Ludwig will automatically override the value by setting it to None and log a warning.", + "other_information": null, + "related_parameters": null, + "short_description": "Used for classifying each element of an input sequence.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "transformer", + "cnnrnn", + "embed", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "rnn" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h` tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps each element of the sequence to an embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sequence_softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "unique": { + "default": false, + "description": "If true, the loss is only computed for unique elements in the sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "unique", + "type": "boolean" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "sequence_softmax_cross_entropy", + "enum": [ + "sequence_softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "Compute encoder embeddings in preprocessing, speeding up training time considerably.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "max_sequence_length": { + "default": 256, + "description": "The maximum length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 256. Every sequence will be truncated to this length.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size, embedding_size" + ], + "short_description": "", + "suggested_values": "Use the lowest value that covers most of your input data. Only increase the value if crucial parts of the input data are truncated.", + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Sequence Length" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a text column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 20000, + "description": "The maximum number of most common tokens in the vocabulary. If the data contains more than this amount, the most infrequent symbols will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": "integer" + }, + "ngram_size": { + "default": 2, + "description": "The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Size of the n-gram when using the `ngram` tokenizer.", + "description_implications": null, + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "n-gram size" + }, + "title": "ngram_size", + "type": "integer" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "padding_symbol": { + "default": "", + "description": "The string used as a padding symbol. This special token is mapped to the integer ID 0 in the vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_symbol", + "type": "string" + }, + "sequence_length": { + "default": null, + "description": "The desired length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated and sequences shorter than this value will be padded. If None, sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is `None`. Which means that the sequence length will be inferred from the dataset, which may save you compute resources on datasets with short sequence samples.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "If tying the weights of multiple sequence encoders together, this parameter may need to be set to ensure that all sequence features have the same sequence length.", + "suggested_values_reasoning": null, + "ui_display_name": "Sequence Length" + }, + "title": "sequence_length", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + }, + "unknown_symbol": { + "default": "", + "description": "The string used as an unknown placeholder. This special token is mapped to the integer ID 1 in the vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unknown_symbol", + "type": "string" + }, + "vocab_file": { + "default": null, + "description": "Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the first string until \t or \n is considered a word.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The vocabulary can be parsed automatically from the incoming input features.", + "description_implications": "It can be useful to specify your own vocabulary list if the vocabulary is very large, there's no out of the box tokenizer that fits your data, or if there are several uncommon or infrequently occurring tokens that we want to guarantee to be a part of the vocabulary, rather than treated as an unknown.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Vocab File" + }, + "title": "vocab_file", + "type": [ + "string", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "set": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "classifier" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_classes": { + "default": null, + "description": "Number of classes to predict.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "classifier", + "enum": [ + "classifier" + ], + "enumDescriptions": { + "classifier": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a vector of size of the number of available classes, followed by a sigmoid.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a vector the size of the number of available classes.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 50, + "description": "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size is the number of different strings appearing in the training set in the input column (plus 1 for the unknown token placeholder ).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If true embeddings are trained during the training process, if false embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when representation is dense as sparse one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "This is the number of stacked fully connected layers that the input to the feature passes through. Their output is projected in the feature's output space.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "By default dense embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the GloVe format. When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if representation is dense.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "The representation of the embedding. Either dense or sparse.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary of the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "embed" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Embed encoder takes the raw binary values coming from the input placeholders and transforms them to sparse integer lists, then they are mapped to either dense or sparse embeddings (one-hot encodings), finally they are reduced on the sequence dimension and returned as an aggregated embedding vector. Inputs are of size b while outputs are of size b x h where b is the batch size and h is the dimensionality of the embeddings.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps raw values to sparse integer lists, then maps to dense/sparse embeddings, then reduces to final vector.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sigmoid_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "sigmoid_cross_entropy", + "enum": [ + "sigmoid_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a set column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to transform the raw text content of the dataset column to a set of elements. The default value space splits the string on spaces. Common options include: underscore (splits on underscore), comma (splits on comma), json (decodes the string into a set or a list through a JSON parser).", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "text": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "generator" + } + } + }, + "then": { + "properties": { + "cell_type": { + "default": "gru", + "description": "Type of recurrent cell to use.", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data and task. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "sum", + "mean", + "avg", + "max", + "concat", + "last" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "\u201clast\u201d: Reduces tensor by taking the last non-zero element per sequence in the sequence dimension.\n\u201csum\u201d: Reduces tensor by summing across the sequence dimension.\n\u201cmean\u201d: Reduces tensor by taking the mean of the sequence dimension.\n\u201cavg\u201d: synonym for \u201cmean\u201d.\n\u201cmax\u201d: Reduces tensor by taking the maximum value of the last dimension across the sequence dimension.\n\u201cconcat\u201d: Reduces tensor by concatenating the second and last dimension.\n\u201cattention\u201d: Reduces tensor by summing across the sequence dimension after applying feedforward attention.\n\u201cnone\u201d: no reduction.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Combiner Reduce Mode" + }, + "title": "reduce_input", + "type": "string" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tagger" + } + } + }, + "then": { + "properties": { + "attention_embedding_size": { + "default": 256, + "description": "The embedding size of the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "Increasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "128 - 2048", + "suggested_values_reasoning": "Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Attention Embedding Size" + }, + "title": "attention_embedding_size", + "type": "integer" + }, + "attention_num_heads": { + "default": 8, + "description": "The number of attention heads in the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attention_num_heads", + "type": "integer" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_attention": { + "default": false, + "description": "Whether to apply a multi-head self attention layer before prediction.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_attention", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "generator", + "enum": [ + "generator", + "tagger" + ], + "enumDescriptions": { + "generator": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The generator decoder is a (potentially empty) stack of fully connected layers, followed by an RNN that generates outputs feeding on its own previous predictions and generates a tensor of size `b x s' x c`, where `b` is the batch size, `s'` is the length of the generated sequence and `c` is the number of classes, followed by a softmax_cross_entropy. During training teacher forcing is adopted, meaning the list of targets is provided as both inputs and outputs (shifted by 1), while at evaluation time greedy decoding (generating one token at a time and feeding it as input for the next step) is performed by beam search, using a beam of 1 by default. In general a generator expects a `b x h` shaped input tensor, where `h` is a hidden dimension. The `h` vectors are (after an optional stack of fully connected layers) fed into the rnn generator. One exception is when the generator uses attention, as in that case the expected size of the input tensor is `b x s x h`, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. If a `b x h` input is provided to a generator decoder using an RNN with attention instead, an error will be raised during model building.", + "other_information": null, + "related_parameters": null, + "short_description": "Generates a sequence by sampling from the model.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "tagger": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The tagger decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of size `b x s x c`, where `b` is the batch size, `s` is the length of the sequence and `c` is the number of classes, followed by a `softmax_cross_entropy`.\nThis decoder requires its input to be shaped as `b x s x h`, where `h` is a hidden dimension, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. This can be done by ensuring that at least one of the sequence, text or time series input feature's encoders has `reduce_output` set to `None`. This will prevent a `b x h` input from being provided to this decoder and an error from being raised during model building.\nThe tagger decoder also requires the `reduce_input` parameter of the output feature to be set to `None`. If this is not set, Ludwig will automatically override the value by setting it to None and log a warning.", + "other_information": null, + "related_parameters": null, + "short_description": "Used for classifying each element of an input sequence.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "albert" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.0, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, classifier_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "bos_token_id": { + "default": 2, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "classifier_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for attached classifiers.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, attention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout_prob" + }, + "title": "classifier_dropout_prob", + "type": "number" + }, + "embedding_size": { + "default": 128, + "description": "Dimensionality of vocabulary embeddings.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "eos_token_id": { + "default": 3, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sentence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "hidden_act": { + "default": "gelu_new", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Hugging face docs for ALBERT config](https://huggingface.co/docs/transformers/model_doc/albert#transformers.AlbertConfig.hidden_act)\n\r\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.0, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob,\nclassifier_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "inner_group_num": { + "default": 1, + "description": "The number of inner repetition of attention and ffn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "inner_group_num", + "type": "integer" + }, + "intermediate_size": { + "default": 3072, + "description": "The dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_groups": { + "default": 1, + "description": "Number of groups for the hidden layers, parameters in the same group are shared.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_groups", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 0, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "These arguments typically don't need to be specified.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "pretrained_model_name_or_path" + ], + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "albert-base-v2", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default model is the canonical model for this model architecture, and is therefore a good starting point for most use cases.", + "description_implications": "There are two factors to consider when choosing a pre-trained model: (1) size, and (2) task similarity. \n\nThe larger the model, the more subtle its comprehension of inputs can become. However, larger models are also more compute and memory-intensive to train.\n\nModels pretrained on highly-related source tasks are more likely to be successful on the target task. Consider searching the HuggingFace model repository for models trained on similar tasks.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1909.11942" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, trainable, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": "albert-large-v2, albert-base-chinese", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, increasing model capacity can potentially provide a richer representation than the default. The suggested value upsizes the model while maintaining the same model architecture.\n\nLanguage models trained on general corpora typically generalize well. Consider deviating from the default only if the text in the dataset originates from another domain (e.g. languages other than English).", + "ui_display_name": "Pretrained model" + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 2, + "description": "The vocabulary size of the token_type_ids passed when calling AlbertModel or TFAlbertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30000, + "description": "Vocabulary size of the ALBERT model. Defines the number of different tokens that can be represented by the inputs_ids passed.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "xlmroberta" + } + } + }, + "then": { + "properties": { + "add_pooling_layer": { + "default": true, + "description": "Whether to add a pooling layer to the encoder.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "add_pooling_layer", + "type": "boolean" + }, + "bos_token_id": { + "default": 0, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "eos_token_id": { + "default": 2, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sentence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "max_position_embeddings": { + "default": 514, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pad_token_id": { + "default": 1, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "xlm-roberta-base", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 1, + "description": "The vocabulary size of the token_type_ids passed in.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "Vocabulary size of the XLMRoBERTa model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "bert" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, classifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "classifier_dropout": { + "default": null, + "description": "The dropout ratio for the classification head.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, attention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout" + }, + "title": "classifier_dropout", + "type": [ + "number", + "null" + ] + }, + "gradient_checkpointing": { + "default": false, + "description": "Whether to use gradient checkpointing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gradient_checkpointing", + "type": "boolean" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Huggingface docs for BERT config](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertConfig.hidden_act)\n\r\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob, classifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 3072, + "description": "Dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 0, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "Type of position embedding.", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "bert-base-uncased", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 2, + "description": "The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the BERT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling BertModel or TFBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "deberta" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "tanh", + "gelu_fast", + "mish", + "linear", + "sigmoid", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 1536, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": null, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 6144, + "description": "Dimensionality of the 'intermediate' (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": null, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": null, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_relative_positions": { + "default": -1, + "description": "The range of relative positions `[-max_position_embeddings, max_position_embeddings]`. Use the same value as `max_position_embeddings`.", + "parameter_metadata": null, + "title": "max_relative_positions", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm_rel_ebd": { + "default": "layer_norm", + "description": "The normalization method for relative embeddings.", + "enum": [ + "layer_norm", + "none" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "norm_rel_ebd", + "type": "string" + }, + "num_attention_heads": { + "default": 24, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": null, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 24, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": null, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 0, + "description": "The value used to pad input_ids.", + "parameter_metadata": null, + "title": "pad_token_id", + "type": "integer" + }, + "pooler_dropout": { + "default": 0, + "description": "The dropout ratio for the pooler layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "pooler_dropout", + "type": "number" + }, + "pooler_hidden_act": { + "default": "gelu", + "description": "The activation function (function or string) in the pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "tanh", + "gelu_fast", + "mish", + "linear", + "sigmoid", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "pooler_hidden_act", + "type": "string" + }, + "pooler_hidden_size": { + "default": 1536, + "description": "The hidden size of the pooler layers.", + "minimum": 1, + "parameter_metadata": null, + "title": "pooler_hidden_size", + "type": "integer" + }, + "pos_att_type": { + "default": [ + "p2c", + "c2p" + ], + "description": "The type of relative position attention, it can be a combination of `['p2c', 'c2p']`, e.g. `['p2c']`, `['p2c', 'c2p']`, `['p2c', 'c2p']`.", + "items": { + "title": "pos_att_type", + "type": "string" + }, + "parameter_metadata": null, + "title": "pos_att_type", + "type": [ + "array", + "null" + ] + }, + "position_biased_input": { + "default": false, + "description": "Whether add absolute position embedding to content embedding.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "position_biased_input", + "type": "boolean" + }, + "position_buckets": { + "default": 256, + "description": "The number of buckets to use for each attention layer.", + "minimum": 1, + "parameter_metadata": null, + "title": "position_buckets", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": null, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "sileod/deberta-v3-base-tasksource-nli", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default model was selected based on the benchmarking work done by IBM's [model recycling](https://ibm.github.io/model-recycling/microsoft_deberta-v3-base_table.html) project. In that study, the selected model ranked first among all variants of the `microsoft/deberta-v3-base` architecture on an evaluation across 36 different datasets.", + "description_implications": "Considerations when selecting a pretrained model version include number of parameters (how long the model will take to fine-tuning / perform inference), general model performance on various benchmarks, and specific model performance on the task you wish to fine-tune it on.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, trainable, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained model" + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "enum": [ + "cls_pooled", + "last", + "sum", + "mean", + "max", + "concat", + "attention", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "relative_attention": { + "default": true, + "description": "Whether use relative position encoding.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "relative_attention", + "type": "boolean" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "share_att_key": { + "default": true, + "description": "Whether to share attention key across layers.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "share_att_key", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable is disabled by default to make the model useful for generating fast baselines, which can be further sped up by setting `preprocessing.cache_encoder_embeddings`. In many cases strong performance can be achieved without adjusting the weights of the pretrained model, but for best performance we recommend setting this to true.", + "description_implications": "Ludwig currently supports two variations on fine-tuning, configured via the trainable encoder parameter: (1) modifying the weights of the pretrained encoder to adapt them to the downstream task (trainable=true), or (2) keeping the pretrained encoder weights fixed and training a stack of dense layers that sit downstream as the combiner and decoder modules (trainable=false, default). This is sometimes distinguished as transfer learning. Allowing the weights to be modified by setting trainable=true can significantly improve performance on the downstream task, but will take significantly longer to train (due to the additional backward passes over the pretrained model parameters). Additionally, more care needs to be taken when selecting hyperparameters when trainable=true to prevent [catastrophic forgettng](https://en.wikipedia.org/wiki/Catastrophic_interference), whereby the model forgets all of the valuable information it learned during pretraining.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 0, + "description": "The vocabulary size of the `token_type_ids`.", + "minimum": 0, + "parameter_metadata": null, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab_size": { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "gpt" + } + } + }, + "then": { + "properties": { + "afn": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "afn", + "type": "string" + }, + "attn_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the attention.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_pdrop", + "type": "number" + }, + "embd_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the embeddings.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embd_pdrop", + "type": "number" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "layer_norm_epsilon": { + "default": 1e-05, + "description": "The epsilon to use in the layer normalization layers", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_epsilon", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_ctx": { + "default": 512, + "description": "Dimensionality of the causal mask (usually same as n_positions)", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_ctx", + "type": "integer" + }, + "n_embd": { + "default": 768, + "description": "Dimensionality of the embeddings and hidden states.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_embd", + "type": "integer" + }, + "n_head": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_layer": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "n_positions": { + "default": 40478, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_positions", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "openai-gpt", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "resid_pdrop": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "resid_pdrop", + "type": "number" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the GPT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling OpenAIGPTModel or TFOpenAIGPTModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "gpt2" + } + } + }, + "then": { + "properties": { + "activation_function": { + "default": "gelu_new", + "description": "Activation function, to be selected in the list ['relu', 'silu', 'gelu', 'tanh', 'gelu_new'].", + "enum": [ + "relu", + "silu", + "gelu", + "tanh", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "activation_function", + "type": "string" + }, + "attn_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the attention.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_pdrop", + "type": "number" + }, + "embd_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the embeddings.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embd_pdrop", + "type": "number" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "layer_norm_epsilon": { + "default": 1e-05, + "description": "The epsilon to use in the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_epsilon", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_ctx": { + "default": 1024, + "description": "Dimensionality of the causal mask (usually same as n_positions)", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_ctx", + "type": "integer" + }, + "n_embd": { + "default": 768, + "description": "Dimensionality of the embeddings and hidden states.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_embd", + "type": "integer" + }, + "n_head": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_inner": { + "default": null, + "description": "Dimensionality of the inner feed-forward layers. None will set it to 4 times n_embd", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_inner", + "type": [ + "integer", + "null" + ] + }, + "n_layer": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "n_positions": { + "default": 1024, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_positions", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "gpt2", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "resid_pdrop": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "resid_pdrop", + "type": "number" + }, + "scale_attn_weights": { + "default": true, + "description": "Scale attention weights by dividing by sqrt(hidden_size).", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "scale_attn_weights", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 50257, + "description": "Vocabulary size of the GPT-2 model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling GPT2Model or TFGPT2Model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "roberta" + } + } + }, + "then": { + "properties": { + "bos_token_id": { + "default": 0, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "eos_token_id": { + "default": 2, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "", + "description_implications": null, + "example_value": [ + "Default value used in pre-trained HF encoder." + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "eos_token_id", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pad_token_id": { + "default": 1, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "roberta-base", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "Vocabulary size of the RoBERTa model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer_xl" + } + } + }, + "then": { + "properties": { + "adaptive": { + "default": true, + "description": "Whether or not to use adaptive softmax.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Adaptive softmax is a speedup technique for computing probability distributions over words. For text with large vocabulary, adaptive softmax improves both training speed.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Adaptive Softmax" + }, + "title": "adaptive", + "type": "boolean" + }, + "attn_type": { + "default": 0, + "description": "Attention type. 0 for Transformer-XL, 1 for Shaw et al, 2 for Vaswani et al, 3 for Al Rfou et al.", + "maximum": 3, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_type", + "type": "integer" + }, + "clamp_len": { + "default": 1000, + "description": "Use the same pos embeddings after clamp_len.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "clamp_len", + "type": "integer" + }, + "cutoffs": { + "default": [ + 20000, + 40000, + 200000 + ], + "description": "Cutoffs for the adaptive softmax.", + "items": { + "title": "cutoffs", + "type": "integer" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cutoffs", + "type": [ + "array", + "null" + ] + }, + "d_embed": { + "default": 1024, + "description": "Dimensionality of the embeddings", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_embed", + "type": "integer" + }, + "d_head": { + "default": 64, + "description": "Dimensionality of the model\u2019s heads.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_head", + "type": "integer" + }, + "d_inner": { + "default": 4096, + "description": " Inner dimension in FF", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_inner", + "type": "integer" + }, + "d_model": { + "default": 1024, + "description": "Dimensionality of the model\u2019s hidden states.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_model", + "type": "integer" + }, + "div_val": { + "default": 4, + "description": "Divident value for adapative input and softmax.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "div_val", + "type": "integer" + }, + "dropatt": { + "default": 0.0, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "dropatt", + "type": "number" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "eos_token_id": { + "default": 0, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sequence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "init": { + "default": "normal", + "description": "Parameter initializer to use.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init", + "type": "string" + }, + "init_range": { + "default": 0.01, + "description": "Parameters initialized by U(-init_range, init_range).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init_range", + "type": "number" + }, + "init_std": { + "default": 0.02, + "description": "Parameters initialized by N(0, init_std)", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init_std", + "type": "number" + }, + "layer_norm_epsilon": { + "default": 1e-05, + "description": "The epsilon to use in the layer normalization layers", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_epsilon", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "mem_len": { + "default": 1600, + "description": "Length of the retained previous heads.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "mem_len", + "type": "integer" + }, + "n_head": { + "default": 16, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_layer": { + "default": 18, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "pre_lnorm": { + "default": false, + "description": "Whether or not to apply LayerNorm to the input instead of the output in the blocks.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pre_lnorm", + "type": "boolean" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "transfo-xl-wt103", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "proj_init_std": { + "default": 0.01, + "description": "Parameters initialized by N(0, init_std)", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "proj_init_std", + "type": "number" + }, + "proj_share_all_but_first": { + "default": true, + "description": "True to share all but first projs, False not to share.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "proj_share_all_but_first", + "type": "boolean" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "same_length": { + "default": true, + "description": "Whether or not to use the same attn length for all tokens", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "same_length", + "type": "boolean" + }, + "sample_softmax": { + "default": -1, + "description": "Number of samples in the sampled softmax.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sample_softmax", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "untie_r": { + "default": true, + "description": "Whether ot not to untie relative position biases.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "untie_r", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 267735, + "description": "Vocabulary size of the TransfoXL model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling TransfoXLModel or TFTransfoXLModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "xlnet" + } + } + }, + "then": { + "properties": { + "attn_type": { + "default": "bi", + "description": "The attention type used by the model. Currently only 'bi' is supported.", + "enum": [ + "bi" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_type", + "type": "string" + }, + "bi_data": { + "default": false, + "description": "Whether or not to use bidirectional input pipeline. Usually set to True during pretraining and False during finetuning.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bi_data", + "type": "boolean" + }, + "bos_token_id": { + "default": 1, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "clamp_len": { + "default": -1, + "description": "Clamp all relative distances larger than clamp_len. Setting this attribute to -1 means no clamping.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "clamp_len", + "type": "integer" + }, + "d_inner": { + "default": 3072, + "description": "Dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_inner", + "type": "integer" + }, + "d_model": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_model", + "type": "integer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "summary_last_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "end_n_top": { + "default": 5, + "description": " Used in the SQuAD evaluation script.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "end_n_top", + "type": "integer" + }, + "eos_token_id": { + "default": 2, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sequence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "ff_activation": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler. If string, 'gelu', 'relu', 'silu' and 'gelu_new' are supported.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "ff_activation", + "type": "string" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "mem_len": { + "default": null, + "description": "The number of tokens to cache. The key/value pairs that have already been pre-computed in a previous forward pass won\u2019t be re-computed. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "mem_len", + "type": [ + "integer", + "null" + ] + }, + "n_head": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_layer": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "pad_token_id": { + "default": 5, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "xlnet-base-cased", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "reuse_len": { + "default": null, + "description": "The number of tokens in the current batch to be cached and reused in the future.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reuse_len", + "type": [ + "integer", + "null" + ] + }, + "same_length": { + "default": false, + "description": "Whether or not to use the same attention length for each token.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "same_length", + "type": "boolean" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "start_n_top": { + "default": 5, + "description": "Used in the SQuAD evaluation script.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "start_n_top", + "type": "integer" + }, + "summary_activation": { + "default": "tanh", + "description": "Argument used when doing sequence summary. Used in the sequence classification and multiple choice models.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Summary Activation Function" + }, + "title": "summary_activation", + "type": "string" + }, + "summary_last_dropout": { + "default": 0.1, + "description": "Used in the sequence classification and multiple choice models.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "summary_last_dropout" + }, + "title": "summary_last_dropout", + "type": "number" + }, + "summary_type": { + "default": "last", + "description": "Argument used when doing sequence summary. Used in the sequence classification and multiple choice models.", + "enum": [ + "last", + "first", + "mean", + "cls_index", + "attn" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "summary_type", + "type": "string" + }, + "summary_use_proj": { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "summary_use_proj", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "untie_r": { + "default": true, + "description": "Whether or not to untie relative position biases", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "untie_r", + "type": "boolean" + }, + "use_mems_eval": { + "default": true, + "description": "Whether or not the model should make use of the recurrent memory mechanism in evaluation mode.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_mems_eval", + "type": "boolean" + }, + "use_mems_train": { + "default": false, + "description": "Whether or not the model should make use of the recurrent memory mechanism in train mode.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_mems_train", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 32000, + "description": "Vocabulary size of the XLNet model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling XLNetModel or TFXLNetModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "distilbert" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler. If string, 'gelu', 'relu', 'silu' and 'gelu_new' are supported.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "This is the default activation function used in the Distillbert huggingface implementation", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": "string" + }, + "attention_dropout": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout, qa_dropout, seq_classif_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_dropout" + }, + "title": "attention_dropout", + "type": "number" + }, + "dim": { + "default": 768, + "description": " Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "dim", + "type": "integer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_dropout,\nqa_dropout,\nseq_classif_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "hidden_dim": { + "default": 3072, + "description": "The size of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "hidden_dim", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_heads": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_heads", + "type": "integer" + }, + "n_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layers", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "distilbert-base-uncased", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "qa_dropout": { + "default": 0.1, + "description": "The dropout probabilities used in the question answering model DistilBertForQuestionAnswering.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout, attention_dropout, seq_classif_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "qa_dropout" + }, + "title": "qa_dropout", + "type": "number" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "seq_classif_dropout": { + "default": 0.2, + "description": "The dropout probabilities used in the sequence classification and the multiple choice model DistilBertForSequenceClassification.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nattention_dropout,\nqa_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "seq_classif_dropout" + }, + "title": "seq_classif_dropout", + "type": "number" + }, + "sinusoidal_pos_embds": { + "default": false, + "description": "Whether to use sinusoidal positional embeddings.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sinusoidal_pos_embds", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the DistilBERT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling DistilBertModel or TFDistilBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "camembert" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "classifier_dropout, hidden_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "classifier_dropout": { + "default": null, + "description": "The dropout ratio for the classification head.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob, hidden_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout" + }, + "title": "classifier_dropout", + "type": [ + "number", + "null" + ] + }, + "gradient_checkpointing": { + "default": false, + "description": "Whether to use gradient checkpointing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gradient_checkpointing", + "type": "boolean" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob, \nclassifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 3072, + "description": "Dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-05, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 514, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 1, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "Type of position embedding.", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "camembert-base", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 1, + "description": "The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 32005, + "description": "Vocabulary size of the CamemBERT model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "t5" + } + } + }, + "then": { + "properties": { + "d_ff": { + "default": 2048, + "description": "Size of the intermediate feed forward layer in each T5Block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value matches the pre-trained encoder.", + "description_implications": "If using a pre-trained encoder, this parameter will be automatically derived from the pre-trained model.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Dimensionality of Feed-Forward Layer" + }, + "title": "d_ff", + "type": "integer" + }, + "d_kv": { + "default": 64, + "description": "Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // num_heads.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_kv", + "type": "integer" + }, + "d_model": { + "default": 512, + "description": "Size of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_model", + "type": "integer" + }, + "dropout_rate": { + "default": 0.1, + "description": "The ratio for all dropout layers.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout_rate" + }, + "title": "dropout_rate", + "type": "number" + }, + "feed_forward_proj": { + "default": "relu", + "description": "Type of feed forward layer to be used. Should be one of 'relu' or 'gated-gelu'. T5v1.1 uses the 'gated-gelu' feed forward projection. Original T5 uses 'relu'.", + "enum": [ + "relu", + "gated-gelu" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "feed_forward_proj", + "type": "string" + }, + "initializer_factor": { + "default": 1, + "description": "A factor for initializing all weight matrices (should be kept to 1, used internally for initialization testing).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "initializer_factor", + "type": "number" + }, + "layer_norm_eps": { + "default": 1e-06, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_decoder_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not set.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_decoder_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value matches the number of layers in the default pretrained encoder.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nIf using a pre-trained model, this parameter will be automatically derived from the pre-trained model.", + "example_value": [ + 6 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "pretrained_model_or_path" + ], + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "t5-small", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "relative_attention_num_buckets": { + "default": 32, + "description": "The number of buckets to use for each attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "relative_attention_num_buckets", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 32128, + "description": "Vocabulary size of the T5 model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling T5Model or TFT5Model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "flaubert" + } + } + }, + "then": { + "properties": { + "asm": { + "default": false, + "description": "Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the prediction layer.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "asm", + "type": "boolean" + }, + "attention_dropout": { + "default": 0.1, + "description": "The dropout probability for the attention mechanism", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_dropout" + }, + "title": "attention_dropout", + "type": "number" + }, + "bos_index": { + "default": 0, + "description": "The index of the beginning of sentence token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bos_index", + "type": "integer" + }, + "causal": { + "default": false, + "description": "Whether or not the model should behave in a causal manner. Causal models use a triangular attention mask in order to only attend to the left-side context instead if a bidirectional context.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "causal", + "type": "boolean" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "emb_dim": { + "default": 512, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "emb_dim", + "type": "integer" + }, + "embed_init_std": { + "default": 0.02209708691207961, + "description": "The standard deviation of the truncated_normal_initializer for initializing the embedding matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embed_init_std", + "type": "number" + }, + "eos_index": { + "default": 1, + "description": "The index of the end of sentence token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "eos_index", + "type": "integer" + }, + "gelu_activation": { + "default": true, + "description": "Whether or not to use a gelu activation instead of relu.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gelu_activation", + "type": "boolean" + }, + "init_std": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices except the embedding matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init_std", + "type": "number" + }, + "is_encoder": { + "default": true, + "description": "Whether or not the initialized model should be a transformer encoder or decoder as seen in Vaswani et al.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "is_encoder", + "type": "boolean" + }, + "lang_id": { + "default": 0, + "description": "The ID of the language used by the model. This parameter is used when generating text in a given language.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lang_id", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-06, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "layerdrop": { + "default": 0.2, + "description": "Probability to drop layers during training (Fan et al., Reducing Transformer Depth on Demand with Structured Dropout. ICLR 2020)", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layerdrop", + "type": "number" + }, + "mask_index": { + "default": 5, + "description": "The index of the masking token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "mask_index", + "type": "integer" + }, + "mask_token_id": { + "default": 0, + "description": "Model agnostic parameter to identify masked tokens when generating text in an MLM context.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Mask Token ID" + }, + "title": "mask_token_id", + "type": "integer" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_heads": { + "default": 8, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_heads", + "type": "integer" + }, + "n_langs": { + "default": 1, + "description": "The number of languages the model handles. Set to 1 for monolingual models.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Languages" + }, + "title": "n_langs", + "type": "integer" + }, + "n_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layers", + "type": "integer" + }, + "pad_index": { + "default": 2, + "description": "The index of the padding token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_index", + "type": "integer" + }, + "pre_norm": { + "default": true, + "description": "Whether to apply the layer normalization before or after the feed forward layer following the attention in each layer (Vaswani et al., Tensor2Tensor for Neural Machine Translation. 2018)", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pre_norm", + "type": "boolean" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "flaubert/flaubert_small_cased", + "description": "Name of path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "sinusoidal_embeddings": { + "default": false, + "description": "Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sinusoidal_embeddings", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "unk_index": { + "default": 3, + "description": "The index of the unknown token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unk_index", + "type": "integer" + }, + "use_lang_emb": { + "default": true, + "description": "Whether to use language embeddings. Some models use additional language embeddings, see the multilingual models page for information on how to use them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_lang_emb", + "type": "boolean" + }, + "use_pretrained": { + "default": false, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30145, + "description": "Vocabulary size of the FlauBERT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling FlaubertModel or TFFlaubertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "electra" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, classifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "classifier_dropout": { + "default": null, + "description": "The dropout ratio for the classification head.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, attention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout" + }, + "title": "classifier_dropout", + "type": [ + "number", + "null" + ] + }, + "embedding_size": { + "default": 128, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Huggingface docs for ELECTRA config](https://huggingface.co/docs/transformers/model_doc/electra#transformers.ElectraConfig.hidden_act)\n\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob,\nclassifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 256, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 1024, + "description": "Dimensionality of the \u201cintermediate\u201d (i.e., feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 4, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "Type of position embedding.", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "google/electra-small-discriminator", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 2, + "description": "The vocabulary size of the token_type_ids passed when calling ElectraModel or TFElectraModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the ELECTRA model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling ElectraModel or TFElectraModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "longformer" + } + } + }, + "then": { + "properties": { + "attention_window": { + "default": 512, + "description": "Size of an attention window around each token. If an int, use the same size for all layers. To specify a different window size for each layer, use a List[int] where len(attention_window) == num_hidden_layers.", + "oneOf": [ + { + "default": 512, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "attention_window_integer_option", + "type": "integer" + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "attention_window_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attention_window" + }, + "max_position_embeddings": { + "default": 4098, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words or positions, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "allenai/longformer-base-4096", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "sep_token_id": { + "default": 2, + "description": "ID of the separator token, which is used when building a sequence from multiple sequences", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sep_token_id", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 1, + "description": "The vocabulary size of the token_type_ids passed when calling LongformerEncoder", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 50265, + "description": "Vocabulary size of the Longformer model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "auto_transformer" + } + } + }, + "then": { + "properties": { + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": null, + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "Vocabulary size of the AutoTransformer model. If None, the vocab size will be inferred from the given pretrained model", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tf_idf" + } + } + }, + "then": { + "properties": { + "max_sequence_length": { + "default": null, + "description": "", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "str2idf": { + "additionalProperties": {}, + "default": null, + "description": "", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "str2idf", + "type": [ + "object", + "null" + ] + }, + "vocab": { + "default": null, + "description": "", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "embed", + "camembert", + "longformer", + "auto_transformer", + "gpt2", + "xlmroberta", + "flaubert", + "gpt", + "parallel_cnn", + "rnn", + "xlnet", + "deberta", + "albert", + "bert", + "transformer_xl", + "transformer", + "cnnrnn", + "t5", + "electra", + "tf_idf", + "stacked_parallel_cnn", + "distilbert", + "stacked_cnn", + "roberta" + ], + "enumDescriptions": { + "albert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `albert` encoder loads a pretrained [ALBERT](https://arxiv.org/abs/1909.11942) (default `albert-base-v2`) model using the Hugging Face transformers package. Albert is similar to BERT, with significantly lower memory usage and somewhat faster training time:.", + "other_information": null, + "related_parameters": null, + "short_description": "Similar to BERT with lower memory footprint and faster training.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "auto_transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://huggingface.co/docs/transformers/model_doc/auto" + ], + "long_description": "The `auto_transformer` encoder automatically instantiates the model architecture for the specified `pretrained_model_name_or_path`. Unlike the other HF encoders, `auto_transformer` does not provide a default value for `pretrained_model_name_or_path`, this is its only mandatory parameter. See the Hugging Face [AutoModels documentation](https://huggingface.co/docs/transformers/model_doc/auto) for more details.", + "other_information": null, + "related_parameters": null, + "short_description": "Automatically retrieves the architecture from the provided model name/path.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "bert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1810.04805" + ], + "long_description": "The bert encoder loads a pretrained BERT (default bert-base-uncased) model using the Hugging Face transformers package. BERT is a bidirectional transformer pretrained using a combination of masked language modeling objective and next sentence prediction on a large corpus comprising the Toronto Book Corpus and Wikipedia.", + "other_information": null, + "related_parameters": null, + "short_description": "Bidirectional transformer great for language modeling.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "camembert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1911.03894" + ], + "long_description": "The `camembert` encoder loads a pretrained [CamemBERT](https://arxiv.org/abs/1911.03894) (default `jplu/tf-camembert-base`) model using the Hugging Face transformers package. CamemBERT is pre-trained on a large French language web-crawled text corpus.", + "other_information": null, + "related_parameters": null, + "short_description": "Language model trained on large French text corpus.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "deberta": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/2006.03654", + "https://arxiv.org/abs/2111.09543" + ], + "long_description": "The [DeBERTa](https://arxiv.org/abs/2006.03654) encoder improves the BERT and RoBERTa models using disentangled attention and enhanced mask decoder. With those two improvements, DeBERTa out performs RoBERTa on a majority of NLU tasks with 80GB training data.\nIn [DeBERTa V3](https://arxiv.org/abs/2111.09543), the authors further improved the efficiency of DeBERTa using ELECTRA-Style pre-training with Gradient Disentangled Embedding Sharing. Compared to DeBERTa, the V3 version significantly improves the model performance on downstream tasks.", + "other_information": null, + "related_parameters": null, + "short_description": "Improved version of BERT and RoBERTa, achieving good baseline performance on many tasks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "distilbert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `distilbert` encoder loads a pretrained [DistilBERT](https://medium.com/huggingface/distilbert-8cf3380435b5) (default `distilbert-base-uncased`) model using the Hugging Face transformers package. DistilBERT is a small, fast, cheap and light Transformer model trained by distilling BERT base. It has 40% less parameters than bert-base-uncased, runs 60% faster while preserving over 95% of BERT\u2019s performances as measured on the GLUE language understanding benchmark.", + "other_information": null, + "related_parameters": null, + "short_description": "A distilled version of BERT base that is 40% smaller and 60% faster with 95% of performance preserved.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "electra": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://openreview.net/pdf?id=r1xMH1BtvB" + ], + "long_description": "The `electra`` encoder loads a pretrained [ELECTRA](https://openreview.net/pdf?id=r1xMH1BtvB) model using the Hugging Face transformers package. ELECTRA is a new pretraining approach which trains two transformer models the generator and the discriminator. The generator\u2019s role is to replace tokens in a sequence, and is therefore trained as a masked language model. The discriminator, which is the model we\u2019re interested in, tries to identify which tokens were replaced by the generator in the sequence.", + "other_information": null, + "related_parameters": null, + "short_description": "Transformer encoder that can be used to encode a sequence of tokens with little compute", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h` tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps each element of the sequence to an embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "flaubert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1912.05372" + ], + "long_description": "The `flaubert`` encoder loads a pretrained [FlauBERT](https://arxiv.org/abs/1912.05372) (default `jplu/tf-flaubert-base-uncased``) model using the Hugging Face transformers package. FlauBERT has an architecture similar to BERT and is pre-trained on a large French language corpus.", + "other_information": null, + "related_parameters": null, + "short_description": "Language model with BERT related architecture trained on large French text corpus.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "gpt": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf" + ], + "long_description": "The `gpt` encoder loads a pretrained [GPT](https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf) (default `openai-gpt`) model using the Hugging Face transformers package. GPT is a causal (unidirectional) transformer pre-trained using language modeling on a large corpus with long range dependencies, the Toronto Book Corpus.", + "other_information": null, + "related_parameters": null, + "short_description": "GPT is a pre-trained language model used for NLP tasks like generation, summarization, and translation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "gpt2": { + "commonly_used": false, + "compute_tier": 3, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf" + ], + "long_description": "The `gpt2` encoder loads a pretrained [GPT-2](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) (default `gpt2`) model using the Hugging Face transformers package. GPT-2 is a causal (unidirectional) transformer pretrained using language modeling on a very large corpus of ~40 GB of text data.", + "other_information": null, + "related_parameters": null, + "short_description": "GPT-2 is a pre-trained language model used for NLP tasks like generation, summarization, and translation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "longformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/pdf/2004.05150.pdf" + ], + "long_description": "The `longformer` encoder loads a pretrained [Longformer](https://arxiv.org/pdf/2004.05150.pdf) (default `allenai/longformer-base-4096`) model using the Hugging Face transformers package. Longformer is a good choice for longer text, as it supports sequences up to 4096 tokens long.", + "other_information": null, + "related_parameters": null, + "short_description": "Transformer optimized for longer text inputs.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "roberta": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1907.11692" + ], + "long_description": "The `roberta` encoder loads a pretrained [RoBERTa](https://arxiv.org/abs/1907.11692) (default `roberta-base`) model using the Hugging Face transformers package. Replication of BERT pretraining which may match or exceed the performance of BERT. RoBERTa builds on BERT and modifies key hyperparameters, removing the next-sentence pretraining objective and training with much larger mini-batches and learning rates.", + "other_information": null, + "related_parameters": null, + "short_description": "BERT based model that has higher accuracy and is easier parallelize due to larger mini-batches.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "t5": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `t5` encoder loads a pretrained [T5](https://arxiv.org/pdf/1910.10683.pdf) (default `t5-small`) model using the Hugging Face transformers package. T5 (Text-to-Text Transfer Transformer) is pre-trained on a huge text dataset crawled from the web and shows good transfer performance on multiple tasks.", + "other_information": null, + "related_parameters": null, + "short_description": "Text-to-text approach transformer with good transfer performance on multiple tasks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer_xl": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `transformer_xl` encoder loads a pretrained [Transformer-XL](https://arxiv.org/abs/1901.02860) (default `transfo-xl-wt103`) model using the Hugging Face transformers package. Adds novel positional encoding scheme which improves understanding and generation of long-form text up to thousands of tokens. Transformer-XL is a causal (uni-directional) transformer with relative positioning (sinuso\u00efdal) embeddings which can reuse previously computed hidden-states to attend to longer context (memory). This model also uses adaptive softmax inputs and outputs (tied).", + "other_information": null, + "related_parameters": null, + "short_description": "Transformer architecture that introduces the notion of recurrence to the deep self-attention network.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "xlmroberta": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `xlmroberta` encoder loads a pretrained [XLM-RoBERTa](https://arxiv.org/abs/1911.02116) (default `jplu/tf-xlm-reoberta-base`) model using the Hugging Face transformers package. XLM-RoBERTa is a multi-language model similar to BERT, trained on 100 languages. XLM-RoBERTa is based on Facebook\u2019s RoBERTa model released in 2019. It is a large multi-lingual language model, trained on 2.5TB of filtered CommonCrawl data.", + "other_information": null, + "related_parameters": null, + "short_description": "XLM-RoBERTa a large multi-lingual language model trained on 2.5TB of filtered CommonCrawl data.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "xlnet": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `xlnet` encoder loads a pretrained [XLNet](https://arxiv.org/abs/1906.08237) (default `xlnet-base-cased`) model using the Hugging Face transformers package. XLnet is an extension of the Transformer-XL model pre-trained using an autoregressive method to learn bidirectional contexts by maximizing the expected likelihood over all permutations of the input sequence factorization order. XLNet outperforms BERT on a variety of benchmarks.", + "other_information": null, + "related_parameters": null, + "short_description": "XLNet is a transformer that outperforms BERT on a variety of benchmarks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sequence_softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "unique": { + "default": false, + "description": "If true, the loss is only computed for unique elements in the sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "unique", + "type": "boolean" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "sequence_softmax_cross_entropy", + "enum": [ + "sequence_softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "For pretrained encoders, compute encoder embeddings in preprocessing, speeding up training time considerably. Only supported when `encoder.trainable=false`.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "compute_idf": { + "default": false, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "compute_idf", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the `missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": [ + "Depends on dtype" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "missing_value_strategy, fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "DOCSTRING ONLY" + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": true, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Reading the text in lowercase enables the model to treat capitalized and lowercase words as the same, effectively increasing the number of data points per word.", + "description_implications": "If you set lowercase to False, then capitalized words are seen as completely separate entities than lowercase words.", + "example_value": [ + true + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": "If there is a strong reason to treat capitalized words and lowercased words differently, then set this to False. Otherwise, it is preferable to bucket the words and make the model case-insensitive.", + "ui_display_name": "Convert to lowercase" + }, + "title": "lowercase", + "type": "boolean" + }, + "max_sequence_length": { + "default": 256, + "description": "The maximum length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 256. Every sequence will be truncated to this length.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size, embedding_size" + ], + "short_description": "", + "suggested_values": "Use the lowest value that covers most of your input data. Only increase the value if crucial parts of the input data are truncated.", + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Sequence Length" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a text column.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 20000, + "description": "The maximum number of most common tokens in the vocabulary. If the data contains more than this amount, the most infrequent symbols will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": "integer" + }, + "ngram_size": { + "default": 2, + "description": "The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Size of the n-gram when using the `ngram` tokenizer.", + "description_implications": null, + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "n-gram size" + }, + "title": "ngram_size", + "type": "integer" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "We usually want to add padding to the end of a text sequence to fill in any remaining space as opposed to the beggining so we set the default to right.", + "description_implications": "If you pad to the left, the encoded vector will have leading padding tokens as opposed to trailing padding tokens. This could matter based on the type of text input you are expecting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "padding_symbol,\nmax_sequence_length" + ], + "short_description": "", + "suggested_values": "'right'", + "suggested_values_reasoning": "right padding is the usual way to add padding to a text sequence", + "ui_display_name": "Padding" + }, + "title": "padding", + "type": "string" + }, + "padding_symbol": { + "default": "", + "description": "The string used as the padding symbol for sequence features. Ignored for features using huggingface encoders, which have their own vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_symbol", + "type": "string" + }, + "pretrained_model_name_or_path": { + "default": null, + "description": "This can be either the name of a pretrained HuggingFace model or a path where it was downloaded.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": [ + "string", + "null" + ] + }, + "sequence_length": { + "default": null, + "description": "The desired length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated and sequences shorter than this value will be padded. If None, sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": null, + "title": "sequence_length", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space_punct", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default tokenizer is `space_punct`, an abbreviation of \"Space punctuation\". This tokenizer creates sub-words by dividing the text on whitespace and punctuation characters. For example: The text `'hello world!isn't this great?'` would be transformed to `['hello', 'world', '!', 'isn', \"'\", 't', 'this', 'great', '?']`. This is the default value because it is a fast tokenizer that works reasonably well.", + "description_implications": "Choosing a tokenizer can be difficult. The primary thing to check is that the tokenizer you have selected is compatible with the language(s) in your text data. This means either selecting a tokenizer that is language-specific (i.e. `french_tokenize` if working with French text) or general enough that its tokenizations are language-agnostic (i.e. `space_punct`).", + "example_value": [ + "space_punct" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://huggingface.co/course/chapter2/4?fw=pt" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_file, pretrained_model_name_or_path" + ], + "short_description": "", + "suggested_values": "sentencepiece", + "suggested_values_reasoning": "SentencePiece is a tokenizer developed by Google which utilizes Byte-Pair Encoding (BPE), which strikes a good balance between character-level and word-level tokenization (more info on BPE here: https://towardsdatascience.com/byte-pair-encoding-the-dark-horse-of-modern-nlp-eb36c7df4f10 ). This tokenizer is language-agnostic and more sophisticated than the default.", + "ui_display_name": "Tokenizer" + }, + "title": "tokenizer", + "type": "string" + }, + "unknown_symbol": { + "default": "", + "description": "The string used as the unknown symbol for sequence features. Ignored for features using huggingface encoders, which have their own vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unknown_symbol", + "type": "string" + }, + "vocab_file": { + "default": null, + "description": "Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the first string until `\\t` or `\\n` is considered a word.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The vocabulary can be parsed automatically from the incoming input features.", + "description_implications": "It can be useful to specify your own vocabulary list if the vocabulary is very large, there's no out of the box tokenizer that fits your data, or if there are several uncommon or infrequently occurring tokens that we want to guarantee to be a part of the vocabulary, rather than treated as an unknown.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Vocab File" + }, + "title": "vocab_file", + "type": [ + "string", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "timeseries": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "projector" + } + } + }, + "then": { + "properties": { + "activation": { + "default": null, + "description": " Indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "clip": { + "default": null, + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": null, + "description": "Clip the output of the decoder to be within the given range.", + "items": [ + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + }, + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "description": "None", + "title": "null_float_tuple_option", + "type": "null" + } + ], + "title": "clip" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "multiplier": { + "default": 1.0, + "description": "Multiplier to scale the activated outputs by. Useful when setting `activation` to something that outputs a value between [-1, 1] like tanh to re-scale values back to order of magnitude of the data you're trying to predict. A good rule of thumb in such cases is to pick a value like `x * (max - min)` where x is a scalar in the range [1, 2]. For example, if you're trying to predict something like temperature, it might make sense to pick a multiplier on the order of `100`.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "multiplier", + "type": "number" + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": null, + "description": "Size of the output of the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": [ + "integer", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "projector", + "enum": [ + "projector" + ], + "enumDescriptions": { + "projector": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Projector decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of the vector size (optionally followed by a softmax in the case of multi-class classification).", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output into an output vector.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "encoding_size": { + "default": null, + "description": "The size of the encoding vector, or None if sequence elements are scalars.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `reduce_output` method does not use this parameter, so by default this parameter is not set.", + "description_implications": "This parameter must be equal to the size of the input. Otherwise, an error will occur.", + "example_value": [ + 128 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "reduce_output" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "NONE", + "ui_display_name": null + }, + "title": "encoding_size", + "type": [ + "integer", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "reduce_output": { + "default": null, + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "rnn", + "passthrough", + "transformer", + "cnnrnn", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "dense" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply transforms each input value into a float value and adds a dimension to the input tensor, creating a b x s x 1 tensor where b is the batch size and s is the length of the sequence. The tensor is reduced along the s dimension to obtain a single vector of size h for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Transforms sequence values to a floats then reduces to obtain a vector for each element.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "huber" + } + } + }, + "then": { + "properties": { + "delta": { + "default": 1.0, + "description": "Threshold at which to change between delta-scaled L1 and L2 loss.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "delta", + "type": "number" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "huber", + "enum": [ + "mean_squared_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "huber" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the `missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when a row of data is missing.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "padding_value": { + "default": 0.0, + "description": "Float value that is used for padding and replacing missing values within a row.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_value", + "type": "number" + }, + "timeseries_length_limit": { + "default": 256, + "description": "Defines the maximum length of the timeseries. All timeseries longer than this limit are cut off.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "timeseries_length_limit", + "type": "integer" + }, + "tokenizer": { + "default": "space", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + }, + "window_size": { + "default": 0, + "description": "Optional lookback window size used to convert a column-major dataset (one observation per row) into a row-major dataset (each row has a timeseries window of observations). Starting from a given observation, a sliding window is taken going `window_size - 1` rows back to form the timeseries input feature. If this value is left as 0, then it is assumed that the dataset has been provided in row-major format (i.e., it has already been preprocessed such that each row is a timeseries window).", + "minimum": 0, + "parameter_metadata": null, + "title": "window_size", + "type": "integer" + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + }, + "vector": { + "additionalProperties": false, + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "projector" + } + } + }, + "then": { + "properties": { + "activation": { + "default": null, + "description": " Indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "clip": { + "default": null, + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": null, + "description": "Clip the output of the decoder to be within the given range.", + "items": [ + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + }, + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "description": "None", + "title": "null_float_tuple_option", + "type": "null" + } + ], + "title": "clip" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "multiplier": { + "default": 1.0, + "description": "Multiplier to scale the activated outputs by. Useful when setting `activation` to something that outputs a value between [-1, 1] like tanh to re-scale values back to order of magnitude of the data you're trying to predict. A good rule of thumb in such cases is to pick a value like `x * (max - min)` where x is a scalar in the range [1, 2]. For example, if you're trying to predict something like temperature, it might make sense to pick a multiplier on the order of `100`.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "multiplier", + "type": "number" + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": null, + "description": "Size of the output of the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": [ + "integer", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "projector", + "enum": [ + "projector" + ], + "enumDescriptions": { + "projector": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Projector decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of the vector size (optionally followed by a softmax in the case of multi-class classification).", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output into an output vector.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "dense", + "enum": [ + "passthrough", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "huber" + } + } + }, + "then": { + "properties": { + "delta": { + "default": 1.0, + "description": "Threshold at which to change between delta-scaled L1 and L2 loss.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "delta", + "type": "number" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "mean_squared_error", + "enum": [ + "mean_squared_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "softmax_cross_entropy", + "huber" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "pattern": "^([0-9]+(\\.[0-9]*)?\\s*)*$", + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "pattern": "^([0-9]+(\\.[0-9]*)?\\s*)*$", + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a vector column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "vector_size": { + "default": null, + "description": "The size of the vector. If None, the vector size will be inferred from the data.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "vector_size", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + } + }, + "title": "defaults_options", + "type": "object" + } + }, + "type": "object" + }, + "hyperopt": { + "description": "Settings for hyperopt", + "properties": { + "executor": { + "additionalProperties": true, + "description": "specifies how to execute the hyperparameter optimization. The execution could happen locally in a serial manner or in parallel across multiple workers and with GPUs as well if available. The executor section includes specification for work scheduling and the number of samples to generate.", + "properties": { + "cpu_resources_per_trial": { + "default": 1, + "description": "The number of CPU cores allocated to each trial", + "minimum": 1, + "parameter_metadata": null, + "title": "cpu_resources_per_trial", + "type": "integer" + }, + "gpu_resources_per_trial": { + "default": 0, + "description": "The number of GPU devices allocated to each trial", + "minimum": 0, + "parameter_metadata": null, + "title": "gpu_resources_per_trial", + "type": "integer" + }, + "kubernetes_namespace": { + "default": null, + "description": "When running on Kubernetes, provide the namespace of the Ray cluster to sync results between pods. See the Ray docs for more info.", + "parameter_metadata": null, + "title": "kubernetes_namespace", + "type": [ + "string", + "null" + ] + }, + "max_concurrent_trials": { + "default": "auto", + "description": "The maximum number of trials to train concurrently. Defaults to auto if not specified.", + "oneOf": [ + { + "default": 1, + "description": "Manually set a number of concurrent trials.", + "minimum": 1, + "parameter_metadata": null, + "title": "max_concurrent_trials_integer_option", + "type": "integer" + }, + { + "default": "auto", + "description": "Automatically set number of concurrent trials.", + "enum": [ + "auto" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "max_concurrent_trials_string_option", + "type": "string" + }, + { + "description": "Disable this parameter.", + "title": "null_option", + "type": "null" + } + ], + "parameter_metadata": null, + "title": "max_concurrent_trials" + }, + "num_samples": { + "default": null, + "description": "This parameter, along with the space specifications in the parameters section, controls how many trials are generated.", + "minimum": 1, + "parameter_metadata": null, + "title": "num_samples", + "type": [ + "integer", + "null" + ] + }, + "scheduler": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "asynchyperband" + } + } + }, + "then": { + "properties": { + "grace_period": { + "default": 1, + "description": "Only stop trials at least this old in time. The units are the same as the attribute named by time_attr.", + "minimum": 1, + "parameter_metadata": null, + "title": "grace_period", + "type": "integer" + }, + "max_t": { + "default": 100, + "description": "max time units per trial. Trials will be stopped after max_t time units (determined by time_attr) have passed.", + "minimum": 1, + "parameter_metadata": null, + "title": "max_t", + "type": "integer" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "reduction_factor": { + "default": 4, + "description": "Used to set halving rate and amount. This is simply a unit-less scalar.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "reduction_factor", + "type": "number" + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "async_hyperband" + } + } + }, + "then": { + "properties": { + "grace_period": { + "default": 1, + "description": "Only stop trials at least this old in time. The units are the same as the attribute named by time_attr.", + "minimum": 1, + "parameter_metadata": null, + "title": "grace_period", + "type": "integer" + }, + "max_t": { + "default": 100, + "description": "max time units per trial. Trials will be stopped after max_t time units (determined by time_attr) have passed.", + "minimum": 1, + "parameter_metadata": null, + "title": "max_t", + "type": "integer" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "reduction_factor": { + "default": 4, + "description": "Used to set halving rate and amount. This is simply a unit-less scalar.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "reduction_factor", + "type": "number" + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "hyperband" + } + } + }, + "then": { + "properties": { + "max_t": { + "default": 81, + "description": "max time units per trial. Trials will be stopped after max_t time units (determined by time_attr) have passed.", + "minimum": 1, + "parameter_metadata": null, + "title": "max_t", + "type": "integer" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "reduction_factor": { + "default": 3, + "description": "Used to set halving rate and amount. This is simply a unit-less scalar.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "reduction_factor", + "type": "number" + }, + "stop_last_trials": { + "default": true, + "description": "Whether to terminate the trials after reaching max_t. Defaults to True.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "stop_last_trials", + "type": "boolean" + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "medianstoppingrule" + } + } + }, + "then": { + "properties": { + "grace_period": { + "default": 60.0, + "description": "Only stop trials at least this old in time. The mean will only be computed from this time onwards. The units are the same as the attribute named by time_attr.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "grace_period", + "type": "number" + }, + "hard_stop": { + "default": true, + "description": "If False, pauses trials instead of stopping them. When all other trials are complete, paused trials will be resumed and allowed to run FIFO.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "hard_stop", + "type": "boolean" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "min_samples_required": { + "default": 3, + "description": "Minimum number of trials to compute median over.", + "minimum": 1, + "parameter_metadata": null, + "title": "min_samples_required", + "type": "integer" + }, + "min_time_slice": { + "default": 0, + "description": "Each trial runs at least this long before yielding (assuming it isn\u2019t stopped). Note: trials ONLY yield if there are not enough samples to evaluate performance for the current result AND there are other trials waiting to run. The units are the same as the attribute named by time_attr.", + "minimum": 0, + "parameter_metadata": null, + "title": "min_time_slice", + "type": "integer" + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "time_attr": { + "default": "time_total_s", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "median_stopping_rule" + } + } + }, + "then": { + "properties": { + "grace_period": { + "default": 60.0, + "description": "Only stop trials at least this old in time. The mean will only be computed from this time onwards. The units are the same as the attribute named by time_attr.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "grace_period", + "type": "number" + }, + "hard_stop": { + "default": true, + "description": "If False, pauses trials instead of stopping them. When all other trials are complete, paused trials will be resumed and allowed to run FIFO.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "hard_stop", + "type": "boolean" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "min_samples_required": { + "default": 3, + "description": "Minimum number of trials to compute median over.", + "minimum": 1, + "parameter_metadata": null, + "title": "min_samples_required", + "type": "integer" + }, + "min_time_slice": { + "default": 0, + "description": "Each trial runs at least this long before yielding (assuming it isn\u2019t stopped). Note: trials ONLY yield if there are not enough samples to evaluate performance for the current result AND there are other trials waiting to run. The units are the same as the attribute named by time_attr.", + "minimum": 0, + "parameter_metadata": null, + "title": "min_time_slice", + "type": "integer" + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "time_attr": { + "default": "time_total_s", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "pbt" + } + } + }, + "then": { + "properties": { + "burn_in_period": { + "default": 60.0, + "description": "Models will not be considered for perturbation before this interval of time_attr has passed. This guarantees that models are trained for at least a certain amount of time or timesteps before being perturbed.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "burn_in_period", + "type": "number" + }, + "custom_explore_fn": { + "default": null, + "description": "You can also specify a custom exploration function. This function is invoked as f(config) after built-in perturbations from hyperparam_mutations are applied, and should return config updated as needed. You must specify at least one of hyperparam_mutations or custom_explore_fn.", + "parameter_metadata": null, + "title": "custom_explore_fn", + "type": [ + "string", + "null" + ] + }, + "hyperparam_mutations": { + "additionalProperties": {}, + "default": null, + "description": "Hyperparams to mutate. The format is as follows: for each key, either a list, function, or a tune search space object (tune.loguniform, tune.uniform, etc.) can be provided. A list specifies an allowed set of categorical values. A function or tune search space object specifies the distribution of a continuous parameter. You must use tune.choice, tune.uniform, tune.loguniform, etc.. Arbitrary tune.sample_from objects are not supported. A key can also hold a dict for nested hyperparameters. You must specify at least one of hyperparam_mutations or custom_explore_fn. Tune will sample the search space provided by hyperparam_mutations for the initial hyperparameter values if the corresponding hyperparameters are not present in a trial\u2019s initial config.", + "parameter_metadata": null, + "title": "hyperparam_mutations", + "type": [ + "object", + "null" + ] + }, + "log_config": { + "default": true, + "description": "Whether to log the ray config of each model to local_dir at each exploit. Allows config schedule to be reconstructed.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "log_config", + "type": "boolean" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "perturbation_factors": { + "default": [ + 1.2, + 0.8 + ], + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": [ + 1.2, + 0.8 + ], + "description": "Scaling factors to choose between when mutating a continuous hyperparameter.", + "items": [ + { + "minimum": 0, + "type": "number" + }, + { + "minimum": 0, + "type": "number" + } + ], + "type": "array" + } + ], + "title": "perturbation_factors" + }, + "perturbation_interval": { + "default": 60.0, + "description": "Models will be considered for perturbation at this interval of time_attr. Note that perturbation incurs checkpoint overhead, so you shouldn\u2019t set this to be too frequent.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "perturbation_interval", + "type": "number" + }, + "quantile_fraction": { + "default": 0.25, + "description": "Parameters are transferred from the top quantile_fraction fraction of trials to the bottom quantile_fraction fraction. Needs to be between 0 and 0.5. Setting it to 0 essentially implies doing no exploitation at all.", + "format": "float", + "maximum": 0.5, + "minimum": 0, + "parameter_metadata": null, + "title": "quantile_fraction", + "type": "number" + }, + "require_attrs": { + "default": true, + "description": "Whether to require time_attr and metric to appear in result for every iteration. If True, error will be raised if these values are not present in trial result.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "require_attrs", + "type": "boolean" + }, + "resample_probability": { + "default": 0.25, + "description": "The probability of resampling from the original distribution when applying hyperparam_mutations. If not resampled, the value will be perturbed by a factor chosen from perturbation_factors if continuous, or changed to an adjacent value if discrete.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "resample_probability", + "type": "number" + }, + "synch": { + "default": false, + "description": "If False, will use asynchronous implementation of PBT. Trial perturbations occur every perturbation_interval for each trial independently. If True, will use synchronous implementation of PBT. Perturbations will occur only after all trials are synced at the same time_attr every perturbation_interval. Defaults to False. See Appendix A.1 here https://arxiv.org/pdf/1711.09846.pdf.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "synch", + "type": "boolean" + }, + "time_attr": { + "default": "time_total_s", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "pbt_replay" + } + } + }, + "then": { + "properties": { + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "policy_file": { + "default": null, + "description": "The PBT policy file. Usually this is stored in ~/ray_results/experiment_name/pbt_policy_xxx.txt where xxx is the trial ID.", + "parameter_metadata": null, + "title": "policy_file", + "type": [ + "string", + "null" + ] + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "pb2" + } + } + }, + "then": { + "properties": { + "hyperparam_bounds": { + "additionalProperties": {}, + "default": null, + "description": "Hyperparameters to mutate. The format is as follows: for each key, enter a list of the form [min, max] representing the minimum and maximum possible hyperparam values.", + "parameter_metadata": null, + "title": "hyperparam_bounds", + "type": [ + "object", + "null" + ] + }, + "log_config": { + "default": true, + "description": "Whether to log the ray config of each model to local_dir at each exploit. Allows config schedule to be reconstructed.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "log_config", + "type": "boolean" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "perturbation_interval": { + "default": 60.0, + "description": "Models will be considered for perturbation at this interval of time_attr. Note that perturbation incurs checkpoint overhead, so you shouldn\u2019t set this to be too frequent.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "perturbation_interval", + "type": "number" + }, + "quantile_fraction": { + "default": 0.25, + "description": "Parameters are transferred from the top quantile_fraction fraction of trials to the bottom quantile_fraction fraction. Needs to be between 0 and 0.5. Setting it to 0 essentially implies doing no exploitation at all.", + "format": "float", + "maximum": 0.5, + "minimum": 0, + "parameter_metadata": null, + "title": "quantile_fraction", + "type": "number" + }, + "require_attrs": { + "default": true, + "description": "Whether to require time_attr and metric to appear in result for every iteration. If True, error will be raised if these values are not present in trial result.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "require_attrs", + "type": "boolean" + }, + "synch": { + "default": false, + "description": "If False, will use asynchronous implementation of PBT. Trial perturbations occur every perturbation_interval for each trial independently. If True, will use synchronous implementation of PBT. Perturbations will occur only after all trials are synced at the same time_attr every perturbation_interval. Defaults to False. See Appendix A.1 here https://arxiv.org/pdf/1711.09846.pdf.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "synch", + "type": "boolean" + }, + "time_attr": { + "default": "time_total_s", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "hb_bohb" + } + } + }, + "then": { + "properties": { + "max_t": { + "default": 81, + "description": "max time units per trial. Trials will be stopped after max_t time units (determined by time_attr) have passed.", + "minimum": 1, + "parameter_metadata": null, + "title": "max_t", + "type": "integer" + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "reduction_factor": { + "default": 3, + "description": "Used to set halving rate and amount. This is simply a unit-less scalar.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "reduction_factor", + "type": "number" + }, + "stop_last_trials": { + "default": true, + "description": "Whether to terminate the trials after reaching max_t. Defaults to True.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "stop_last_trials", + "type": "boolean" + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "fifo" + } + } + }, + "then": { + "properties": { + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "resource_changing" + } + } + }, + "then": { + "properties": { + "base_scheduler": { + "default": null, + "description": "The scheduler to provide decisions about trials. If None, a default FIFOScheduler will be used.", + "parameter_metadata": null, + "title": "base_scheduler", + "type": [ + "string", + "null" + ] + }, + "metric": { + "default": null, + "description": "The training result objective value attribute. Stopping procedures will use this attribute. If None but a mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss", + "_metric", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": [ + "string", + "null" + ] + }, + "mode": { + "default": null, + "description": "One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.", + "enum": [ + "min", + "max", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "mode", + "type": [ + "string", + "null" + ] + }, + "resources_allocation_function": { + "default": null, + "description": "The callable used to change live trial resource requiements during tuning. This callable will be called on each trial as it finishes one step of training. The callable must take four arguments: TrialRunner, current Trial, current result dict and the ResourceChangingScheduler calling it. The callable must return a PlacementGroupFactory, Resources, dict or None (signifying no need for an update). If resources_allocation_function is None, no resource requirements will be changed at any time. By default, DistributeResources will be used, distributing available CPUs and GPUs over all running trials in a robust way, without any prioritization.", + "parameter_metadata": null, + "title": "resources_allocation_function", + "type": [ + "string", + "null" + ] + }, + "time_attr": { + "default": "training_iteration", + "description": "A training result attr to use for comparing time. Note that you can pass in something non-temporal such as training_iteration as a measure of progress, the only requirement is that the attribute should increase monotonically.", + "enum": [ + "training_iteration", + "time_total_s", + "timesteps_total", + "mean_accuracy", + "mean_loss" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "time_attr", + "type": "string" + } + } + } + } + ], + "description": "", + "properties": { + "type": { + "default": "fifo", + "description": "The type of scheduler to use during hyperopt", + "enum": [ + "asynchyperband", + "async_hyperband", + "hyperband", + "medianstoppingrule", + "median_stopping_rule", + "pbt", + "pbt_replay", + "pb2", + "hb_bohb", + "fifo", + "resource_changing" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "scheduler_options", + "type": "object" + }, + "time_budget_s": { + "default": 3600, + "description": "The number of seconds for the entire hyperopt run.", + "minimum": 1, + "parameter_metadata": null, + "title": "time_budget_s", + "type": [ + "integer", + "null" + ] + }, + "trial_driver_resources": { + "additionalProperties": {}, + "default": null, + "description": "The resources reserved by each trial driver. This differs from cpu_resources_per_trial and gpu_resources_per_trial because these resources are reserved for the driver, not its subsequent workers. Only used when the trials themselves are on the Ray backend. Defaults to 1 CPU.", + "parameter_metadata": null, + "title": "trial_driver_resources", + "type": [ + "object", + "null" + ] + }, + "type": { + "default": "ray", + "description": "", + "enum": [ + "ray" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "type", + "type": "string" + } + }, + "title": "executor", + "type": "object" + }, + "goal": { + "default": "minimize", + "description": "Indicates if to minimize or maximize a metric or a loss of any of the output features on any of the dataset splits. Available values are: minimize (default) or maximize.", + "enum": [ + "minimize", + "maximize" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "goal", + "type": "string" + }, + "metric": { + "default": "loss", + "description": "The metric that we want to optimize for. The default one is loss, but depending on the type of the feature defined in output_feature, different metrics and losses are available. Check the metrics section of the specific output feature type to figure out what metrics are available to use.", + "enum": [ + "root_mean_squared_error", + "precision", + "recall", + "roc_auc", + "specificity", + "root_mean_squared_percentage_error", + "r2", + "loss", + "binary_weighted_cross_entropy", + "softmax_cross_entropy", + "sequence_softmax_cross_entropy", + "sigmoid_cross_entropy", + "token_accuracy", + "sequence_accuracy", + "perplexity", + "char_error_rate", + "accuracy", + "hits_at_k", + "mean_absolute_error", + "mean_squared_error", + "mean_absolute_percentage_error", + "jaccard", + "huber" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "metric", + "type": "string" + }, + "output_feature": { + "default": "combined", + "title": "output_feature", + "type": "string" + }, + "parameters": { + "additionalProperties": {}, + "default": null, + "description": "", + "parameter_metadata": null, + "title": "parameters", + "type": [ + "object", + "null" + ] + }, + "search_alg": { + "additionalProperties": true, + "description": "Specifies the algorithm to sample the defined parameters space. Candidate algorithms are those found in Ray Tune's Search Algorithms.", + "properties": { + "type": { + "default": "hyperopt", + "description": "", + "enum": [ + "variant_generator", + "random", + "hyperopt", + "bohb", + "ax", + "bayesopt", + "blendsearch", + "cfo", + "dragonfly", + "hebo", + "skopt", + "nevergrad", + "optuna", + "zoopt" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "type", + "type": "string" + } + }, + "title": "scheduler", + "type": "object" + }, + "split": { + "default": "validation", + "description": "The split of data that we want to compute our metric on. By default it is the validation split, but you have the flexibility to specify also train or test splits.", + "enum": [ + "train", + "validation", + "test" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "split", + "type": "string" + } + }, + "title": "hyperopt_options", + "type": [ + "object", + "null" + ] + }, + "input_features": { + "items": { + "additionalProperties": true, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "audio" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "cnnrnn", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "rnn" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "audio_file_length_limit_in_s": { + "default": 7.5, + "description": "Float value that defines the maximum limit of the audio file in seconds. All files longer than this limit are cut off. All files shorter than this limit are padded with padding_value", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "audio_file_length_limit_in_s", + "type": "number" + }, + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": [ + "number", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": [ + "number", + "null" + ] + }, + "in_memory": { + "default": true, + "description": "Defines whether the audio dataset will reside in memory during the training process or will be dynamically fetched from disk (useful for large datasets). In the latter case a training batch of input audio will be fetched from disk each training iteration.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "in_memory", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "bfill", + "description": "What strategy to follow when there's a missing value in an audio column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "norm": { + "default": null, + "description": "Normalization strategy for the audio files. If None, no normalization is performed. If per_file, z-norm is applied on a 'per file' level", + "enum": [ + "per_file", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "num_fft_points": { + "default": null, + "description": "Defines the number of fft points used for the short time Fourier transformation", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_fft_points", + "type": [ + "number", + "null" + ] + }, + "num_filter_bands": { + "default": 80, + "description": "Defines the number of filters used in the filterbank. Only needed if audio_feature_type is 'fbank'", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "window_length_in_s", + "type", + "window_shift_in_s" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Type" + }, + "title": "num_filter_bands", + "type": "integer" + }, + "padding_value": { + "default": 0.0, + "description": "Float value that is used for padding.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_value", + "type": "number" + }, + "type": { + "default": "fbank", + "description": "Defines the type of audio feature to be used.", + "enum": [ + "fbank", + "group_delay", + "raw", + "stft", + "stft_phase" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default type fbank is set based on values that we have tested and determined to be a good starting point for audio feature preprocessing. This is not to say that it is the best way to process every audio feature, it is just a good starting place that performs well in general.", + "description_implications": "The different type of audio you select hear will determine how your audio feature is preprocessed and transformed into trainable data for the model.", + "example_value": [ + "stft" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": "Audio feature preprocessing depends heavily on the type of audio data you are dealing with. The type of audio preprocessing you will want to use will be dictated by the audio data you are dealing with.", + "related_parameters": [ + "audio_file_length_limit_in_s", + "norm", + "padding_value", + "in_memory" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Type" + }, + "title": "type", + "type": "string" + }, + "window_length_in_s": { + "default": 0.04, + "description": "Defines the window length used for the short time Fourier transformation. This is only needed if the audio_feature_type is 'raw'.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "window_shift_in_s", + "type", + "num_filter_bands" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Window Length in Seconds" + }, + "title": "window_length_in_s", + "type": "number" + }, + "window_shift_in_s": { + "default": 0.02, + "description": "Defines the window shift used for the short time Fourier transformation (also called hop_length). This is only needed if the audio_feature_type is 'raw'. ", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "window_length_in_s", + "type", + "num_filter_bands" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Window Shift in Seconds" + }, + "title": "window_shift_in_s", + "type": "number" + }, + "window_type": { + "default": "hamming", + "description": "Defines the type window the signal is weighted before the short time Fourier transformation.", + "enum": [ + "bartlett", + "blackman", + "hamming", + "hann" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "window_type", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "bag" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 50, + "description": "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size is the number of different strings appearing in the training set in the input column (plus 1 for the unknown token placeholder ).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If true embeddings are trained during the training process, if false embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding fine tuning them. This parameter has effect only when representation is dense as sparse one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "force_embedding_size": { + "default": false, + "description": "Force the embedding size to be equal to the vocabulary size. This parameter has effect only if representation is dense.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is not often the case that the user has a strict need for using an embedding size that should be larger than the vocabulary size.", + "description_implications": "Should only be True if the user has a strict need for using an embedding size that should be larger than the vocabulary size. For example, there may be size requirements across multiple features imposed by downstream modules like the ComparatorCombiner.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "True for advanced usage only.", + "ui_display_name": "Force Embedding Size" + }, + "title": "force_embedding_size", + "type": "boolean" + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "This is the number of stacked fully connected layers that the input to the feature passes through. Their output is projected in the feature's output space.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "By default dense embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the GloVe format. When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if representation is dense.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "The representation of the embedding. Either dense or sparse.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary of the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from published [literature](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "embed" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed weighted encoder first transforms the element frequency vector to sparse integer lists, which are then mapped to either dense or sparse embeddings (one-hot encodings). Lastly, embeddings are aggregated as a weighted sum where each embedding is multiplied by its respective element's frequency. Inputs are of size b while outputs are of size b x h where b is the batch size and h is the dimensionality of the embeddings.", + "other_information": null, + "related_parameters": null, + "short_description": "Transforms feature to vector, maps to sparse or dense embeddings, then aggregates.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a set column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to transform the raw text content of the dataset column to a set of elements. The default value space splits the string on spaces. Common options include: underscore (splits on underscore), comma (splits on comma), json (decodes the string into a set or a list through a JSON parser).", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "binary" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "passthrough", + "enum": [ + "passthrough", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "oneOf": [ + { + "default": 1.0, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "computed_fill_value_float_option", + "type": "number" + }, + { + "default": "Y", + "description": "", + "enum": [ + "-1", + "-1.0", + "0", + "0.0", + "1", + "1.0", + "F", + "FALSE", + "False", + "N", + "NO", + "No", + "T", + "TRUE", + "True", + "Y", + "YES", + "Yes", + "f", + "false", + "n", + "no", + "t", + "true", + "y", + "yes" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "computed_fill_value_string_option", + "type": "string" + }, + { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "computed_fill_value_boolean_option", + "type": "boolean" + }, + { + "description": "Disable this parameter.", + "title": "null_option", + "type": "null" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value" + }, + "fallback_true_label": { + "default": null, + "description": "The label to interpret as 1 (True) when the binary feature doesn't have a conventional boolean value", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Modeling performance should not be affected, but the semantics of some binary metrics may change like for \"false positives\", \"false negatives\", etc. if the true label is pinned to the other value.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fallback True Label" + }, + "title": "fallback_true_label", + "type": [ + "string", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "oneOf": [ + { + "default": null, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "fill_value_float_option", + "type": [ + "number", + "null" + ] + }, + { + "default": "Y", + "description": "", + "enum": [ + "-1", + "-1.0", + "0", + "0.0", + "1", + "1.0", + "F", + "FALSE", + "False", + "N", + "NO", + "No", + "T", + "TRUE", + "True", + "Y", + "YES", + "Yes", + "f", + "false", + "n", + "no", + "t", + "true", + "y", + "yes" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "fill_value_string_option", + "type": "string" + }, + { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "fill_value_boolean_option", + "type": "boolean" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value" + }, + "missing_value_strategy": { + "default": "fill_with_false", + "description": "What strategy to follow when there's a missing value in a binary column", + "enum": [ + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_false", + "fill_with_true" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "category" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_initializer": { + "default": null, + "description": "Initializer for the embedding matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "description_implications": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "example_value": [ + "kaiming" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1711.09160" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "kaiming", + "suggested_values_reasoning": "https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326", + "ui_display_name": "Embedding Initialization" + }, + "title": "embedding_initializer", + "type": [ + "string", + "null" + ] + }, + "embedding_size": { + "default": 50, + "description": "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size is the number of different strings appearing in the training set in the column the feature is named after (plus 1 for ).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "sparse" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_initializer": { + "default": null, + "description": "Initializer for the embedding matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "description_implications": "According to https://arxiv.org/abs/1711.09160, choice of embedding initialization is not important as long as the variance is kept reasonably low.", + "example_value": [ + "kaiming" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1711.09160" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "kaiming", + "suggested_values_reasoning": "https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326", + "ui_display_name": "Embedding Initialization" + }, + "title": "embedding_initializer", + "type": [ + "string", + "null" + ] + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": false, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "onehot" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "dense", + "enum": [ + "passthrough", + "onehot", + "sparse", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder maps to a dense embedding and is returned as outputs of size `b x h`, where `b` is the batch size and `h` is the dimensionality of the embeddings.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps the categorical feature to a dense embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "For fixed encoders, compute encoder embeddings in preprocessing to avoid this step at train time. Can speed up the time taken per step during training, but will invalidate the preprocessed data if the encoder type is changed. Some model types (GBM) require caching encoder embeddings to use embedding features, and those models will override this value to `true` automatically.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "Whether the string has to be lowercased before being handled by the tokenizer.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a category column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. if the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "date" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "wave" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "The number of stacked fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "wave", + "embed" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Embed encoder passes the year through a fully connected layer of one neuron and embeds all other elements for the date, concatenates them and passes the concatenated representation through fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Embeds the date elements passes them through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "wave": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Wave encoder passes the year through a fully connected layer of one neuron and represents all other elements for the date by taking the cosine of their value with a different period (12 for months, 31 for days, etc.), concatenates them and passes the concatenated representation through fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Embeds the date elements by taking the cosine of their value before passing through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "datetime_format": { + "default": null, + "description": "This parameter can either be a datetime format string, or null, in which case the datetime format will be inferred automatically.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Ludwig will try to infer the date format automatically, but a specific format can be provided. The date string spec is the same as the one described in python's datetime.", + "description_implications": "If Ludwig has trouble parsing dates, it could be useful to specify an explicit format that Ludwig should parse date feature values as. This could also serve as a form of normalization, for example, if not all datetimes have the same granularity (some have days, some have times), then the common format (i.e. %d %m %Y) serves as a truncator.", + "example_value": [ + "%d %b %Y" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Have Ludwig figure out the date format automatically.", + "ui_display_name": "Datetime format" + }, + "title": "datetime_format", + "type": [ + "string", + "null" + ] + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a date column", + "enum": [ + "fill_with_const", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "h3" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "weighted_sum" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "The number of stacked fully connected layers.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If an output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "should_softmax": { + "default": false, + "description": "Determines if the weights of the weighted sum should be passed though a softmax layer before being used.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "should_softmax", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The activation function to use", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "For short sequences, it is reasonable to use a vanilla RNN.", + "description_implications": "Setting bidirectional to True may increase the compute and memory requirements of the model, but may also increase model performance on long sequences.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://devopedia.org/bidirectional-rnn#:~:text=RNN%20has%20the%20limitation%20that,forward%20and%20reverse%20time%20order." + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "cell_type, activation, recurrent_activation, use_bias" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": "RNNs can sometimes suffer from catastrophic forgetting (source: https://en.wikipedia.org/wiki/Catastrophic_interference ) on long sequences. Allowing the RNN to read from both the beginning and end of the sequence can improve its representation at each timestep.", + "ui_display_name": "Bidirectional" + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `lstm_block`, `lstm`, `ln`, `lstm_cudnn`, `gru`, `gru_block`, `gru_cudnn`. For reference about the differences between the cells please refer to PyTorch's documentation. We suggest to use the `block` variants on CPU and the `cudnn` variants on GPU because of their increased speed. ", + "enum": [ + "rnn", + "lstm", + "lstm_block", + "ln", + "lstm_cudnn", + "gru", + "gru_block", + "gru_cudnn" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The LSTM cell has proven to be the most performant of the three cells.", + "description_implications": "There are two reasons to consider other cell types: (1) compute costs and (2) catastrophic forgetting (source: https://en.wikipedia.org/wiki/Catastrophic_interference ). RNNs have marginally less compute costs, but are prone to catastrophic forgetting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "bidirectional\nactivation\nrecurrent_activation\nuse_bias" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cell Type" + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "The dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "recurrent_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 10, + "description": "The maximum embedding size adopted.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "hidden_size": { + "default": 10, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "H3 values numbers, so a small RNN dimensionality is likely sufficient.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If you have multiple input features, varying the number of layers in the combiner or output feature decoder will have more impact.", + "related_parameters": [ + "layers" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "embed", + "weighted_sum", + "rnn" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Embed encoder encodes each component of the H3 representation (mode, edge, resolution, base cell and children cells) with embeddings. Children cells with value 0 will be masked out. After the embedding, all embeddings are summed and optionally passed through a stack of fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Encodes each H3 component with embeddings then takes a sum and passes them through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The RNN encoder encodes each component of the H3 representation (mode, edge, resolution, base cell and children cells) with embeddings. Children cells with value 0 will be masked out. After the embedding, all embeddings are passed through an RNN encoder. The intuition behind this is that, starting from the base cell, the sequence of children cells can be seen as a sequence encoding the path in the tree of all H3 hexes.", + "other_information": null, + "related_parameters": null, + "short_description": "Encodes each H3 component with embeddings then passes them through an RNN encoder.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "weighted_sum": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Weighted Sum encoder encodes each component of the H3 representation (mode, edge, resolution, base cell and children cells) with embeddings. Children cells with value 0 will be masked out. After the embedding, all embeddings are summed with a weighted sum (with learned weights) and optionally passed through a stack of fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Encodes each H3 component with embeddings then takes a weighted sum.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": 576495936675512319, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "integer" + }, + "fill_value": { + "default": 576495936675512319, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "integer" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in an h3 column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "image" + } + } + }, + "then": { + "properties": { + "augmentation": { + "oneOf": [ + { + "items": { + "additionalProperties": true, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "random_horizontal_flip" + } + } + }, + "then": { + "properties": {} + } + }, + { + "if": { + "properties": { + "type": { + "const": "auto_augmentation" + } + } + }, + "then": { + "properties": { + "method": { + "default": "trivial_augment", + "description": "Specifies the method for applying automatic data augmentation.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trivial augment is computationally more efficient than the other options.", + "description_implications": "The type of auto-augment method to apply to a batch of images.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": "Trivial-augment: https://arxiv.org/abs/2103.10158. Rand-augment: https://arxiv.org/abs/1909.13719. Auto-augment: https://arxiv.org/pdf/1805.09501.pdf.", + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Automatic Augmentation Method" + }, + "title": "method", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_vertical_flip" + } + } + }, + "then": { + "properties": {} + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_rotate" + } + } + }, + "then": { + "properties": { + "degree": { + "default": 15, + "description": "Range of angle for random rotation, i.e., [-degree, +degree].", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 15 means that the image will be randomly rotated between -15 to +15 degrees.", + "description_implications": "The degree of rotation to apply to the image.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Rotation Degree" + }, + "title": "degree", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_blur" + } + } + }, + "then": { + "properties": { + "kernel_size": { + "default": 3, + "description": "Kernel size for random blur.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 3.", + "description_implications": "The kernel size is the size of the filter matrix. A larger kernel size will result in a blurrier image, while a smaller kernel size will result in less blurring.", + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + 3, + 5, + 7 + ], + "suggested_values_reasoning": "The default value is 3, which is a common value for image processing", + "ui_display_name": "Kernel Size" + }, + "title": "kernel_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_brightness" + } + } + }, + "then": { + "properties": { + "max": { + "default": 2.0, + "description": "Maximum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 3.0.", + "description_implications": "The maximum factor by which the brightness of the image will be randomly changed.", + "example_value": [ + 3.9 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Brightness" + }, + "title": "max", + "type": "number" + }, + "min": { + "default": 0.5, + "description": "Minimum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 0.1.", + "description_implications": "The minimum brightness factor to apply to the image.", + "example_value": [ + 0.5 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Minimum Brightness" + }, + "title": "min", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "random_contrast" + } + } + }, + "then": { + "properties": { + "max": { + "default": 2.0, + "description": "Maximum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 3.0", + "description_implications": "The maximum factor by which the contrast of the image will be randomly changed.", + "example_value": [ + 3.0 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Contrast" + }, + "title": "max", + "type": "number" + }, + "min": { + "default": 0.5, + "description": "Minimum factor for random brightness.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value of 0.1.", + "description_implications": "The minimum contrast factor to apply to the image.", + "example_value": [ + 0.1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Minimum contrast" + }, + "title": "min", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "description": "Type of augmentation to apply.", + "enum": [ + "auto_augmentation", + "random_blur", + "random_brightness", + "random_contrast", + "random_horizontal_flip", + "random_rotate", + "random_vertical_flip" + ], + "title": "type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "augmentation", + "type": "object" + }, + "type": "array" + }, + { + "description": "Apply standard augmentation pipeline.", + "type": "boolean" + } + ] + }, + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "conv_activation": { + "default": "relu", + "description": "If an activation is not already specified in conv_layers this is the default activation that will be used for each layer. It indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Activation" + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "Dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "List of convolutional layers to use in the encoder. ", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "conv_norm": { + "default": null, + "description": "If a norm is not already specified in conv_layers this is the default norm that will be used for each layer. It indicates the normalization applied to the activations and can be null, batch or layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Normalization" + }, + "title": "conv_norm", + "type": [ + "string", + "null" + ] + }, + "conv_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if conv_norm is either batch or layer. ", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Normalization Parameters" + }, + "title": "conv_norm_params", + "type": [ + "object", + "null" + ] + }, + "conv_use_bias": { + "default": true, + "description": "If bias not already specified in conv_layers, specifies if the 2D convolutional kernel should have a bias term.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "conv_use_bias", + "type": "boolean" + }, + "dilation": { + "default": 1, + "description": "An int or pair of ints specifying the dilation rate to use for dilated convolution. If dilation is not already specified in conv_layers, specifies the default dilation of the 2D convolutional kernel that will be used for each layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "dilation_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "dilation_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Dilation" + }, + "title": "dilation" + }, + "fc_activation": { + "default": "relu", + "description": "If an activation is not already specified in fc_layers this is the default activation that will be used for each layer. It indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "activation, activation_function, conv_activation, recurrent_activation" + ], + "short_description": "", + "suggested_values": "relu, alternatively leakyRelu or elu", + "suggested_values_reasoning": "The default value will work well in the majority of the cases", + "ui_display_name": "FC Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "fc_bias_initializer", + "type": "string" + }, + "fc_dropout": { + "default": 0.0, + "description": "Dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "FC Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "A list of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and weights_initializer. If any of those values is missing from the dictionary, the default one specified as a parameter of the encoder will be used instead. ", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "If a norm is not already specified in fc_layers this is the default norm that will be used for each layer. It indicates the norm of the output and can be null, batch or layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate. See Torch's documentation on batch normalization or for layer see Torch's documentation on layer normalization.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fc_norm_params" + ], + "short_description": "", + "suggested_values": "batch", + "suggested_values_reasoning": null, + "ui_display_name": "Fully Connected Normalization" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either batch or layer. For information on parameters used with batch see Torch's documentation on batch normalization or for layer see Torch's documentation on layer normalization.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fc_norm" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": null, + "ui_display_name": "Fully Connected Normalization Parameters" + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "FC Use Bias" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weights matrix.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "FC Weights Initializer" + }, + "title": "fc_weights_initializer", + "type": "string" + }, + "groups": { + "default": 1, + "description": "Groups controls the connectivity between convolution inputs and outputs. When groups = 1, each output channel depends on every input channel. When groups > 1, input and output channels are divided into groups separate groups, where each output channel depends only on the inputs in its respective input channel group. in_channels and out_channels must both be divisible by groups.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Groups" + }, + "title": "groups", + "type": "integer" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed internally, automatically, based on image data preprocessing.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "NOT DISPLAYED" + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "kernel_size": { + "default": 3, + "description": "An integer or pair of integers specifying the kernel size. A single integer specifies a square kernel, while a pair of integers specifies the height and width of the kernel in that order (h, w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for each layer.", + "oneOf": [ + { + "default": 3, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "kernel_size_integer_option", + "type": "integer" + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Kernel Size" + }, + "title": "kernel_size" + }, + "num_channels": { + "default": null, + "description": "Number of channels to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed internally, automatically, based on image data preprocessing.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "NOT DISPLAYED" + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "Number of convolutional layers to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "The number of stacked fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "out_channels": { + "default": 32, + "description": "Indicates the number of filters, and by consequence the output channels of the 2d convolution. If out_channels is not already specified in conv_layers this is the default out_channels that will be used for each layer. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Output Channels" + }, + "title": "out_channels", + "type": "integer" + }, + "output_size": { + "default": 128, + "description": "If output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "valid", + "description": "An int, pair of ints (h, w), or one of ['valid', 'same'] specifying the padding used forconvolution kernels.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 0, + "parameter_metadata": null, + "title": "padding_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "padding_list_option", + "type": "array" + }, + { + "default": "valid", + "description": "", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "padding_string_option", + "type": "string" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "When padding is set to 'valid' like in the default case, no padding is added. As a default value putting in the raw image is the goal here.", + "description_implications": "By increasing the amount of padding, you can increase the accuracy of the image analysis for certain circumstances.", + "example_value": [ + "'same'" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://www.geeksforgeeks.org/cnn-introduction-to-padding/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "padding_mode,\nresize method" + ], + "short_description": "", + "suggested_values": "Same' padding if images are of different dimensions. \nSpecific [h, w] entries can be valuable on a per dataset basis.", + "suggested_values_reasoning": "If your images already have padding, there is no need to add padding, so the default is fine. If your images come in different dimensions, then 'same' padding can help pad the images to standardized dimensions. For certain images, adding padding to the edges can help the CNN process the images better which can improve model performance. This depends on the images however.", + "ui_display_name": "Padding" + }, + "title": "padding" + }, + "padding_mode": { + "default": "zeros", + "description": "If padding_mode is not already specified in conv_layers, specifies the default padding_mode of the 2D convolutional kernel that will be used for each layer.", + "enum": [ + "zeros", + "reflect", + "replicate", + "circular" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Padding Mode" + }, + "title": "padding_mode", + "type": "string" + }, + "pool_dilation": { + "default": 1, + "description": "An integer or pair of ints specifying pooling dilation rate (h, w).", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "pool_dilation_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_dilation_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Dilation" + }, + "title": "pool_dilation" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use.", + "enum": [ + "max", + "average", + "avg", + "mean" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": "string" + }, + "pool_kernel_size": { + "default": 2, + "description": "An integer or pair of integers specifying the pooling size. If pool_kernel_size is not specified in conv_layers this is the default value that will be used for each layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "pool_kernel_size_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Kernel Size" + }, + "title": "pool_kernel_size" + }, + "pool_padding": { + "default": 0, + "description": "An integer or pair of ints specifying pooling padding (h, w).", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 0, + "parameter_metadata": null, + "title": "pool_padding_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_padding_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Padding" + }, + "title": "pool_padding" + }, + "pool_stride": { + "default": null, + "description": "An integer or pair of integers specifying the pooling stride, which is the factor by which the pooling layer downsamples the feature map. Defaults to pool_kernel_size.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "pool_stride_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "pool_stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pool Stride" + }, + "title": "pool_stride" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stride": { + "default": 1, + "description": "An integer or pair of integers specifying the stride of the convolution along the height and width. If a stride is not already specified in conv_layers, specifies the default stride of the 2D convolutional kernel that will be used for each layer.", + "oneOf": [ + { + "default": 1, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "stride_integer_option", + "type": "integer" + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Stride" + }, + "title": "stride" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed internally, automatically, based on image data preprocessing.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "NOT DISPLAYED" + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "_resnet_legacy" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "if an activation is not already specified in fc_layers this is the default activation that will be used for each layer. It indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "batch_norm_epsilon": { + "default": 0.001, + "description": "Epsilon of the batch norm.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "batch_norm_epsilon", + "type": "number" + }, + "batch_norm_momentum": { + "default": 0.9, + "description": "Momentum of the batch norm running statistics.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "batch_norm_momentum", + "type": "number" + }, + "bias_initializer": { + "default": "zeros", + "description": "initializer for the bias vector.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "conv_stride": { + "default": 1, + "description": "An integer or pair of integers specifying the stride of the initial convolutional layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "conv_stride_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "conv_stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_stride" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "A list of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and weights_initializer. If any of those values is missing from the dictionary, the default one specified as a parameter of the encoder will be used instead. ", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "first_pool_kernel_size": { + "default": null, + "description": "Pool size to be used for the first pooling layer. If none, the first pooling layer is skipped.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "first_pool_kernel_size_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "first_pool_kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "first_pool_kernel_size" + }, + "first_pool_stride": { + "default": null, + "description": "Stride for first pooling layer. If null, defaults to first_pool_kernel_size.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "first_pool_stride_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "first_pool_stride_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "first_pool_stride" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "kernel_size": { + "default": 3, + "description": "An integer or pair of integers specifying the kernel size. A single integer specifies a square kernel, while a pair of integers specifies the height and width of the kernel in that order (h, w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for each layer.", + "oneOf": [ + { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "kernel_size_integer_option", + "type": [ + "integer", + "null" + ] + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "kernel_size_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "kernel_size" + }, + "norm": { + "default": null, + "description": "if a norm is not already specified in fc_layers this is the default norm that will be used for each layer. It indicates the norm of the output and can be null, batch or layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "parameters used if norm is either batch or layer. For information on parameters used with batch see Torch's documentation on batch normalization or for layer see Torch's documentation on layer normalization.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_channels": { + "default": null, + "description": "Number of channels to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 1, + "description": "The number of stacked fully connected layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "out_channels": { + "default": 32, + "description": "Indicates the number of filters, and by consequence the output channels of the 2d convolution. If out_channels is not already specified in conv_layers this is the default out_channels that will be used for each layer. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "out_channels", + "type": "integer" + }, + "output_size": { + "default": 128, + "description": "if output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "resnet_size": { + "default": 50, + "description": "The size of the ResNet model to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "resnet_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weights matrix.", + "enum": [ + "constant", + "dirac", + "eye", + "identity", + "kaiming_normal", + "kaiming_uniform", + "normal", + "ones", + "orthogonal", + "sparse", + "uniform", + "xavier_normal", + "xavier_uniform", + "zeros" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mlp_mixer" + } + } + }, + "then": { + "properties": { + "avg_pool": { + "default": true, + "description": "If true, pools output over patch dimension, outputs a vector of shape (embed_size). If false, the output tensor is of shape (n_patches, embed_size), where n_patches is img_height x img_width / patch_size\u00b2.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "avg_pool", + "type": "boolean" + }, + "channel_dim": { + "default": 256, + "description": "Number of channels in hidden layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "channel_dim", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embed_size": { + "default": 512, + "description": "The patch embedding size, the output size of the mixer if avg_pool is true.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embed_size", + "type": "integer" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "num_channels": { + "default": null, + "description": "Number of channels to use in the encoder. ", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_layers": { + "default": 8, + "description": "The depth of the network (the number of Mixer blocks).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the size and complexity of the input images. The default value is used in the paper and tested on several image datasets.", + "description_implications": "Increasing the number of layers may improve model performance for larger images or more complex image tasks.", + "example_value": [ + 8 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "MLP-Mixer: An all-MLP Architecture for Vision\nhttps://arxiv.org/abs/2105.01601" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "4 - 32", + "suggested_values_reasoning": "Values from 8 - 32 are tested in the paper. It is possible that fewer layers will be sufficient for some tasks.", + "ui_display_name": "Number of Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "patch_size": { + "default": 16, + "description": "The image patch size. Each patch is patch_size\u00b2 pixels. Must evenly divide the image width and height.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from MLP-Mixer paper.", + "description_implications": "The implications of the image patch size for this layer depend on other factors, such as the true resolution of the incoming image dataset. If the patch size is kept consistent but a higher resolution image is used as input, then the resulting chunked sequence of tokens will be longer than it would have been if the input resolution was lower. \n\nThe original MLP-Mixer paper also notes that there is a tradeoff with respect to the projection units learned by a model. In their findings, a 32x32 patch size model learned very structured low frequency projection units, while the equivalent 16x16 model learned high frequencies and showed no clear structure.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[MLP Mixer paper](https://arxiv.org/pdf/2105.01601.pdf)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + 16, + 32 + ], + "suggested_values_reasoning": "16 and 32 are the values used in the original MLP Mixer paper", + "ui_display_name": "Patch Size" + }, + "title": "patch_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "token_size": { + "default": 2048, + "description": "The per-patch embedding size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "token_size", + "type": "integer" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "_vit_legacy" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout rate for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/2010.11929).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob,\nattention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Attention Dropout" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "gradient_checkpointing": { + "default": false, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gradient_checkpointing", + "type": "boolean" + }, + "height": { + "default": null, + "description": "Height of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "hidden_act": { + "default": "gelu", + "description": "Hidden layer activation, one of gelu, relu, selu or gelu_new.", + "enum": [ + "relu", + "gelu", + "selu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[Huggingface docs for ViT config](https://huggingface.co/docs/transformers/model_doc/vit#transformers.ViTConfig.hidden_act)\n\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout rate for all fully connected layers in the embeddings, encoder, and pooling.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/2010.11929).", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob,\nattention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Hidden Dropout" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooling layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 3072, + "description": "Dimensionality of the intermediate (i.e., feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads in each attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "patch_size": { + "default": 16, + "description": "The image patch size. Each patch is patch_size\u00b2 pixels. Must evenly divide the image width and height.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from ViT paper.", + "description_implications": "The implications of the image patch size for this layer depend on other factors, such as the true resolution of the incoming image dataset. If the patch size is kept consistent but a higher resolution image is used as input, then the resulting chunked sequence of tokens will be longer than it would have been if the input resolution was lower. \n\nThe ViT paper notes that decreasing the patch size in this way led to robust improvements without introducing other parameters.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[Huggingface docs](https://huggingface.co/docs/transformers/model_doc/vit)\n\n[ViT paper](https://arxiv.org/abs/2010.11929)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + 16, + 32 + ], + "suggested_values_reasoning": "16 and 32 are the values used in the original ViT paper.", + "ui_display_name": "Patch Size" + }, + "title": "patch_size", + "type": "integer" + }, + "pretrained_model": { + "default": "google/vit-base-patch16-224", + "description": "The name of the pre-trained model to use.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default model is the canonical model for this model architecture, and is therefore a good starting point for most use cases.", + "description_implications": "There are two factors to consider when choosing a pre-trained model: (1) size, and (2) task similarity. \n\nThe larger the model, the more subtle its comprehension of inputs can become. However, larger models are also more compute and memory-intensive to train.\n\nModels pretrained on highly-related source tasks are more likely to be successful on the target task. Consider searching the HuggingFace model repository for models trained on similar tasks.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/2010.11929" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, trainable, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": "google/vit-large-patch16-224", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, increasing model capacity can potentially provide a richer representation than the default. The suggested value upsizes the model while maintaining the same model architecture.\n\nModel trained on internet-scale datasets typically generalize well. Consider deviating from the default only if the images in the dataset originate from another domain (e.g. medical images, geospatial data).", + "ui_display_name": "Pretrained model name" + }, + "title": "pretrained_model", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Use pre-trained model weights from Hugging Face.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "width": { + "default": null, + "description": "Width of the input image.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "alexnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "convnext" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "tiny", + "small", + "base", + "large" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "densenet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": 121, + "description": "Pretrained model variant to use.", + "enum": [ + 121, + 161, + 169, + 201 + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "efficientnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "b0", + "description": "Pretrained model variant to use.", + "enum": [ + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "v2_s", + "v2_m", + "v2_l" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "googlenet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "inceptionv3" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "maxvit" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "t", + "description": "Pretrained model variant to use.", + "enum": [ + "t" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mnasnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "0_5", + "description": "Pretrained model variant to use.", + "enum": [ + "0_5", + "0_75", + "1_0", + "1_3" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mobilenetv2" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "base", + "description": "Pretrained model variant to use.", + "enum": [ + "base" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mobilenetv3" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "small", + "description": "Pretrained model variant to use.", + "enum": [ + "small", + "large" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "regnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "x_1_6gf", + "description": "Pretrained model variant to use.", + "enum": [ + "x_1_6gf", + "x_16gf", + "x_32gf", + "x_3_2gf", + "x_400mf", + "x_800mf", + "x_8gf", + "y_128gf", + "y_16gf", + "y_1_6gf", + "y_32gf", + "y_3_2gf", + "y_400mf", + "y_800mf", + "y_8gf" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "resnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": 50, + "description": "Pretrained model variant to use.", + "enum": [ + 18, + 34, + 50, + 101, + 152 + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "resnext" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "50_32x4d", + "description": "Pretrained model variant to use.", + "enum": [ + "50_32x4d", + "101_32x8d", + "101_64x4d" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "shufflenet_v2" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "x0_5", + "description": "Pretrained model variant to use.", + "enum": [ + "x0_5", + "x1_0", + "x1_5", + "x2_0" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "squeezenet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "1_0", + "description": "Pretrained model variant to use.", + "enum": [ + "1_0", + "1_1" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "swin_transformer" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "t", + "description": "Pretrained model variant to use.", + "enum": [ + "t", + "s", + "b" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "vit" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "b_16", + "description": "Pretrained model variant to use.", + "enum": [ + "b_16", + "b_32", + "l_16", + "l_32", + "h_14" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "vgg" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": 11, + "description": "Pretrained model variant to use.", + "oneOf": [ + { + "default": 11, + "description": "", + "enum": [ + 11, + 13, + 16, + 19 + ], + "enumNames": [], + "parameter_metadata": null, + "title": "model_variant_integer_option", + "type": "integer" + }, + { + "default": "11_bn", + "description": "", + "enum": [ + "11_bn", + "13_bn", + "16_bn", + "19_bn" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "model_variant_string_option", + "type": "string" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "wide_resnet" + } + } + }, + "then": { + "properties": { + "model_cache_dir": { + "default": null, + "description": "Directory path to cache pretrained model weights.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Cache Directory" + }, + "title": "model_cache_dir", + "type": [ + "string", + "null" + ] + }, + "model_variant": { + "default": "50_2", + "description": "Pretrained model variant to use.", + "enum": [ + "50_2", + "101_2" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Model Variant" + }, + "title": "model_variant", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Whether to save the weights in the checkpoint.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": "Saved Weights in Checkpoint" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": true, + "description": "Is the encoder trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, model components are trainable.", + "description_implications": "The tradeoff when using `trainable` is between speed and flexibility. If False, less weights are subject to change and the model will therefore train faster. However, the representations output by this component are fixed for each input.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.ibm.com/cloud/learn/overfitting\n\nhttp://d2l.ai/chapter_computer-vision/fine-tuning.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, pretrained_model, saved_weights_in_checkpoint" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Freezing the weights (i.e. `trainable = False`) is only worth trying if you are loading in pretrained weights. In that case, check to see if your model is overfitting. If so, freezing the weights (and therefore reducing model complexity) may be beneficial.", + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Download model weights from pre-trained model.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + } + } + } + } + ], + "properties": { + "type": { + "default": "stacked_cnn", + "enum": [ + "densenet", + "mnasnet", + "regnet", + "swin_transformer", + "convnext", + "vit", + "squeezenet", + "efficientnet", + "resnext", + "maxvit", + "wide_resnet", + "_resnet_legacy", + "mobilenetv3", + "shufflenet_v2", + "resnet", + "inceptionv3", + "mlp_mixer", + "googlenet", + "mobilenetv2", + "alexnet", + "_vit_legacy", + "vgg", + "stacked_cnn" + ], + "enumDescriptions": { + "_resnet_legacy": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "ResNet - short for residual network is part of a family of extremely deep architectures showing compelling accuracy and nice convergence behaviors for computer vision applications. It is a type of CNN architecture designed to support hundreds or thousands of convolutional layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Residual network achieving very high performance on computer vision tasks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "_vit_legacy": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "ViT, short for Vision Transformer, divides the image into equal-sized patches, uses a linear transformation to encode each flattened patch, then applies a deep transformer architecture to the sequence of encoded patches.", + "other_information": null, + "related_parameters": null, + "short_description": "ViT encoder divides images into patches, performs a linear transformation, and then applies a transformer.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "mlp_mixer": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "MLP-Mixer divides the image into equal-sized patches, applying fully connected layers to each patch to compute per-patch representations (tokens) and combining the representations with fully-connected mixer layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Image encoder which applies fully connected layers to different patches of the image.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "Stack of 2D convolutional layers with optional normalization, dropout, and down-sampling pooling layers, followed by an optional stack of fully connected layers.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of 2D convolutional layers followed by an optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": [ + "number", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": [ + "number", + "null" + ] + }, + "height": { + "default": null, + "description": "The image height in pixels. If this parameter is set, images will be resized to the specified height using the resize_method parameter. If None, images will be resized to the size of the first image in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "height", + "type": [ + "integer", + "null" + ] + }, + "in_memory": { + "default": true, + "description": "Defines whether image dataset will reside in memory during the training process or will be dynamically fetched from disk (useful for large datasets). In the latter case a training batch of input images will be fetched from disk each training iteration.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "in_memory", + "type": "boolean" + }, + "infer_image_dimensions": { + "default": true, + "description": "If true, then the height and width of images in the dataset will be inferred from a sample of the first image in the dataset. Each image that doesn't conform to these dimensions will be resized according to resize_method. If set to false, then the height and width of images in the dataset will be specified by the user.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_dimensions", + "type": "boolean" + }, + "infer_image_max_height": { + "default": 256, + "description": "If infer_image_dimensions is set, this is used as the maximum height of the images in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_max_height", + "type": "integer" + }, + "infer_image_max_width": { + "default": 256, + "description": "If infer_image_dimensions is set, this is used as the maximum width of the images in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_max_width", + "type": "integer" + }, + "infer_image_num_channels": { + "default": true, + "description": "If true, then the number of channels in the dataset is inferred from a sample of the first image in the dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_num_channels", + "type": "boolean" + }, + "infer_image_sample_size": { + "default": 100, + "description": "The sample size used for inferring dimensions of images in infer_image_dimensions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "infer_image_sample_size", + "type": "integer" + }, + "missing_value_strategy": { + "default": "bfill", + "description": "What strategy to follow when there's a missing value in an image column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "num_channels": { + "default": null, + "description": "Number of channels in the images. If specified, images will be read in the mode specified by the number of channels. If not specified, the number of channels will be inferred from the image format of the first valid image in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_channels", + "type": [ + "integer", + "null" + ] + }, + "num_processes": { + "default": 1, + "description": "Specifies the number of processes to run for preprocessing images.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_processes", + "type": "integer" + }, + "requires_equal_dimensions": { + "default": false, + "description": "If true, then width and height must be equal.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "requires_equal_dimensions", + "type": "boolean" + }, + "resize_method": { + "default": "interpolate", + "description": "The method to use for resizing images.", + "enum": [ + "crop_or_pad", + "interpolate" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Interpolation may stretch or squish the image, but it does not remove content or change the statistical distribution of image values so it is more appropriate for most tasks.", + "description_implications": "interpolation will not change the content of the image, but it will change the aspect ratio.\n\ncrop_or_pad will preserve the aspect ratio of the image, but may remove some content (in the case of cropping).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "height, width" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Resize Method" + }, + "title": "resize_method", + "type": "string" + }, + "standardize_image": { + "default": null, + "description": "Standardize image by per channel mean centering and standard deviation scaling .", + "enum": [ + "imagenet1k", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "standardize_image", + "type": [ + "string", + "null" + ] + }, + "width": { + "default": null, + "description": "The image width in pixels. If this parameter is set, images will be resized to the specified width using the resize_method parameter. If None, images will be resized to the size of the first image in the dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "width", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "number" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "passthrough", + "enum": [ + "passthrough", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": 0.0, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "number" + }, + "computed_outlier_fill_value": { + "default": 0.0, + "description": "The internally computed fill value to replace outliers with in case the outlier_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_outlier_fill_value", + "type": "number" + }, + "fill_value": { + "default": 0.0, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "number" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a number column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_mean" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "normalization": { + "default": "zscore", + "description": "Normalization strategy to use for this number feature. If the value is `null` no normalization is performed.", + "enum": [ + "zscore", + "minmax", + "log1p", + "iq", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Z-score normalization helps improve the training stability and convergence of neural networks by rescaling the numeric input features to have a mean of 0 and a standard deviation of 1, reducing the variability and distribution of the data. This improves neural network training.", + "description_implications": "The goal of normalization is to transform features to be on a similar scale. Normalization can be a form of feature smoothing that improves the performance and training stability of the model. Normalizations may result in different effects on the semantics of your number features. The best normalization technique is one that empirically works well, so try new ideas if you think they'll work well on your feature distribution.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/data-prep/transform/normalization" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "z-score", + "suggested_values_reasoning": "Z-score is a variation of scaling that represents the number of standard deviations away from the mean. You would use z-score to ensure your feature distributions have mean = 0 and std = 1. It\u2019s useful when there are a few outliers, but not so extreme that you need clipping.", + "ui_display_name": "Normalization" + }, + "title": "normalization", + "type": [ + "string", + "null" + ] + }, + "outlier_strategy": { + "default": null, + "description": "Determines how outliers will be handled in the dataset. In most cases, replacing outliers with the column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers is to leave them in the data, which is the behavior when this parameter is left as `null`.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_mean", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Outlier definitions and how to handle them are very task-specific, so we leave this feature disabled by default and ask the user to choose the strategy that works best for them.", + "description_implications": "Determines how outliers will be handled in the dataset. In most cases replacing outliers with the column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers is to leave them in the data, which is the behavior when this parameter is left as `null`.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "outlier_threshold" + ], + "short_description": "", + "suggested_values": "fill_with_mean", + "suggested_values_reasoning": null, + "ui_display_name": "Outlier Strategy" + }, + "title": "outlier_strategy", + "type": [ + "string", + "null" + ] + }, + "outlier_threshold": { + "default": 3.0, + "description": "Standard deviations from the mean past which a value is considered an outlier. The 3-sigma rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations of the mean (see: [68\u201395\u201399.7 rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule)). As such anything farther away than that is highly likely to be an outlier, and may distort the learning process by disproportionately affecting the model.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The definition of an outlier is often dataset and task dependent, but 2 or 3 standard deviations from the mean is a common heuristic.", + "description_implications": "Determines the threshold past which a number will be considered an outlier in the dataset. The 3-sigma rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations of the mean (see: 68\u201395\u201399.7 rule). As such anything farther away than that is highly likely to be an outlier, and may distort the learning process by disproportionately affecting the model.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "outlier_strategy" + ], + "short_description": "", + "suggested_values": "2 - 3", + "suggested_values_reasoning": null, + "ui_display_name": "Outlier Threshold" + }, + "title": "outlier_threshold", + "type": "number" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "sequence" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "transformer", + "cnnrnn", + "embed", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "rnn" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h` tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps each element of the sequence to an embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "Compute encoder embeddings in preprocessing, speeding up training time considerably.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "max_sequence_length": { + "default": 256, + "description": "The maximum length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 256. Every sequence will be truncated to this length.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size, embedding_size" + ], + "short_description": "", + "suggested_values": "Use the lowest value that covers most of your input data. Only increase the value if crucial parts of the input data are truncated.", + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Sequence Length" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a text column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 20000, + "description": "The maximum number of most common tokens in the vocabulary. If the data contains more than this amount, the most infrequent symbols will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": "integer" + }, + "ngram_size": { + "default": 2, + "description": "The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Size of the n-gram when using the `ngram` tokenizer.", + "description_implications": null, + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "n-gram size" + }, + "title": "ngram_size", + "type": "integer" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "padding_symbol": { + "default": "", + "description": "The string used as a padding symbol. This special token is mapped to the integer ID 0 in the vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_symbol", + "type": "string" + }, + "sequence_length": { + "default": null, + "description": "The desired length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated and sequences shorter than this value will be padded. If None, sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is `None`. Which means that the sequence length will be inferred from the dataset, which may save you compute resources on datasets with short sequence samples.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "If tying the weights of multiple sequence encoders together, this parameter may need to be set to ensure that all sequence features have the same sequence length.", + "suggested_values_reasoning": null, + "ui_display_name": "Sequence Length" + }, + "title": "sequence_length", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + }, + "unknown_symbol": { + "default": "", + "description": "The string used as an unknown placeholder. This special token is mapped to the integer ID 1 in the vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unknown_symbol", + "type": "string" + }, + "vocab_file": { + "default": null, + "description": "Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the first string until \t or \n is considered a word.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The vocabulary can be parsed automatically from the incoming input features.", + "description_implications": "It can be useful to specify your own vocabulary list if the vocabulary is very large, there's no out of the box tokenizer that fits your data, or if there are several uncommon or infrequently occurring tokens that we want to guarantee to be a part of the vocabulary, rather than treated as an unknown.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Vocab File" + }, + "title": "vocab_file", + "type": [ + "string", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "set" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer to use for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout probability for the embedding.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 50, + "description": "The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size is the number of different strings appearing in the training set in the input column (plus 1 for the unknown token placeholder ).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If true embeddings are trained during the training process, if false embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when representation is dense as sparse one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "This is the number of stacked fully connected layers that the input to the feature passes through. Their output is projected in the feature's output space.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": 10, + "description": "If output_size is not already specified in fc_layers this is the default output_size that will be used for each layer. It indicates the size of the output of a fully connected layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "By default dense embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the GloVe format. When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if representation is dense.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "The representation of the embedding. Either dense or sparse.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary of the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer to use for the weights matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.\n\nXavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "embed", + "enum": [ + "embed" + ], + "enumDescriptions": { + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Embed encoder takes the raw binary values coming from the input placeholders and transforms them to sparse integer lists, then they are mapped to either dense or sparse embeddings (one-hot encodings), finally they are reduced on the sequence dimension and returned as an aggregated embedding vector. Inputs are of size b while outputs are of size b x h where b is the batch size and h is the dimensionality of the embeddings.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps raw values to sparse integer lists, then maps to dense/sparse embeddings, then reduces to final vector.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a set column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to transform the raw text content of the dataset column to a set of elements. The default value space splits the string on spaces. Common options include: underscore (splits on underscore), comma (splits on comma), json (decodes the string into a set or a list through a JSON parser).", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "text" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "embed" + } + } + }, + "then": { + "properties": { + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "albert" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.0, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, classifier_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "bos_token_id": { + "default": 2, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "classifier_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for attached classifiers.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, attention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout_prob" + }, + "title": "classifier_dropout_prob", + "type": "number" + }, + "embedding_size": { + "default": 128, + "description": "Dimensionality of vocabulary embeddings.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "eos_token_id": { + "default": 3, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sentence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "hidden_act": { + "default": "gelu_new", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Hugging face docs for ALBERT config](https://huggingface.co/docs/transformers/model_doc/albert#transformers.AlbertConfig.hidden_act)\n\r\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.0, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob,\nclassifier_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "inner_group_num": { + "default": 1, + "description": "The number of inner repetition of attention and ffn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "inner_group_num", + "type": "integer" + }, + "intermediate_size": { + "default": 3072, + "description": "The dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_groups": { + "default": 1, + "description": "Number of groups for the hidden layers, parameters in the same group are shared.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_groups", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 0, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "These arguments typically don't need to be specified.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "pretrained_model_name_or_path" + ], + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "albert-base-v2", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default model is the canonical model for this model architecture, and is therefore a good starting point for most use cases.", + "description_implications": "There are two factors to consider when choosing a pre-trained model: (1) size, and (2) task similarity. \n\nThe larger the model, the more subtle its comprehension of inputs can become. However, larger models are also more compute and memory-intensive to train.\n\nModels pretrained on highly-related source tasks are more likely to be successful on the target task. Consider searching the HuggingFace model repository for models trained on similar tasks.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1909.11942" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, trainable, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": "albert-large-v2, albert-base-chinese", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, increasing model capacity can potentially provide a richer representation than the default. The suggested value upsizes the model while maintaining the same model architecture.\n\nLanguage models trained on general corpora typically generalize well. Consider deviating from the default only if the text in the dataset originates from another domain (e.g. languages other than English).", + "ui_display_name": "Pretrained model" + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 2, + "description": "The vocabulary size of the token_type_ids passed when calling AlbertModel or TFAlbertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30000, + "description": "Vocabulary size of the ALBERT model. Defines the number of different tokens that can be represented by the inputs_ids passed.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "xlmroberta" + } + } + }, + "then": { + "properties": { + "add_pooling_layer": { + "default": true, + "description": "Whether to add a pooling layer to the encoder.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "add_pooling_layer", + "type": "boolean" + }, + "bos_token_id": { + "default": 0, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "eos_token_id": { + "default": 2, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sentence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "max_position_embeddings": { + "default": 514, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pad_token_id": { + "default": 1, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "xlm-roberta-base", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 1, + "description": "The vocabulary size of the token_type_ids passed in.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "Vocabulary size of the XLMRoBERTa model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "bert" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, classifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "classifier_dropout": { + "default": null, + "description": "The dropout ratio for the classification head.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, attention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout" + }, + "title": "classifier_dropout", + "type": [ + "number", + "null" + ] + }, + "gradient_checkpointing": { + "default": false, + "description": "Whether to use gradient checkpointing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gradient_checkpointing", + "type": "boolean" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Huggingface docs for BERT config](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertConfig.hidden_act)\n\r\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob, classifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 3072, + "description": "Dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 0, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "Type of position embedding.", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "bert-base-uncased", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 2, + "description": "The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the BERT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling BertModel or TFBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "deberta" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "tanh", + "gelu_fast", + "mish", + "linear", + "sigmoid", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 1536, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": null, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 6144, + "description": "Dimensionality of the 'intermediate' (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": null, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": null, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_relative_positions": { + "default": -1, + "description": "The range of relative positions `[-max_position_embeddings, max_position_embeddings]`. Use the same value as `max_position_embeddings`.", + "parameter_metadata": null, + "title": "max_relative_positions", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm_rel_ebd": { + "default": "layer_norm", + "description": "The normalization method for relative embeddings.", + "enum": [ + "layer_norm", + "none" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "norm_rel_ebd", + "type": "string" + }, + "num_attention_heads": { + "default": 24, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": null, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 24, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": null, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 0, + "description": "The value used to pad input_ids.", + "parameter_metadata": null, + "title": "pad_token_id", + "type": "integer" + }, + "pooler_dropout": { + "default": 0, + "description": "The dropout ratio for the pooler layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "pooler_dropout", + "type": "number" + }, + "pooler_hidden_act": { + "default": "gelu", + "description": "The activation function (function or string) in the pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "tanh", + "gelu_fast", + "mish", + "linear", + "sigmoid", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "pooler_hidden_act", + "type": "string" + }, + "pooler_hidden_size": { + "default": 1536, + "description": "The hidden size of the pooler layers.", + "minimum": 1, + "parameter_metadata": null, + "title": "pooler_hidden_size", + "type": "integer" + }, + "pos_att_type": { + "default": [ + "p2c", + "c2p" + ], + "description": "The type of relative position attention, it can be a combination of `['p2c', 'c2p']`, e.g. `['p2c']`, `['p2c', 'c2p']`, `['p2c', 'c2p']`.", + "items": { + "title": "pos_att_type", + "type": "string" + }, + "parameter_metadata": null, + "title": "pos_att_type", + "type": [ + "array", + "null" + ] + }, + "position_biased_input": { + "default": false, + "description": "Whether add absolute position embedding to content embedding.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "position_biased_input", + "type": "boolean" + }, + "position_buckets": { + "default": 256, + "description": "The number of buckets to use for each attention layer.", + "minimum": 1, + "parameter_metadata": null, + "title": "position_buckets", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": null, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "sileod/deberta-v3-base-tasksource-nli", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default model was selected based on the benchmarking work done by IBM's [model recycling](https://ibm.github.io/model-recycling/microsoft_deberta-v3-base_table.html) project. In that study, the selected model ranked first among all variants of the `microsoft/deberta-v3-base` architecture on an evaluation across 36 different datasets.", + "description_implications": "Considerations when selecting a pretrained model version include number of parameters (how long the model will take to fine-tuning / perform inference), general model performance on various benchmarks, and specific model performance on the task you wish to fine-tune it on.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "use_pretrained, trainable, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained model" + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "enum": [ + "cls_pooled", + "last", + "sum", + "mean", + "max", + "concat", + "attention", + null + ], + "enumNames": [], + "parameter_metadata": null, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "relative_attention": { + "default": true, + "description": "Whether use relative position encoding.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "relative_attention", + "type": "boolean" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "share_att_key": { + "default": true, + "description": "Whether to share attention key across layers.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "share_att_key", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable is disabled by default to make the model useful for generating fast baselines, which can be further sped up by setting `preprocessing.cache_encoder_embeddings`. In many cases strong performance can be achieved without adjusting the weights of the pretrained model, but for best performance we recommend setting this to true.", + "description_implications": "Ludwig currently supports two variations on fine-tuning, configured via the trainable encoder parameter: (1) modifying the weights of the pretrained encoder to adapt them to the downstream task (trainable=true), or (2) keeping the pretrained encoder weights fixed and training a stack of dense layers that sit downstream as the combiner and decoder modules (trainable=false, default). This is sometimes distinguished as transfer learning. Allowing the weights to be modified by setting trainable=true can significantly improve performance on the downstream task, but will take significantly longer to train (due to the additional backward passes over the pretrained model parameters). Additionally, more care needs to be taken when selecting hyperparameters when trainable=true to prevent [catastrophic forgettng](https://en.wikipedia.org/wiki/Catastrophic_interference), whereby the model forgets all of the valuable information it learned during pretraining.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Trainable" + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 0, + "description": "The vocabulary size of the `token_type_ids`.", + "minimum": 0, + "parameter_metadata": null, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab_size": { + "default": null, + "description": "", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "gpt" + } + } + }, + "then": { + "properties": { + "afn": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "afn", + "type": "string" + }, + "attn_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the attention.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_pdrop", + "type": "number" + }, + "embd_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the embeddings.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embd_pdrop", + "type": "number" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "layer_norm_epsilon": { + "default": 1e-05, + "description": "The epsilon to use in the layer normalization layers", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_epsilon", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_ctx": { + "default": 512, + "description": "Dimensionality of the causal mask (usually same as n_positions)", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_ctx", + "type": "integer" + }, + "n_embd": { + "default": 768, + "description": "Dimensionality of the embeddings and hidden states.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_embd", + "type": "integer" + }, + "n_head": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_layer": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "n_positions": { + "default": 40478, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_positions", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "openai-gpt", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "resid_pdrop": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "resid_pdrop", + "type": "number" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the GPT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling OpenAIGPTModel or TFOpenAIGPTModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "gpt2" + } + } + }, + "then": { + "properties": { + "activation_function": { + "default": "gelu_new", + "description": "Activation function, to be selected in the list ['relu', 'silu', 'gelu', 'tanh', 'gelu_new'].", + "enum": [ + "relu", + "silu", + "gelu", + "tanh", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "activation_function", + "type": "string" + }, + "attn_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the attention.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_pdrop", + "type": "number" + }, + "embd_pdrop": { + "default": 0.1, + "description": "The dropout ratio for the embeddings.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embd_pdrop", + "type": "number" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "layer_norm_epsilon": { + "default": 1e-05, + "description": "The epsilon to use in the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_epsilon", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_ctx": { + "default": 1024, + "description": "Dimensionality of the causal mask (usually same as n_positions)", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_ctx", + "type": "integer" + }, + "n_embd": { + "default": 768, + "description": "Dimensionality of the embeddings and hidden states.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_embd", + "type": "integer" + }, + "n_head": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_inner": { + "default": null, + "description": "Dimensionality of the inner feed-forward layers. None will set it to 4 times n_embd", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_inner", + "type": [ + "integer", + "null" + ] + }, + "n_layer": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "n_positions": { + "default": 1024, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_positions", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "gpt2", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "resid_pdrop": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "resid_pdrop", + "type": "number" + }, + "scale_attn_weights": { + "default": true, + "description": "Scale attention weights by dividing by sqrt(hidden_size).", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "scale_attn_weights", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 50257, + "description": "Vocabulary size of the GPT-2 model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling GPT2Model or TFGPT2Model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "roberta" + } + } + }, + "then": { + "properties": { + "bos_token_id": { + "default": 0, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "eos_token_id": { + "default": 2, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "", + "description_implications": null, + "example_value": [ + "Default value used in pre-trained HF encoder." + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "eos_token_id", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pad_token_id": { + "default": 1, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "roberta-base", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "Vocabulary size of the RoBERTa model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer_xl" + } + } + }, + "then": { + "properties": { + "adaptive": { + "default": true, + "description": "Whether or not to use adaptive softmax.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Adaptive softmax is a speedup technique for computing probability distributions over words. For text with large vocabulary, adaptive softmax improves both training speed.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Adaptive Softmax" + }, + "title": "adaptive", + "type": "boolean" + }, + "attn_type": { + "default": 0, + "description": "Attention type. 0 for Transformer-XL, 1 for Shaw et al, 2 for Vaswani et al, 3 for Al Rfou et al.", + "maximum": 3, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_type", + "type": "integer" + }, + "clamp_len": { + "default": 1000, + "description": "Use the same pos embeddings after clamp_len.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "clamp_len", + "type": "integer" + }, + "cutoffs": { + "default": [ + 20000, + 40000, + 200000 + ], + "description": "Cutoffs for the adaptive softmax.", + "items": { + "title": "cutoffs", + "type": "integer" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cutoffs", + "type": [ + "array", + "null" + ] + }, + "d_embed": { + "default": 1024, + "description": "Dimensionality of the embeddings", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_embed", + "type": "integer" + }, + "d_head": { + "default": 64, + "description": "Dimensionality of the model\u2019s heads.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_head", + "type": "integer" + }, + "d_inner": { + "default": 4096, + "description": " Inner dimension in FF", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_inner", + "type": "integer" + }, + "d_model": { + "default": 1024, + "description": "Dimensionality of the model\u2019s hidden states.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_model", + "type": "integer" + }, + "div_val": { + "default": 4, + "description": "Divident value for adapative input and softmax.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "div_val", + "type": "integer" + }, + "dropatt": { + "default": 0.0, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "dropatt", + "type": "number" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "eos_token_id": { + "default": 0, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sequence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "init": { + "default": "normal", + "description": "Parameter initializer to use.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init", + "type": "string" + }, + "init_range": { + "default": 0.01, + "description": "Parameters initialized by U(-init_range, init_range).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init_range", + "type": "number" + }, + "init_std": { + "default": 0.02, + "description": "Parameters initialized by N(0, init_std)", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init_std", + "type": "number" + }, + "layer_norm_epsilon": { + "default": 1e-05, + "description": "The epsilon to use in the layer normalization layers", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_epsilon", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "mem_len": { + "default": 1600, + "description": "Length of the retained previous heads.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "mem_len", + "type": "integer" + }, + "n_head": { + "default": 16, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_layer": { + "default": 18, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "pre_lnorm": { + "default": false, + "description": "Whether or not to apply LayerNorm to the input instead of the output in the blocks.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pre_lnorm", + "type": "boolean" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "transfo-xl-wt103", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "proj_init_std": { + "default": 0.01, + "description": "Parameters initialized by N(0, init_std)", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "proj_init_std", + "type": "number" + }, + "proj_share_all_but_first": { + "default": true, + "description": "True to share all but first projs, False not to share.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "proj_share_all_but_first", + "type": "boolean" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "same_length": { + "default": true, + "description": "Whether or not to use the same attn length for all tokens", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "same_length", + "type": "boolean" + }, + "sample_softmax": { + "default": -1, + "description": "Number of samples in the sampled softmax.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sample_softmax", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "untie_r": { + "default": true, + "description": "Whether ot not to untie relative position biases.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "untie_r", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 267735, + "description": "Vocabulary size of the TransfoXL model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling TransfoXLModel or TFTransfoXLModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "xlnet" + } + } + }, + "then": { + "properties": { + "attn_type": { + "default": "bi", + "description": "The attention type used by the model. Currently only 'bi' is supported.", + "enum": [ + "bi" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attn_type", + "type": "string" + }, + "bi_data": { + "default": false, + "description": "Whether or not to use bidirectional input pipeline. Usually set to True during pretraining and False during finetuning.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bi_data", + "type": "boolean" + }, + "bos_token_id": { + "default": 1, + "description": "The beginning of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Beginning-of-Sentence Token Id" + }, + "title": "bos_token_id", + "type": "integer" + }, + "clamp_len": { + "default": -1, + "description": "Clamp all relative distances larger than clamp_len. Setting this attribute to -1 means no clamping.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "clamp_len", + "type": "integer" + }, + "d_inner": { + "default": 3072, + "description": "Dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_inner", + "type": "integer" + }, + "d_model": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_model", + "type": "integer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "summary_last_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "end_n_top": { + "default": 5, + "description": " Used in the SQuAD evaluation script.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "end_n_top", + "type": "integer" + }, + "eos_token_id": { + "default": 2, + "description": "The end of sequence token ID.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "End-of-Sequence Token Id" + }, + "title": "eos_token_id", + "type": "integer" + }, + "ff_activation": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler. If string, 'gelu', 'relu', 'silu' and 'gelu_new' are supported.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "ff_activation", + "type": "string" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "mem_len": { + "default": null, + "description": "The number of tokens to cache. The key/value pairs that have already been pre-computed in a previous forward pass won\u2019t be re-computed. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "mem_len", + "type": [ + "integer", + "null" + ] + }, + "n_head": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_head", + "type": "integer" + }, + "n_layer": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layer", + "type": "integer" + }, + "pad_token_id": { + "default": 5, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "xlnet-base-cased", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "reuse_len": { + "default": null, + "description": "The number of tokens in the current batch to be cached and reused in the future.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reuse_len", + "type": [ + "integer", + "null" + ] + }, + "same_length": { + "default": false, + "description": "Whether or not to use the same attention length for each token.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "same_length", + "type": "boolean" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "start_n_top": { + "default": 5, + "description": "Used in the SQuAD evaluation script.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "start_n_top", + "type": "integer" + }, + "summary_activation": { + "default": "tanh", + "description": "Argument used when doing sequence summary. Used in the sequence classification and multiple choice models.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Summary Activation Function" + }, + "title": "summary_activation", + "type": "string" + }, + "summary_last_dropout": { + "default": 0.1, + "description": "Used in the sequence classification and multiple choice models.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "summary_last_dropout" + }, + "title": "summary_last_dropout", + "type": "number" + }, + "summary_type": { + "default": "last", + "description": "Argument used when doing sequence summary. Used in the sequence classification and multiple choice models.", + "enum": [ + "last", + "first", + "mean", + "cls_index", + "attn" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "summary_type", + "type": "string" + }, + "summary_use_proj": { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "summary_use_proj", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "untie_r": { + "default": true, + "description": "Whether or not to untie relative position biases", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "untie_r", + "type": "boolean" + }, + "use_mems_eval": { + "default": true, + "description": "Whether or not the model should make use of the recurrent memory mechanism in evaluation mode.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_mems_eval", + "type": "boolean" + }, + "use_mems_train": { + "default": false, + "description": "Whether or not the model should make use of the recurrent memory mechanism in train mode.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_mems_train", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 32000, + "description": "Vocabulary size of the XLNet model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling XLNetModel or TFXLNetModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "distilbert" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler. If string, 'gelu', 'relu', 'silu' and 'gelu_new' are supported.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "This is the default activation function used in the Distillbert huggingface implementation", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": "string" + }, + "attention_dropout": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout, qa_dropout, seq_classif_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_dropout" + }, + "title": "attention_dropout", + "type": "number" + }, + "dim": { + "default": 768, + "description": " Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "dim", + "type": "integer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_dropout,\nqa_dropout,\nseq_classif_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "hidden_dim": { + "default": 3072, + "description": "The size of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "hidden_dim", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_heads": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_heads", + "type": "integer" + }, + "n_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layers", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "distilbert-base-uncased", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "qa_dropout": { + "default": 0.1, + "description": "The dropout probabilities used in the question answering model DistilBertForQuestionAnswering.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout, attention_dropout, seq_classif_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "qa_dropout" + }, + "title": "qa_dropout", + "type": "number" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "seq_classif_dropout": { + "default": 0.2, + "description": "The dropout probabilities used in the sequence classification and the multiple choice model DistilBertForSequenceClassification.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nattention_dropout,\nqa_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "seq_classif_dropout" + }, + "title": "seq_classif_dropout", + "type": "number" + }, + "sinusoidal_pos_embds": { + "default": false, + "description": "Whether to use sinusoidal positional embeddings.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sinusoidal_pos_embds", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the DistilBERT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling DistilBertModel or TFDistilBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "camembert" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "classifier_dropout, hidden_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "classifier_dropout": { + "default": null, + "description": "The dropout ratio for the classification head.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob, hidden_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout" + }, + "title": "classifier_dropout", + "type": [ + "number", + "null" + ] + }, + "gradient_checkpointing": { + "default": false, + "description": "Whether to use gradient checkpointing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gradient_checkpointing", + "type": "boolean" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob, \nclassifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 768, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 3072, + "description": "Dimensionality of the \u201cintermediate\u201d (often named feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-05, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 514, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 12, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "pad_token_id": { + "default": 1, + "description": "The ID of the token to use as padding.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_token_id", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "Type of position embedding.", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "camembert-base", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 1, + "description": "The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 32005, + "description": "Vocabulary size of the CamemBERT model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "t5" + } + } + }, + "then": { + "properties": { + "d_ff": { + "default": 2048, + "description": "Size of the intermediate feed forward layer in each T5Block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value matches the pre-trained encoder.", + "description_implications": "If using a pre-trained encoder, this parameter will be automatically derived from the pre-trained model.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Dimensionality of Feed-Forward Layer" + }, + "title": "d_ff", + "type": "integer" + }, + "d_kv": { + "default": 64, + "description": "Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // num_heads.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_kv", + "type": "integer" + }, + "d_model": { + "default": 512, + "description": "Size of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "d_model", + "type": "integer" + }, + "dropout_rate": { + "default": 0.1, + "description": "The ratio for all dropout layers.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout_rate" + }, + "title": "dropout_rate", + "type": "number" + }, + "feed_forward_proj": { + "default": "relu", + "description": "Type of feed forward layer to be used. Should be one of 'relu' or 'gated-gelu'. T5v1.1 uses the 'gated-gelu' feed forward projection. Original T5 uses 'relu'.", + "enum": [ + "relu", + "gated-gelu" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "feed_forward_proj", + "type": "string" + }, + "initializer_factor": { + "default": 1, + "description": "A factor for initializing all weight matrices (should be kept to 1, used internally for initialization testing).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "initializer_factor", + "type": "number" + }, + "layer_norm_eps": { + "default": 1e-06, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_decoder_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not set.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_decoder_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value matches the number of layers in the default pretrained encoder.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nIf using a pre-trained model, this parameter will be automatically derived from the pre-trained model.", + "example_value": [ + 6 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "pretrained_model_or_path" + ], + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "t5-small", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "relative_attention_num_buckets": { + "default": 32, + "description": "The number of buckets to use for each attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "relative_attention_num_buckets", + "type": "integer" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 32128, + "description": "Vocabulary size of the T5 model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling T5Model or TFT5Model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "flaubert" + } + } + }, + "then": { + "properties": { + "asm": { + "default": false, + "description": "Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the prediction layer.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "asm", + "type": "boolean" + }, + "attention_dropout": { + "default": 0.1, + "description": "The dropout probability for the attention mechanism", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_dropout" + }, + "title": "attention_dropout", + "type": "number" + }, + "bos_index": { + "default": 0, + "description": "The index of the beginning of sentence token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bos_index", + "type": "integer" + }, + "causal": { + "default": false, + "description": "Whether or not the model should behave in a causal manner. Causal models use a triangular attention mask in order to only attend to the left-side context instead if a bidirectional context.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "causal", + "type": "boolean" + }, + "dropout": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "dropout" + }, + "title": "dropout", + "type": "number" + }, + "emb_dim": { + "default": 512, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "emb_dim", + "type": "integer" + }, + "embed_init_std": { + "default": 0.02209708691207961, + "description": "The standard deviation of the truncated_normal_initializer for initializing the embedding matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "embed_init_std", + "type": "number" + }, + "eos_index": { + "default": 1, + "description": "The index of the end of sentence token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "eos_index", + "type": "integer" + }, + "gelu_activation": { + "default": true, + "description": "Whether or not to use a gelu activation instead of relu.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "gelu_activation", + "type": "boolean" + }, + "init_std": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices except the embedding matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "init_std", + "type": "number" + }, + "is_encoder": { + "default": true, + "description": "Whether or not the initialized model should be a transformer encoder or decoder as seen in Vaswani et al.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "is_encoder", + "type": "boolean" + }, + "lang_id": { + "default": 0, + "description": "The ID of the language used by the model. This parameter is used when generating text in a given language.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lang_id", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-06, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "layerdrop": { + "default": 0.2, + "description": "Probability to drop layers during training (Fan et al., Reducing Transformer Depth on Demand with Structured Dropout. ICLR 2020)", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layerdrop", + "type": "number" + }, + "mask_index": { + "default": 5, + "description": "The index of the masking token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "mask_index", + "type": "integer" + }, + "mask_token_id": { + "default": 0, + "description": "Model agnostic parameter to identify masked tokens when generating text in an MLM context.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Mask Token ID" + }, + "title": "mask_token_id", + "type": "integer" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "n_heads": { + "default": 8, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_heads", + "type": "integer" + }, + "n_langs": { + "default": 1, + "description": "The number of languages the model handles. Set to 1 for monolingual models.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Default value used in pre-trained HF encoder.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Languages" + }, + "title": "n_langs", + "type": "integer" + }, + "n_layers": { + "default": 6, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "n_layers", + "type": "integer" + }, + "pad_index": { + "default": 2, + "description": "The index of the padding token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pad_index", + "type": "integer" + }, + "pre_norm": { + "default": true, + "description": "Whether to apply the layer normalization before or after the feed forward layer following the attention in each layer (Vaswani et al., Tensor2Tensor for Neural Machine Translation. 2018)", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pre_norm", + "type": "boolean" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "flaubert/flaubert_small_cased", + "description": "Name of path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "sinusoidal_embeddings": { + "default": false, + "description": "Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sinusoidal_embeddings", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "unk_index": { + "default": 3, + "description": "The index of the unknown token in the vocabulary.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unk_index", + "type": "integer" + }, + "use_lang_emb": { + "default": true, + "description": "Whether to use language embeddings. Some models use additional language embeddings, see the multilingual models page for information on how to use them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_lang_emb", + "type": "boolean" + }, + "use_pretrained": { + "default": false, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30145, + "description": "Vocabulary size of the FlauBERT model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling FlaubertModel or TFFlaubertModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "electra" + } + } + }, + "then": { + "properties": { + "attention_probs_dropout_prob": { + "default": 0.1, + "description": "The dropout ratio for the attention probabilities.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, classifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "attention_probs_dropout_prob" + }, + "title": "attention_probs_dropout_prob", + "type": "number" + }, + "classifier_dropout": { + "default": null, + "description": "The dropout ratio for the classification head.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "hidden_dropout_prob, attention_probs_dropout_prob" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "classifier_dropout" + }, + "title": "classifier_dropout", + "type": [ + "number", + "null" + ] + }, + "embedding_size": { + "default": 128, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "hidden_act": { + "default": "gelu", + "description": "The non-linear activation function (function or string) in the encoder and pooler.", + "enum": [ + "gelu", + "relu", + "silu", + "gelu_new" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "Changing this activation function will only affect the feed-forward layers of the transformer.", + "example_value": [ + "relu" + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "[Huggingface docs for ELECTRA config](https://huggingface.co/docs/transformers/model_doc/electra#transformers.ElectraConfig.hidden_act)\n\n[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "gelu", + "suggested_values_reasoning": "Taken from huggingface defaults.", + "ui_display_name": "Hidden Layer Activation" + }, + "title": "hidden_act", + "type": "string" + }, + "hidden_dropout_prob": { + "default": 0.1, + "description": "The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "attention_probs_dropout_prob,\nclassifier_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "hidden_dropout_prob" + }, + "title": "hidden_dropout_prob", + "type": "number" + }, + "hidden_size": { + "default": 256, + "description": "Dimensionality of the encoder layers and the pooler layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Huggingface default.", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "initializer_range": { + "default": 0.02, + "description": "The standard deviation of the truncated_normal_initializer for initializing all weight matrices.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "There is an ideal value for this variable that doesn't lead to the outputs of these matrices to vanish or explode", + "example_value": [ + 0.02 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Must be greater than 0", + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "0.01-0.05", + "suggested_values_reasoning": "Large values will likely lead to very large outputs. Small values will lead to vanishing outputs.", + "ui_display_name": null + }, + "title": "initializer_range", + "type": "number" + }, + "intermediate_size": { + "default": 1024, + "description": "Dimensionality of the \u201cintermediate\u201d (i.e., feed-forward) layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "intermediate_size", + "type": "integer" + }, + "layer_norm_eps": { + "default": 1e-12, + "description": "The epsilon used by the layer normalization layers.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "layer_norm_eps", + "type": "number" + }, + "max_position_embeddings": { + "default": 512, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "The size of the position embeddings table. This typically coincides with the maximum sequence length this model might ever be used with. Typically set this to something large just in case (e.g. 512, 1024, 2048).", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_attention_heads": { + "default": 4, + "description": "Number of attention heads for each attention layer in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_attention_heads", + "type": "integer" + }, + "num_hidden_layers": { + "default": 12, + "description": "Number of hidden layers in the Transformer encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_hidden_layers", + "type": "integer" + }, + "position_embedding_type": { + "default": "absolute", + "description": "Type of position embedding.", + "enum": [ + "absolute", + "relative_key", + "relative_key_query" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "position_embedding_type", + "type": "string" + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "google/electra-small-discriminator", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The weights of the encoder are not necessarily saved in the checkpoint. The user has to save them first.", + "description_implications": "The memory footprint for some of these encoders can be large.", + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "skip_save_model" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "Some of these encoders are large, so it might be better to load them as needed, especially if 1. they're not used frequently 2. the user doesn't have a lot of storage.", + "ui_display_name": null + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 2, + "description": "The vocabulary size of the token_type_ids passed when calling ElectraModel or TFElectraModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 30522, + "description": "Vocabulary size of the ELECTRA model. Defines the number of different tokens that can be represented by the inputs_ids passed when calling ElectraModel or TFElectraModel.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "longformer" + } + } + }, + "then": { + "properties": { + "attention_window": { + "default": 512, + "description": "Size of an attention window around each token. If an int, use the same size for all layers. To specify a different window size for each layer, use a List[int] where len(attention_window) == num_hidden_layers.", + "oneOf": [ + { + "default": 512, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "attention_window_integer_option", + "type": "integer" + }, + { + "default": [], + "description": "", + "items": { + "title": "tmp", + "type": "integer" + }, + "parameter_metadata": null, + "title": "attention_window_list_option", + "type": "array" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attention_window" + }, + "max_position_embeddings": { + "default": 4098, + "description": "The maximum sequence length that this model might ever be used with. Typically set this to something large just in case (e.g., 512 or 1024 or 2048).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from huggingface.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words or positions, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": 512, + "suggested_values_reasoning": "Out of the box value based on published literature. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Max Position Embeddings" + }, + "title": "max_position_embeddings", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": "allenai/longformer-base-4096", + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": "string" + }, + "reduce_output": { + "default": "cls_pooled", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": "string" + }, + "saved_weights_in_checkpoint": { + "default": false, + "description": "Are the pretrained encoder weights saved in this model's checkpoint? Automatically set toTrue for trained models to prevent loading pretrained encoder weights from model hub.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "saved_weights_in_checkpoint", + "type": "boolean" + }, + "sep_token_id": { + "default": 2, + "description": "ID of the separator token, which is used when building a sequence from multiple sequences", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "sep_token_id", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "type_vocab_size": { + "default": 1, + "description": "The vocabulary size of the token_type_ids passed when calling LongformerEncoder", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "type_vocab_size", + "type": "integer" + }, + "use_pretrained": { + "default": true, + "description": "Whether to use the pretrained weights for the model. If false, the model will train from scratch which is very computationally expensive.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, the model is initialized as a pretrained model.", + "description_implications": "Pretrained models have typically already learned features that are difficult to learn from scratch. They are particularly beneficial when training on small amounts of data.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/transfer-learning-for-deep-learning/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs" + ], + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If you have a large amount of data and/or you have data that differs from the typical distribution, then it might be worth training the model from scratch.", + "ui_display_name": "Use Pretrained" + }, + "title": "use_pretrained", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": 50265, + "description": "Vocabulary size of the Longformer model.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "auto_transformer" + } + } + }, + "then": { + "properties": { + "max_sequence_length": { + "default": null, + "description": "Maximum length of the input sequence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "pretrained_kwargs": { + "additionalProperties": {}, + "default": null, + "description": "Additional kwargs to pass to the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_kwargs", + "type": [ + "object", + "null" + ] + }, + "pretrained_model_name_or_path": { + "default": null, + "description": "Name or path of the pretrained model.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "The method used to reduce a sequence of tensors down to a single tensor.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "trainable": { + "default": false, + "description": "Whether to finetune the model on your dataset.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "trainable", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "Vocabulary for the encoder", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "Vocabulary size of the AutoTransformer model. If None, the vocab size will be inferred from the given pretrained model", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tf_idf" + } + } + }, + "then": { + "properties": { + "max_sequence_length": { + "default": null, + "description": "", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "str2idf": { + "additionalProperties": {}, + "default": null, + "description": "", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "str2idf", + "type": [ + "object", + "null" + ] + }, + "vocab": { + "default": null, + "description": "", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "vocab_size": { + "default": null, + "description": "", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "embed", + "camembert", + "longformer", + "auto_transformer", + "gpt2", + "xlmroberta", + "flaubert", + "gpt", + "parallel_cnn", + "rnn", + "xlnet", + "deberta", + "albert", + "bert", + "transformer_xl", + "transformer", + "cnnrnn", + "t5", + "electra", + "tf_idf", + "stacked_parallel_cnn", + "distilbert", + "stacked_cnn", + "roberta" + ], + "enumDescriptions": { + "albert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `albert` encoder loads a pretrained [ALBERT](https://arxiv.org/abs/1909.11942) (default `albert-base-v2`) model using the Hugging Face transformers package. Albert is similar to BERT, with significantly lower memory usage and somewhat faster training time:.", + "other_information": null, + "related_parameters": null, + "short_description": "Similar to BERT with lower memory footprint and faster training.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "auto_transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://huggingface.co/docs/transformers/model_doc/auto" + ], + "long_description": "The `auto_transformer` encoder automatically instantiates the model architecture for the specified `pretrained_model_name_or_path`. Unlike the other HF encoders, `auto_transformer` does not provide a default value for `pretrained_model_name_or_path`, this is its only mandatory parameter. See the Hugging Face [AutoModels documentation](https://huggingface.co/docs/transformers/model_doc/auto) for more details.", + "other_information": null, + "related_parameters": null, + "short_description": "Automatically retrieves the architecture from the provided model name/path.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "bert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1810.04805" + ], + "long_description": "The bert encoder loads a pretrained BERT (default bert-base-uncased) model using the Hugging Face transformers package. BERT is a bidirectional transformer pretrained using a combination of masked language modeling objective and next sentence prediction on a large corpus comprising the Toronto Book Corpus and Wikipedia.", + "other_information": null, + "related_parameters": null, + "short_description": "Bidirectional transformer great for language modeling.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "camembert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1911.03894" + ], + "long_description": "The `camembert` encoder loads a pretrained [CamemBERT](https://arxiv.org/abs/1911.03894) (default `jplu/tf-camembert-base`) model using the Hugging Face transformers package. CamemBERT is pre-trained on a large French language web-crawled text corpus.", + "other_information": null, + "related_parameters": null, + "short_description": "Language model trained on large French text corpus.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "deberta": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/2006.03654", + "https://arxiv.org/abs/2111.09543" + ], + "long_description": "The [DeBERTa](https://arxiv.org/abs/2006.03654) encoder improves the BERT and RoBERTa models using disentangled attention and enhanced mask decoder. With those two improvements, DeBERTa out performs RoBERTa on a majority of NLU tasks with 80GB training data.\nIn [DeBERTa V3](https://arxiv.org/abs/2111.09543), the authors further improved the efficiency of DeBERTa using ELECTRA-Style pre-training with Gradient Disentangled Embedding Sharing. Compared to DeBERTa, the V3 version significantly improves the model performance on downstream tasks.", + "other_information": null, + "related_parameters": null, + "short_description": "Improved version of BERT and RoBERTa, achieving good baseline performance on many tasks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "distilbert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `distilbert` encoder loads a pretrained [DistilBERT](https://medium.com/huggingface/distilbert-8cf3380435b5) (default `distilbert-base-uncased`) model using the Hugging Face transformers package. DistilBERT is a small, fast, cheap and light Transformer model trained by distilling BERT base. It has 40% less parameters than bert-base-uncased, runs 60% faster while preserving over 95% of BERT\u2019s performances as measured on the GLUE language understanding benchmark.", + "other_information": null, + "related_parameters": null, + "short_description": "A distilled version of BERT base that is 40% smaller and 60% faster with 95% of performance preserved.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "electra": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://openreview.net/pdf?id=r1xMH1BtvB" + ], + "long_description": "The `electra`` encoder loads a pretrained [ELECTRA](https://openreview.net/pdf?id=r1xMH1BtvB) model using the Hugging Face transformers package. ELECTRA is a new pretraining approach which trains two transformer models the generator and the discriminator. The generator\u2019s role is to replace tokens in a sequence, and is therefore trained as a masked language model. The discriminator, which is the model we\u2019re interested in, tries to identify which tokens were replaced by the generator in the sequence.", + "other_information": null, + "related_parameters": null, + "short_description": "Transformer encoder that can be used to encode a sequence of tokens with little compute", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "embed": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h` tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps each element of the sequence to an embedding.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "flaubert": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1912.05372" + ], + "long_description": "The `flaubert`` encoder loads a pretrained [FlauBERT](https://arxiv.org/abs/1912.05372) (default `jplu/tf-flaubert-base-uncased``) model using the Hugging Face transformers package. FlauBERT has an architecture similar to BERT and is pre-trained on a large French language corpus.", + "other_information": null, + "related_parameters": null, + "short_description": "Language model with BERT related architecture trained on large French text corpus.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "gpt": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf" + ], + "long_description": "The `gpt` encoder loads a pretrained [GPT](https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf) (default `openai-gpt`) model using the Hugging Face transformers package. GPT is a causal (unidirectional) transformer pre-trained using language modeling on a large corpus with long range dependencies, the Toronto Book Corpus.", + "other_information": null, + "related_parameters": null, + "short_description": "GPT is a pre-trained language model used for NLP tasks like generation, summarization, and translation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "gpt2": { + "commonly_used": false, + "compute_tier": 3, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf" + ], + "long_description": "The `gpt2` encoder loads a pretrained [GPT-2](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) (default `gpt2`) model using the Hugging Face transformers package. GPT-2 is a causal (unidirectional) transformer pretrained using language modeling on a very large corpus of ~40 GB of text data.", + "other_information": null, + "related_parameters": null, + "short_description": "GPT-2 is a pre-trained language model used for NLP tasks like generation, summarization, and translation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "longformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/pdf/2004.05150.pdf" + ], + "long_description": "The `longformer` encoder loads a pretrained [Longformer](https://arxiv.org/pdf/2004.05150.pdf) (default `allenai/longformer-base-4096`) model using the Hugging Face transformers package. Longformer is a good choice for longer text, as it supports sequences up to 4096 tokens long.", + "other_information": null, + "related_parameters": null, + "short_description": "Transformer optimized for longer text inputs.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "roberta": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1907.11692" + ], + "long_description": "The `roberta` encoder loads a pretrained [RoBERTa](https://arxiv.org/abs/1907.11692) (default `roberta-base`) model using the Hugging Face transformers package. Replication of BERT pretraining which may match or exceed the performance of BERT. RoBERTa builds on BERT and modifies key hyperparameters, removing the next-sentence pretraining objective and training with much larger mini-batches and learning rates.", + "other_information": null, + "related_parameters": null, + "short_description": "BERT based model that has higher accuracy and is easier parallelize due to larger mini-batches.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "t5": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `t5` encoder loads a pretrained [T5](https://arxiv.org/pdf/1910.10683.pdf) (default `t5-small`) model using the Hugging Face transformers package. T5 (Text-to-Text Transfer Transformer) is pre-trained on a huge text dataset crawled from the web and shows good transfer performance on multiple tasks.", + "other_information": null, + "related_parameters": null, + "short_description": "Text-to-text approach transformer with good transfer performance on multiple tasks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer_xl": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `transformer_xl` encoder loads a pretrained [Transformer-XL](https://arxiv.org/abs/1901.02860) (default `transfo-xl-wt103`) model using the Hugging Face transformers package. Adds novel positional encoding scheme which improves understanding and generation of long-form text up to thousands of tokens. Transformer-XL is a causal (uni-directional) transformer with relative positioning (sinuso\u00efdal) embeddings which can reuse previously computed hidden-states to attend to longer context (memory). This model also uses adaptive softmax inputs and outputs (tied).", + "other_information": null, + "related_parameters": null, + "short_description": "Transformer architecture that introduces the notion of recurrence to the deep self-attention network.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "xlmroberta": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `xlmroberta` encoder loads a pretrained [XLM-RoBERTa](https://arxiv.org/abs/1911.02116) (default `jplu/tf-xlm-reoberta-base`) model using the Hugging Face transformers package. XLM-RoBERTa is a multi-language model similar to BERT, trained on 100 languages. XLM-RoBERTa is based on Facebook\u2019s RoBERTa model released in 2019. It is a large multi-lingual language model, trained on 2.5TB of filtered CommonCrawl data.", + "other_information": null, + "related_parameters": null, + "short_description": "XLM-RoBERTa a large multi-lingual language model trained on 2.5TB of filtered CommonCrawl data.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "xlnet": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The `xlnet` encoder loads a pretrained [XLNet](https://arxiv.org/abs/1906.08237) (default `xlnet-base-cased`) model using the Hugging Face transformers package. XLnet is an extension of the Transformer-XL model pre-trained using an autoregressive method to learn bidirectional contexts by maximizing the expected likelihood over all permutations of the input sequence factorization order. XLNet outperforms BERT on a variety of benchmarks.", + "other_information": null, + "related_parameters": null, + "short_description": "XLNet is a transformer that outperforms BERT on a variety of benchmarks.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "For pretrained encoders, compute encoder embeddings in preprocessing, speeding up training time considerably. Only supported when `encoder.trainable=false`.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "compute_idf": { + "default": false, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "compute_idf", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the `missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": [ + "Depends on dtype" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "missing_value_strategy, fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "DOCSTRING ONLY" + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": true, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Reading the text in lowercase enables the model to treat capitalized and lowercase words as the same, effectively increasing the number of data points per word.", + "description_implications": "If you set lowercase to False, then capitalized words are seen as completely separate entities than lowercase words.", + "example_value": [ + true + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": "If there is a strong reason to treat capitalized words and lowercased words differently, then set this to False. Otherwise, it is preferable to bucket the words and make the model case-insensitive.", + "ui_display_name": "Convert to lowercase" + }, + "title": "lowercase", + "type": "boolean" + }, + "max_sequence_length": { + "default": 256, + "description": "The maximum length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 256. Every sequence will be truncated to this length.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size, embedding_size" + ], + "short_description": "", + "suggested_values": "Use the lowest value that covers most of your input data. Only increase the value if crucial parts of the input data are truncated.", + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Sequence Length" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a text column.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 20000, + "description": "The maximum number of most common tokens in the vocabulary. If the data contains more than this amount, the most infrequent symbols will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": "integer" + }, + "ngram_size": { + "default": 2, + "description": "The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Size of the n-gram when using the `ngram` tokenizer.", + "description_implications": null, + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "n-gram size" + }, + "title": "ngram_size", + "type": "integer" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "We usually want to add padding to the end of a text sequence to fill in any remaining space as opposed to the beggining so we set the default to right.", + "description_implications": "If you pad to the left, the encoded vector will have leading padding tokens as opposed to trailing padding tokens. This could matter based on the type of text input you are expecting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "padding_symbol,\nmax_sequence_length" + ], + "short_description": "", + "suggested_values": "'right'", + "suggested_values_reasoning": "right padding is the usual way to add padding to a text sequence", + "ui_display_name": "Padding" + }, + "title": "padding", + "type": "string" + }, + "padding_symbol": { + "default": "", + "description": "The string used as the padding symbol for sequence features. Ignored for features using huggingface encoders, which have their own vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_symbol", + "type": "string" + }, + "pretrained_model_name_or_path": { + "default": null, + "description": "This can be either the name of a pretrained HuggingFace model or a path where it was downloaded.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": [ + "string", + "null" + ] + }, + "sequence_length": { + "default": null, + "description": "The desired length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated and sequences shorter than this value will be padded. If None, sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": null, + "title": "sequence_length", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space_punct", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default tokenizer is `space_punct`, an abbreviation of \"Space punctuation\". This tokenizer creates sub-words by dividing the text on whitespace and punctuation characters. For example: The text `'hello world!isn't this great?'` would be transformed to `['hello', 'world', '!', 'isn', \"'\", 't', 'this', 'great', '?']`. This is the default value because it is a fast tokenizer that works reasonably well.", + "description_implications": "Choosing a tokenizer can be difficult. The primary thing to check is that the tokenizer you have selected is compatible with the language(s) in your text data. This means either selecting a tokenizer that is language-specific (i.e. `french_tokenize` if working with French text) or general enough that its tokenizations are language-agnostic (i.e. `space_punct`).", + "example_value": [ + "space_punct" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://huggingface.co/course/chapter2/4?fw=pt" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_file, pretrained_model_name_or_path" + ], + "short_description": "", + "suggested_values": "sentencepiece", + "suggested_values_reasoning": "SentencePiece is a tokenizer developed by Google which utilizes Byte-Pair Encoding (BPE), which strikes a good balance between character-level and word-level tokenization (more info on BPE here: https://towardsdatascience.com/byte-pair-encoding-the-dark-horse-of-modern-nlp-eb36c7df4f10 ). This tokenizer is language-agnostic and more sophisticated than the default.", + "ui_display_name": "Tokenizer" + }, + "title": "tokenizer", + "type": "string" + }, + "unknown_symbol": { + "default": "", + "description": "The string used as the unknown symbol for sequence features. Ignored for features using huggingface encoders, which have their own vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unknown_symbol", + "type": "string" + }, + "vocab_file": { + "default": null, + "description": "Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the first string until `\\t` or `\\n` is considered a word.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The vocabulary can be parsed automatically from the incoming input features.", + "description_implications": "It can be useful to specify your own vocabulary list if the vocabulary is very large, there's no out of the box tokenizer that fits your data, or if there are several uncommon or infrequently occurring tokens that we want to guarantee to be a part of the vocabulary, rather than treated as an unknown.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Vocab File" + }, + "title": "vocab_file", + "type": [ + "string", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "timeseries" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "encoding_size": { + "default": null, + "description": "The size of the encoding vector, or None if sequence elements are scalars.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `reduce_output` method does not use this parameter, so by default this parameter is not set.", + "description_implications": "This parameter must be equal to the size of the input. Otherwise, an error will occur.", + "example_value": [ + 128 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "reduce_output" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "NONE", + "ui_display_name": null + }, + "title": "encoding_size", + "type": [ + "integer", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "reduce_output": { + "default": null, + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "Whether to embed the input sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stacked_parallel_cnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "The default activation function that will be used for each layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate applied to the embedding. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters for each fully connected layer.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 3, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": "\"batch\" or \"layer\"", + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Parameters used if norm is either `batch` or `layer`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default parameters that come with Torch's implementation of these normalization types are a trusted starting point.", + "description_implications": "There are a variety of ways a certain set of parameters specificed could influence performance here. Broadly speaking the different values passed in here allow for different levels of smoothness to be observed in the learning curves. Since setting this parameters depends on the type of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html) for more information on the parameters to set for batch normalization, and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html) for more information on the parameters to set for layer normalization.", + "example_value": [ + { + "affine": false, + "momentum": 0.2, + "num_features": 100 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\nFor LayerNorm: https://arxiv.org/abs/1607.06450" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "`norm`" + ], + "short_description": "", + "suggested_values": "Depends on the type of `norm` set.", + "suggested_values_reasoning": "NO", + "ui_display_name": "Normalization Parameters" + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": null, + "description": "Number of parallel fully connected layers to use.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": [ + "integer", + "null" + ] + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_stacked_layers": { + "default": null, + "description": "If stacked_layers is null, this is the number of elements in the stack of parallel convolutional layers. ", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "While superceded by `stacked_layers`, this can directly change the depth of the current stack of parallel convolutional layers.", + "example_value": [ + 1 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "stacked_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Stacked Layers" + }, + "title": "num_stacked_layers", + "type": [ + "integer", + "null" + ] + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "sum", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "stacked_layers": { + "default": null, + "description": "a nested list of lists of dictionaries containing the parameters of the stack of parallel convolutional layers. The length of the list determines the number of stacked parallel convolutional layers, length of the sub-lists determines the number of parallel conv layers and the content of each dictionary determines the parameters for a specific layer. ", + "items": { + "additionalProperties": {}, + "title": "stacked_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "stacked_layers", + "type": [ + "array", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "dropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "cnnrnn" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "tanh", + "description": "The default activation function to use.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "bidirectional": { + "default": false, + "description": "If true, two recurrent networks will perform encoding in the forward and backward direction and their outputs will be concatenated.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "bidirectional", + "type": "boolean" + }, + "cell_type": { + "default": "rnn", + "description": "The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference about the differences between the cells please refer to [torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "conv_activation": { + "default": "relu", + "description": "The default activation function that will be used for each convolutional layer.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "conv_activation", + "type": [ + "string", + "null" + ] + }, + "conv_dropout": { + "default": 0.0, + "description": "The dropout rate for the convolutional layers", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Convolutional Dropout" + }, + "title": "conv_dropout", + "type": "number" + }, + "conv_layers": { + "default": null, + "description": "\nA list of dictionaries containing the parameters of all the convolutional layers.\nThe length of the list determines the number of stacked convolutional layers and the content of each dictionary\ndetermines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`,\n`norm`, `norm_params`, `num_filters`, `filter_size`, `strides`, `padding`, `dilation_rate`, `use_bias`, `pool_function`,\n`pool_padding`, `pool_size`, `pool_strides`, `bias_initializer`, `weights_initializer`. If any of those values is\nmissing from the dictionary, the default one specified as a parameter of the encoder will be used instead. If both\n`conv_layers` and `num_conv_layers` are `null`, a default list will be assigned to `conv_layers` with the value\n`[{filter_size: 7, pool_size: 3}, {filter_size: 7, pool_size: 3}, {filter_size: 3, pool_size: null},\n{filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: null}, {filter_size: 3, pool_size: 3}]`.\n", + "items": { + "additionalProperties": {}, + "title": "conv_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "num_conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Convolutional Layers" + }, + "title": "conv_layers", + "type": [ + "array", + "null" + ] + }, + "dilation_rate": { + "default": 1, + "description": "Dilation rate to use for dilated convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The standard discrete convolution is the same as a 1-dilated convolution.", + "description_implications": "Higher dilation rates increase the effective size of the convolutional filter. Dilated convolution may improve performance if the data is very correlated locally and also contains long-term dependencies.", + "example_value": [ + 2 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Dilated convolution is also known as atrous convolution.", + "related_parameters": [ + "filter_size" + ], + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "The dilation rate is a factor which increases the spacing between elements of the convolutional filter", + "ui_display_name": "Dilation Rate" + }, + "title": "dilation_rate", + "type": "integer" + }, + "dropout": { + "default": 0.0, + "description": "Dropout rate. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "filter_size": { + "default": 5, + "description": "Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "filter_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_conv_layers": { + "default": null, + "description": "The number of stacked convolutional layers when `conv_layers` is `null`.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a large amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_layers" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Number of Convolutional Layers" + }, + "title": "num_conv_layers", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_filters": { + "default": 256, + "description": "Number of filters, and by consequence number of output channels of the 1d convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_filters", + "type": "integer" + }, + "num_rec_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_rec_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "pool_function": { + "default": "max", + "description": "Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or `mean` will compute the mean value", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Within a given sliding window (e.g. a \"patch\" of a 3-channel image), the maximum value for each channel is kept. All other values in the patch are discarded. Repeat this step for every patch and you have a more compact representation of the image. \n\nIntuitively, each patch encodes the features from a particular part of an image, and it is more informative to look at the most prominent features of an image than the average of all of them.", + "description_implications": "Both average and max pooling can achieve strong performance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\nhttps://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Default", + "suggested_values_reasoning": "No", + "ui_display_name": "Pooling function" + }, + "title": "pool_function", + "type": [ + "string", + "null" + ] + }, + "pool_padding": { + "default": "same", + "description": "Padding to use.", + "enum": [ + "valid", + "same" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_padding", + "type": "string" + }, + "pool_size": { + "default": null, + "description": "The default pool_size that will be used for each layer. If a pool_size is not already specified in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of the max pooling that will be performed along the `s` sequence dimension after the convolution operation.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_size", + "type": [ + "integer", + "null" + ] + }, + "pool_strides": { + "default": null, + "description": "Factor to scale down.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pool_strides", + "type": [ + "integer", + "null" + ] + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "recurrent_activation": { + "default": "sigmoid", + "description": "The activation function to use in the recurrent step", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "sigmoid' is commonly used", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "I don't think that this parameter is used anywhere in the code base. It's being passed down but not used in the actual RNN forwarding functions.", + "related_parameters": null, + "short_description": "", + "suggested_values": "sigmoid, ReLu, tanh", + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_activation", + "type": [ + "string", + "null" + ] + }, + "recurrent_dropout": { + "default": 0.0, + "description": "The dropout rate for the recurrent state", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "conv_dropout,\ndropout,\nrecurrent_dropout,\nfc_dropout" + ], + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Recurrent Dropout" + }, + "title": "recurrent_dropout", + "type": "number" + }, + "recurrent_initializer": { + "default": "orthogonal", + "description": "The initializer for recurrent matrix weights", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "recurrent_initializer", + "type": "string" + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "state_size": { + "default": 256, + "description": "The size of the state of the rnn.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "state_size", + "type": "integer" + }, + "strides": { + "default": 1, + "description": "Stride length of the convolution.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it makes sense to have a smaller stride that fits the input. Imagining the simple 2D image as our input, two pixels next to eachother are strongly correlated while pixels that are further apart will have a comparatively weaker correlation. Consequently, a higher stride may cause significant information loss.", + "description_implications": "Changing the stride of a convolutional layer is one form of downsampling (another being pooling). In the case of a large stride, significant amounts of information is thrown away as the filter convolves over its input. This should be usually avoided but may be desirable in cases in which the user has some deep knowledge of the filter or of the rest of the model architecture that makes it comfortable to allow a higher level compression in the output feature map of this layer.", + "example_value": [ + 1 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n[machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n[crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "pool_strides, default_strides, default_pool_strides, block_strides" + ], + "short_description": "", + "suggested_values": "1-2", + "suggested_values_reasoning": "In general, points that are closer to eachother in the input feature space will be more strongly correlated to eachother, so it is a good idea to select a stride that captures these neighboring relationships.", + "ui_display_name": "Stride" + }, + "title": "strides", + "type": "integer" + }, + "unit_forget_bias": { + "default": true, + "description": "If true, add 1 to the bias of the forget gate at initialization", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unit_forget_bias", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "transformer" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.1, + "description": "The dropout rate for the transformer block. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "embedding_size": { + "default": 256, + "description": "The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for `dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` is the number of unique strings appearing in the training set input column plus the number of special tokens (``, ``, ``, ``).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "An embedding is a relatively low-dimensional space that is used to translate high-dimensional vectors like words, which can have a large vocbulary size. Ideally, after an embedding is trained, it captures some of the semantics of the input by placing semantically similar inputs close together in the embedding space.\n\nIn most cases, the embedding size is chosen empirically, by trial and error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is to use the fourth root of the total number of unique categorical elements while another is that the embedding dimension should be approximately 1.6 times the square root of the number of unique elements in the category, and no less than 600.\"\n\nIncreasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1.6 * sqrt(vocab_size)", + "suggested_values_reasoning": "Rule of thumb suggested by a deep learning textbook. Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Embedding Size" + }, + "title": "embedding_size", + "type": "integer" + }, + "embeddings_on_cpu": { + "default": false, + "description": "Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, but in some cases the embedding matrix may be too large. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the process as a result of data transfer between CPU and GPU memory.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access.", + "description_implications": "By default embeddings matrices are stored on GPU memory if a GPU is used, as it allows for faster access. However, in some cases when the vocabulary size is very large, the full embedding matrix may be really big and unwieldy to have in GPU memory. This parameter forces the placement of the embedding matrix in regular memory and the CPU is used to access them. This may slow down training due to additional data transfer between CPU and GPU memory, but can lead to healthier GPU memory resource usage.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": [ + false + ], + "suggested_values_reasoning": "If GPU memory is not a constraint, having embeddings stored and accessed within the GPU is faster.", + "ui_display_name": "Embeddings on CPU" + }, + "title": "embeddings_on_cpu", + "type": "boolean" + }, + "embeddings_trainable": { + "default": true, + "description": "If `true` embeddings are trained during the training process, if `false` embeddings are fixed. It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If trained from scratch, embedding vectors are typically learned alongside the rest of the model.", + "description_implications": "Typically this value is only set to False if pre-trained embeddings are uploaded. Even then, it is reasonable to leave it as True in order to fine-tune the embeddings.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, representation, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "(under Embeddings header) Trainable?" + }, + "title": "embeddings_trainable", + "type": "boolean" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "hidden_size": { + "default": 256, + "description": "The size of the hidden representation within the transformer block. It is usually the same as the embedding_size, but if the two values are different, a projection layer will be added before the first transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from literature (https://arxiv.org/abs/1706.03762)", + "description_implications": "Increasing the hidden size makes the model larger and slower to train, increases the model's capacity to capture more complexity. It also increases the chance of overfitting.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "10 - 2048", + "suggested_values_reasoning": "Increasing the hidden size makes sense if the model is underfitting. It's useful to train both smaller and larger models to see how model capacity affects performance. This should only be explored after the architecture of the model has been settled.", + "ui_display_name": "Hidden Size" + }, + "title": "hidden_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "The default norm that will be used for each layer.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of parallel fully connected layers to use. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_heads": { + "default": 8, + "description": "Number of attention heads in each transformer block.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "num_heads", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of transformer layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data. For many data types, one layer is sufficient.", + "description_implications": "The ideal number of transformer layers depends on the length and complexity of input sequences, as well as the task.\n\nFor more complex tasks, and higher number of transformer layers may be useful. However, too many layers will increase memory and slow training while providing diminishing returns of model performance.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1 - 12", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Transformer Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "The default output_size that will be used for each layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "pretrained_embeddings": { + "default": null, + "description": "Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized randomly, but this parameter allows to specify a path to a file containing embeddings in the [GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. If the vocabulary contains strings that have no match in the embeddings file, their embeddings are initialized with the average of all other embedding plus some random noise to make them different from each other. This parameter has effect only if `representation` is `dense`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Embeddings are commonly trained from scratch, or incorporated as part of a pre-trained model package.", + "description_implications": "If pretrained embeddings are specified, then the model may have a head start in its representation of various input entities.", + "example_value": [ + "~/Downloads/glove.6B.100d.txt" + ], + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "embedding_size, embeddings_trainable" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Pretrained embeddings path" + }, + "title": "pretrained_embeddings", + "type": [ + "string", + "null" + ] + }, + "reduce_output": { + "default": "last", + "description": "How to reduce the output tensor along the `s` sequence length dimension if the rank of the tensor is greater than 2.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sums the tensors along the sequence dimension.", + "description_implications": "\"last\", \"sum\", \"mean\", and \"max\" are the fastest and most memory-efficient operations\u2013 they result in tensors that are the same-size as a single item in the input sequence. However, these are simple aggregation operations, therefore some information may be lost. \n\n\"concat\" concatenates each tensor together, creating a `(sequence length)*(tensor size)`-element tensor. \"concat\" preserves this information, but can be very memory-intensive and should only be applied if the sequence length and/or tensor size is small. \n\n\"attention\" takes a weighted sum of the items in the sequence, where the weights for each item in the sequence are determined by the model on-the-fly based on the features of the item itself. This is both slower and and more memory-intensive than the other operations; however, it can also provide a richer \"global\" representation of the sequence.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "\"attention\". This and the default covers 95% of use cases.", + "suggested_values_reasoning": "If you would like better performance and are not compute/memory-constrained, attention-based reduction can potentially provide a richer global representation than the default, but note that attention reduction does not work with `cache_encoder_embeddings=true`.", + "ui_display_name": "Sequence Reducer" + }, + "title": "reduce_output", + "type": [ + "string", + "null" + ] + }, + "representation": { + "default": "dense", + "description": "Representation of the embedding. `dense` means the embeddings are initialized randomly, `sparse` means they are initialized to be one-hot encodings.", + "enum": [ + "dense", + "sparse" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Trainable, randomly initialized embedding vectors often lead to more subtle representations of input entities than one-hot vectors.", + "description_implications": "If set to sparse, the representations for input entities are fixed as one-hot vectors. This leads to less flexible representations for input entities, but could lead to faster training since there are less learnable parameters.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "", + "related_parameters": [ + "embedding_size, embeddings_trainable, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Representation approach" + }, + "title": "representation", + "type": "string" + }, + "should_embed": { + "default": true, + "description": "If True the input sequence is expected to be made of integers and will be mapped into embeddings", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "should_embed", + "type": "boolean" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "transformer_output_size": { + "default": 256, + "description": "Size of the fully connected layer after self attention in the transformer block. This is usually the same as hidden_size and embedding_size.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Transformer Output Size" + }, + "title": "transformer_output_size", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether to use a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab": { + "default": null, + "description": "[internal] Vocabulary for the encoder from preprocessing.", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Computed and passed along internally according to preprocessing settings.", + "description_implications": null, + "example_value": [ + "a", + "b", + "c" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "vocab", + "type": [ + "array", + "null" + ] + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "parallel_cnn", + "enum": [ + "rnn", + "passthrough", + "transformer", + "cnnrnn", + "stacked_parallel_cnn", + "stacked_cnn", + "parallel_cnn", + "dense" + ], + "enumDescriptions": { + "cnnrnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of convolutional layers (by default 2), that is followed by a stack of recurrent layers (by default 1), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a number of parallel 1d convolutional layers with different filter size (by default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation. This single vector concatenating the outputs of the parallel convolutional layers is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Default option for processing sequence, audio, and text data types.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply transforms each input value into a float value and adds a dimension to the input tensor, creating a b x s x 1 tensor where b is the batch size and s is the length of the sequence. The tensor is reduced along the s dimension to obtain a single vector of size h for each element of the batch.", + "other_information": null, + "related_parameters": null, + "short_description": "Transforms sequence values to a floats then reduces to obtain a vector for each element.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "rnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce operation that by default only returns the last output, but can perform other reduce functions.", + "other_information": null, + "related_parameters": null, + "short_description": "Utilizes a stack of recurrent layers followed by a reduce operation.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of 1d convolutional layers with different filter size (by default 6 layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a flatten operation. This single flatten vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "stacked_parallel_cnn": { + "commonly_used": false, + "compute_tier": 1, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN encoders where each layer of the stack is composed of parallel convolutional layers. It works by first mapping the input integer sequence b x s (where b is the batch size and s is the length of the sequence) into a sequence of embeddings, then it passes the embedding through a stack of several parallel 1d convolutional layers with different filter size, followed by an optional final pool and by a flatten operation. This single flattened vector is then passed through a stack of fully connected layers and returned as a b x h tensor where h is the output size of the last fully connected layer.", + "other_information": null, + "related_parameters": null, + "short_description": "Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "transformer": { + "commonly_used": false, + "compute_tier": 2, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1706.03762" + ], + "long_description": "The transformer encoder implements a stack of transformer blocks, replicating the architecture introduced in the Attention is all you need paper, and adds am optional stack of fully connected layers at the end.", + "other_information": null, + "related_parameters": null, + "short_description": "Stack of transformer blocks with optional stack of fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the `missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when a row of data is missing.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "padding_value": { + "default": 0.0, + "description": "Float value that is used for padding and replacing missing values within a row.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_value", + "type": "number" + }, + "timeseries_length_limit": { + "default": 256, + "description": "Defines the maximum length of the timeseries. All timeseries longer than this limit are cut off.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "timeseries_length_limit", + "type": "integer" + }, + "tokenizer": { + "default": "space", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + }, + "window_size": { + "default": 0, + "description": "Optional lookback window size used to convert a column-major dataset (one observation per row) into a row-major dataset (each row has a timeseries window of observations). Starting from a given observation, a sliding window is taken going `window_size - 1` rows back to form the timeseries input feature. If this value is left as 0, then it is assumed that the dataset has been provided in row-major format (i.e., it has already been preprocessed such that each row is a timeseries window).", + "minimum": 0, + "parameter_metadata": null, + "title": "window_size", + "type": "integer" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "vector" + } + } + }, + "then": { + "properties": { + "encoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "passthrough" + } + } + }, + "then": { + "properties": { + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "dense" + } + } + }, + "then": { + "properties": { + "activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "bias_initializer_custom_option", + "type": "object" + }, + { + "default": "zeros", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "bias_initializer" + }, + "dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the dense encoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "norm", + "type": [ + "string", + "null" + ] + }, + "norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "norm_params", + "type": [ + "object", + "null" + ] + }, + "num_layers": { + "default": 1, + "description": "Number of stacked fully connected layers to apply. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "output_size": { + "default": 256, + "description": "Size of the output of the feature.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": "integer" + }, + "skip": { + "default": false, + "description": "[internal] Whether to skip encoder and use input as output.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "skip", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix. Options: `uniform`, `normal`, `constant`, `ones`, `zeros`, `eye`, `dirac`, `xavier_uniform`, `xavier_normal`, `kaiming_uniform`, `kaiming_normal`, `orthogonal`, `sparse`, `identity`. \nAlternatively it is possible to specify a dictionary with a key `type` that identifies the type of initializer and\nother keys for its parameters, e.g. `{type: normal, mean: 0, stddev: 0}`. For a description of the parameters of each\ninitializer, see [torch.nn.init](https://pytorch.org/docs/stable/nn.init.html).\n", + "oneOf": [ + { + "additionalProperties": true, + "description": "Customize an existing initializer.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "properties": { + "type": { + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "weights_initializer_custom_option", + "type": "object" + }, + { + "default": "xavier_uniform", + "description": "Pick a preconfigured initializer.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer_preconfigured_option", + "type": "string" + } + ], + "title": "weights_initializer" + } + } + } + } + ], + "properties": { + "type": { + "default": "dense", + "enum": [ + "passthrough", + "dense" + ], + "enumDescriptions": { + "dense": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The dense encoder passes the raw numerical values through fully connected layers. In this case the inputs of size `b` are transformed to size `b x h`.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw numerical values through fully connected layers.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "passthrough": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The passthrough encoder simply returns the raw numerical values coming from the input placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is the batch size.", + "other_information": null, + "related_parameters": null, + "short_description": "Passes the raw input through to the combiner.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "encoder_options", + "type": "object" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "pattern": "^([0-9]+(\\.[0-9]*)?\\s*)*$", + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "pattern": "^([0-9]+(\\.[0-9]*)?\\s*)*$", + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "fill_with_const", + "description": "What strategy to follow when there's a missing value in a vector column", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "vector_size": { + "default": null, + "description": "The size of the vector. If None, the vector size will be inferred from the data.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "vector_size", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "tied": { + "default": null, + "description": "Name of input feature to tie the weights of the encoder with. It needs to be the name of a feature of the same type and with the same encoder parameters. If text or sequence features are tied, consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have equal sized outputs. This is necessary when using the `sequence` combiner.", + "parameter_metadata": null, + "title": "tied", + "type": [ + "string", + "null" + ] + } + } + } + } + ], + "properties": { + "column": { + "description": "Name of the column.", + "title": "column", + "type": "string" + }, + "name": { + "description": "Name of the input feature.", + "title": "name", + "type": "string" + }, + "type": { + "description": "Type of the input feature", + "enum": [ + "audio", + "bag", + "binary", + "category", + "date", + "h3", + "image", + "number", + "sequence", + "set", + "text", + "timeseries", + "vector" + ], + "title": "type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "input_feature", + "type": "object" + }, + "minItems": 1, + "title": "input_features", + "type": "array", + "uniqueItemProperties": [ + "name" + ] + }, + "ludwig_version": { + "default": "0.8.dev", + "description": "", + "enum": [ + "0.8.dev" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "ludwig_version", + "type": "string" + }, + "model_type": { + "default": "ecd", + "description": "", + "enum": [ + "ecd" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "model_type", + "type": "string" + }, + "output_features": { + "items": { + "additionalProperties": true, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "binary" + } + } + }, + "then": { + "properties": { + "calibration": { + "default": false, + "description": "Calibrate the model's output probabilities using temperature scaling.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "calibration", + "type": "boolean" + }, + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "regressor" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "regressor", + "enum": [ + "regressor" + ], + "enumDescriptions": { + "regressor": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The regressor decoder is a (potentially empty) stack of fully connected layers, followed by a projection to a single number.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a single number.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "roc_auc", + "description": "Internal only use parameter: default validation metric for binary output feature.", + "enum": [ + "roc_auc" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "binary_weighted_cross_entropy" + } + } + }, + "then": { + "properties": { + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "positive_class_weight": { + "default": null, + "description": "Weight of the positive class.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "positive_class_weight", + "type": [ + "number", + "null" + ] + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "binary_weighted_cross_entropy", + "enum": [ + "binary_weighted_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": null, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "oneOf": [ + { + "default": 1.0, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "computed_fill_value_float_option", + "type": "number" + }, + { + "default": "Y", + "description": "", + "enum": [ + "-1", + "-1.0", + "0", + "0.0", + "1", + "1.0", + "F", + "FALSE", + "False", + "N", + "NO", + "No", + "T", + "TRUE", + "True", + "Y", + "YES", + "Yes", + "f", + "false", + "n", + "no", + "t", + "true", + "y", + "yes" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "computed_fill_value_string_option", + "type": "string" + }, + { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "computed_fill_value_boolean_option", + "type": "boolean" + }, + { + "description": "Disable this parameter.", + "title": "null_option", + "type": "null" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value" + }, + "fallback_true_label": { + "default": null, + "description": "The label to interpret as 1 (True) when the binary feature doesn't have a conventional boolean value", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Modeling performance should not be affected, but the semantics of some binary metrics may change like for \"false positives\", \"false negatives\", etc. if the true label is pinned to the other value.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fallback True Label" + }, + "title": "fallback_true_label", + "type": [ + "string", + "null" + ] + }, + "fill_value": { + "default": null, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "oneOf": [ + { + "default": null, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "fill_value_float_option", + "type": [ + "number", + "null" + ] + }, + { + "default": "Y", + "description": "", + "enum": [ + "-1", + "-1.0", + "0", + "0.0", + "1", + "1.0", + "F", + "FALSE", + "False", + "N", + "NO", + "No", + "T", + "TRUE", + "True", + "Y", + "YES", + "Yes", + "f", + "false", + "n", + "no", + "t", + "true", + "y", + "yes" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "fill_value_string_option", + "type": "string" + }, + { + "default": true, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": null, + "title": "fill_value_boolean_option", + "type": "boolean" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value" + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a binary output feature", + "enum": [ + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_false", + "fill_with_true" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + }, + "threshold": { + "default": 0.5, + "description": "The threshold used to convert output probabilities to predictions. Predicted probabilities greaterthan or equal to threshold are mapped to True.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "threshold", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "category" + } + } + }, + "then": { + "properties": { + "calibration": { + "default": false, + "description": "Calibrate the model's output probabilities using temperature scaling.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "calibration", + "type": "boolean" + }, + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "classifier" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_classes": { + "default": null, + "description": "Number of classes to predict.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "classifier", + "enum": [ + "classifier" + ], + "enumDescriptions": { + "classifier": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a vector of size of the number of available classes, followed by a sigmoid.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a vector the size of the number of available classes.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "accuracy", + "description": "Internal only use parameter: default validation metric for category output feature.", + "enum": [ + "accuracy" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "softmax_cross_entropy", + "enum": [ + "softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "For fixed encoders, compute encoder embeddings in preprocessing to avoid this step at train time. Can speed up the time taken per step during training, but will invalidate the preprocessed data if the encoder type is changed. Some model types (GBM) require caching encoder embeddings to use embedding features, and those models will override this value to `true` automatically.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "Whether the string has to be lowercased before being handled by the tokenizer.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a category output feature", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. if the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + }, + "top_k": { + "default": 3, + "description": "Determines the parameter k, the number of categories to consider when computing the top_k measure. It computes accuracy but considering as a match if the true category appears in the first k predicted categories ranked by decoder's confidence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "top_k", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "category_distribution" + } + } + }, + "then": { + "properties": { + "calibration": { + "default": false, + "description": "Calibrate the model's output probabilities using temperature scaling.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "calibration", + "type": "boolean" + }, + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "classifier" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_classes": { + "default": null, + "description": "Number of classes to predict.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "classifier", + "enum": [ + "classifier" + ], + "enumDescriptions": { + "classifier": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a vector of size of the number of available classes, followed by a sigmoid.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a vector the size of the number of available classes.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "accuracy", + "description": "Internal only use parameter: default validation metric for category output feature.", + "enum": [ + "accuracy" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "softmax_cross_entropy", + "enum": [ + "softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a category output feature", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "vocab": { + "default": null, + "description": "", + "items": { + "title": "vocab", + "type": "string" + }, + "parameter_metadata": null, + "title": "vocab", + "type": [ + "array", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + }, + "top_k": { + "default": 3, + "description": "Determines the parameter k, the number of categories to consider when computing the top_k measure. It computes accuracy but considering as a match if the true category appears in the first k predicted categories ranked by decoder's confidence.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "top_k", + "type": "integer" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "number" + } + } + }, + "then": { + "properties": { + "clip": { + "default": null, + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": null, + "description": "Clip the predicted output to the specified range.", + "items": [ + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + }, + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "description": "None", + "title": "null_float_tuple_option", + "type": "null" + } + ], + "title": "clip" + }, + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "regressor" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "regressor", + "enum": [ + "regressor" + ], + "enumDescriptions": { + "regressor": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The regressor decoder is a (potentially empty) stack of fully connected layers, followed by a projection to a single number.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a single number.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "mean_squared_error", + "description": "Internal only use parameter: default validation metric for number output feature.", + "enum": [ + "mean_squared_error" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "root_mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "root_mean_squared_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "huber" + } + } + }, + "then": { + "properties": { + "delta": { + "default": 1.0, + "description": "Threshold at which to change between delta-scaled L1 and L2 loss.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "delta", + "type": "number" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "mean_squared_error", + "enum": [ + "mean_squared_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "root_mean_squared_error", + "root_mean_squared_percentage_error", + "huber" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": 0.0, + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "number" + }, + "computed_outlier_fill_value": { + "default": 0.0, + "description": "The internally computed fill value to replace outliers with in case the outlier_strategy is fill_with_mode or fill_with_mean", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_outlier_fill_value", + "type": "number" + }, + "fill_value": { + "default": 0.0, + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "number" + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a number output feature", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_mean" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "normalization": { + "default": null, + "description": "Normalization strategy to use for this number feature.", + "enum": [ + "zscore", + "minmax", + "log1p", + "iq", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Z-score normalization helps improve the training stability and convergence of neural networks by rescaling the numeric input features to have a mean of 0 and a standard deviation of 1, reducing the variability and distribution of the data. This improves neural network training.", + "description_implications": "The goal of normalization is to transform features to be on a similar scale. Normalization can be a form of feature smoothing that improves the performance and training stability of the model. Normalizations may result in different effects on the semantics of your number features. The best normalization technique is one that empirically works well, so try new ideas if you think they'll work well on your feature distribution.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/data-prep/transform/normalization" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "z-score", + "suggested_values_reasoning": "Z-score is a variation of scaling that represents the number of standard deviations away from the mean. You would use z-score to ensure your feature distributions have mean = 0 and std = 1. It\u2019s useful when there are a few outliers, but not so extreme that you need clipping.", + "ui_display_name": "Normalization" + }, + "title": "normalization", + "type": [ + "string", + "null" + ] + }, + "outlier_strategy": { + "default": null, + "description": "Determines how outliers will be handled in the dataset. In most cases, replacing outliers with the column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers is to leave them in the data, which is the behavior when this parameter is left as `null`.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row", + "fill_with_mean", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Outlier definitions and how to handle them are very task-specific, so we leave this feature disabled by default and ask the user to choose the strategy that works best for them.", + "description_implications": "Determines how outliers will be handled in the dataset. In most cases replacing outliers with the column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers is to leave them in the data, which is the behavior when this parameter is left as `null`.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "outlier_threshold" + ], + "short_description": "", + "suggested_values": "fill_with_mean", + "suggested_values_reasoning": null, + "ui_display_name": "Outlier Strategy" + }, + "title": "outlier_strategy", + "type": [ + "string", + "null" + ] + }, + "outlier_threshold": { + "default": 3.0, + "description": "Standard deviations from the mean past which a value is considered an outlier. The 3-sigma rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations of the mean (see: [68\u201395\u201399.7 rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule)). As such anything farther away than that is highly likely to be an outlier, and may distort the learning process by disproportionately affecting the model.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The definition of an outlier is often dataset and task dependent, but 2 or 3 standard deviations from the mean is a common heuristic.", + "description_implications": "Determines the threshold past which a number will be considered an outlier in the dataset. The 3-sigma rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations of the mean (see: 68\u201395\u201399.7 rule). As such anything farther away than that is highly likely to be an outlier, and may distort the learning process by disproportionately affecting the model.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "outlier_strategy" + ], + "short_description": "", + "suggested_values": "2 - 3", + "suggested_values_reasoning": null, + "ui_display_name": "Outlier Threshold" + }, + "title": "outlier_threshold", + "type": "number" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "sequence" + } + } + }, + "then": { + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "generator" + } + } + }, + "then": { + "properties": { + "cell_type": { + "default": "gru", + "description": "Type of recurrent cell to use.", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data and task. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "sum", + "mean", + "avg", + "max", + "concat", + "last" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "\u201clast\u201d: Reduces tensor by taking the last non-zero element per sequence in the sequence dimension.\n\u201csum\u201d: Reduces tensor by summing across the sequence dimension.\n\u201cmean\u201d: Reduces tensor by taking the mean of the sequence dimension.\n\u201cavg\u201d: synonym for \u201cmean\u201d.\n\u201cmax\u201d: Reduces tensor by taking the maximum value of the last dimension across the sequence dimension.\n\u201cconcat\u201d: Reduces tensor by concatenating the second and last dimension.\n\u201cattention\u201d: Reduces tensor by summing across the sequence dimension after applying feedforward attention.\n\u201cnone\u201d: no reduction.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Combiner Reduce Mode" + }, + "title": "reduce_input", + "type": "string" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tagger" + } + } + }, + "then": { + "properties": { + "attention_embedding_size": { + "default": 256, + "description": "The embedding size of the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "Increasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "128 - 2048", + "suggested_values_reasoning": "Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Attention Embedding Size" + }, + "title": "attention_embedding_size", + "type": "integer" + }, + "attention_num_heads": { + "default": 8, + "description": "The number of attention heads in the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attention_num_heads", + "type": "integer" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_attention": { + "default": false, + "description": "Whether to apply a multi-head self attention layer before prediction.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_attention", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "generator", + "enum": [ + "generator", + "tagger" + ], + "enumDescriptions": { + "generator": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The generator decoder is a (potentially empty) stack of fully connected layers, followed by an RNN that generates outputs feeding on its own previous predictions and generates a tensor of size `b x s' x c`, where `b` is the batch size, `s'` is the length of the generated sequence and `c` is the number of classes, followed by a softmax_cross_entropy. During training teacher forcing is adopted, meaning the list of targets is provided as both inputs and outputs (shifted by 1), while at evaluation time greedy decoding (generating one token at a time and feeding it as input for the next step) is performed by beam search, using a beam of 1 by default. In general a generator expects a `b x h` shaped input tensor, where `h` is a hidden dimension. The `h` vectors are (after an optional stack of fully connected layers) fed into the rnn generator. One exception is when the generator uses attention, as in that case the expected size of the input tensor is `b x s x h`, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. If a `b x h` input is provided to a generator decoder using an RNN with attention instead, an error will be raised during model building.", + "other_information": null, + "related_parameters": null, + "short_description": "Generates a sequence by sampling from the model.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "tagger": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The tagger decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of size `b x s x c`, where `b` is the batch size, `s` is the length of the sequence and `c` is the number of classes, followed by a `softmax_cross_entropy`.\nThis decoder requires its input to be shaped as `b x s x h`, where `h` is a hidden dimension, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. This can be done by ensuring that at least one of the sequence, text or time series input feature's encoders has `reduce_output` set to `None`. This will prevent a `b x h` input from being provided to this decoder and an error from being raised during model building.\nThe tagger decoder also requires the `reduce_input` parameter of the output feature to be set to `None`. If this is not set, Ludwig will automatically override the value by setting it to None and log a warning.", + "other_information": null, + "related_parameters": null, + "short_description": "Used for classifying each element of an input sequence.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "loss", + "description": "Internal only use parameter: default validation metric for sequence output feature.", + "enum": [ + "loss" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sequence_softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "unique": { + "default": false, + "description": "If true, the loss is only computed for unique elements in the sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "unique", + "type": "boolean" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "sequence_softmax_cross_entropy", + "enum": [ + "sequence_softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "Compute encoder embeddings in preprocessing, speeding up training time considerably.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "max_sequence_length": { + "default": 256, + "description": "The maximum length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 256. Every sequence will be truncated to this length.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size, embedding_size" + ], + "short_description": "", + "suggested_values": "Use the lowest value that covers most of your input data. Only increase the value if crucial parts of the input data are truncated.", + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Sequence Length" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a sequence output feature", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 20000, + "description": "The maximum number of most common tokens in the vocabulary. If the data contains more than this amount, the most infrequent symbols will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": "integer" + }, + "ngram_size": { + "default": 2, + "description": "The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Size of the n-gram when using the `ngram` tokenizer.", + "description_implications": null, + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "n-gram size" + }, + "title": "ngram_size", + "type": "integer" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "padding_symbol": { + "default": "", + "description": "The string used as a padding symbol. This special token is mapped to the integer ID 0 in the vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_symbol", + "type": "string" + }, + "sequence_length": { + "default": null, + "description": "The desired length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated and sequences shorter than this value will be padded. If None, sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": null, + "title": "sequence_length", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + }, + "unknown_symbol": { + "default": "", + "description": "The string used as an unknown placeholder. This special token is mapped to the integer ID 1 in the vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unknown_symbol", + "type": "string" + }, + "vocab_file": { + "default": null, + "description": "Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the first string until \t or \n is considered a word.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The vocabulary can be parsed automatically from the incoming input features.", + "description_implications": "It can be useful to specify your own vocabulary list if the vocabulary is very large, there's no out of the box tokenizer that fits your data, or if there are several uncommon or infrequently occurring tokens that we want to guarantee to be a part of the vocabulary, rather than treated as an unknown.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Vocab File" + }, + "title": "vocab_file", + "type": [ + "string", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "set" + } + } + }, + "then": { + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "classifier" + } + } + }, + "then": { + "properties": { + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "num_classes": { + "default": null, + "description": "Number of classes to predict.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "classifier", + "enum": [ + "classifier" + ], + "enumDescriptions": { + "classifier": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a vector of size of the number of available classes, followed by a sigmoid.", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output to a vector the size of the number of available classes.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "jaccard", + "description": "Internal only use parameter: default validation metric for set output feature.", + "enum": [ + "jaccard" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sigmoid_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "sigmoid_cross_entropy", + "enum": [ + "sigmoid_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": false, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "lowercase", + "type": "boolean" + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a set output feature", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 10000, + "description": "The maximum number of most common tokens to be considered. If the data contains more than this amount, the most infrequent tokens will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space", + "description": "Defines how to transform the raw text content of the dataset column to a set of elements. The default value space splits the string on spaces. Common options include: underscore (splits on underscore), comma (splits on comma), json (decodes the string into a set or a list through a JSON parser).", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + }, + "threshold": { + "default": 0.5, + "description": "The threshold used to convert output probabilities to predictions. Tokens with predictedprobabilities greater than or equal to threshold are predicted to be in the output set (True).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "threshold", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "text" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not null this parameter is a c x c matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. ", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "generator" + } + } + }, + "then": { + "properties": { + "cell_type": { + "default": "gru", + "description": "Type of recurrent cell to use.", + "enum": [ + "rnn", + "lstm", + "gru" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "cell_type", + "type": "string" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "num_layers": { + "default": 1, + "description": "The number of stacked recurrent layers.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The ideal number of layers depends on the data and task. For many data types, one layer is sufficient.", + "description_implications": "Increasing the number of layers may improve model performance for longer sequences or more complex tasks.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "1-3", + "suggested_values_reasoning": "Increasing the number of layers may improve encoder performance. However, more layers will increase training time and may cause overfitting. Small numbers of layers usually work best.", + "ui_display_name": "Number of Recurrent Layers" + }, + "title": "num_layers", + "type": "integer" + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "sum", + "mean", + "avg", + "max", + "concat", + "last" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "\u201clast\u201d: Reduces tensor by taking the last non-zero element per sequence in the sequence dimension.\n\u201csum\u201d: Reduces tensor by summing across the sequence dimension.\n\u201cmean\u201d: Reduces tensor by taking the mean of the sequence dimension.\n\u201cavg\u201d: synonym for \u201cmean\u201d.\n\u201cmax\u201d: Reduces tensor by taking the maximum value of the last dimension across the sequence dimension.\n\u201cconcat\u201d: Reduces tensor by concatenating the second and last dimension.\n\u201cattention\u201d: Reduces tensor by summing across the sequence dimension after applying feedforward attention.\n\u201cnone\u201d: no reduction.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Combiner Reduce Mode" + }, + "title": "reduce_input", + "type": "string" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tagger" + } + } + }, + "then": { + "properties": { + "attention_embedding_size": { + "default": 256, + "description": "The embedding size of the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "Increasing the embedding size may cause the model to train more slowly, but the higher dimensionality can also improve overall quality.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "128 - 2048", + "suggested_values_reasoning": "Try models with smaller or larger embedding sizes to observe relative impact.", + "ui_display_name": "Attention Embedding Size" + }, + "title": "attention_embedding_size", + "type": "integer" + }, + "attention_num_heads": { + "default": 8, + "description": "The number of attention heads in the multi-head self attention layer.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "attention_num_heads", + "type": "integer" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": 256, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": "integer" + }, + "max_sequence_length": { + "default": null, + "description": "[internal] Maximum sequence length from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Sets the maximum sequence length of the expected inputs, so input/output shapes are computed accurately.", + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "use_attention": { + "default": false, + "description": "Whether to apply a multi-head self attention layer before prediction.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "use_attention", + "type": "boolean" + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": [ + true + ], + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "vocab_size": { + "default": null, + "description": "[internal] Size of the vocabulary from preprocessing.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not displayed" + }, + "title": "vocab_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "generator", + "enum": [ + "generator", + "tagger" + ], + "enumDescriptions": { + "generator": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The generator decoder is a (potentially empty) stack of fully connected layers, followed by an RNN that generates outputs feeding on its own previous predictions and generates a tensor of size `b x s' x c`, where `b` is the batch size, `s'` is the length of the generated sequence and `c` is the number of classes, followed by a softmax_cross_entropy. During training teacher forcing is adopted, meaning the list of targets is provided as both inputs and outputs (shifted by 1), while at evaluation time greedy decoding (generating one token at a time and feeding it as input for the next step) is performed by beam search, using a beam of 1 by default. In general a generator expects a `b x h` shaped input tensor, where `h` is a hidden dimension. The `h` vectors are (after an optional stack of fully connected layers) fed into the rnn generator. One exception is when the generator uses attention, as in that case the expected size of the input tensor is `b x s x h`, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. If a `b x h` input is provided to a generator decoder using an RNN with attention instead, an error will be raised during model building.", + "other_information": null, + "related_parameters": null, + "short_description": "Generates a sequence by sampling from the model.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "tagger": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The tagger decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of size `b x s x c`, where `b` is the batch size, `s` is the length of the sequence and `c` is the number of classes, followed by a `softmax_cross_entropy`.\nThis decoder requires its input to be shaped as `b x s x h`, where `h` is a hidden dimension, which is the output of a sequence, text or time series input feature without reduced outputs or the output of a sequence-based combiner. This can be done by ensuring that at least one of the sequence, text or time series input feature's encoders has `reduce_output` set to `None`. This will prevent a `b x h` input from being provided to this decoder and an error from being raised during model building.\nThe tagger decoder also requires the `reduce_input` parameter of the output feature to be set to `None`. If this is not set, Ludwig will automatically override the value by setting it to None and log a warning.", + "other_information": null, + "related_parameters": null, + "short_description": "Used for classifying each element of an input sequence.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "loss", + "description": "Internal only use parameter: default validation metric for binary output feature.", + "enum": [ + "loss" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sequence_softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "unique": { + "default": false, + "description": "If true, the loss is only computed for unique elements in the sequence.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "unique", + "type": "boolean" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "sequence_softmax_cross_entropy", + "enum": [ + "sequence_softmax_cross_entropy" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "cache_encoder_embeddings": { + "default": false, + "description": "For pretrained encoders, compute encoder embeddings in preprocessing, speeding up training time considerably. Only supported when `encoder.trainable=false`.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so it's not always the case that you would always want to enable it when possible.", + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Cache Encoder Embeddings" + }, + "title": "cache_encoder_embeddings", + "type": "boolean" + }, + "compute_idf": { + "default": false, + "description": "", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "compute_idf", + "type": "boolean" + }, + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the `missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": [ + "Depends on dtype" + ], + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "missing_value_strategy, fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "DOCSTRING ONLY" + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "lowercase": { + "default": true, + "description": "If true, converts the string to lowercase before tokenizing.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Reading the text in lowercase enables the model to treat capitalized and lowercase words as the same, effectively increasing the number of data points per word.", + "description_implications": "If you set lowercase to False, then capitalized words are seen as completely separate entities than lowercase words.", + "example_value": [ + true + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size" + ], + "short_description": "", + "suggested_values": "TRUE", + "suggested_values_reasoning": "If there is a strong reason to treat capitalized words and lowercased words differently, then set this to False. Otherwise, it is preferable to bucket the words and make the model case-insensitive.", + "ui_display_name": "Convert to lowercase" + }, + "title": "lowercase", + "type": "boolean" + }, + "max_sequence_length": { + "default": 256, + "description": "The maximum length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 256. Every sequence will be truncated to this length.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_size, embedding_size" + ], + "short_description": "", + "suggested_values": "Use the lowest value that covers most of your input data. Only increase the value if crucial parts of the input data are truncated.", + "suggested_values_reasoning": null, + "ui_display_name": "Maximum Sequence Length" + }, + "title": "max_sequence_length", + "type": [ + "integer", + "null" + ] + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a text output feature.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "most_common": { + "default": 20000, + "description": "The maximum number of most common tokens in the vocabulary. If the data contains more than this amount, the most infrequent symbols will be treated as unknown.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "If there are more than 10000 unique categories in the data, it is likely that they will follow a long-tailed distribution and the least common ones may not provide a lot of information", + "description_implications": "A smaller number will reduce the vocabulary, making the embedding matrix smaller and reduce the memory footprint, but will also collapse more tokens into the rare one, so the model may perform worse when rare tokens appear in the data", + "example_value": [ + 10000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Specifying a vocab_file overrides this parameter", + "related_parameters": [ + "vocab_file, pretrained_embeddings" + ], + "short_description": "", + "suggested_values": "A value that covers at least 95% of the tokens in the data", + "suggested_values_reasoning": "Depending on the data distribution and how important rare tokens are, 90%, 95% or 99% of the number of tokens will leave out only very rare tokens that should not influence performance substantially", + "ui_display_name": "Most common (vocabulary size)" + }, + "title": "most_common", + "type": "integer" + }, + "ngram_size": { + "default": 2, + "description": "The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Size of the n-gram when using the `ngram` tokenizer.", + "description_implications": null, + "example_value": [ + 3 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "n-gram size" + }, + "title": "ngram_size", + "type": "integer" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "We usually want to add padding to the end of a text sequence to fill in any remaining space as opposed to the beggining so we set the default to right.", + "description_implications": "If you pad to the left, the encoded vector will have leading padding tokens as opposed to trailing padding tokens. This could matter based on the type of text input you are expecting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "padding_symbol,\nmax_sequence_length" + ], + "short_description": "", + "suggested_values": "'right'", + "suggested_values_reasoning": "right padding is the usual way to add padding to a text sequence", + "ui_display_name": "Padding" + }, + "title": "padding", + "type": "string" + }, + "padding_symbol": { + "default": "", + "description": "The string used as the padding symbol for sequence features. Ignored for features using huggingface encoders, which have their own vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_symbol", + "type": "string" + }, + "pretrained_model_name_or_path": { + "default": null, + "description": "This can be either the name of a pretrained HuggingFace model or a path where it was downloaded.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "pretrained_model_name_or_path", + "type": [ + "string", + "null" + ] + }, + "sequence_length": { + "default": null, + "description": "The desired length (number of tokens) of the sequence. Sequences that are longer than this value will be truncated and sequences shorter than this value will be padded. If None, sequence length will be inferred from the training dataset.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is `None`. Which means that the sequence length will be inferred from the dataset, which may save you compute resources on datasets with short text samples.", + "description_implications": "A larger sequence length keeps more information from the data, but also makes it more computationally expensive (more memory and longer training time). A smaller sequence length keeps less information from the data, but also makes it less computationally expensive (less memory and shorter training time).", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "max_sequence_length" + ], + "short_description": "", + "suggested_values": "If tying the weights of multiple text encoders together, this parameter may need to be set to ensure that all text features have the same sequence length.", + "suggested_values_reasoning": null, + "ui_display_name": "Sequence Length" + }, + "title": "sequence_length", + "type": [ + "integer", + "null" + ] + }, + "tokenizer": { + "default": "space_punct", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default tokenizer is `space_punct`, an abbreviation of \"Space punctuation\". This tokenizer creates sub-words by dividing the text on whitespace and punctuation characters. For example: The text `'hello world!isn't this great?'` would be transformed to `['hello', 'world', '!', 'isn', \"'\", 't', 'this', 'great', '?']`. This is the default value because it is a fast tokenizer that works reasonably well.", + "description_implications": "Choosing a tokenizer can be difficult. The primary thing to check is that the tokenizer you have selected is compatible with the language(s) in your text data. This means either selecting a tokenizer that is language-specific (i.e. `french_tokenize` if working with French text) or general enough that its tokenizations are language-agnostic (i.e. `space_punct`).", + "example_value": [ + "space_punct" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://huggingface.co/course/chapter2/4?fw=pt" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "vocab_file, pretrained_model_name_or_path" + ], + "short_description": "", + "suggested_values": "sentencepiece", + "suggested_values_reasoning": "SentencePiece is a tokenizer developed by Google which utilizes Byte-Pair Encoding (BPE), which strikes a good balance between character-level and word-level tokenization (more info on BPE here: https://towardsdatascience.com/byte-pair-encoding-the-dark-horse-of-modern-nlp-eb36c7df4f10 ). This tokenizer is language-agnostic and more sophisticated than the default.", + "ui_display_name": "Tokenizer" + }, + "title": "tokenizer", + "type": "string" + }, + "unknown_symbol": { + "default": "", + "description": "The string used as the unknown symbol for sequence features. Ignored for features using huggingface encoders, which have their own vocabulary.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "unknown_symbol", + "type": "string" + }, + "vocab_file": { + "default": null, + "description": "Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the first string until `\\t` or `\\n` is considered a word.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The vocabulary can be parsed automatically from the incoming input features.", + "description_implications": "It can be useful to specify your own vocabulary list if the vocabulary is very large, there's no out of the box tokenizer that fits your data, or if there are several uncommon or infrequently occurring tokens that we want to guarantee to be a part of the vocabulary, rather than treated as an unknown.", + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Vocab File" + }, + "title": "vocab_file", + "type": [ + "string", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": "sum", + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": "sum", + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "timeseries" + } + } + }, + "then": { + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "projector" + } + } + }, + "then": { + "properties": { + "activation": { + "default": null, + "description": " Indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "clip": { + "default": null, + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": null, + "description": "Clip the output of the decoder to be within the given range.", + "items": [ + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + }, + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "description": "None", + "title": "null_float_tuple_option", + "type": "null" + } + ], + "title": "clip" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "multiplier": { + "default": 1.0, + "description": "Multiplier to scale the activated outputs by. Useful when setting `activation` to something that outputs a value between [-1, 1] like tanh to re-scale values back to order of magnitude of the data you're trying to predict. A good rule of thumb in such cases is to pick a value like `x * (max - min)` where x is a scalar in the range [1, 2]. For example, if you're trying to predict something like temperature, it might make sense to pick a multiplier on the order of `100`.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "multiplier", + "type": "number" + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": null, + "description": "Size of the output of the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": [ + "integer", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "projector", + "enum": [ + "projector" + ], + "enumDescriptions": { + "projector": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Projector decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of the vector size (optionally followed by a softmax in the case of multi-class classification).", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output into an output vector.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "mean_squared_error", + "description": "Internal parameter.", + "enum": [ + "mean_squared_error" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "horizon": { + "default": null, + "description": "Internal parameter. Obtained from preprocessing", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "horizon", + "type": [ + "integer", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "huber" + } + } + }, + "then": { + "properties": { + "delta": { + "default": 1.0, + "description": "Threshold at which to change between delta-scaled L1 and L2 loss.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "delta", + "type": "number" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "huber", + "enum": [ + "mean_squared_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "huber" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the `missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "title": "fill_value", + "type": "string" + }, + "horizon": { + "default": 0, + "description": "Optional forecasting horizon used to convert a column-major dataset (one observation per row) into a row-major dataset (each row has a timeseries window of observations). Starting from a given observation, a sliding window is token going `horizon` rows forward in time, excluding the observation in the current row. If this value is left as 0, then it is assumed that the dataset has been provided in row-major format (i.e., it has already been preprocessed such that each row is a timeseries window).", + "minimum": 0, + "parameter_metadata": null, + "title": "horizon", + "type": "integer" + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when a row of data is missing.", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "padding": { + "default": "right", + "description": "The direction of the padding.", + "enum": [ + "left", + "right" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding", + "type": "string" + }, + "padding_value": { + "default": 0.0, + "description": "Float value that is used for padding and replacing missing values within a row.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "padding_value", + "type": "number" + }, + "timeseries_length_limit": { + "default": 256, + "description": "Defines the maximum length of the timeseries. All timeseries longer than this limit are cut off.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "timeseries_length_limit", + "type": "integer" + }, + "tokenizer": { + "default": "space", + "description": "Defines how to map from the raw string content of the dataset column to a sequence of elements.", + "enum": [ + "space", + "space_punct", + "ngram", + "characters", + "underscore", + "comma", + "untokenized", + "stripped", + "english_tokenize", + "english_tokenize_filter", + "english_tokenize_remove_stopwords", + "english_lemmatize", + "english_lemmatize_filter", + "english_lemmatize_remove_stopwords", + "italian_tokenize", + "italian_tokenize_filter", + "italian_tokenize_remove_stopwords", + "italian_lemmatize", + "italian_lemmatize_filter", + "italian_lemmatize_remove_stopwords", + "spanish_tokenize", + "spanish_tokenize_filter", + "spanish_tokenize_remove_stopwords", + "spanish_lemmatize", + "spanish_lemmatize_filter", + "spanish_lemmatize_remove_stopwords", + "german_tokenize", + "german_tokenize_filter", + "german_tokenize_remove_stopwords", + "german_lemmatize", + "german_lemmatize_filter", + "german_lemmatize_remove_stopwords", + "french_tokenize", + "french_tokenize_filter", + "french_tokenize_remove_stopwords", + "french_lemmatize", + "french_lemmatize_filter", + "french_lemmatize_remove_stopwords", + "portuguese_tokenize", + "portuguese_tokenize_filter", + "portuguese_tokenize_remove_stopwords", + "portuguese_lemmatize", + "portuguese_lemmatize_filter", + "portuguese_lemmatize_remove_stopwords", + "dutch_tokenize", + "dutch_tokenize_filter", + "dutch_tokenize_remove_stopwords", + "dutch_lemmatize", + "dutch_lemmatize_filter", + "dutch_lemmatize_remove_stopwords", + "greek_tokenize", + "greek_tokenize_filter", + "greek_tokenize_remove_stopwords", + "greek_lemmatize", + "greek_lemmatize_filter", + "greek_lemmatize_remove_stopwords", + "norwegian_tokenize", + "norwegian_tokenize_filter", + "norwegian_tokenize_remove_stopwords", + "norwegian_lemmatize", + "norwegian_lemmatize_filter", + "norwegian_lemmatize_remove_stopwords", + "lithuanian_tokenize", + "lithuanian_tokenize_filter", + "lithuanian_tokenize_remove_stopwords", + "lithuanian_lemmatize", + "lithuanian_lemmatize_filter", + "lithuanian_lemmatize_remove_stopwords", + "danish_tokenize", + "danish_tokenize_filter", + "danish_tokenize_remove_stopwords", + "danish_lemmatize", + "danish_lemmatize_filter", + "danish_lemmatize_remove_stopwords", + "polish_tokenize", + "polish_tokenize_filter", + "polish_tokenize_remove_stopwords", + "polish_lemmatize", + "polish_lemmatize_filter", + "polish_lemmatize_remove_stopwords", + "romanian_tokenize", + "romanian_tokenize_filter", + "romanian_tokenize_remove_stopwords", + "romanian_lemmatize", + "romanian_lemmatize_filter", + "romanian_lemmatize_remove_stopwords", + "japanese_tokenize", + "japanese_tokenize_filter", + "japanese_tokenize_remove_stopwords", + "japanese_lemmatize", + "japanese_lemmatize_filter", + "japanese_lemmatize_remove_stopwords", + "chinese_tokenize", + "chinese_tokenize_filter", + "chinese_tokenize_remove_stopwords", + "chinese_lemmatize", + "chinese_lemmatize_filter", + "chinese_lemmatize_remove_stopwords", + "multi_tokenize", + "multi_tokenize_filter", + "multi_tokenize_remove_stopwords", + "multi_lemmatize", + "multi_lemmatize_filter", + "multi_lemmatize_remove_stopwords", + "sentencepiece", + "clip", + "gpt2bpe", + "bert", + "hf_tokenizer" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "tokenizer", + "type": "string" + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": null, + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": null, + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "vector" + } + } + }, + "then": { + "properties": { + "decoder": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "projector" + } + } + }, + "then": { + "properties": { + "activation": { + "default": null, + "description": " Indicates the activation function applied to the output.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "The default value will work well in the majority of the cases", + "suggested_values_reasoning": null, + "ui_display_name": "Activation" + }, + "title": "activation", + "type": [ + "string", + "null" + ] + }, + "bias_initializer": { + "default": "zeros", + "description": "Initializer for the bias vector.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights.", + "description_implications": "It's rare to see any performance gains from choosing a different bias initialization. Some practitioners like to use a small constant value such as 0.01 for all biases to ensure that all ReLU units are activated in the beginning and have some effect on the gradient. However, it's still an open question as to whether this provides consistent improvement.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://cs231n.github.io/neural-networks-2/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "weights_initializer" + ], + "short_description": "", + "suggested_values": "zeros", + "suggested_values_reasoning": "It is possible and common to initialize the biases to be zero, since the asymmetry breaking is provided by the small random numbers in the weights. For ReLU non-linearities, some people like to use small constant value such as 0.01 for all biases because this ensures that all ReLU units fire in the beginning and therefore obtain and propagate some gradient. However, it is not clear if this provides a consistent improvement (in fact some results seem to indicate that this performs worse) and it is more common to simply use 0 bias initialization.", + "ui_display_name": "Bias Initializer" + }, + "title": "bias_initializer", + "type": "string" + }, + "clip": { + "default": null, + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": null, + "description": "Clip the output of the decoder to be within the given range.", + "items": [ + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + }, + { + "maximum": 999999999, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + }, + { + "description": "None", + "title": "null_float_tuple_option", + "type": "null" + } + ], + "title": "clip" + }, + "fc_activation": { + "default": "relu", + "description": "Default activation function applied to the output of the fully connected layers.", + "enum": [ + "elu", + "leakyRelu", + "logSigmoid", + "relu", + "sigmoid", + "tanh", + "softmax", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The Rectified Linear Units (ReLU) function is the standard activation function used for adding non-linearity. It is simple, fast, and empirically works well (https://arxiv.org/abs/1803.08375).", + "description_implications": "Changing the activation functions has an impact on the computational load of the model and might require further hypterparameter tuning", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "relu", + "suggested_values_reasoning": "ReLU will work well in the majority of the cases", + "ui_display_name": "Activation" + }, + "title": "fc_activation", + "type": [ + "string", + "null" + ] + }, + "fc_bias_initializer": { + "default": "zeros", + "description": "The bias initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured bias initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom bias initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_bias_initializer" + }, + "fc_dropout": { + "default": 0.0, + "description": "Default dropout rate applied to fully connected layers. Increasing dropout is a common form of regularization to combat overfitting. The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Dropout can cause training to become less stable. Consider start with a dropout-free baseline, and add dropout gradually in subsequent experiments.", + "description_implications": "Dropout is a computationally cheap regularization method where during training, some neurons are randomly ignored or \u201cdropped out\u201d. Increasing dropout has the effect of making the training process more noisy and lowering overall network capacity, but it can be an effective regularization method to reduce overfitting and improve generalization.", + "example_value": [ + 0.2 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "0.05 - 0.8", + "suggested_values_reasoning": "Tuning dropout is really something to be done when all of the big choices about architecture have been settled. Consider starting with 0.5 and adjusting the dropout depending on observed model performance.", + "ui_display_name": "Dropout" + }, + "title": "fc_dropout", + "type": "number" + }, + "fc_layers": { + "default": null, + "description": "List of dictionaries containing the parameters of all the fully connected layers. The length of the list determines the number of stacked fully connected layers and the content of each dictionary determines the parameters for a specific layer. The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, `output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values is missing from the dictionary, the default one provided as a standalone parameter will be used instead.", + "items": { + "additionalProperties": {}, + "title": "fc_layers", + "type": "object" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default the stack is built by using num_fc_layers, output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params, activation, dropout. When a list of dictionaries is provided, the stack is built following the parameters of each dict for building each layer.", + "description_implications": "The more layers that are specified the deeper and higher capacity the model will be. This makes it possible to potentially achieve better performance when a big anough amount of data is provided, but also makes the model more computationally expensive and potentially more prone to overfitting.", + "example_value": [ + { + "dropout": 0.1, + "output_size": 128 + }, + { + "norm": "layer", + "output_size": 64 + } + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "output_size", + "use_bias", + "weights_initializer", + "bias_initializer", + "norm", + "norm_params", + "activation", + "dropout" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It is easier to define a stack of fully connected layers by just specifying num_fc_layers, output_size and the other individual parameters. It will create a stack of layers with identical properties. Use this parameter only if you need a fine grained level of control of each individual layer in the stack.", + "ui_display_name": "Fully Connected Layers" + }, + "title": "fc_layers", + "type": [ + "array", + "null" + ] + }, + "fc_norm": { + "default": null, + "description": "Default normalization applied at the beginnging of fully connected layers.", + "enum": [ + "batch", + "layer", + "ghost", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "While batch normalization and layer normalization usually lead to improvements, it can be useful to start with fewer bells and whistles.", + "description_implications": "Normalization helps stabilize the learning process and can have a regularizing effect that can help with generalization. It's often suggested that with normalization, you can use a higher learning rate.", + "example_value": [ + "batch" + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "norm_params" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "Normalization tries to solve \"internal covariate shift\" that comes from the changing distributions of the inputs to layers deep in the network when weights are updated. For example, batch normalization standardizes the inputs to a layer for each mini-batch. Try out different normalizations to see if that helps with training stability", + "ui_display_name": "Normalization Type" + }, + "title": "fc_norm", + "type": [ + "string", + "null" + ] + }, + "fc_norm_params": { + "additionalProperties": {}, + "default": null, + "description": "Default parameters passed to the `norm` module.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "fc_norm_params", + "type": [ + "object", + "null" + ] + }, + "fc_output_size": { + "default": 256, + "description": "Output size of fully connected stack.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_output_size", + "type": "integer" + }, + "fc_use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector in the fc_stack.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_use_bias", + "type": "boolean" + }, + "fc_weights_initializer": { + "default": "xavier_uniform", + "description": "The weights initializer to use for the layers in the fc_stack", + "oneOf": [ + { + "default": "xavier_uniform", + "description": "Preconfigured initializer to use for the layers in the fc_stack.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_string_option", + "type": "string" + }, + { + "additionalProperties": {}, + "default": null, + "description": "Custom initializer to use for the layers in the fc_stack.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer_dict_option", + "type": [ + "object", + "null" + ] + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "fc_weights_initializer" + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": "Internal Only", + "related_parameters": [ + "No" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Not Displayed" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "multiplier": { + "default": 1.0, + "description": "Multiplier to scale the activated outputs by. Useful when setting `activation` to something that outputs a value between [-1, 1] like tanh to re-scale values back to order of magnitude of the data you're trying to predict. A good rule of thumb in such cases is to pick a value like `x * (max - min)` where x is a scalar in the range [1, 2]. For example, if you're trying to predict something like temperature, it might make sense to pick a multiplier on the order of `100`.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "multiplier", + "type": "number" + }, + "num_fc_layers": { + "default": 0, + "description": "Number of fully-connected layers if `fc_layers` not specified. Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The encoder already has learnable parameters.Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "description_implications": "Increasing num_fc_layers will increase the capacity of the model. The model will be slower to train, and there's a higher risk of overfitting.", + "example_value": [ + 1 + ], + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Not all modules that have fc_layers also have an accompanying num_fc_layers parameter. Where both are present, fc_layers takes precedent over num_fc_layers. Specifying num_fc_layers alone uses fully connected layers that are configured by the defaults in FCStack.", + "related_parameters": [ + "fc_layers" + ], + "short_description": "", + "suggested_values": "0-1", + "suggested_values_reasoning": "The full model likely contains many learnable parameters. Consider starting with very few, or without any additional fully connected layers and add them if you observe evidence of limited model capacity. Sometimes the default is 1 for modules where the FC stack is used for shape management, or the only source of learnable parameters.", + "ui_display_name": "Number of Fully Connected Layers" + }, + "title": "num_fc_layers", + "type": "integer" + }, + "output_size": { + "default": null, + "description": "Size of the output of the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A modest value, not too small, not too large.", + "description_implications": "If there are fully connected layers in this module, increasing the output size of each fully connected layer will increase the capacity of the model. However, the model may be slower to train, and there's a higher risk of overfitting. If it seems like the model could use even more capacity, consider increasing the number of fully connected layers, or explore other architectures.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If num_fc_layers=0 and fc_layers=None, and there are no fully connected layers defined on the module, then this parameter may have no effect on the module's final output shape.", + "related_parameters": [ + "num_fc_layers, fc_layers" + ], + "short_description": "", + "suggested_values": "10 - 1024", + "suggested_values_reasoning": "Increasing the output size increases the capacity of the model. If this seems to have a positive effect, then it could be worth increasing the number of layers, or trying a different architecture with a larger capacity.", + "ui_display_name": "Output Size" + }, + "title": "output_size", + "type": [ + "integer", + "null" + ] + }, + "use_bias": { + "default": true, + "description": "Whether the layer uses a bias vector.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to use bias terms.\n\nBatch Normalization, however, adds a trainable shift parameter which is added to the activation. When Batch Normalization is used in a layer, bias terms are redundant and may be removed.", + "description_implications": "Bias terms may improve model accuracy, and don't have much impact in terms of memory or training speed. For most models it is reasonable to leave this parameter set to True.", + "example_value": [ + true + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "If fc_layers is not specified, or use_bias is not specified for individual layers, the value of use_bias will be used as the default for all layers.", + "related_parameters": [ + "bias_initializer, fc_layers" + ], + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": null, + "ui_display_name": "Use Bias" + }, + "title": "use_bias", + "type": "boolean" + }, + "weights_initializer": { + "default": "xavier_uniform", + "description": "Initializer for the weight matrix.", + "enum": [ + "uniform", + "normal", + "constant", + "ones", + "zeros", + "eye", + "dirac", + "xavier_uniform", + "xavier_normal", + "kaiming_uniform", + "kaiming_normal", + "orthogonal", + "sparse", + "identity" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).", + "description_implications": "The method you choose to initialize layer weights during training can have a big impact on performance as well as the reproducibility of your final model between runs. As an example, if you were to randomly initialize weights you would risk non-reproducibility (and possibly general training performance), but sticking with constant values for initialization might significantly increase the time needed for model convergence. Generally, choosing one of the probabilistic approaches strikes a balance between the two extremes, and the literature kicked off by the landmark [*Xavier et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf) provides a few good options. See this nice discussion from [Weights and Biases](https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.) for more information.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "Weights and Biases blog post: https://wandb.ai/site/articles/the-effects-of-weight-initialization-on-neural-nets#:~:text=Studies%20have%20shown%20that%20initializing,net%20train%20better%20and%20faster.", + "Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "xavier_uniform", + "suggested_values_reasoning": "Changing the weights initialization scheme is something to consider if a model is having trouble with convergence, or otherwise it is something to experiment with after other factors are considered. The default choice (`xavier_uniform`) is a suitable starting point for most tasks.", + "ui_display_name": "Layer Weights Initializer" + }, + "title": "weights_initializer", + "type": "string" + } + } + } + } + ], + "properties": { + "type": { + "default": "projector", + "enum": [ + "projector" + ], + "enumDescriptions": { + "projector": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": false, + "literature_references": null, + "long_description": "The Projector decoder is a (potentially empty) stack of fully connected layers, followed by a projection into a tensor of the vector size (optionally followed by a softmax in the case of multi-class classification).", + "other_information": null, + "related_parameters": null, + "short_description": "Projects combiner output into an output vector.", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + } + }, + "type": "string" + } + }, + "title": "decoder_options", + "type": "object" + }, + "default_validation_metric": { + "default": "mean_squared_error", + "description": "Internal only use parameter: default validation metric for binary output feature.", + "enum": [ + "mean_squared_error" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "default_validation_metric", + "type": "string" + }, + "dependencies": { + "default": [], + "description": "List of input features that this feature depends on.", + "items": { + "title": "dependencies", + "type": "string" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dependencies", + "type": [ + "array", + "null" + ] + }, + "input_size": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "input_size", + "type": [ + "integer", + "null" + ] + }, + "loss": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "mean_squared_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "mean_absolute_percentage_error" + } + } + }, + "then": { + "properties": { + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "softmax_cross_entropy" + } + } + }, + "then": { + "properties": { + "class_similarities": { + "default": null, + "description": "If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too).", + "items": { + "items": { + "format": "float", + "title": "class_similarities", + "type": "number" + }, + "title": "class_similarities", + "type": "array" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities", + "type": [ + "array", + "null" + ] + }, + "class_similarities_temperature": { + "default": 0, + "description": "The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more tolerable than errors between really different classes.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_similarities_temperature", + "type": "integer" + }, + "class_weights": { + "default": null, + "description": "Weights to apply to each class in the loss. If not specified, all classes are weighted equally. The value can be a vector of weights, one for each class, that is multiplied to the loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in case of unbalanced class distribution. The ordering of the vector follows the category to integer ID mapping in the JSON metadata file (the `` class needs to be included too). Alternatively, the value can be a dictionary with class strings as keys and weights as values, like `{class_a: 0.5, class_b: 0.7, ...}`.", + "items": { + "format": "float", + "title": "class_weights", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "class_weights", + "type": [ + "array", + "null" + ] + }, + "confidence_penalty": { + "default": 0, + "description": "Penalizes overconfident predictions (low entropy) by adding an additional term that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` term to the loss, where a is the value of this parameter. Useful in case of noisy labels.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "confidence_penalty", + "type": "number" + }, + "robust_lambda": { + "default": 0, + "description": "Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of classes. Useful in case of noisy labels.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "robust_lambda", + "type": "integer" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "huber" + } + } + }, + "then": { + "properties": { + "delta": { + "default": 1.0, + "description": "Threshold at which to change between delta-scaled L1 and L2 loss.", + "exclusiveMinimum": 0, + "format": "float", + "parameter_metadata": null, + "title": "delta", + "type": "number" + }, + "weight": { + "default": 1.0, + "description": "Weight of the loss.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight", + "type": "number" + } + } + } + } + ], + "properties": { + "type": { + "default": "mean_squared_error", + "enum": [ + "mean_squared_error", + "mean_absolute_error", + "mean_absolute_percentage_error", + "softmax_cross_entropy", + "huber" + ], + "type": "string" + } + }, + "title": "loss_options", + "type": "object" + }, + "num_classes": { + "default": null, + "description": "Size of the input to the decoder.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "num_classes", + "type": [ + "integer", + "null" + ] + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "computed_fill_value": { + "default": "", + "description": "The internally computed fill value to replace missing values with in case the missing_value_strategy is fill_with_mode or fill_with_mean", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 0, + "internal_only": true, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "pattern": "^([0-9]+(\\.[0-9]*)?\\s*)*$", + "title": "computed_fill_value", + "type": "string" + }, + "fill_value": { + "default": "", + "description": "The value to replace missing values with in case the missing_value_strategy is fill_with_const", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Fill Value" + }, + "pattern": "^([0-9]+(\\.[0-9]*)?\\s*)*$", + "title": "fill_value", + "type": "string" + }, + "missing_value_strategy": { + "default": "drop_row", + "description": "What strategy to follow when there's a missing value in a vector output feature", + "enum": [ + "fill_with_const", + "fill_with_mode", + "bfill", + "ffill", + "drop_row" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default `fill_with_const` replaces missing values with the value specified by `fill_value`.", + "description_implications": "Determines how missing values will be handled in the dataset. Not all strategies are valid for all datatypes. For example, `fill_with_mean` is applicable to continuous numerical data. Note that choosing to drop rows with missing values could result in losing information, especially if there is a high proportion of missing values in the dataset.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "fill_value" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Missing Value Strategy" + }, + "title": "missing_value_strategy", + "type": "string" + }, + "vector_size": { + "default": null, + "description": "The size of the vector. If None, the vector size will be inferred from the data.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": null + }, + "title": "vector_size", + "type": [ + "integer", + "null" + ] + } + }, + "title": "preprocessing_options", + "type": "object" + }, + "reduce_dependencies": { + "default": null, + "description": "How to reduce the dependencies of the output feature.", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_dependencies", + "type": [ + "string", + "null" + ] + }, + "reduce_input": { + "default": null, + "description": "How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first dimension (second if you count the batch dimension)", + "enum": [ + "last", + "sum", + "mean", + "avg", + "max", + "concat", + "attention", + "none", + "None", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "reduce_input", + "type": [ + "string", + "null" + ] + }, + "softmax": { + "default": false, + "description": "Determines whether to apply a softmax at the end of the decoder. This is useful for predicting a vector of values that sum up to 1 and can be interpreted as probabilities.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "softmax", + "type": "boolean" + }, + "vector_size": { + "default": null, + "description": "The size of the vector. If None, the vector size will be inferred from the data.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "vector_size", + "type": [ + "integer", + "null" + ] + } + } + } + } + ], + "properties": { + "column": { + "description": "Name of the column.", + "title": "column", + "type": "string" + }, + "name": { + "description": "Name of the output feature.", + "title": "name", + "type": "string" + }, + "type": { + "description": "Type of the output feature", + "enum": [ + "binary", + "category", + "category_distribution", + "number", + "sequence", + "set", + "text", + "timeseries", + "vector" + ], + "title": "type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "output_feature", + "type": "object" + }, + "minItems": 1, + "title": "output_features", + "type": "array" + }, + "preprocessing": { + "additionalProperties": true, + "properties": { + "oversample_minority": { + "default": null, + "description": "If not None, the minority class will be oversampled to reach the specified ratio respective to the majority class. ", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "We do not want to randomly oversample by default since this is a strategy to deal with imbalanced datasets, but can cause issues if not implemented correctly.", + "description_implications": "The higher the value you choose gets to 1, the closer you will be to having an equal imbalance ratio (i.e. 1:1 positive to negative class), however this can lead to problems of overfitting when oversampling is used too liberally. As a rule of thumb, starting oversampling with a very conservative approach and increasing in small incremements is probably the best way to improve your model without experiencing model overfitting.", + "example_value": [ + 0.5 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/random-oversampling-and-undersampling-for-imbalanced-classification/" + ], + "long_description": "", + "other_information": "This parameter is one of many strategies to combat issues with class imbalance, though it is not a cure all. Oversampling too much can cause overfitting which can adversely affect your model so use with caution.", + "related_parameters": null, + "short_description": "", + "suggested_values": "Depends on imbalance ratio and dataset size", + "suggested_values_reasoning": null, + "ui_display_name": "Oversample Minority" + }, + "title": "oversample_minority", + "type": [ + "number", + "null" + ] + }, + "sample_ratio": { + "default": 1.0, + "description": "The ratio of the dataset to use. For instance, if 0.5, half of the dataset provided will be used.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "The default value is 1.0 because we do not want to shrink the dataset by default. In the rare occurences when you do want to downsample the entire dataset, this parameter is available, however it is not enabled by default, hence a default value of 1.0", + "description_implications": "Decreases the amount of data you are inputting into the model. Could be useful if you have more data than you need and you are concerned with computational costs.", + "example_value": [ + 0.8 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "Depends on data size", + "suggested_values_reasoning": null, + "ui_display_name": "Sample Ratio" + }, + "title": "sample_ratio", + "type": "number" + }, + "split": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "random" + } + } + }, + "then": { + "properties": { + "probabilities": { + "default": [ + 0.7, + 0.1, + 0.2 + ], + "description": "Probabilities for splitting data into train, validation, and test sets.", + "items": { + "format": "float", + "title": "probabilities", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Most of the dataset should be used for training, with some portion heldout for validation and testing.", + "description_implications": "In machine learning, data splitting is typically done to avoid overfitting. That is an instance where a machine learning model fits its training data too well and fails to reliably fit additional data.\n\nThe training set is the portion of data used to train the model. The model should observe and learn from the training set, optimizing any of its parameters.\n\nThe dev set is a data set of examples used to change learning process parameters. It is also called the cross-validation or model validation set. This set of data has the goal of ranking the model's accuracy and can help with model selection.\n\nThe testing set is the portion of data that is tested in the final model and is compared against the previous sets of data. The testing set acts as an evaluation of the final mode and algorithm.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.techtarget.com/searchenterpriseai/definition/data-splitting#:~:text=Data%20splitting%20is%20when%20data,creating%20models%20based%20on%20data. " + ], + "long_description": "", + "other_information": "Split data into train, validation, and test.\n\nBy default, Ludwig looks for a column named split (case-sensitive) which is expected to consist of 3 possible values that correspond to different datasets:\n\n0: train\n1: validation\n2: test\nIf the data does not contain the split column, then data is randomly split based on splitting percentages, defined by split_probabilities.\n\nIf force_split is true, the the split column in the dataset is ignored and the dataset is randomly split based on splitting percentages, defined by split_probabilities.", + "related_parameters": [ + "force_split, stratify" + ], + "short_description": "", + "suggested_values": [ + 0.8, + 0.1, + 0.1 + ], + "suggested_values_reasoning": "For larger datasets, it can be beneficial to use more data for training, since the test and validation sets are still plenty big for getting a good sense of model generalization.", + "ui_display_name": "Split Probabilities" + }, + "title": "probabilities", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "fixed" + } + } + }, + "then": { + "properties": { + "column": { + "default": "split", + "description": "The column name to use for fixed splitting.", + "parameter_metadata": null, + "title": "column", + "type": "string" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "stratify" + } + } + }, + "then": { + "properties": { + "column": { + "default": null, + "description": "The column name to base the stratified splitting on.", + "parameter_metadata": null, + "title": "column", + "type": [ + "string", + "null" + ] + }, + "probabilities": { + "default": [ + 0.7, + 0.1, + 0.2 + ], + "description": "Probabilities for splitting data into train, validation, and test sets.", + "items": { + "format": "float", + "title": "probabilities", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Most of the dataset should be used for training, with some portion heldout for validation and testing.", + "description_implications": "In machine learning, data splitting is typically done to avoid overfitting. That is an instance where a machine learning model fits its training data too well and fails to reliably fit additional data.\n\nThe training set is the portion of data used to train the model. The model should observe and learn from the training set, optimizing any of its parameters.\n\nThe dev set is a data set of examples used to change learning process parameters. It is also called the cross-validation or model validation set. This set of data has the goal of ranking the model's accuracy and can help with model selection.\n\nThe testing set is the portion of data that is tested in the final model and is compared against the previous sets of data. The testing set acts as an evaluation of the final mode and algorithm.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.techtarget.com/searchenterpriseai/definition/data-splitting#:~:text=Data%20splitting%20is%20when%20data,creating%20models%20based%20on%20data. " + ], + "long_description": "", + "other_information": "Split data into train, validation, and test.\n\nBy default, Ludwig looks for a column named split (case-sensitive) which is expected to consist of 3 possible values that correspond to different datasets:\n\n0: train\n1: validation\n2: test\nIf the data does not contain the split column, then data is randomly split based on splitting percentages, defined by split_probabilities.\n\nIf force_split is true, the the split column in the dataset is ignored and the dataset is randomly split based on splitting percentages, defined by split_probabilities.", + "related_parameters": [ + "force_split, stratify" + ], + "short_description": "", + "suggested_values": [ + 0.8, + 0.1, + 0.1 + ], + "suggested_values_reasoning": "For larger datasets, it can be beneficial to use more data for training, since the test and validation sets are still plenty big for getting a good sense of model generalization.", + "ui_display_name": "Split Probabilities" + }, + "title": "probabilities", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "datetime" + } + } + }, + "then": { + "properties": { + "column": { + "default": null, + "description": "The column name to perform datetime splitting on.", + "parameter_metadata": null, + "title": "column", + "type": [ + "string", + "null" + ] + }, + "probabilities": { + "default": [ + 0.7, + 0.1, + 0.2 + ], + "description": "Proportion of data to split into train, validation, and test sets.", + "items": { + "format": "float", + "title": "probabilities", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Most of the dataset should be used for training, with some portion heldout for validation and testing.", + "description_implications": "In machine learning, data splitting is typically done to avoid overfitting. That is an instance where a machine learning model fits its training data too well and fails to reliably fit additional data.\n\nThe training set is the portion of data used to train the model. The model should observe and learn from the training set, optimizing any of its parameters.\n\nThe dev set is a data set of examples used to change learning process parameters. It is also called the cross-validation or model validation set. This set of data has the goal of ranking the model's accuracy and can help with model selection.\n\nThe testing set is the portion of data that is tested in the final model and is compared against the previous sets of data. The testing set acts as an evaluation of the final mode and algorithm.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.techtarget.com/searchenterpriseai/definition/data-splitting#:~:text=Data%20splitting%20is%20when%20data,creating%20models%20based%20on%20data. " + ], + "long_description": "", + "other_information": "Split data into train, validation, and test.\n\nBy default, Ludwig looks for a column named split (case-sensitive) which is expected to consist of 3 possible values that correspond to different datasets:\n\n0: train\n1: validation\n2: test\nIf the data does not contain the split column, then data is randomly split based on splitting percentages, defined by split_probabilities.\n\nIf force_split is true, the the split column in the dataset is ignored and the dataset is randomly split based on splitting percentages, defined by split_probabilities.", + "related_parameters": [ + "force_split, stratify" + ], + "short_description": "", + "suggested_values": [ + 0.8, + 0.1, + 0.1 + ], + "suggested_values_reasoning": "For larger datasets, it can be beneficial to use more data for training, since the test and validation sets are still plenty big for getting a good sense of model generalization.", + "ui_display_name": "Split Probabilities" + }, + "title": "probabilities", + "type": [ + "array", + "null" + ] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "hash" + } + } + }, + "then": { + "properties": { + "column": { + "default": null, + "description": "The column name to perform hash splitting on.", + "parameter_metadata": null, + "title": "column", + "type": [ + "string", + "null" + ] + }, + "probabilities": { + "default": [ + 0.7, + 0.1, + 0.2 + ], + "description": "Proportion of data to split into train, validation, and test sets.", + "items": { + "format": "float", + "title": "probabilities", + "type": "number" + }, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Most of the dataset should be used for training, with some portion heldout for validation and testing.", + "description_implications": "In machine learning, data splitting is typically done to avoid overfitting. That is an instance where a machine learning model fits its training data too well and fails to reliably fit additional data.\n\nThe training set is the portion of data used to train the model. The model should observe and learn from the training set, optimizing any of its parameters.\n\nThe dev set is a data set of examples used to change learning process parameters. It is also called the cross-validation or model validation set. This set of data has the goal of ranking the model's accuracy and can help with model selection.\n\nThe testing set is the portion of data that is tested in the final model and is compared against the previous sets of data. The testing set acts as an evaluation of the final mode and algorithm.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://www.techtarget.com/searchenterpriseai/definition/data-splitting#:~:text=Data%20splitting%20is%20when%20data,creating%20models%20based%20on%20data. " + ], + "long_description": "", + "other_information": "Split data into train, validation, and test.\n\nBy default, Ludwig looks for a column named split (case-sensitive) which is expected to consist of 3 possible values that correspond to different datasets:\n\n0: train\n1: validation\n2: test\nIf the data does not contain the split column, then data is randomly split based on splitting percentages, defined by split_probabilities.\n\nIf force_split is true, the the split column in the dataset is ignored and the dataset is randomly split based on splitting percentages, defined by split_probabilities.", + "related_parameters": [ + "force_split, stratify" + ], + "short_description": "", + "suggested_values": [ + 0.8, + 0.1, + 0.1 + ], + "suggested_values_reasoning": "For larger datasets, it can be beneficial to use more data for training, since the test and validation sets are still plenty big for getting a good sense of model generalization.", + "ui_display_name": "Split Probabilities" + }, + "title": "probabilities", + "type": [ + "array", + "null" + ] + } + } + } + } + ], + "properties": { + "type": { + "default": "random", + "description": "Type of splitting to use during preprocessing.", + "enum": [ + "random", + "fixed", + "stratify", + "datetime", + "hash" + ], + "type": "string" + } + }, + "title": "split_options", + "type": "object" + }, + "undersample_majority": { + "default": null, + "description": "If not None, the majority class will be undersampled to reach the specified ratio respective to the minority class. ", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "We do not want to randomly undersample by default since this is a strategy to deal with imbalanced datasets, but can cause issues if not implemented correctly.", + "description_implications": "The higher the value you choose gets to 1, the closer you will be to having an equal imbalance ratio (i.e. 1:1 positive to negative class), however this can lead to problems of data loss when undersampling is used too liberally. As a rule of thumb, starting undersampling with a very conservative approach and increasing in small incremements is probably the best way to improve your model without experiencing catastrophic data loss effects.", + "example_value": [ + 0.5 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://machinelearningmastery.com/random-oversampling-and-undersampling-for-imbalanced-classification/" + ], + "long_description": "", + "other_information": "This parameter is one of many strategies to combat issues with class imbalance, though it is not a cure all. Undersampling too much can cause loss of data which can adversely affect your model so use with caution.", + "related_parameters": null, + "short_description": "", + "suggested_values": "Depends on imbalance ratio and dataset size", + "suggested_values_reasoning": null, + "ui_display_name": "Undersample Majority" + }, + "title": "undersample_majority", + "type": [ + "number", + "null" + ] + } + }, + "type": "object" + }, + "trainer": { + "additionalProperties": false, + "description": "Schema for trainer determined by Model Type", + "properties": { + "batch_size": { + "default": "auto", + "description": "The number of training examples utilized in one training step of the model. If \u2019auto\u2019, the batch size that maximized training throughput (samples / sec) will be used. For CPU training, the tuned batch size is capped at 128 as throughput benefits of large batch sizes are less noticeable without a GPU.", + "oneOf": [ + { + "default": 128, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "batch_size_integer_option", + "type": "integer" + }, + { + "default": "auto", + "description": "", + "enum": [ + "auto" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "batch_size_string_option", + "type": "string" + } + ], + "parameter_metadata": { + "commonly_used": true, + "compute_tier": 0, + "default_value_reasoning": "Not too big, not too small.", + "description_implications": "There's conflicting evidence about what batch size to use. Using a higher batch size will achieve the highest throughput and training efficiency. However, there's also evidence that depending on other hyperparameters, a smaller batch size may produce a higher quality model. Batch size and learning rate are strongly intertwined, so a commonly adopted strategy to set them is to find a the largest batch size that allows the training process not to run out of memory, and then find the best learning rate that makes the training converge with that batch size.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "eval_batch_size", + "learning_rate" + ], + "short_description": "", + "suggested_values": "auto", + "suggested_values_reasoning": "Auto batch size will determine the largest batch size that allows the training process not to run out of memory. Alternatively, try at least a few different batch sizes to get a sense of whether and how batch size affects model performance.", + "ui_display_name": "Batch Size" + }, + "title": "batch_size" + }, + "bucketing_field": { + "default": null, + "description": "Feature to use for bucketing datapoints", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "When not null, when creating batches, instead of shuffling randomly, the length along the last dimension of the matrix of the specified input feature (i.e. the length of a sequence or text) is used for bucketing examples and then randomly shuffled examples from the same bin are sampled. Padding is trimmed to the longest example in the batch. The specified feature should be either a sequence or text feature and the encoder encoding it has to be rnn. When used, bucketing improves speed of rnn encoding up to 1.5x, depending on the length distribution of the inputs.", + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Bucketing Field" + }, + "title": "bucketing_field", + "type": [ + "string", + "null" + ] + }, + "checkpoints_per_epoch": { + "default": 0, + "description": "Number of checkpoints per epoch. For example, 2 -> checkpoints are written every half of an epoch. Note that it is invalid to specify both non-zero `steps_per_checkpoint` and non-zero `checkpoints_per_epoch`.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Per-epoch behavior, which scales according to the dataset size.", + "description_implications": "Epoch-based evaluation (using the default: 0) is an appropriate fit for small datasets that fit in memory and train quickly. Commonly available tabular datasets fit in this cateogry. However, this is a poor fit for unstructured datasets, which tend to be much larger, and train more slowly due to larger models. It's important to setup evaluation such that you do not wait several hours before getting a single evaluation result. In general, it is not necessary for models to train over the entirety of a dataset, nor evaluate over the entirety of a test set, to produce useful monitoring metrics and signals to indicate model health. It is also more engaging and more valuable to ensure a frequent pulse of evaluation metrics, even if they are partial.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "train_steps", + "steps_per_checkpoint" + ], + "short_description": "", + "suggested_values": "2 - 10, for larger datasets", + "suggested_values_reasoning": "Running evaluation too frequently can be wasteful while running evaluation not frequently enough can be prohibitively uninformative. In many large-scale training runs, evaluation is often configured to run on a sub-epoch time scale, or every few thousand steps.", + "ui_display_name": "Checkpoints per epoch" + }, + "title": "checkpoints_per_epoch", + "type": "integer" + }, + "compile": { + "default": false, + "description": "Whether to compile the model before training.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Model compilation has been shown to significantly speedup training by upwards of 20%, but does impose some delay to compile the model at the beginning of training. This feature is experimental for now, but may become the default in future versions.", + "description_implications": "Model compilation on GPU, when used in conjunction with automatic mixed precision, can speed up training by upwards of 20%.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": false, + "suggested_values_reasoning": "Suggested to enable this if training is taking too long on GPU.", + "ui_display_name": "Compile" + }, + "title": "compile", + "type": "boolean" + }, + "early_stop": { + "default": 5, + "description": "Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that triggers training to stop. Can be set to -1, which disables early stopping entirely.", + "minimum": -1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Deep learning models are prone to overfitting. It's generally a good policy to set up some early stopping criteria as it's not useful to have a model train after it's maximized what it can learn. 5 consecutive rounds of evaluation where there hasn't been any improvement on the validation set (including chance) is a reasonable policy to start with.", + "description_implications": "Decreasing this value is a more aggressive policy. Decreasing early stopping makes model training less forgiving, as the model has less runway to demonstrate consecutive metric improvements before the training run is quit. This can be efficient for pruning bad models earlier, but since the training process is inherently non-deterministic and noisy, sometimes improvements happen very gradually over a long period of time. Extending this value leads to longer training times, but potentially also better final performance.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "epochs", + "train_steps" + ], + "short_description": "", + "suggested_values": "5 - 10", + "suggested_values_reasoning": "There's potentially a lot of randomness in how models train, but so many consecutive rounds of no improvement is usually a good indicator that the model converged or overfitted.", + "ui_display_name": "Early Stop" + }, + "title": "early_stop", + "type": "integer" + }, + "epochs": { + "default": 100, + "description": "Number of epochs the algorithm is intended to be run over. Overridden if `train_steps` is set", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A very high training length ceiling. Models will almost always hit early stopping criteria before hitting a 100-epoch ceiling.", + "description_implications": "Decreasing this will shorten the overall runway for training the model.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "train_steps" + ], + "short_description": "", + "suggested_values": 100, + "suggested_values_reasoning": "Usually it's sensible to leave this very high and rely on a solid early stopping policy to dictate when the model should stop training. Some models and hyperparameter configurations require many epochs through the dataset to converge while others converge before a single epoch through the data.", + "ui_display_name": "Epochs" + }, + "title": "epochs", + "type": "integer" + }, + "eval_batch_size": { + "default": null, + "description": "Size of batch to pass to the model for evaluation. If it is `0` or `None`, the same value of `batch_size` is used. This is useful to speedup evaluation with a much bigger batch size than training, if enough memory is available. If \u2019auto\u2019, the biggest batch size (power of 2) that can fit in memory will be used.", + "oneOf": [ + { + "default": 128, + "description": "", + "minimum": 1, + "parameter_metadata": null, + "title": "eval_batch_size_integer_option", + "type": "integer" + }, + { + "default": "auto", + "description": "", + "enum": [ + "auto" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "eval_batch_size_string_option", + "type": "string" + }, + { + "description": "Disable this parameter.", + "title": "null_option", + "type": "null" + } + ], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Use the same batch size used for training.", + "description_implications": "By increasing the `eval_batch_size` past the `batch_size` parameter set value, you allow for more parallelism in the batch evaluation step and speed up evaluation. For example, if you have to evaluate the model on a test set of size 1000, it is faster to evaluate two times with two batches of size 500 as opposed to ten times with ten batches of 100. Setting this parameter higher without getting past out memory limits will speed up the model training process overall.", + "example_value": [ + 512 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": "Should only set the eval_batch_size to a level that you can fit in memory.", + "related_parameters": [ + "batch_size" + ], + "short_description": "", + "suggested_values": [ + 256, + 512, + 1024 + ], + "suggested_values_reasoning": "By observing memory consumption on training jobs, you can get a sense of how much extra memory is available for increasing this value. A good rule of thumb can be experimentally doubling the eval batch size if you do not have insight into memory usage.", + "ui_display_name": "Evaluation Batch Size" + }, + "title": "eval_batch_size" + }, + "evaluate_training_set": { + "default": false, + "description": "Whether to evaluate on the entire training set during evaluation. By default, training metrics will be computed at the end of each training step, and accumulated up to the evaluation phase. In practice, computing training set metrics during training is up to 30% faster than running a separate evaluation pass over the training set, but results in more noisy training metrics, particularly during the earlier epochs. It's recommended to only set this to True if you need very exact training set metrics, and are willing to pay a significant performance penalty for them.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "It could be useful to monitor evaluation metrics on the training set to understand convergence.", + "description_implications": "Running evaluation on the full training set, when your training set is large, can be a huge computational cost. Turning off training set evaluation will lead to significant gains in training throughput and efficiency. For small datasets that train and evaluate quickly, the choice is trivial.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": false, + "suggested_values_reasoning": "Running full-scale evaluation on the full training set doesn't usually provide any useful information over the validation dataset. Even with this set to False, continuous training loss metrics are still computed, so it will still be easy to spot signs of overfitting like when the training-validation loss curves diverge.", + "ui_display_name": "Evaluate Training Set" + }, + "title": "evaluate_training_set", + "type": "boolean" + }, + "gradient_clipping": { + "description": "Parameter values for gradient clipping.", + "oneOf": [ + { + "description": "Disable gradient clipping.", + "title": "disabled", + "type": "null" + }, + { + "additionalProperties": true, + "properties": { + "clipglobalnorm": { + "default": 0.5, + "description": "Maximum allowed norm of the gradients", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A conservative cap on the maximum gradient size to apply over a single training step.", + "description_implications": "Gradient clipping is a technique to prevent exploding gradients in very deep networks. Increasing gradient clipping can help with model training loss curve stability, but it can also make training less efficient as weight at each training step is capped.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It's usually sensible to have some conservative notion of gradient clipping to make modeling robust to a particularly bad or noisy batch of examples.", + "ui_display_name": "Gradient Clipping" + }, + "title": "clipglobalnorm", + "type": [ + "number", + "null" + ] + }, + "clipnorm": { + "default": null, + "description": "Maximum allowed norm of the gradients", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A conservative cap on the maximum gradient size to apply over a single training step.", + "description_implications": "Gradient clipping is a technique to prevent exploding gradients in very deep networks. Increasing gradient clipping can help with model training loss curve stability, but it can also make training less efficient as weight at each training step is capped.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It's usually sensible to have some conservative notion of gradient clipping to make modeling robust to a particularly bad or noisy batch of examples.", + "ui_display_name": "Gradient Clipping" + }, + "title": "clipnorm", + "type": [ + "number", + "null" + ] + }, + "clipvalue": { + "default": null, + "description": "Maximum allowed value of the gradients", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "A conservative cap on the maximum gradient size to apply over a single training step.", + "description_implications": "Gradient clipping is a technique to prevent exploding gradients in very deep networks. Increasing gradient clipping can help with model training loss curve stability, but it can also make training less efficient as weight at each training step is capped.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": "It's usually sensible to have some conservative notion of gradient clipping to make modeling robust to a particularly bad or noisy batch of examples.", + "ui_display_name": "Gradient Clipping" + }, + "title": "clipvalue", + "type": [ + "number", + "null" + ] + } + }, + "title": "enabled_options", + "type": "object" + } + ], + "title": "gradient_clipping_options" + }, + "increase_batch_size_eval_metric": { + "default": "loss", + "description": "Which metric to listen on for increasing the batch size.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Batch Size Increase: Evaluation Metric" + }, + "title": "increase_batch_size_eval_metric", + "type": "string" + }, + "increase_batch_size_eval_split": { + "default": "training", + "description": "Which dataset split to listen on for increasing the batch size.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Batch Size Increase: Evaluation Split" + }, + "title": "increase_batch_size_eval_split", + "type": "string" + }, + "increase_batch_size_on_plateau": { + "default": 0, + "description": "The number of times to increase the batch size on a plateau.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Batch Size Increase On Plateau" + }, + "title": "increase_batch_size_on_plateau", + "type": "integer" + }, + "increase_batch_size_on_plateau_patience": { + "default": 5, + "description": "How many epochs to wait for before increasing the batch size.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Batch Size Increase On Plateau: Patience" + }, + "title": "increase_batch_size_on_plateau_patience", + "type": "integer" + }, + "increase_batch_size_on_plateau_rate": { + "default": 2.0, + "description": "Rate at which the batch size increases.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Batch Size Increase On Plateau: Rate" + }, + "title": "increase_batch_size_on_plateau_rate", + "type": "number" + }, + "learning_rate": { + "default": 0.001, + "description": "Controls how much to change the model in response to the estimated error each time the model weights are updated. If 'auto', the optimal learning rate is estimated by choosing the learning rate that produces the smallest non-diverging gradient update.", + "oneOf": [ + { + "default": 0.001, + "description": "", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": null, + "title": "learning_rate_float_option", + "type": "number" + }, + { + "default": "auto", + "description": "", + "enum": [ + "auto" + ], + "enumNames": [], + "parameter_metadata": null, + "title": "learning_rate_string_option", + "type": "string" + } + ], + "parameter_metadata": { + "commonly_used": true, + "compute_tier": 0, + "default_value_reasoning": "Middle of the road learning rate to start with.", + "description_implications": "The learning rate is a hyperparameter that controls how much to change the model in response to the estimated error each time the model weights are updated. Increasing the learning rate may decrease learning curve stability but also increase learning speed and efficiency, leading to faster model convergence. Decreasing the learning rate can help stabilize learning curves at the cost of slower time to convergence.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "decay" + ], + "short_description": "", + "suggested_values": "0.00001 - 0.1 or auto", + "suggested_values_reasoning": "Tabular models trained from scratch typically use learning rates around 1e-3 while learning rates for pre-trained models should be much smaller, typically around 1e-5, which is important to mitigate catastrophic forgetting. To make the model more robust to any specific choice of learning rate, consider turning enabling learning rate decay.", + "ui_display_name": "Learning Rate" + }, + "title": "learning_rate" + }, + "learning_rate_scaling": { + "default": "linear", + "description": "Scale by which to increase the learning rate as the number of distributed workers increases. Traditionally the learning rate is scaled linearly with the number of workers to reflect the proportion by which the effective batch size is increased. For very large batch sizes, a softer square-root scale can sometimes lead to better model performance. If the learning rate is hand-tuned for a given number of workers, setting this value to constant can be used to disable scale-up.", + "enum": [ + "constant", + "sqrt", + "linear" + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Traditionally the learning rate is scaled linearly with the number of workers to reflect the proportion by which the effective batch size is increased.", + "description_implications": "Traditionally the learning rate is scaled linearly with the number of workers to reflect the proportion by which the effective batch size is increased. For very large batch sizes, a softer square-root scale can sometimes lead to better model performance. If the learning rate is hand-tuned for a given number of workers, setting this value to constant can be used to disable scale-up.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": "linear or sqrt", + "suggested_values_reasoning": "Traditionally the learning rate is scaled linearly with the number of workers to reflect the proportion by which the effective batch size is increased. For very large batch sizes, a softer square-root scale can sometimes lead to better model performance. If the learning rate is hand-tuned for a given number of workers, setting this value to constant can be used to disable scale-up.", + "ui_display_name": "Learning Rate Scaling" + }, + "title": "learning_rate_scaling", + "type": "string" + }, + "learning_rate_scheduler": { + "additionalProperties": true, + "description": "Parameter values for learning rate scheduler.", + "properties": { + "decay": { + "default": null, + "description": "Turn on decay of the learning rate.", + "enum": [ + "linear", + "exponential", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "It\u2019s almost always a good idea to use a schedule. For most models, try the exponential decay schedule first.\n\nThe exponential schedule divides the learning rate by the same factor (%) every epoch. This means that the learning rate will decrease rapidly in the first few epochs, and spend more epochs with a lower value, but never reach exactly zero. As a rule of thumb, compared to training without a schedule, you can use a slightly higher maximum learning rate. Since the learning rate changes over time, the whole training is not so sensitive to the value picked.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://peltarion.com/knowledge-center/documentation/modeling-view/run-a-model/optimization-principles-(in-deep-learning)/learning-rate-schedule " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "decay_rate", + "decay_steps", + "learning_rate" + ], + "short_description": "", + "suggested_values": "exponential", + "suggested_values_reasoning": "Starting with exponential decay is a safe place to start, as it is a \"softer\" decrease in the learning rate over time, as compared with linear, which is more steep after the initial drop. Linear decay is most useful when the risk of catastrophic forgetting is very high (e.g, for fine-tuning pretrained models). If you observe your loss curves shooting up (even on the training set) in later epochs, increasing the decay rate may help mitigate this effect.", + "ui_display_name": "Decay" + }, + "title": "decay", + "type": [ + "string", + "null" + ] + }, + "decay_rate": { + "default": 0.96, + "description": "Decay per epoch (%): Factor to decrease the Learning rate.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "4-5% decay each step is an empirically useful decay rate to start with.", + "description_implications": "Increasing the decay rate will lower the learning rate faster. This could make the model more robust to a bad (too high) initial learning rate, but a decay rate that is too high could prohibit the model from learning anything at all.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://peltarion.com/knowledge-center/documentation/modeling-view/run-a-model/optimization-principles-(in-deep-learning)/learning-rate-schedule " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "decay_steps", + "learning_rate" + ], + "short_description": "", + "suggested_values": "0.9 - 0.96", + "suggested_values_reasoning": "Since this controls exponential decay, even a small decay rate will still be strongly impactful.", + "ui_display_name": "Decay Rate" + }, + "title": "decay_rate", + "type": "number" + }, + "decay_steps": { + "default": 10000, + "description": "The number of steps to take in the exponential learning rate decay.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "This default essentially enables the `learning_rate` to decay by a factor of the `decay_rate` at 10000 training steps.", + "description_implications": "By increasing the value of decay steps, you are increasing the number of training steps it takes to decay the learning rate by a factor of `decay_rate`. In other words, the bigger this parameter, the slower the learning rate decays.", + "example_value": [ + 5000 + ], + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "decay_rate", + "learning_rate" + ], + "short_description": "", + "suggested_values": "10000 +/- 500 at a time", + "suggested_values_reasoning": "The decay in the learning rate is calculated as the training step divided by the `decay_steps` plus one. Then the `decay_rate` is raised to the power of this exponent which is then multiplied to the current learning rate. All this to say that the learning rate is only decayed by a factor of the set `decay_rate` when the training step reaches the `decay_steps` and then subsequently when it reaches any multiple of `decay_steps`. You can think of `decay_steps` as a rate of decay for the `decay_rate`.", + "ui_display_name": "Decay Steps" + }, + "title": "decay_steps", + "type": "integer" + }, + "reduce_eval_metric": { + "default": "loss", + "description": "Metric plateau used to trigger when we reduce the learning rate when `reduce_on_plateau > 0`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Reduce Eval Metric" + }, + "title": "reduce_eval_metric", + "type": "string" + }, + "reduce_eval_split": { + "default": "training", + "description": "Which dataset split to listen on for reducing the learning rate when `reduce_on_plateau > 0`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Reduce Eval Split" + }, + "title": "reduce_eval_split", + "type": "string" + }, + "reduce_on_plateau": { + "default": 0, + "description": "How many times to reduce the learning rate when the algorithm hits a plateau (i.e. the performance on thetraining set does not improve", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Reduce On Plateau" + }, + "title": "reduce_on_plateau", + "type": "integer" + }, + "reduce_on_plateau_patience": { + "default": 10, + "description": "How many evaluation steps have to pass before the learning rate reduces when `reduce_on_plateau > 0`.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Reduce On Plateau Patience" + }, + "title": "reduce_on_plateau_patience", + "type": "integer" + }, + "reduce_on_plateau_rate": { + "default": 0.1, + "description": "Rate at which we reduce the learning rate when `reduce_on_plateau > 0`.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Reduce On Plateau Rate" + }, + "title": "reduce_on_plateau_rate", + "type": "number" + }, + "staircase": { + "default": false, + "description": "Decays the learning rate at discrete intervals.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Performs learning rate decay in stepwise discrete manner.", + "description_implications": "An excessively aggressive decay results in optimizers never reaching the minima, whereas a slow decay leads to chaotic updates without significant improvement. Discrete learning rate decay is another parameter to help tune a balance.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://neptune.ai/blog/how-to-choose-a-learning-rate-scheduler" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": false, + "suggested_values_reasoning": "We have not found strong evidence that discretely decaying the learning rate is superior to doing so continuously in general, but in specific tasks it might have a positive impact.", + "ui_display_name": "Staircase" + }, + "title": "staircase", + "type": "boolean" + }, + "warmup_evaluations": { + "default": 0, + "description": "Number of evaluation steps to warmup the learning rate for.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Learning rate warmup is most commonly used when training with large batch sizes / distributed training to avoid taking overly large steps at the beginning of training that might result in the process getting stuck in a local optimum. Conventional wisdom when training with large batch sizes is to use a larger learning rate (see: `learning_rate_scaling`) but gradually warm up to the larger learning rate over a few epochs of training in the beginning. Even when not training with large batch sizes, the randomness of how weights are initialized can result in strange, noisy gradient updates during the beginning of your training run. As such, it's generally recommended to use a small amount of warmup (e.g., 1 epoch / evaluation) even when the batch size is relatively small.", + "description_implications": "Learning rate warmup sets a very low learning rate at the beginning of training and gradually (linearly) increases to the base learning rate each step (batch) during training. After your warmup steps you use your \"regular\" learning rate or learning rate scheduler.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://arxiv.org/abs/1711.00489", + "https://datascience.stackexchange.com/questions/55991/in-the-context-of-deep-learning-what-is-training-warmup-steps" + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "warmup_fraction", + "learning_rate_scaling" + ], + "short_description": "", + "suggested_values": "0 - 5", + "suggested_values_reasoning": "You don't want to warm up for too long, as after the model is starting to hill climb, you want to use the full weight of the learning rate to descend into good loss minima.\nIf you observe your loss curve converging very early into training, within the first few epochs, then increasing learning rate warmup may help to mitigate this effect. Pretrained models can benefit from more warmup to help offset the effects of catastrophic forgetting due to an overly high learning rate.", + "ui_display_name": "Warmup Evaluations" + }, + "title": "warmup_evaluations", + "type": "number" + }, + "warmup_fraction": { + "default": 0.0, + "description": "Fraction of total training steps to warmup the learning rate for.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Similar to `warmup_evaluations` but expressed as a fraction of the total number of training steps, rather that a certain number of evaluation phases.", + "description_implications": "See `warmup_evaluations`.", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "warmup_evaluations", + "learning_rate_scaling" + ], + "short_description": "", + "suggested_values": "0.05 - 0.2", + "suggested_values_reasoning": "You don't want to warm up for too long, as after the model is starting to hill climb, you want to use the full weight of the learning rate to descend into good loss minima.", + "ui_display_name": "Warmup Fraction" + }, + "title": "warmup_fraction", + "type": "number" + } + }, + "title": "learning_rate_scheduler_options", + "type": "object" + }, + "max_batch_size": { + "default": 1099511627776, + "description": "Auto batch size tuning and increasing batch size on plateau will be capped at this value. The default value is 2^40.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Not typically required.", + "description_implications": "Value used to manually limit the batch sizes explored by auto batch size tuning and batch size increasing on plateau.", + "example_value": [ + 1024 + ], + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "batch_size", + "increase_batch_size_on_plateau" + ], + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "Max Batch Size" + }, + "title": "max_batch_size", + "type": [ + "integer", + "null" + ] + }, + "optimizer": { + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "sgd" + } + } + }, + "then": { + "properties": { + "dampening": { + "default": 0.0, + "description": "Dampening for momentum.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "dampening", + "type": "number" + }, + "momentum": { + "default": 0.0, + "description": "Momentum factor.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "momentum", + "type": "number" + }, + "nesterov": { + "default": false, + "description": "Enables Nesterov momentum.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "nesterov", + "type": "boolean" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "lbfgs" + } + } + }, + "then": { + "properties": { + "history_size": { + "default": 100, + "description": "Update history size.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "history_size", + "type": "integer" + }, + "line_search_fn": { + "default": null, + "description": "Line search function to use.", + "enum": [ + "strong_wolfe", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "line_search_fn", + "type": [ + "string", + "null" + ] + }, + "max_eval": { + "default": null, + "description": "Maximum number of function evaluations per optimization step. Default: `max_iter` * 1.25.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "max_eval", + "type": [ + "integer", + "null" + ] + }, + "max_iter": { + "default": 20, + "description": "Maximum number of iterations per optimization step.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "max_iter", + "type": "integer" + }, + "tolerance_change": { + "default": 1e-09, + "description": "Termination tolerance on function value/parameter changes.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "tolerance_change", + "type": "number" + }, + "tolerance_grad": { + "default": 1e-07, + "description": "Termination tolerance on first order optimality.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "tolerance_grad", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "adam" + } + } + }, + "then": { + "properties": { + "amsgrad": { + "default": false, + "description": "Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam and Beyond'.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "amsgrad", + "type": "boolean" + }, + "betas": { + "default": [ + 0.9, + 0.999 + ], + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": [ + 0.9, + 0.999 + ], + "description": "Coefficients used for computing running averages of gradient and its square.", + "items": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "maximum": 1, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + } + ], + "title": "betas" + }, + "eps": { + "default": 1e-08, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay (L2 penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "adamw" + } + } + }, + "then": { + "properties": { + "amsgrad": { + "default": false, + "description": "Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam and Beyond'. ", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "amsgrad", + "type": "boolean" + }, + "betas": { + "default": [ + 0.9, + 0.999 + ], + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": [ + 0.9, + 0.999 + ], + "description": "Coefficients used for computing running averages of gradient and its square.", + "items": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "maximum": 1, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + } + ], + "title": "betas" + }, + "eps": { + "default": 1e-08, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "adadelta" + } + } + }, + "then": { + "properties": { + "eps": { + "default": 1e-06, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "rho": { + "default": 0.9, + "description": "Coefficient used for computing a running average of squared gradients.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "rho", + "type": "number" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "adagrad" + } + } + }, + "then": { + "properties": { + "eps": { + "default": 1e-10, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "initial_accumulator_value": { + "default": 0, + "description": "", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "initial_accumulator_value", + "type": "number" + }, + "lr_decay": { + "default": 0, + "description": "Learning rate decay.", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "lr_decay", + "type": "number" + }, + "weight_decay": { + "default": 0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "adamax" + } + } + }, + "then": { + "properties": { + "betas": { + "default": [ + 0.9, + 0.999 + ], + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": [ + 0.9, + 0.999 + ], + "description": "Coefficients used for computing running averages of gradient and its square.", + "items": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "maximum": 1, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + } + ], + "title": "betas" + }, + "eps": { + "default": 1e-08, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "nadam" + } + } + }, + "then": { + "properties": { + "betas": { + "default": [ + 0.9, + 0.999 + ], + "description": "Valid options for FloatRangeTupleDataclassField.", + "oneOf": [ + { + "default": [ + 0.9, + 0.999 + ], + "description": "Coefficients used for computing running averages of gradient and its square.", + "items": [ + { + "maximum": 1, + "minimum": 0, + "type": "number" + }, + { + "maximum": 1, + "minimum": 0, + "type": "number" + } + ], + "type": "array" + } + ], + "title": "betas" + }, + "eps": { + "default": 1e-08, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "momentum_decay": { + "default": 0.004, + "description": "Momentum decay.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "momentum_decay", + "type": "number" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "weight_decay", + "type": "number" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "rmsprop" + } + } + }, + "then": { + "properties": { + "alpha": { + "default": 0.99, + "description": "Smoothing constant.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "alpha", + "type": "number" + }, + "centered": { + "default": false, + "description": "If True, computes the centered RMSProp, and the gradient is normalized by an estimation of its variance.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "centered", + "type": "boolean" + }, + "eps": { + "default": 1e-08, + "description": "Term added to the denominator to improve numerical stability.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "eps", + "type": "number" + }, + "momentum": { + "default": 0.0, + "description": "Momentum factor.", + "format": "float", + "minimum": 0.0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": null, + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": null, + "suggested_values_reasoning": null, + "ui_display_name": "" + }, + "title": "momentum", + "type": "number" + }, + "weight_decay": { + "default": 0.0, + "description": "Weight decay ($L2$ penalty).", + "format": "float", + "minimum": 0.0, + "parameter_metadata": null, + "title": "weight_decay", + "type": "number" + } + } + } + } + ], + "description": "Optimizer type and its parameters. The optimizer is responsble for applying the gradients computed from the loss during backpropagation as updates to the model weights.", + "properties": { + "type": { + "default": "adam", + "description": "The type of optimizer to use during the learning process", + "enum": [ + "sgd", + "lbfgs", + "adam", + "adamw", + "adadelta", + "adagrad", + "adamax", + "nadam", + "rmsprop" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "optimizer_options", + "type": "object" + }, + "regularization_lambda": { + "default": 0.0, + "description": "Strength of the regularization.", + "format": "float", + "maximum": 1, + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "How to tune the overall impact of the regularization term by multiplying its value by a scalar known as lambda (also called the regularization rate).", + "description_implications": "When choosing a lambda value, the goal is to strike the right balance between simplicity and training-data fit: If your lambda value is too high, your model will be simple, but you run the risk of underfitting your data. Your model won't learn enough about the training data to make useful predictions. If your lambda value is too low, your model will be more complex, and you run the risk of overfitting your data. Your model will learn too much about the particularities of the training data, and won't be able to generalize to new data. The ideal value of lambda produces a model that generalizes well to new, previously unseen data. Unfortunately, that ideal value of lambda is data-dependent, so you'll need to do some tuning. We recommend trying a handful of values (0.001, 0.02, ... 0.4) gradually increasing the value until training curves get worse", + "example_value": null, + "expected_impact": 2, + "internal_only": false, + "literature_references": [ + "https://developers.google.com/machine-learning/crash-course/regularization-for-simplicity/lambda " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "regularization_type" + ], + "short_description": "", + "suggested_values": 0.1, + "suggested_values_reasoning": "The most common type of regularization is L2, also called weight decay, with values often on a logarithmic scale between 0 and 0.1, such as 0.1, 0.001, 0.0001, etc.", + "ui_display_name": "Regularization Lambda" + }, + "title": "regularization_lambda", + "type": "number" + }, + "regularization_type": { + "default": "l2", + "description": "Type of regularization.", + "enum": [ + "l1", + "l2", + "l1_l2", + null + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "L2 is a standard regularization to start with.", + "description_implications": "L1 regularization penalizes the sum of absolute values of the weights, whereas L2 regularization penalizes the sum of squares of the weights. The L1 regularization solution is sparse, meaning some weights will be zero, others will be large. The L2 regularization solution is non-sparse, most weights will be small. L2 regularization does not perform feature selection, since weights are only reduced to values near 0 instead of 0. L1 regularization implicitly performs feature selection. L1 regularization is more robust to outliers.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://neptune.ai/blog/fighting-overfitting-with-l1-or-l2-regularization#:~:text=The%20differences%20between%20L1%20and,regularization%20solution%20is%20non%2Dsparse. " + ], + "long_description": "", + "other_information": null, + "related_parameters": [ + "regularization_lambda" + ], + "short_description": "", + "suggested_values": "L2", + "suggested_values_reasoning": null, + "ui_display_name": "Regularization Type" + }, + "title": "regularization_type", + "type": [ + "string", + "null" + ] + }, + "should_shuffle": { + "default": true, + "description": "Whether to shuffle batches during training when true.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "In general, it's a good idea to mix up data on each batch so that the neural network gets the broadest exposure to the dataset.", + "description_implications": "Turning off mini-batch shuffling can make training faster, but it may lead to worse performance overall as shuffling helps mitigate overfitting.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": [ + "https://stats.stackexchange.com/questions/245502/why-should-we-shuffle-data-while-training-a-neural-network#:~:text=it%20helps%20the%20training%20converge,the%20order%20of%20the%20training " + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": true, + "suggested_values_reasoning": "One of the most powerful things about neural networks is that they can be very complex functions, allowing one to learn very complex relationships between your input and output data. These relationships can include things you would never expect, such as the order in which data is fed in per epoch. If the order of data within each epoch is the same, then the model may use this as a way of reducing the training error, which is a sort of overfitting.", + "ui_display_name": "Should Shuffle" + }, + "title": "should_shuffle", + "type": "boolean" + }, + "steps_per_checkpoint": { + "default": 0, + "description": "How often the model is checkpointed. Also dictates maximum evaluation frequency. If 0 the model is checkpointed after every epoch.", + "minimum": 0, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "By default, we evaluate once per epoch, which scales according to the dataset size.", + "description_implications": "Epoch-based evaluation (using the default: 0) is an appropriate fit for tabular datasets, which are small, fit in memory, and train quickly. However, this is a poor fit for unstructured datasets, which tend to be much larger, and train more slowly due to larger models. It's important to setup evaluation such that you do not wait several hours before getting a single evaluation result. In general, it is not necessary for models to train over the entirety of a dataset, nor evaluate over the entirety of a test set, to produce useful monitoring metrics and signals to indicate model health. It is also more engaging and more valuable to ensure a frequent pulse of evaluation metrics, even if they are partial.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "checkpoints_per_epoch" + ], + "short_description": "", + "suggested_values": "1000-10000 for larger datasets", + "suggested_values_reasoning": "Running evaluation too frequently can be wasteful while running evaluation not frequently enough can be prohibitively uninformative. In many large-scale training runs, evaluation is often configured to run on a sub-epoch time scale, or every few thousand steps.", + "ui_display_name": "Steps Per Checkpoint" + }, + "title": "steps_per_checkpoint", + "type": "integer" + }, + "train_steps": { + "default": null, + "description": "Maximum number of training steps the algorithm is intended to be run over. Unset by default. If set, will override `epochs` and if left unset then `epochs` is used to determine training length.", + "minimum": 1, + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "This defaults to `epochs`, which is a very high training length ceiling. Models will almost always hit early stopping criteria before reaching the absolute end of the training runway.", + "description_implications": "Decreasing this parameter will shorten the overall runway for training the model.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "epochs" + ], + "short_description": "", + "suggested_values": "Leave unset, or 1000000, 1 for debugging", + "suggested_values_reasoning": "Usually it's sensible to leave the value of this parameter very high and rely on a solid early stopping policy to dictate when the model should stop training. Some models and hyperparameter configurations require many epochs through the dataset to converge while others converge before a single epoch through the data.", + "ui_display_name": "Train Steps" + }, + "title": "train_steps", + "type": [ + "integer", + "null" + ] + }, + "use_mixed_precision": { + "default": false, + "description": "Enable automatic mixed-precision (AMP) during training.", + "enum": [ + true, + false + ], + "enumNames": [], + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Speed up training by using float16 parameters where it makes sense.", + "description_implications": "Mixed precision training on GPU can dramatically speedup training, with some risks to model convergence.", + "example_value": null, + "expected_impact": 3, + "internal_only": false, + "literature_references": [ + "https://pytorch.org/blog/what-every-user-should-know-about-mixed-precision-training-in-pytorch/" + ], + "long_description": "", + "other_information": null, + "related_parameters": null, + "short_description": "", + "suggested_values": false, + "suggested_values_reasoning": "Suggested to enable this if training is taking too long on GPU.", + "ui_display_name": "Use Mixed Precision" + }, + "title": "use_mixed_precision", + "type": "boolean" + }, + "validation_field": { + "default": null, + "description": "The field for which the `validation_metric` is used for validation-related mechanics like early stopping, parameter change plateaus, as well as what hyperparameter optimization uses to determine the best trial. If unset (default), the first output feature is used. If explicitly specified, neither `validation_field` nor `validation_metric` are overwritten.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": "Concrete evaluation metrics are usually better than loss, the penalty for a bad prediction, which is only a proxy for prediction correctness.", + "description_implications": "This parameter affects 1) what the early stopping policy looks at to determine when to early stop and 2) hyperparameter optimization for determining the best trial.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "validation_metric" + ], + "short_description": "", + "suggested_values": "default behavior", + "suggested_values_reasoning": null, + "ui_display_name": "Validation Field" + }, + "title": "validation_field", + "type": [ + "string", + "null" + ] + }, + "validation_metric": { + "default": null, + "description": "Metric from `validation_field` that is used. If validation_field is not explicitly specified, this is overwritten to be the first output feature type's `default_validation_metric`, consistent with validation_field. If the validation_metric is specified, then we will use the first output feature that produces this metric as the `validation_field`.", + "parameter_metadata": { + "commonly_used": false, + "compute_tier": 0, + "default_value_reasoning": null, + "description_implications": "This parameter affects 1) what the early stopping policy looks at to determine when to early stop and 2) hyperparameter optimization for determining the best trial.", + "example_value": null, + "expected_impact": 1, + "internal_only": false, + "literature_references": null, + "long_description": "", + "other_information": null, + "related_parameters": [ + "validation_field" + ], + "short_description": "", + "suggested_values": "default behavior", + "suggested_values_reasoning": null, + "ui_display_name": "Validation Metric" + }, + "title": "validation_metric", + "type": [ + "string", + "null" + ] + } + }, + "title": "trainer_options", + "type": "object" + } + }, + "title": "model_options", + "type": "object" +} diff --git a/tests/ludwig/config_sampling/test_config_sampling.py b/tests/ludwig/config_sampling/test_config_sampling.py new file mode 100644 index 0000000..6500169 --- /dev/null +++ b/tests/ludwig/config_sampling/test_config_sampling.py @@ -0,0 +1,49 @@ +import pytest + +from ludwig.utils.data_utils import load_json +from tests.training_success.test_training_success import ( + combiner_config_generator, + defaults_config_generator, + ecd_trainer_config_generator, +) + + +def full_config_generator(generator_fn, *args): + return len(list(generator_fn(*args))) + + +@pytest.mark.combinatorial +@pytest.mark.timeout(600) +def test_config_sampling(): + static_schema = load_json("tests/ludwig/config_sampling/static_schema.json") + total_count = 0 + + total_count += full_config_generator(defaults_config_generator, "number", "preprocessing", static_schema) + total_count += full_config_generator(defaults_config_generator, "number", "encoder", static_schema) + total_count += full_config_generator(defaults_config_generator, "number", "decoder", static_schema) + total_count += full_config_generator(defaults_config_generator, "number", "loss", static_schema) + + total_count += full_config_generator(defaults_config_generator, "category", "preprocessing", static_schema) + total_count += full_config_generator(defaults_config_generator, "category", "encoder", static_schema) + total_count += full_config_generator(defaults_config_generator, "category", "decoder", static_schema) + total_count += full_config_generator(defaults_config_generator, "category", "loss", static_schema) + + total_count += full_config_generator(defaults_config_generator, "binary", "preprocessing", static_schema) + total_count += full_config_generator(defaults_config_generator, "binary", "encoder", static_schema) + total_count += full_config_generator(defaults_config_generator, "binary", "decoder", static_schema) + total_count += full_config_generator(defaults_config_generator, "binary", "loss", static_schema) + + total_count += full_config_generator(ecd_trainer_config_generator, static_schema) + + total_count += full_config_generator(combiner_config_generator, "sequence_concat", static_schema) + total_count += full_config_generator(combiner_config_generator, "sequence", static_schema) + total_count += full_config_generator(combiner_config_generator, "comparator", static_schema) + total_count += full_config_generator(combiner_config_generator, "concat", static_schema) + total_count += full_config_generator(combiner_config_generator, "project_aggregate", static_schema) + total_count += full_config_generator(combiner_config_generator, "tabnet", static_schema) + total_count += full_config_generator(combiner_config_generator, "tabtransformer", static_schema) + total_count += full_config_generator(combiner_config_generator, "transformer", static_schema) + + # In place to check for sudden changes in the number of combinatorially generated configs. Update ranges + # accordingly if new parameters are added. + assert 100 < total_count < 200 diff --git a/tests/ludwig/config_validation/test_checks.py b/tests/ludwig/config_validation/test_checks.py new file mode 100644 index 0000000..24d9479 --- /dev/null +++ b/tests/ludwig/config_validation/test_checks.py @@ -0,0 +1,523 @@ +"""Tests for interdependent parameters. + +Note that all testing should be done with the public API, rather than individual checks. + +``` ModelConfig.from_dict(config) ``` +""" + +import contextlib +from typing import Any + +import pytest +import yaml + +from ludwig.constants import COMBINER, TYPE +from ludwig.error import ConfigValidationError +from ludwig.schema.model_types.base import ModelConfig +from tests.integration_tests.utils import binary_feature, text_feature + + +def test_passthrough_number_decoder(): + config = { + "defaults": {"number": {"decoder": {"fc_norm": None, "fc_output_size": 10, "type": "passthrough"}}}, + "input_features": [ + {"name": "MSSubClass", "type": "category"}, + {"name": "MSZoning", "type": "category"}, + {"name": "Street", "type": "category"}, + {"name": "Neighborhood", "type": "category"}, + ], + "model_type": "ecd", + "output_features": [{"name": "SalePrice", "type": "number", "decoder": {"type": "passthrough"}}], + "trainer": {"train_steps": 1}, + } + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_sequence_combiner_with_embed_encoder(): + config = { + "combiner": { + "encoder": {"dropout": 0.1641014195584432, "embedding_size": 256, "type": "embed"}, + "main_sequence_feature": None, + "type": "sequence", + }, + "input_features": [{"encoder": {"reduce_output": None, "type": "embed"}, "name": "Text", "type": "text"}], + "model_type": "ecd", + "output_features": [{"name": "Category", "type": "category"}], + "preprocessing": {"sample_ratio": 0.05}, + "trainer": {"train_steps": 1}, + } + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_balance_multiple_class_failure(): + config = { + "input_features": [ + {"name": "Index", "proc_column": "Index", "type": "number"}, + {"name": "random_1", "proc_column": "random_1", "type": "number"}, + {"name": "random_2", "proc_column": "random_2", "type": "number"}, + ], + "output_features": [ + {"name": "Label", "proc_column": "Label", "type": "binary"}, + {"name": "Label2", "proc_column": "Label2", "type": "binary"}, + ], + "preprocessing": {"oversample_minority": 0.2}, + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_all_features_present_in_comparator_entities(): + config = { + "combiner": { + "dropout": 0.20198506770751617, + "entity_1": ["Age"], + "entity_2": ["Sex", "Pclass"], + "norm": "batch", + "num_fc_layers": 1, + "output_size": 256, + "type": "comparator", + }, + "input_features": [ + {"column": "Pclass", "name": "Pclass", "type": "category"}, + {"column": "Sex", "name": "Sex", "type": "category"}, + {"column": "Age", "name": "Age", "type": "number"}, + {"column": "SibSp", "name": "SibSp", "type": "number"}, + {"column": "Parch", "name": "Parch", "type": "number"}, + {"column": "Fare", "name": "Fare", "type": "number"}, + {"column": "Embarked", "name": "Embarked", "type": "category"}, + ], + "model_type": "ecd", + "output_features": [{"column": "Survived", "name": "Survived", "type": "category"}], + "trainer": {"train_steps": 1}, + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_balance_non_binary_failure(): + config = { + "input_features": [ + {"name": "Index", "proc_column": "Index", "type": "number"}, + {"name": "random_1", "proc_column": "random_1", "type": "number"}, + {"name": "random_2", "proc_column": "random_2", "type": "number"}, + ], + "output_features": [{"name": "Label", "proc_column": "Label", "type": "number"}], + "preprocessing": {"oversample_minority": 0.2}, + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_supported_features_config(): + # ECD supports output text features. + ModelConfig.from_dict( + { + "input_features": [binary_feature()], + "output_features": [text_feature()], + "model_type": "ecd", + } + ) + + +@pytest.mark.parametrize( + "num_fc_layers,fc_layers,expect_success", + [ + (None, None, True), + (1, None, True), + (None, [{"output_size": 256}], True), + (0, [{"output_size": 256}], True), + (0, None, False), + ], +) +def test_comparator_fc_layer_config(num_fc_layers: int | None, fc_layers: dict[str, Any] | None, expect_success: bool): + config = { + "input_features": [ + {"name": "in1", "type": "category"}, + {"name": "in2", "type": "category"}, + ], + "output_features": [ + {"name": "out1", "type": "binary"}, + ], + "combiner": { + "type": "comparator", + "entity_1": ["in1"], + "entity_2": ["in2"], + }, + } + + if num_fc_layers is not None: + config["combiner"]["num_fc_layers"] = num_fc_layers + + if fc_layers is not None: + config["combiner"]["fc_layers"] = fc_layers + + with pytest.raises(ConfigValidationError) if not expect_success else contextlib.nullcontext(): + ModelConfig.from_dict(config) + + +def test_dense_binary_encoder_0_layer(): + config = { + "defaults": {"binary": {"encoder": {"norm": "ghost", "num_layers": 0, "output_size": 128, "type": "dense"}}}, + "input_features": [ + {"name": "X0", "type": "category"}, + {"name": "X1", "type": "category"}, + {"name": "X10", "type": "binary"}, + {"name": "X11", "type": "binary"}, + {"name": "X14", "type": "binary", "encoder": {"num_layers": 0}}, + ], + "model_type": "ecd", + "output_features": [{"name": "y", "type": "number"}], + "trainer": {"train_steps": 1}, + } + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "entity_1,entity_2,expected", + [ + (["a1"], ["b1", "b2"], True), + (["a1", "a2"], ["b1", "b2", "b3"], True), + ([], ["b1", "b2"], False), + ([], ["a1", "b1", "b2"], False), + (["a1", "b1", "b2"], [], False), + (["a1", "b1"], ["b1", "b2"], False), + (["a1"], ["b1"], False), + ], +) +def test_comparator_combiner_entities(entity_1: list[str], entity_2: list[str], expected: bool): + config = { + "input_features": [ + {"name": "a1", "type": "category"}, + {"name": "b1", "type": "category"}, + {"name": "b2", "type": "category"}, + ], + "output_features": [ + {"name": "out1", "type": "binary"}, + ], + "combiner": { + "type": "comparator", + "entity_1": entity_1, + "entity_2": entity_2, + }, + } + + with pytest.raises(ConfigValidationError) if not expected else contextlib.nullcontext(): + config_obj = ModelConfig.from_dict(config) + assert config_obj.combiner.entity_1 == ["a1"] + assert config_obj.combiner.entity_2 == ["b1", "b2"] + + +def test_experiment_binary_fill_with_const(): + """Test that the tagger decoder doesn't work with category input features.""" + config = { + "defaults": {"binary": {"preprocessing": {"missing_value_strategy": "fill_with_const"}}}, + "input_features": [{"name": "binary_1", "type": "binary"}], + "model_type": "ecd", + "output_features": [{"name": "category_output_1", "type": "category"}], + "trainer": {"train_steps": 1}, + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_check_concat_combiner_requirements(): + config = yaml.safe_load(""" +input_features: + - name: description + type: text + encoder: + type: embed + reduce_output: null + column: description + - name: required_experience + type: category + column: required_experience +output_features: + - name: title + type: category +combiner: + type: concat +trainer: + train_steps: 2 +model_type: ecd +""") + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + # Confirms that the choice of the combiner type is the only reason for the ConfigValidationError. + config[COMBINER][TYPE] = "sequence_concat" + ModelConfig.from_dict(config) + + +def test_check_llm_input_features(): + config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +input_features: + - name: sample_1 + type: text + - name: sample_2 + type: text +output_features: + - name: label + type: text +backend: + type: ray +""") + + # do not allow more than one input feature + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + # do not allow one non-text input feature + config["input_features"].pop(-1) + config["input_features"][0]["type"] = "category" + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + # allow exactly one text input feature + config["input_features"][0]["type"] = "text" + ModelConfig.from_dict(config) + + +def test_retrieval_config_none_type(): + config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +prompt: + retrieval: + type: null + k: 1 + task: "Classify the sample input as either negative, neutral, or positive." +input_features: +- + name: sample + type: text +output_features: +- + name: label + type: text +""") + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + # will not fail + config["prompt"]["retrieval"]["k"] = 0 + ModelConfig.from_dict(config) + + +def test_retrieval_config_random_type(): + config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +prompt: + retrieval: + type: random + task: "Classify the sample input as either negative, neutral, or positive." +input_features: +- + name: sample + type: text +output_features: +- + name: label + type: text +""") + + # should not fail because we auto-set k=1 if k=0 on __post_init__ + ModelConfig.from_dict(config) + + +def test_retrieval_config_semantic_type(): + config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +prompt: + retrieval: + type: semantic + task: "Classify the sample input as either negative, neutral, or positive." +input_features: +- + name: sample + type: text +output_features: +- + name: label + type: text +""") + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["prompt"]["retrieval"]["model_name"] = "some-huggingface-model" + ModelConfig.from_dict(config) + + +@pytest.mark.skip( + reason="TODO(geoffrey, arnav): re-enable this when we have reconciled the config with the backend kwarg in api.py" +) +def test_check_llm_quantization_backend_incompatibility(): + config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +quantization: + bits: 4 +input_features: + - name: sample + type: text +output_features: + - name: label + type: text +backend: + type: ray +""") + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["backend"]["type"] = "local" + ModelConfig.from_dict(config) + + del config["backend"] + ModelConfig.from_dict(config) + + del config["quantization"] + config["backend"] = {"type": "ray"} + ModelConfig.from_dict(config) + + +def test_check_qlora(): + config = yaml.safe_load(""" +model_type: llm +base_model: facebook/opt-350m +quantization: + bits: 4 +input_features: + - name: sample + type: text +output_features: + - name: label + type: text +trainer: + type: finetune +""") + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["adapter"] = { + "type": "adaption_prompt", + } + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["adapter"] = { + "type": "lora", + } + ModelConfig.from_dict(config) + + +def test_check_prompt_requirements(): + config = { + "model_type": "llm", + "input_features": [ + text_feature(name="input1", column="col1", encoder={"type": "passthrough"}), + ], + "output_features": [text_feature(name="output1")], + "base_model": "opt-350m", + } + + ModelConfig.from_dict(config) + + config["prompt"] = {"task": "Some task"} + ModelConfig.from_dict(config) + + config["prompt"] = {"task": "Some task", "template": "Some template not mentioning the task"} + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["prompt"] = {"task": "Some task", "template": "{__invalid__}"} + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["prompt"] = {"task": "Some task", "template": "{__task__}"} + ModelConfig.from_dict(config) + + config["prompt"] = {"template": "{input1}"} + ModelConfig.from_dict(config) + + # Raise an error if template has a placeholder for the output feature. + config["prompt"] = {"template": "{input1}: {output1}"} + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_check_sample_ratio_and_size_compatible(): + config = { + "input_features": [binary_feature()], + "output_features": [binary_feature()], + "model_type": "ecd", + } + ModelConfig.from_dict(config) + + config["preprocessing"] = {"sample_size": 10} + ModelConfig.from_dict(config) + + config["preprocessing"]["sample_ratio"] = 1 + ModelConfig.from_dict(config) + + config["preprocessing"]["sample_ratio"] = 0.1 + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config["preprocessing"]["sample_size"] = 0 + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + del config["preprocessing"]["sample_size"] + ModelConfig.from_dict(config) + + +def test_check_llm_text_encoder_is_not_used_with_ecd(): + config = { + "input_features": [ + { + "name": "Question", + "type": "text", + "encoder": { + "type": "auto_transformer", + "pretrained_model_name_or_path": "meta-llama/Llama-2-7b-hf", + "trainable": False, + }, + "preprocessing": {"cache_encoder_embeddings": True}, + } + ], + "output_features": [{"name": "Answer", "type": "text"}], + } + + with pytest.raises(ConfigValidationError) as excinfo: + ModelConfig.from_dict(config) + + assert "Please use the `model_type: llm` for text-to-text models." in str(excinfo.value) + + +def test_check_grpo_requires_text_output(): + """GRPO trainer with a non-text output feature should raise.""" + config = { + "model_type": "llm", + "base_model": "facebook/opt-350m", + "input_features": [{"name": "prompt", "type": "text"}], + "output_features": [{"name": "label", "type": "category"}], + "trainer": {"type": "grpo"}, + } + with pytest.raises(ConfigValidationError, match="GRPO trainer requires a text output feature"): + ModelConfig.from_dict(config) diff --git a/tests/ludwig/config_validation/test_validate_config_combiner.py b/tests/ludwig/config_validation/test_validate_config_combiner.py new file mode 100644 index 0000000..2ef8a97 --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_combiner.py @@ -0,0 +1,175 @@ +import pytest + +from ludwig.config_validation.validation import check_schema, get_schema +from ludwig.constants import MODEL_ECD, TRAINER +from ludwig.error import ConfigValidationError +from tests.integration_tests.utils import binary_feature, category_feature, number_feature + + +def test_combiner_schema_is_not_empty_for_ECD(): + # Essentially verifies that the combiner registry is not empty at import time: + assert len(get_schema(MODEL_ECD)["properties"]["combiner"]["allOf"]) > 0 + + +@pytest.mark.parametrize("eval_batch_size", [500000, None]) +def test_config_tabnet(eval_batch_size): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + "size": 24, + "output_size": 26, + "sparsity": 0.000001, + "bn_virtual_divider": 32, + "bn_momentum": 0.4, + "num_steps": 5, + "relaxation_factor": 1.5, + "use_keras_batch_norm": False, + "bn_virtual_bs": 512, + }, + TRAINER: { + "batch_size": 16384, + "eval_batch_size": eval_batch_size, + "epochs": 1000, + "early_stop": 20, + "learning_rate": 0.02, + "optimizer": {"type": "adam"}, + "learning_rate_scheduler": { + "decay": "linear", + "decay_steps": 20000, + "decay_rate": 0.9, + "staircase": True, + }, + "regularization_lambda": 1, + "regularization_type": "l2", + }, + } + check_schema(config) + + +def test_config_bad_combiner(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + }, + } + + # config is valid at this point + check_schema(config) + + # combiner without type + del config["combiner"]["type"] + with pytest.raises(ConfigValidationError): + check_schema(config) + + # bad combiner type + config["combiner"]["type"] = "fake" + with pytest.raises(ConfigValidationError): + check_schema(config) + + # bad combiner format (list instead of dict) + config["combiner"] = [{"type": "tabnet"}] + with pytest.raises(ConfigValidationError): + check_schema(config) + + # bad combiner parameter types + config["combiner"] = { + "type": "tabtransformer", + "num_layers": 10, + "dropout": False, + } + with pytest.raises(ConfigValidationError): + check_schema(config) + + # bad combiner parameter range + config["combiner"] = { + "type": "transformer", + "dropout": -1, + } + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_config_bad_combiner_types_enums(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": {"type": "concat", "weights_initializer": "zeros"}, + } + + # config is valid at this point + check_schema(config) + + # Test weights initializer: + config["combiner"]["weights_initializer"] = {"test": "fail"} + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["weights_initializer"] = "fail" + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["weights_initializer"] = {} + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["weights_initializer"] = {"type": "fail"} + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["weights_initializer"] = {"type": "normal", "stddev": 0} + check_schema(config) + + # Test bias initializer: + del config["combiner"]["weights_initializer"] + config["combiner"]["bias_initializer"] = "kaiming_uniform" + check_schema(config) + config["combiner"]["bias_initializer"] = "fail" + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["bias_initializer"] = {} + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["bias_initializer"] = {"type": "fail"} + with pytest.raises(ConfigValidationError): + check_schema(config) + config["combiner"]["bias_initializer"] = {"type": "zeros", "stddev": 0} + check_schema(config) + + # Test norm: + del config["combiner"]["bias_initializer"] + config["combiner"]["norm"] = "batch" + check_schema(config) + config["combiner"]["norm"] = "fail" + with pytest.raises(ConfigValidationError): + check_schema(config) + + # Test activation: + del config["combiner"]["norm"] + config["combiner"]["activation"] = "relu" + check_schema(config) + config["combiner"]["activation"] = 123 + with pytest.raises(ConfigValidationError): + check_schema(config) + + # Test reduce_output: + del config["combiner"]["activation"] + config2 = {**config} + config2["combiner"]["type"] = "tabtransformer" + config2["combiner"]["reduce_output"] = "sum" + check_schema(config) + config2["combiner"]["reduce_output"] = "fail" + with pytest.raises(ConfigValidationError): + check_schema(config2) + + # Test reduce_output = None: + config2["combiner"]["reduce_output"] = None + check_schema(config2) diff --git a/tests/ludwig/config_validation/test_validate_config_encoder.py b/tests/ludwig/config_validation/test_validate_config_encoder.py new file mode 100644 index 0000000..2fb94bf --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_encoder.py @@ -0,0 +1,59 @@ +import pytest + +from ludwig.constants import DEFAULTS, ENCODER, INPUT_FEATURES, NAME, OUTPUT_FEATURES, SEQUENCE, TEXT, TIMESERIES, TYPE +from ludwig.error import ConfigValidationError +from ludwig.schema.model_config import ModelConfig +from tests.integration_tests.utils import ( + binary_feature, + number_feature, + sequence_feature, + text_feature, + timeseries_feature, +) + + +@pytest.mark.parametrize("feature_type", [SEQUENCE, TEXT, TIMESERIES]) +def test_default_transformer_encoder(feature_type): + """Tests that a transformer hyperparameter divisibility error is correctly recognized in feature defaults. + + Transformers require that `hidden_size % num_heads == 0`. 9 and 18 were selected as test values because they were + the values from the original error. + """ + config = { + INPUT_FEATURES: [number_feature(), {TYPE: feature_type, NAME: f"test_{feature_type}"}], + OUTPUT_FEATURES: [binary_feature()], + DEFAULTS: {feature_type: {ENCODER: {TYPE: "transformer", "hidden_size": 9, "num_heads": 18}}}, + } + + with pytest.raises(ConfigValidationError): + m = ModelConfig.from_dict(config) + print(m) + + config[DEFAULTS][feature_type][ENCODER]["hidden_size"] = 18 + config[DEFAULTS][feature_type][ENCODER]["num_heads"] = 9 + + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize("feature_gen", [sequence_feature, text_feature, timeseries_feature]) +def test_input_feature_transformer_encoder(feature_gen): + """Tests that a transformer hyperparameter divisibility error is correctly recognized for a specific feature. + + Transformers require that `hidden_size % num_heads == 0`. 9 and 18 were selected as test values because they were + the values from the original error. + """ + config = { + INPUT_FEATURES: [ + number_feature(), + feature_gen(**{ENCODER: {TYPE: "transformer", "hidden_size": 9, "num_heads": 18}}), + ], + OUTPUT_FEATURES: [binary_feature()], + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config[INPUT_FEATURES][1][ENCODER]["hidden_size"] = 18 + config[INPUT_FEATURES][1][ENCODER]["num_heads"] = 9 + + ModelConfig.from_dict(config) diff --git a/tests/ludwig/config_validation/test_validate_config_features.py b/tests/ludwig/config_validation/test_validate_config_features.py new file mode 100644 index 0000000..97f05a2 --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_features.py @@ -0,0 +1,108 @@ +import pytest + +from ludwig.config_validation.validation import check_schema +from ludwig.error import ConfigValidationError +from tests.integration_tests.utils import binary_feature, category_feature, number_feature, text_feature + + +def test_config_input_output_features(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense"}), + number_feature(encoder={"type": "passthrough"}), + ], + "output_features": [binary_feature(decoder={"type": "regressor"})], + } + + check_schema(config) + + +def test_incorrect_input_features_config(): + config = { + "input_features": [ + category_feature(preprocessing={"normalization": "zscore"}), + ], + "output_features": [binary_feature()], + } + + # TODO(ksbrar): Circle back after discussing whether additional properties should be allowed long-term. + # # Not a preprocessing param for category feature + # with pytest.raises(ValidationError): + # check_schema(config) + + config = { + "input_features": [ + text_feature(preprocessing={"padding_symbol": 0}), + ], + "output_features": [binary_feature()], + } + + # Incorrect type for padding_symbol preprocessing param + with pytest.raises(ConfigValidationError): + check_schema(config) + + config = { + "input_features": [ + binary_feature(), + ], + "output_features": [binary_feature()], + } + del config["input_features"][0]["type"] + + # No type + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_incorrect_output_features_config(): + config = { + "input_features": [ + number_feature(), + ], + "output_features": [binary_feature(decoder="classifier")], + } + + # Invalid decoder for binary output feature + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_too_few_features_config(): + ifeatures = [number_feature()] + ofeatures = [binary_feature()] + + check_schema( + { + "input_features": ifeatures, + "output_features": ofeatures, + } + ) + + # Must have at least one input feature + with pytest.raises(ConfigValidationError): + check_schema( + { + "input_features": [], + "output_features": ofeatures, + } + ) + + # Must have at least one output feature + with pytest.raises(ConfigValidationError): + check_schema( + { + "input_features": ifeatures, + "output_features": [], + } + ) + + +def test_multi_output_features_config(): + # Multi-output is fine for ECD + check_schema( + { + "input_features": [number_feature()], + "output_features": [binary_feature(), number_feature()], + "model_type": "ecd", + } + ) diff --git a/tests/ludwig/config_validation/test_validate_config_hyperopt.py b/tests/ludwig/config_validation/test_validate_config_hyperopt.py new file mode 100644 index 0000000..c855aba --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_hyperopt.py @@ -0,0 +1,296 @@ +from itertools import repeat +from unittest.mock import patch + +import pytest + +# Imported to populate the registry +import ludwig.schema.hyperopt.parameter +import ludwig.schema.hyperopt.scheduler +import ludwig.schema.hyperopt.search_algorithm # noqa: F401 +from ludwig.constants import ( + EXECUTOR, + HYPEROPT, + INPUT_FEATURES, + OUTPUT_FEATURES, + PARAMETERS, + SCHEDULER, + SEARCH_ALG, + TYPE, +) +from ludwig.error import ConfigValidationError +from ludwig.schema.hyperopt import utils +from ludwig.schema.model_types.base import ModelConfig +from tests.integration_tests.utils import binary_feature, text_feature + + +@pytest.mark.parametrize( + "dependencies,raises_exception", + [ + ([], False), + ([("ludwig", "ludwig")], False), + ([("ludwig", "ludwig"), ("marshmallow", "marshmallow")], False), + ([("fake_dependency", "fake_dependency")], True), + ([("ludwig", "ludwig"), ("fake_dependency", "fake_dependency")], True), + ], +) +def test_check_scheduler_dependencies_installed(dependencies, raises_exception): + config = { + INPUT_FEATURES: [text_feature()], + OUTPUT_FEATURES: [binary_feature()], + HYPEROPT: { + PARAMETERS: {"trainer.learning_rate": {"space": "choice", "categories": [0.0001, 0.001, 0.01, 0.1]}}, + EXECUTOR: {SCHEDULER: {TYPE: "fifo"}}, + }, + } + + with patch("ludwig.schema.hyperopt.utils.get_scheduler_dependencies", return_value=dependencies): + if raises_exception: + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + else: + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "dependencies,raises_exception", + [ + ([], False), + ([("ludwig", "ludwig")], False), + ([("ludwig", "ludwig"), ("marshmallow", "marshmallow")], False), + ([("fake_dependency", "fake_dependency")], True), + ([("ludwig", "ludwig"), ("fake_dependency", "fake_dependency")], True), + ], +) +def test_check_search_algorithm_dependencies_installed(dependencies, raises_exception): + config = { + INPUT_FEATURES: [text_feature()], + OUTPUT_FEATURES: [binary_feature()], + HYPEROPT: { + PARAMETERS: {"trainer.learning_rate": {"space": "choice", "categories": [0.0001, 0.001, 0.01, 0.1]}}, + SEARCH_ALG: {TYPE: "random"}, + }, + } + + with patch("ludwig.schema.hyperopt.utils.get_search_algorithm_dependencies", return_value=dependencies): + if raises_exception: + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + else: + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "space,raises_exception", + list(zip(utils.parameter_config_registry.keys(), repeat(False, len(utils.parameter_config_registry)))) + + [("fake_space", True)], +) +def test_parameter_type_check(space, raises_exception): + """Test that the parameter type is a valid hyperparameter search space. + + This should only be valid until the search space schema is updated to validate spaces as config objects rather than + dicts. That update is non-trivial, so to hold over until it is ready we cast the dicts to the corresponding + parameter objects and validate as an aux check. The test covers every valid space and one invalid space. + """ + config = { + INPUT_FEATURES: [text_feature()], + OUTPUT_FEATURES: [binary_feature()], + HYPEROPT: { + SEARCH_ALG: {TYPE: "random"}, + PARAMETERS: { + "trainer.learning_rate": { + "space": space, + } + }, + }, + } + + if not raises_exception: + ModelConfig.from_dict(config) + else: + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "referenced_parameter,raises_exception", + [ + # Passing cases + ("trainer.learning_rate", False), + ("in_feature.encoder.num_fc_layers", False), + ("out_feature.decoder.num_fc_layers", False), + # Invalid cases with various nesting of invalid names + ("", True), + (" ", True), + ("foo.bar", True), + ("trainer.bar", True), + ("foo.learning_rate", True), + ("in_feature.encoder.bar", True), + ("in_feature.foo.num_fc_layers", True), + ("out_feature.encoder.bar", True), + ("out_feature.foo.num_fc_layers", True), + ], +) +def test_parameter_key_check(referenced_parameter, raises_exception): + """Test that references to config parameters are validated correctly. + + Hyperopt parameters reference the config parameters they search with `.` notation to access different subsections, + e.g. `trainer.learning_rate`. These are added to the config as arbitrary strings, and an invalid reference should be + considered a validation error since we will otherwise search over an unused space or defer the error to train time. + """ + config = { + INPUT_FEATURES: [text_feature(name="in_feature")], + OUTPUT_FEATURES: [binary_feature(name="out_feature")], + HYPEROPT: { + SEARCH_ALG: {TYPE: "random"}, + PARAMETERS: {referenced_parameter: {"space": "choice", "categories": [1, 2, 3, 4]}}, + }, + } + + if raises_exception: + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + else: + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "categories,raises_exception", + [ + # Passing case + ( + [ + { + "combiner": {"type": "tabnet", "bn_virtual_bs": 256}, + "trainer": {"learning_rate": 0.001, "batch_size": 64}, + }, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + False, + ), + # Errors in top level parameter names (4 cases) + ( + [ + { + "foo": {"type": "tabnet", "bn_virtual_bs": 256}, + "trainer": {"learning_rate": 0.001, "batch_size": 64}, + }, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + { + "combiner": {"type": "tabnet", "bn_virtual_bs": 256}, + "trainer": {"learning_rate": 0.001, "batch_size": 64}, + }, + {"foo": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + { + "combiner": {"type": "tabnet", "bn_virtual_bs": 256}, + "foo": {"learning_rate": 0.001, "batch_size": 64}, + }, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + { + "combiner": {"type": "tabnet", "bn_virtual_bs": 256}, + "trainer": {"learning_rate": 0.001, "batch_size": 64}, + }, + {"combiner": {"type": "concat"}, "foo": {"batch_size": 256}}, + ], + True, + ), + # Errors in nested parameters (6 cases) + ( + [ + {"combiner": {"bar": "tabnet", "bn_virtual_bs": 256}, "trainer": {"bar": 0.001, "batch_size": 64}}, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + {"combiner": {"type": "tabnet", "bar": 256}, "trainer": {"learning_rate": 0.001, "batch_size": 64}}, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + False, + ), + ( + [ + {"combiner": {"type": "tabnet", "bn_virtual_bs": 256}, "trainer": {"bar": 0.001, "batch_size": 64}}, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + {"combiner": {"type": "tabnet", "bn_virtual_bs": 256}, "trainer": {"bar": 0.001, "batch_size": 64}}, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + {"combiner": {"type": "tabnet", "bn_virtual_bs": 256}, "trainer": {"learning_rate": 0.001, "bar": 64}}, + {"combiner": {"type": "concat"}, "trainer": {"batch_size": 256}}, + ], + True, + ), + ( + [ + { + "combiner": {"type": "tabnet", "bn_virtual_bs": 256}, + "trainer": {"learning_rate": 0.001, "batch_size": 64}, + }, + {"combiner": {"type": "concat"}, "trainer": {"bar": 256}}, + ], + True, + ), + ], +) +def test_nested_parameter_key_check(categories, raises_exception): + """Test that nested parameters are validated correctly.""" + config = { + INPUT_FEATURES: [text_feature(name="in_feature")], + OUTPUT_FEATURES: [binary_feature(name="out_feature")], + HYPEROPT: {SEARCH_ALG: {TYPE: "random"}, PARAMETERS: {".": {"space": "choice", "categories": categories}}}, + } + + if raises_exception: + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + else: + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "config", + [ + { + "out_feature.decoder.fc_layers": { + "space": "choice", + "categories": [ + [{"output_size": 64}, {"output_size": 32}], + [{"output_size": 64}], + [{"output_size": 32}], + ], + } + } + ], +) +def test_flat_parameter_edge_cases(config): + config = { + INPUT_FEATURES: [text_feature(name="in_feature")], + OUTPUT_FEATURES: [binary_feature(name="out_feature")], + HYPEROPT: {SEARCH_ALG: {TYPE: "random"}, PARAMETERS: config}, + } + + ModelConfig.from_dict(config) diff --git a/tests/ludwig/config_validation/test_validate_config_misc.py b/tests/ludwig/config_validation/test_validate_config_misc.py new file mode 100644 index 0000000..9dda66a --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_misc.py @@ -0,0 +1,439 @@ +import pytest + +from ludwig.config_validation.validation import check_schema, get_schema +from ludwig.constants import ( + ACTIVE, + BACKEND, + CATEGORY, + COLUMN, + DECODER, + DEFAULTS, + ENCODER, + LOSS, + MODEL_ECD, + MODEL_LLM, + NAME, + PREPROCESSING, + PROC_COLUMN, + TRAINER, + TYPE, +) +from ludwig.error import ConfigValidationError +from ludwig.features.feature_registries import get_output_type_registry +from ludwig.schema import utils as schema_utils +from ludwig.schema.combiners.utils import get_combiner_jsonschema +from ludwig.schema.defaults.ecd import ECDDefaultsConfig +from ludwig.schema.features.preprocessing.audio import AudioPreprocessingConfig +from ludwig.schema.features.preprocessing.bag import BagPreprocessingConfig +from ludwig.schema.features.preprocessing.binary import BinaryPreprocessingConfig +from ludwig.schema.features.preprocessing.category import CategoryPreprocessingConfig +from ludwig.schema.features.preprocessing.date import DatePreprocessingConfig +from ludwig.schema.features.preprocessing.h3 import H3PreprocessingConfig +from ludwig.schema.features.preprocessing.image import ImagePreprocessingConfig +from ludwig.schema.features.preprocessing.number import NumberPreprocessingConfig +from ludwig.schema.features.preprocessing.sequence import SequencePreprocessingConfig +from ludwig.schema.features.preprocessing.set import SetPreprocessingConfig +from ludwig.schema.features.preprocessing.text import TextPreprocessingConfig +from ludwig.schema.features.preprocessing.timeseries import TimeseriesPreprocessingConfig +from ludwig.schema.features.preprocessing.vector import VectorPreprocessingConfig +from ludwig.schema.features.utils import get_input_feature_jsonschema, get_output_feature_jsonschema +from ludwig.schema.llms.peft import LoraConfig +from ludwig.schema.model_types.base import ModelConfig +from ludwig.schema.utils import unload_jsonschema_from_config_class +from tests.integration_tests.utils import ( + audio_feature, + bag_feature, + binary_feature, + category_feature, + date_feature, + ENCODERS, + h3_feature, + image_feature, + number_feature, + sequence_feature, + set_feature, + text_feature, + timeseries_feature, + vector_feature, +) + + +def test_config_features(): + all_input_features = [ + audio_feature("/tmp/destination_folder", encoder={"type": "parallel_cnn"}), + bag_feature(encoder={"type": "embed"}), + binary_feature(encoder={"type": "passthrough"}), + category_feature(encoder={"type": "dense"}), + date_feature(encoder={"type": "embed"}), + h3_feature(encoder={"type": "embed"}), + image_feature("/tmp/destination_folder", encoder={"type": "stacked_cnn"}), + number_feature(encoder={"type": "passthrough"}), + sequence_feature(encoder={"type": "parallel_cnn"}), + set_feature(encoder={"type": "embed"}), + text_feature(encoder={"type": "parallel_cnn"}), + timeseries_feature(encoder={"type": "parallel_cnn"}), + vector_feature(encoder={"type": "dense"}), + ] + all_output_features = [ + binary_feature(decoder={"type": "regressor"}), + category_feature(decoder={"type": "classifier"}), + number_feature(decoder={"type": "regressor"}), + sequence_feature(decoder={"type": "generator"}), + set_feature(decoder={"type": "classifier"}), + text_feature(decoder={"type": "generator"}), + vector_feature(decoder={"type": "projector"}), + ] + + # validate config with all features + config = { + "input_features": all_input_features, + "output_features": all_output_features, + } + check_schema(config) + + # test various invalid output features + input_only_features = [ + feature for feature in all_input_features if feature["type"] not in get_output_type_registry().keys() + ] + for input_feature in input_only_features: + config = { + "input_features": all_input_features, + "output_features": all_output_features + [input_feature], + } + + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_config_encoders(): + for encoder in ENCODERS: + config = { + "input_features": [ + sequence_feature(encoder={"type": encoder, "reduce_output": "sum"}), + image_feature("/tmp/destination_folder"), + ], + "output_features": [category_feature(decoder={"type": "classifier", "vocab_size": 2}, reduce_input="sum")], + "combiner": {"type": "concat", "output_size": 14}, + } + check_schema(config) + + +def test_config_with_backend(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + "size": 24, + "output_size": 26, + "sparsity": 0.000001, + "bn_virtual_divider": 32, + "bn_momentum": 0.4, + "num_steps": 5, + "relaxation_factor": 1.5, + "bn_virtual_bs": 512, + }, + TRAINER: { + "batch_size": 16384, + "eval_batch_size": 500000, + "epochs": 1000, + "early_stop": 20, + "learning_rate": 0.02, + "optimizer": {"type": "adam"}, + "learning_rate_scheduler": { + "decay": "linear", + "decay_steps": 20000, + "decay_rate": 0.9, + "staircase": True, + }, + "regularization_lambda": 1, + "regularization_type": "l2", + }, + BACKEND: {"type": "ray", "trainer": {"num_workers": 2}}, + } + check_schema(config) + + +def test_config_bad_feature_type(): + config = { + "input_features": [{"name": "foo", "type": "fake"}], + "output_features": [category_feature(encoder={"vocab_size": 2}, reduce_input="sum")], + "combiner": {"type": "concat", "output_size": 14}, + } + + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_config_bad_encoder_name(): + config = { + "input_features": [sequence_feature(encoder={"type": "fake", "reduce_output": "sum"})], + "output_features": [category_feature(decoder={"type": "classifier", "vocab_size": 2}, reduce_input="sum")], + "combiner": {"type": "concat", "output_size": 14}, + } + + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_config_fill_values(): + vector_fill_values = ["1.0 0.0 1.04 10.49", "1 2 3 4 501.0"] + binary_fill_values = ["yes", "No", "1", "TRUE", 1] + for vector_fill_value, binary_fill_value in zip(vector_fill_values, binary_fill_values): + config = { + "input_features": [ + vector_feature(preprocessing={"fill_value": vector_fill_value}), + ], + "output_features": [binary_feature(preprocessing={"fill_value": binary_fill_value})], + } + check_schema(config) + + bad_vector_fill_values = ["one two three", "1,2,3", 0] + bad_binary_fill_values = ["one", 2, "maybe"] + for vector_fill_value, binary_fill_value in zip(bad_vector_fill_values, bad_binary_fill_values): + config = { + "input_features": [ + vector_feature(preprocessing={"fill_value": vector_fill_value}), + ], + "output_features": [binary_feature(preprocessing={"fill_value": binary_fill_value})], + } + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_validate_with_preprocessing_defaults(): + config = { + "input_features": [ + audio_feature( + "/tmp/destination_folder", + preprocessing=AudioPreprocessingConfig().to_dict(), + encoder={"type": "parallel_cnn"}, + ), + bag_feature(preprocessing=BagPreprocessingConfig().to_dict(), encoder={"type": "embed"}), + binary_feature(preprocessing=BinaryPreprocessingConfig().to_dict(), encoder={"type": "passthrough"}), + category_feature(preprocessing=CategoryPreprocessingConfig().to_dict(), encoder={"type": "dense"}), + date_feature(preprocessing=DatePreprocessingConfig().to_dict(), encoder={"type": "embed"}), + h3_feature(preprocessing=H3PreprocessingConfig().to_dict(), encoder={"type": "embed"}), + image_feature( + "/tmp/destination_folder", + preprocessing=ImagePreprocessingConfig().to_dict(), + encoder={"type": "stacked_cnn"}, + ), + number_feature(preprocessing=NumberPreprocessingConfig().to_dict(), encoder={"type": "passthrough"}), + sequence_feature(preprocessing=SequencePreprocessingConfig().to_dict(), encoder={"type": "parallel_cnn"}), + set_feature(preprocessing=SetPreprocessingConfig().to_dict(), encoder={"type": "embed"}), + text_feature(preprocessing=TextPreprocessingConfig().to_dict(), encoder={"type": "parallel_cnn"}), + timeseries_feature( + preprocessing=TimeseriesPreprocessingConfig().to_dict(), encoder={"type": "parallel_cnn"} + ), + vector_feature(preprocessing=VectorPreprocessingConfig().to_dict(), encoder={"type": "dense"}), + ], + "output_features": [{"name": "target", "type": "category"}], + TRAINER: { + "learning_rate_scheduler": { + "decay": "linear", + }, + "learning_rate": 0.001, + "validation_field": "target", + "validation_metric": "accuracy", + }, + } + + check_schema(config) + + +def test_ecd_defaults_schema(): + schema = ECDDefaultsConfig() + assert schema.binary.decoder.type == "regressor" + assert schema.binary.encoder.type == "passthrough" + assert schema.category.encoder.dropout == 0.0 + assert ENCODER in schema.category.to_dict() + assert PREPROCESSING in schema.category.to_dict() + assert DECODER in schema.category.to_dict() + assert LOSS in schema.category.to_dict() + + +def test_validate_defaults_schema(): + config = { + "input_features": [ + category_feature(), + number_feature(), + ], + "output_features": [category_feature(output_feature=True)], + "defaults": { + "category": { + "preprocessing": { + "missing_value_strategy": "drop_row", + }, + "encoder": { + "type": "sparse", + }, + "decoder": { + "type": "classifier", + "norm_params": None, + "dropout": 0.0, + "use_bias": True, + }, + "loss": { + "type": "softmax_cross_entropy", + "confidence_penalty": 0, + }, + }, + "number": { + "preprocessing": { + "missing_value_strategy": "fill_with_const", + "fill_value": 0, + }, + "loss": {"type": "mean_absolute_error"}, + }, + }, + } + + check_schema(config) + + config[DEFAULTS][CATEGORY][NAME] = "TEST" + + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_validate_no_trainer_type(): + config = { + "model_type": "ecd", + "input_features": [ + category_feature(), + number_feature(), + ], + "output_features": [category_feature(output_feature=True)], + "trainer": {"learning_rate": "auto", "batch_size": "auto"}, + } + + # Ensure validation succeeds with ECD trainer params and ECD model type + check_schema(config) + + +def test_schema_no_duplicates(): + schema = get_schema() + + popped_fields = [NAME, TYPE, COLUMN, PROC_COLUMN, ACTIVE] + + for field in popped_fields: + assert field not in schema["properties"]["input_features"]["items"]["allOf"][0]["then"]["properties"] + assert field not in schema["properties"]["output_features"]["items"]["allOf"][0]["then"]["properties"] + assert field not in schema["properties"]["combiner"]["allOf"][0]["then"]["properties"] + assert field not in schema["properties"]["trainer"]["properties"]["optimizer"]["allOf"][0]["then"]["properties"] + assert ( + field + not in schema["properties"]["input_features"]["items"]["allOf"][0]["then"]["properties"]["encoder"][ + "allOf" + ][0]["then"]["properties"] + ) + assert ( + field + not in schema["properties"]["output_features"]["items"]["allOf"][0]["then"]["properties"]["decoder"][ + "allOf" + ][0]["then"]["properties"] + ) + + +@pytest.mark.parametrize("model_type", [MODEL_ECD, MODEL_LLM]) +def test_ludwig_schema_serialization(model_type): + import json + + schema = get_schema(model_type) + + try: + json.dumps(schema) + except TypeError as e: + raise TypeError( + f"Ludwig schema of type `{model_type}` cannot be represented by valid JSON. See further details: {e}" + ) + + +def test_encoder_descriptions(): + """This test tests that each encoder in the enum for each feature type has a description.""" + schema = get_input_feature_jsonschema(MODEL_ECD) + + for feature_schema in schema["allOf"]: + type_data = feature_schema["then"]["properties"]["encoder"]["properties"]["type"] + assert len(set(type_data["enumDescriptions"].keys())) > 0 + assert set(type_data["enumDescriptions"].keys()).issubset(set(type_data["enum"])) + + +def test_combiner_descriptions(): + """This test tests that each combiner in the enum for available combiners has a description.""" + combiner_json_schema = get_combiner_jsonschema() + type_data = combiner_json_schema["properties"]["type"] + assert len(set(type_data["enumDescriptions"].keys())) > 0 + assert set(type_data["enumDescriptions"].keys()).issubset(set(type_data["enum"])) + + +def test_decoder_descriptions(): + """This test tests that each decoder in the enum for each feature type has a description.""" + schema = get_output_feature_jsonschema(MODEL_ECD) + + for feature_schema in schema["allOf"]: + type_data = feature_schema["then"]["properties"]["decoder"]["properties"]["type"] + assert len(type_data["enumDescriptions"].keys()) > 0 + assert set(type_data["enumDescriptions"].keys()).issubset(set(type_data["enum"])) + + +def test_deprecation_warning_raised_for_unknown_parameters(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + "unknown_parameter_combiner": False, + }, + TRAINER: { + "epochs": 1000, + }, + } + with pytest.warns(DeprecationWarning, match="not a valid parameter"): + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "encoder_config,expected_adapter", + [ + ({"type": "bert", "trainable": True}, None), + ({"type": "bert", "trainable": True, "adapter": None}, None), + ({"type": "bert", "trainable": True, "adapter": {"type": "lora"}}, LoraConfig()), + ( + { + "type": "bert", + "trainable": True, + "adapter": {"type": "lora", "r": 16, "alpha": 32, "dropout": 0.1, "bias_type": "all"}, + }, + LoraConfig(r=16, alpha=32, dropout=0.1, bias_type="all"), + ), + ], +) +def test_text_encoder_adapter(encoder_config, expected_adapter): + config = { + "input_features": [text_feature(encoder=encoder_config)], + "output_features": [category_feature(decoder={"type": "classifier", "vocab_size": 2}, reduce_input="sum")], + } + config_obj = ModelConfig.from_dict(config) + + assert config_obj.input_features[0].encoder.adapter == expected_adapter + + +def test_default_param_metadata(): + class TestClass(schema_utils.LudwigBaseConfig): + test_schema_entry: str = schema_utils.StringOptions( + options=["test"], + default="test", + description="", + ) + + test_class = unload_jsonschema_from_config_class(TestClass) + + assert test_class["properties"]["test_schema_entry"]["parameter_metadata"] is not None diff --git a/tests/ludwig/config_validation/test_validate_config_preprocessing.py b/tests/ludwig/config_validation/test_validate_config_preprocessing.py new file mode 100644 index 0000000..bc05a1d --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_preprocessing.py @@ -0,0 +1,55 @@ +import pytest + +from ludwig.config_validation.preprocessing import check_global_max_sequence_length_fits_prompt_template +from ludwig.config_validation.validation import check_schema +from tests.integration_tests.utils import binary_feature, category_feature + + +def test_config_preprocessing(): + input_features = [category_feature(), category_feature()] + output_features = [binary_feature()] + + config = { + "input_features": input_features, + "output_features": output_features, + "preprocessing": { + "split": { + "type": "random", + "probabilities": [0.6, 0.2, 0.2], + }, + "oversample_minority": 0.4, + }, + } + + check_schema(config) + + # TODO(ksbrar): Circle back after discussing whether additional properties should be allowed long-term. + # config["preprocessing"]["fake_parameter"] = True + + # with pytest.raises(Exception): + # ModelConfig(config) + + +def test_check_global_max_sequence_length_fits_prompt_template(): + check_global_max_sequence_length_fits_prompt_template( + {"input_feature": {"prompt_template_num_tokens": 10}}, {"global_max_sequence_length": 10} + ) + check_global_max_sequence_length_fits_prompt_template( + {"input_feature": {"prompt_template_num_tokens": 100}}, {"global_max_sequence_length": 1000} + ) + check_global_max_sequence_length_fits_prompt_template( + {"input_feature": {"prompt_template_num_tokens": 100}}, {"global_max_sequence_length": None} + ) + + with pytest.raises(ValueError): + # Prompt template token length cannot be larger than the global max sequence length. + check_global_max_sequence_length_fits_prompt_template( + {"input_feature": {"prompt_template_num_tokens": 10}}, {"global_max_sequence_length": 5} + ) + + with pytest.raises(ValueError): + # Any input feature's prompt template token length can trigger the global max sequence length. + check_global_max_sequence_length_fits_prompt_template( + {"input_feature": {"prompt_template_num_tokens": 5}, "input_feature_2": {"prompt_template_num_tokens": 20}}, + {"global_max_sequence_length": 10}, + ) diff --git a/tests/ludwig/config_validation/test_validate_config_trainer.py b/tests/ludwig/config_validation/test_validate_config_trainer.py new file mode 100644 index 0000000..de4aa35 --- /dev/null +++ b/tests/ludwig/config_validation/test_validate_config_trainer.py @@ -0,0 +1,152 @@ +import pytest + +from ludwig.config_validation.validation import check_schema +from ludwig.constants import TRAINER +from ludwig.error import ConfigValidationError +from ludwig.schema.optimizers import optimizer_registry +from ludwig.schema.trainer import ECDTrainerConfig +from tests.integration_tests.utils import binary_feature, category_feature, number_feature + +# Note: simple tests for now, but once we add dependent fields we can add tests for more complex relationships in this +# file. Currently verifies that the nested fields work, as the others are covered by basic marshmallow validation: + + +def test_config_trainer_empty_null_and_default(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + }, + TRAINER: {}, + } + check_schema(config) + + config[TRAINER] = None + with pytest.raises(ConfigValidationError): + check_schema(config) + + config[TRAINER] = ECDTrainerConfig.model_validate({}).to_dict() + check_schema(config) + + +def test_config_trainer_bad_optimizer(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + }, + TRAINER: {}, + } + check_schema(config) + + # Test manually set-to-null optimizer vs unspecified: + config[TRAINER]["optimizer"] = None + with pytest.raises(ConfigValidationError): + check_schema(config) + assert ECDTrainerConfig.model_validate({}).optimizer is not None + + # Test all types in optimizer_registry supported: + for key in optimizer_registry.keys(): + config[TRAINER]["optimizer"] = {"type": key} + check_schema(config) + + # Test invalid optimizer type: + config[TRAINER]["optimizer"] = {"type": 0} + with pytest.raises(ConfigValidationError): + check_schema(config) + config[TRAINER]["optimizer"] = {"type": "invalid"} + with pytest.raises(ConfigValidationError): + check_schema(config) + + +def test_optimizer_property_validation(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + }, + TRAINER: {}, + } + check_schema(config) + + # Test that an optimizer's property types are enforced: + config[TRAINER]["optimizer"] = {"type": "rmsprop"} + check_schema(config) + + config[TRAINER]["optimizer"]["momentum"] = "invalid" + with pytest.raises(ConfigValidationError): + check_schema(config) + + # Test extra keys are excluded and defaults are loaded appropriately: + config[TRAINER]["optimizer"]["momentum"] = 10 + config[TRAINER]["optimizer"]["extra_key"] = "invalid" + check_schema(config) + assert not hasattr(ECDTrainerConfig.model_validate(config[TRAINER]).optimizer, "extra_key") + + # Test bad parameter range: + config[TRAINER]["optimizer"] = {"type": "rmsprop", "eps": -1} + with pytest.raises(ConfigValidationError): + check_schema(config) + + # Test config validation for tuple types: + config[TRAINER]["optimizer"] = {"type": "adam", "betas": (0.1, 0.1)} + check_schema(config) + + +def test_clipper_property_validation(): + config = { + "input_features": [ + category_feature(encoder={"type": "dense", "vocab_size": 2}, reduce_input="sum"), + number_feature(), + ], + "output_features": [binary_feature()], + "combiner": { + "type": "tabnet", + }, + TRAINER: {}, + } + check_schema(config) + + # Test null/empty clipper: + config[TRAINER]["gradient_clipping"] = None + check_schema(config) + config[TRAINER]["gradient_clipping"] = {} + check_schema(config) + assert ( + ECDTrainerConfig.model_validate(config[TRAINER]).gradient_clipping + == ECDTrainerConfig.model_validate({}).gradient_clipping + ) + + # Test invalid clipper type: + config[TRAINER]["gradient_clipping"] = 0 + with pytest.raises(ConfigValidationError): + check_schema(config) + config[TRAINER]["gradient_clipping"] = "invalid" + with pytest.raises(ConfigValidationError): + check_schema(config) + + # Test that an optimizer's property types are enforced: + config[TRAINER]["gradient_clipping"] = {"clipglobalnorm": None} + check_schema(config) + config[TRAINER]["gradient_clipping"] = {"clipglobalnorm": 1} + check_schema(config) + config[TRAINER]["gradient_clipping"] = {"clipglobalnorm": "invalid"} + with pytest.raises(ConfigValidationError): + check_schema(config) + + # Test extra keys are excluded and defaults are loaded appropriately: + config[TRAINER]["gradient_clipping"] = {"clipnorm": 1} + config[TRAINER]["gradient_clipping"]["extra_key"] = "invalid" + assert not hasattr(ECDTrainerConfig.model_validate(config[TRAINER]).gradient_clipping, "extra_key") diff --git a/tests/ludwig/contrib/test_contrib.py b/tests/ludwig/contrib/test_contrib.py new file mode 100644 index 0000000..395a2ff --- /dev/null +++ b/tests/ludwig/contrib/test_contrib.py @@ -0,0 +1,31 @@ +import argparse +from collections.abc import Sequence + +import pytest + +from ludwig.contrib import add_contrib_callback_args +from ludwig.contribs.aim import AimCallback +from ludwig.contribs.comet import CometCallback +from ludwig.contribs.mlflow import MlflowCallback +from ludwig.contribs.wandb import WandbCallback + + +@pytest.mark.parametrize( + "sys_argv,expected", + [ + ([], []), + (["--mlflow"], [MlflowCallback]), + (["--aim"], [AimCallback]), + (["--comet"], [CometCallback]), + (["--wandb"], [WandbCallback]), + ], +) +def test_add_contrib_callback_args(sys_argv: Sequence[str], expected: list[type]): + parser = argparse.ArgumentParser() + add_contrib_callback_args(parser) + args = parser.parse_args(sys_argv) + callbacks = args.callbacks or [] + + assert len(callbacks) == len(expected) + for callback, expected_cls in zip(callbacks, expected): + assert isinstance(callback, expected_cls) diff --git a/tests/ludwig/data/__init__.py b/tests/ludwig/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/data/dataframe/test_dask.py b/tests/ludwig/data/dataframe/test_dask.py new file mode 100644 index 0000000..f80f6e6 --- /dev/null +++ b/tests/ludwig/data/dataframe/test_dask.py @@ -0,0 +1,81 @@ +import io + +import numpy as np +import pandas as pd +import pytest + +from ludwig.api import LudwigModel +from tests.integration_tests.utils import generate_data_as_dataframe + + +def test_dask_image_bytes_no_unicode_error(): + """Regression test for GitHub #4149. + + Dask's default dataframe.convert-string:True tries to decode all object-dtype + columns as UTF-8 strings. JPEG/PNG bytes start with 0xFF/0x89 — invalid UTF-8 + start bytes — so the conversion raises UnicodeDecodeError. + + Ludwig fixes this by setting dataframe.convert-string:False at import time + (ludwig/__init__.py), before the caller creates any Dask DataFrame. The old fix + in RayBackend.initialize() was too late: user DataFrames are created before + model.train() is called, so the broken _to_string_dtype node was already baked + into the task graph. + """ + import dask + import dask.dataframe as dd + from PIL import Image + + from ludwig.data.dataframe.dask import reset_index_across_all_partitions + + assert dask.config.get("dataframe.convert-string") is False, ( + "Ludwig must set dataframe.convert-string:False at import time (ludwig/__init__.py)" + ) + + def _jpeg_bytes() -> bytes: + buf = io.BytesIO() + Image.fromarray(np.zeros((8, 8, 3), dtype=np.uint8)).save(buf, "JPEG") + return buf.getvalue() + + n = 16 + df = dd.from_pandas( + pd.DataFrame({"image_data": [_jpeg_bytes() for _ in range(n)], "label": np.arange(n, dtype=float)}), + npartitions=4, + ) + + # reset_index is called inside Ludwig's build_dataset; it must not raise. + result = reset_index_across_all_partitions(df) + computed = result.compute() + assert len(computed) == n + assert computed["image_data"].iloc[0][:2] == b"\xff\xd8" # JPEG magic bytes intact + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_from_ray_dataset_empty(tmpdir, ray_cluster_2cpu): + import dask.dataframe as dd + + # Verifies that when the dataset is an empty MapBatches(BatchInferModel), we mitigate Ray's native to_dask() + # IndexError. + config = { + "input_features": [ + {"name": "cat1", "type": "category", "vocab_size": 2}, + {"name": "num1", "type": "number"}, + ], + "output_features": [ + {"name": "bin1", "type": "binary"}, + ], + "trainer": {"epochs": 1}, + } + train_input_df = generate_data_as_dataframe(config["input_features"], config["output_features"]) + model = LudwigModel(config, backend="ray") + model.train( + train_input_df, + output_directory=tmpdir, + skip_save_model=True, + skip_save_progress=True, + skip_save_processed_output=True, + skip_save_processed_input=True, + ) + + predict_input_df = dd.from_pandas(pd.DataFrame([], columns=["cat1", "num1", "bin1"]), npartitions=1) + model.predict(predict_input_df) diff --git a/tests/ludwig/data/test_cache_util.py b/tests/ludwig/data/test_cache_util.py new file mode 100644 index 0000000..042fabc --- /dev/null +++ b/tests/ludwig/data/test_cache_util.py @@ -0,0 +1,138 @@ +import copy +import uuid +from unittest import mock + +import pytest + +from ludwig.constants import INPUT_FEATURES, OUTPUT_FEATURES +from ludwig.data.cache.util import calculate_checksum +from ludwig.schema.model_types.base import ModelConfig +from ludwig.types import FeatureConfigDict, ModelConfigDict +from ludwig.utils.misc_utils import merge_dict + + +def _gen_config(input_features: list[FeatureConfigDict]) -> ModelConfigDict: + return {INPUT_FEATURES: input_features, OUTPUT_FEATURES: [{"name": "out1", "type": "binary"}]} + + +@pytest.mark.parametrize( + "input_features,diff,expected", + [ + ( + [ + { + "name": "in1", + "type": "text", + "encoder": {"type": "parallel_cnn"}, + } + ], + [ + { + "encoder": {"type": "stacked_cnn"}, + } + ], + True, + ), + ( + [ + { + "name": "in1", + "type": "text", + "preprocessing": {"cache_encoder_embeddings": True}, + "encoder": {"type": "bert"}, + } + ], + [ + { + "encoder": {"type": "distilbert"}, + } + ], + False, + ), + ], +) +def test_calculate_checksum(input_features: list[FeatureConfigDict], diff: list[FeatureConfigDict], expected: bool): + config = _gen_config(input_features) + + diff_features = [merge_dict(f, df) for f, df in zip(input_features, diff)] + diff_config = _gen_config(diff_features) + + mock_dataset = mock.Mock() + mock_dataset.checksum = uuid.uuid4().hex + + assert ( + calculate_checksum(mock_dataset, ModelConfig.from_dict(config).to_dict()) + == calculate_checksum(mock_dataset, ModelConfig.from_dict(diff_config).to_dict()) + ) == expected + + +def test_proc_col_checksum_consistency(): + """Tests that proc_col is equal if checksum are equal.""" + config_dict1 = { + "input_features": [{"name": "txt1", "type": "text", "encoder": {"type": "bert"}}], + "output_features": [{"name": "bin1", "type": "binary"}], + } + config1 = ModelConfig.from_dict(config_dict1) + + config_dict2 = copy.deepcopy(config_dict1) + config_dict2["input_features"][0]["preprocessing"] = { + "tokenizer": "bert", + } + config2 = ModelConfig.from_dict(config_dict2) + + mock_dataset = mock.Mock() + mock_dataset.checksum = uuid.uuid4().hex + assert calculate_checksum(mock_dataset, config1.to_dict()) == calculate_checksum(mock_dataset, config2.to_dict()) + + for if1, if2 in zip(config1.input_features, config2.input_features): + assert if1.name == if2.name + assert if1.proc_column == if2.proc_column + + for of1, of2 in zip(config1.output_features, config2.output_features): + assert of1.name == of2.name + assert of1.proc_column == of2.proc_column + + +def test_proc_col_checksum_consistency_same_preprocessing_different_types(): + """Tests that proc_col is different if preprocessing and names are the same but types are different.""" + config = { + "input_features": [ + # Same name, different types, same preprocessing + {"name": "num1", "type": "number", "preprocessing": {"missing_value_strategy": "fill_with_mode"}}, + {"name": "num2", "type": "category", "preprocessing": {"missing_value_strategy": "fill_with_mode"}}, + ], + "output_features": [ + {"name": "num3", "type": "number", "preprocessing": {"missing_value_strategy": "fill_with_mode"}} + ], + } + config = ModelConfig.from_dict(config) + + assert config.input_features[0].proc_column != config.input_features[1].proc_column + + +@pytest.mark.distributed +@pytest.mark.distributed_e +def test_checksum_determinism(ray_cluster_2cpu): + """Tests that checksums are deterministic across different processes (no unordered hash maps).""" + import ray + + # Generate a lot of features so the probability of a reordering of feature sets is very high. + config = { + INPUT_FEATURES: [{"name": f"in{i}", "type": "number"} for i in range(100)], + OUTPUT_FEATURES: [{"name": "out1", "type": "binary"}], + } + config = ModelConfig.from_dict(config) + + mock_dataset = mock.Mock() + mock_dataset.checksum = uuid.uuid4().hex + + @ray.remote(max_calls=1) + def calculate_checksum_remote(dataset, config): + return calculate_checksum(dataset, config) + + # Run each checksum calculation as a remote function so it gets its own Python interpreter, as + # the hash function in Python is deterministic within a process, but not between different processes. + # See: https://docs.python.org/3/reference/datamodel.html#object.__hash__ + checksum1 = ray.get(calculate_checksum_remote.remote(mock_dataset, config.to_dict())) + checksum2 = ray.get(calculate_checksum_remote.remote(mock_dataset, config.to_dict())) + assert checksum1 == checksum2 diff --git a/tests/ludwig/data/test_dask_preprocessing.py b/tests/ludwig/data/test_dask_preprocessing.py new file mode 100644 index 0000000..d8037d2 --- /dev/null +++ b/tests/ludwig/data/test_dask_preprocessing.py @@ -0,0 +1,292 @@ +"""Regression tests for Dask metadata inference failures. + +These tests guard against the pattern of calling `.map(fn)` on a Dask Series without a +`meta=` argument. Without `meta=`, Dask tries to infer the output dtype by calling `fn` +on a dummy sample, which fails for functions that return Python lists or numpy arrays +(e.g. `len`, tokenisers, array transformers). + +All tests use the Ray backend with `processor: dask` so that the code paths that were +broken are actually exercised. They are marked `distributed` and will be skipped when +Ray is not installed. + +See https://github.com/ludwig-ai/ludwig/issues/4142 (PR #4144). +""" + +import numpy as np +import pandas as pd +import pytest + +ray = pytest.importorskip("ray") +dask = pytest.importorskip("dask") + +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_e] + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +RNG = np.random.default_rng(42) +VOCAB = [ + "the", + "quick", + "brown", + "fox", + "jumps", + "over", + "lazy", + "dog", + "cat", + "sat", + "mat", + "hat", + "bat", + "rat", + "big", + "small", +] +RAY_DASK_BACKEND = {"type": "ray", "processor": {"type": "dask"}} + + +def _make_text_df(n: int = 400) -> pd.DataFrame: + texts = [" ".join(RNG.choice(VOCAB, size=RNG.integers(6, 14))) for _ in range(n)] + labels = RNG.integers(0, 3, size=n).tolist() + return pd.DataFrame({"text": texts, "label": labels}) + + +def _make_number_df(n: int = 400) -> pd.DataFrame: + return pd.DataFrame( + {f"f{i}": RNG.standard_normal(n).astype(np.float32) for i in range(5)} + | {"target": RNG.integers(0, 2, size=n).tolist()} + ) + + +def _make_vector_df(n: int = 400, vec_size: int = 8) -> pd.DataFrame: + vecs = [" ".join(str(x) for x in RNG.standard_normal(vec_size).round(4)) for _ in range(n)] + return pd.DataFrame({"vec": vecs, "label": RNG.integers(0, 2, size=n).tolist()}) + + +def _train_smoke(config: dict, df: pd.DataFrame) -> None: + """Train for 1 epoch and assert no exceptions.""" + from ludwig.api import LudwigModel + + LudwigModel(config, logging_level=30).train(dataset=df) + + +# --------------------------------------------------------------------------- +# Text feature — bare .map() in create_vocabulary and build_sequence_matrix +# --------------------------------------------------------------------------- + + +def test_dask_text_create_vocabulary_no_meta_error(ray_cluster_2cpu): + """Regression: strings_utils.create_vocabulary must not call .map(len) without meta=. + + Before the fix, `processed_lines.map(len)` in `create_vocabulary` caused + `ValueError: Metadata inference failed in map` when using the Dask engine. + """ + df = _make_text_df() + config = { + "input_features": [ + { + "name": "text", + "type": "text", + "encoder": {"type": "parallel_cnn"}, + "preprocessing": {"max_sequence_length": 16}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + _train_smoke(config, df) + + +def test_dask_text_build_sequence_matrix_no_meta_error(ray_cluster_2cpu): + """Regression: strings_utils.build_sequence_matrix must use processor.map_objects, not .map(). + + Before the fix, `unit_vectors.map(len)` and the lambda map in `build_sequence_matrix` + caused `ValueError: Metadata inference failed in map` with the Dask engine. + """ + df = _make_text_df() + config = { + "input_features": [ + { + "name": "text", + "type": "text", + "encoder": {"type": "stacked_cnn"}, + "preprocessing": {"max_sequence_length": 16}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + _train_smoke(config, df) + + +# --------------------------------------------------------------------------- +# Vector feature — bare .map(len) in get_feature_meta +# --------------------------------------------------------------------------- + + +def test_dask_vector_get_feature_meta_no_meta_error(ray_cluster_2cpu): + """Regression: vector_feature.get_feature_meta must not call .map(len) without meta=. + + Before the fix the `.map(len).max()` call in `get_feature_meta` raised + `ValueError: Metadata inference failed in map` with the Dask engine. + """ + df = _make_vector_df() + config = { + "input_features": [{"name": "vec", "type": "vector"}], + "output_features": [{"name": "label", "type": "binary"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + _train_smoke(config, df) + + +# --------------------------------------------------------------------------- +# Number features — verify that basic tabular Dask path still works +# --------------------------------------------------------------------------- + + +def test_dask_number_features_train(ray_cluster_2cpu): + """Sanity check: tabular number features train without errors under Dask.""" + df = _make_number_df() + config = { + "input_features": [{"name": f"f{i}", "type": "number"} for i in range(5)], + "output_features": [{"name": "target", "type": "binary"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + _train_smoke(config, df) + + +# --------------------------------------------------------------------------- +# Category output — bare .map() in postprocess_predictions +# --------------------------------------------------------------------------- + + +def test_dask_category_postprocess_no_meta_error(ray_cluster_2cpu): + """Regression: category_feature.postprocess_predictions must use meta= on all .map() calls. + + Before the fix, `predictions[predictions_col].map(lambda pred: metadata["idx2str"][pred])`, + `predictions[probabilities_col].map(max)`, and similar calls raised + `ValueError: Metadata inference failed in map` with the Dask engine. + """ + df = _make_text_df() + config = { + "input_features": [{"name": "text", "type": "text", "encoder": {"type": "parallel_cnn"}}], + "output_features": [{"name": "label", "type": "category"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + from ludwig.api import LudwigModel + + model = LudwigModel(config, logging_level=30) + _, _, output_dir = model.train(dataset=df) + preds, _ = model.predict(dataset=df) + assert preds is not None + + +# --------------------------------------------------------------------------- +# Binary output — bare .map() in postprocess_predictions +# --------------------------------------------------------------------------- + + +def _make_binary_df(n: int = 400) -> pd.DataFrame: + rng = np.random.default_rng(0) + texts = [" ".join(RNG.choice(VOCAB, size=rng.integers(6, 14))) for _ in range(n)] + labels = rng.integers(0, 2, size=n).tolist() + return pd.DataFrame({"text": texts, "label": labels}) + + +def test_dask_binary_postprocess_no_meta_error(ray_cluster_2cpu): + """Regression: binary_feature.postprocess_predictions must use meta= on .map() calls. + + Before the fix, `result[predictions_col].map(lambda pred: metadata["bool2str"][pred])` and + `result[probabilities_col].map(lambda x: [1 - x, x])` raised + `ValueError: Metadata inference failed in map` with the Dask engine. + """ + df = _make_binary_df() + config = { + "input_features": [{"name": "text", "type": "text", "encoder": {"type": "parallel_cnn"}}], + "output_features": [{"name": "label", "type": "binary"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + from ludwig.api import LudwigModel + + model = LudwigModel(config, logging_level=30) + _, _, output_dir = model.train(dataset=df) + preds, _ = model.predict(dataset=df) + assert preds is not None + + +# --------------------------------------------------------------------------- +# Sequence output — bare .map() in postprocess_predictions +# --------------------------------------------------------------------------- + + +def _make_sequence_df(n: int = 400) -> pd.DataFrame: + rng = np.random.default_rng(1) + # Input: space-separated token sequences; output: another sequence (seq2seq toy) + inputs = [" ".join(RNG.choice(VOCAB, size=rng.integers(4, 10))) for _ in range(n)] + targets = [" ".join(RNG.choice(VOCAB, size=rng.integers(3, 7))) for _ in range(n)] + return pd.DataFrame({"src": inputs, "tgt": targets}) + + +def test_dask_sequence_postprocess_no_meta_error(ray_cluster_2cpu): + """Regression: sequence_feature.postprocess_predictions must use meta= on .map() calls. + + Before the fix, `result[last_preds_col].map(last_idx2str)` and + `result[probs_col].map(compute_token_probabilities)` raised + `ValueError: Metadata inference failed in map` with the Dask engine. + """ + df = _make_sequence_df() + config = { + "input_features": [{"name": "src", "type": "sequence", "encoder": {"type": "embed"}}], + "output_features": [{"name": "tgt", "type": "sequence"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + from ludwig.api import LudwigModel + + model = LudwigModel(config, logging_level=30) + _, _, output_dir = model.train(dataset=df) + preds, _ = model.predict(dataset=df) + assert preds is not None + + +# --------------------------------------------------------------------------- +# Timeseries — bare .map(len) in build_matrix and bare .map() in postprocess +# --------------------------------------------------------------------------- + + +def _make_timeseries_df(n: int = 400, ts_len: int = 12) -> pd.DataFrame: + rng = np.random.default_rng(2) + ts = [" ".join(str(round(x, 4)) for x in rng.standard_normal(ts_len)) for _ in range(n)] + targets = rng.standard_normal((n, ts_len)).round(4) + target_strs = [" ".join(str(x) for x in row) for row in targets] + return pd.DataFrame({"ts_in": ts, "ts_out": target_strs}) + + +def test_dask_timeseries_no_meta_error(ray_cluster_2cpu): + """Regression: timeseries_feature must use meta= on .map(len) and .map(lambda pred: pred.tolist()). + + Before the fix, `ts_vectors.map(len).max()` in `build_matrix` and + `result[predictions_col].map(lambda pred: pred.tolist())` in `postprocess_predictions` + raised `ValueError: Metadata inference failed in map` with the Dask engine. + """ + df = _make_timeseries_df() + config = { + "input_features": [{"name": "ts_in", "type": "timeseries"}], + "output_features": [{"name": "ts_out", "type": "timeseries"}], + "trainer": {"epochs": 1, "batch_size": 64}, + "backend": RAY_DASK_BACKEND, + } + from ludwig.api import LudwigModel + + model = LudwigModel(config, logging_level=30) + _, _, output_dir = model.train(dataset=df) + preds, _ = model.predict(dataset=df) + assert preds is not None diff --git a/tests/ludwig/data/test_dataset_synthesizer.py b/tests/ludwig/data/test_dataset_synthesizer.py new file mode 100644 index 0000000..a7429c7 --- /dev/null +++ b/tests/ludwig/data/test_dataset_synthesizer.py @@ -0,0 +1,20 @@ +from ludwig.data import dataset_synthesizer + + +def test_build_synthetic_dataset(tmpdir): + features = [ + {"name": "text", "type": "text"}, + {"name": "category", "type": "category"}, + {"name": "number", "type": "number"}, + {"name": "binary", "type": "binary"}, + {"name": "set", "type": "set"}, + {"name": "bag", "type": "bag"}, + {"name": "sequence", "type": "sequence"}, + {"name": "timeseries", "type": "timeseries"}, + {"name": "date", "type": "date"}, + {"name": "h3", "type": "h3"}, + {"name": "vector", "type": "vector"}, + {"name": "audio", "type": "audio"}, + {"name": "image", "type": "image"}, + ] + assert len(list(dataset_synthesizer.build_synthetic_dataset(100, features, tmpdir))) == 101 # Extra for the header. diff --git a/tests/ludwig/data/test_format_preprocessors.py b/tests/ludwig/data/test_format_preprocessors.py new file mode 100644 index 0000000..dc569e3 --- /dev/null +++ b/tests/ludwig/data/test_format_preprocessors.py @@ -0,0 +1,177 @@ +"""Unit tests for FileBasedPreprocessor, ParquetPreprocessor, and data_format_preprocessor_registry.""" + +import pytest + +from ludwig.data.preprocessing import ( + data_format_preprocessor_registry, + DataFramePreprocessor, + DictPreprocessor, + FileBasedPreprocessor, + HDF5Preprocessor, + ParquetPreprocessor, +) +from ludwig.utils.data_utils import ( + CSV_FORMATS, + FEATHER_FORMATS, + HDF5_FORMATS, + JSON_FORMATS, + PARQUET_FORMATS, + TSV_FORMATS, +) + + +class TestFileBasedPreprocessorInit: + def test_stores_read_fn(self): + read_fn = lambda path, df_lib: df_lib.read_csv(path) + p = FileBasedPreprocessor(read_fn) + assert p._read_fn is read_fn + + def test_distinct_instances_per_format(self): + csv_preprocessors = [data_format_preprocessor_registry[fmt] for fmt in CSV_FORMATS] + assert all(isinstance(p, FileBasedPreprocessor) for p in csv_preprocessors) + # All CSV formats map to the same instance (dict.fromkeys semantics) + assert len({id(p) for p in csv_preprocessors}) == 1 + + def test_different_formats_get_different_instances(self): + csv_inst = data_format_preprocessor_registry[next(iter(CSV_FORMATS))] + tsv_inst = data_format_preprocessor_registry[next(iter(TSV_FORMATS))] + assert csv_inst is not tsv_inst + + def test_json_instance_differs_from_csv(self): + csv_inst = data_format_preprocessor_registry[next(iter(CSV_FORMATS))] + json_inst = data_format_preprocessor_registry[next(iter(JSON_FORMATS))] + assert csv_inst is not json_inst + + +class TestDataFormatPreprocessorRegistry: + def test_all_csv_formats_registered(self): + for fmt in CSV_FORMATS: + assert fmt in data_format_preprocessor_registry + + def test_all_tsv_formats_registered(self): + for fmt in TSV_FORMATS: + assert fmt in data_format_preprocessor_registry + + def test_all_json_formats_registered(self): + for fmt in JSON_FORMATS: + assert fmt in data_format_preprocessor_registry + + def test_all_parquet_formats_registered(self): + for fmt in PARQUET_FORMATS: + assert fmt in data_format_preprocessor_registry + + def test_all_feather_formats_registered(self): + for fmt in FEATHER_FORMATS: + assert fmt in data_format_preprocessor_registry + + def test_all_hdf5_formats_registered(self): + for fmt in HDF5_FORMATS: + assert fmt in data_format_preprocessor_registry + + def test_parquet_maps_to_parquet_preprocessor(self): + for fmt in PARQUET_FORMATS: + assert isinstance(data_format_preprocessor_registry[fmt], ParquetPreprocessor) + + def test_hdf5_maps_to_hdf5_preprocessor(self): + for fmt in HDF5_FORMATS: + assert isinstance(data_format_preprocessor_registry[fmt], HDF5Preprocessor) + + def test_file_based_formats_use_file_based_preprocessor(self): + file_based_formats = CSV_FORMATS | TSV_FORMATS | JSON_FORMATS | FEATHER_FORMATS + for fmt in file_based_formats: + inst = data_format_preprocessor_registry[fmt] + assert isinstance(inst, FileBasedPreprocessor), f"{fmt} should use FileBasedPreprocessor, got {type(inst)}" + + def test_dict_format_uses_dict_preprocessor(self): + assert isinstance(data_format_preprocessor_registry["dict"], DictPreprocessor) + + def test_dataframe_format_uses_dataframe_preprocessor(self): + assert isinstance(data_format_preprocessor_registry["df"], DataFramePreprocessor) + + +class TestParquetPreprocessorInit: + def test_is_file_based_preprocessor(self): + assert isinstance(ParquetPreprocessor(), FileBasedPreprocessor) + + def test_uses_read_parquet(self): + from ludwig.utils.data_utils import read_parquet + + p = ParquetPreprocessor() + assert p._read_fn is read_parquet + + +class TestParquetPreprocessorPrepareProcessedData: + def test_records_training_path(self): + from ludwig.utils.data_utils import DATA_TRAIN_PARQUET_FP + + p = ParquetPreprocessor() + metadata = {} + p.prepare_processed_data( + features=[], + training_set="/tmp/train.parquet", + training_set_metadata=metadata, + ) + assert metadata[DATA_TRAIN_PARQUET_FP] == "/tmp/train.parquet" + + def test_does_not_overwrite_existing_training_path(self): + from ludwig.utils.data_utils import DATA_TRAIN_PARQUET_FP + + p = ParquetPreprocessor() + metadata = {DATA_TRAIN_PARQUET_FP: "/original/train.parquet"} + p.prepare_processed_data( + features=[], + training_set="/new/train.parquet", + training_set_metadata=metadata, + ) + # Should not overwrite when key already exists + assert metadata[DATA_TRAIN_PARQUET_FP] == "/original/train.parquet" + + def test_nonexistent_test_set_is_dropped(self, tmp_path): + from ludwig.utils.data_utils import DATA_TEST_PARQUET_FP + + p = ParquetPreprocessor() + metadata = {} + training, test, val, meta = p.prepare_processed_data( + features=[], + training_set=str(tmp_path), + test_set="/nonexistent/path.parquet", + training_set_metadata=metadata, + ) + # Non-existent paths should be treated as None + assert test is None + assert DATA_TEST_PARQUET_FP not in metadata + + def test_existing_test_set_is_recorded(self, tmp_path): + from ludwig.utils.data_utils import DATA_TEST_PARQUET_FP + + test_file = tmp_path / "test.parquet" + test_file.write_bytes(b"") # create the file so path_exists returns True + + p = ParquetPreprocessor() + metadata = {} + training, test, val, meta = p.prepare_processed_data( + features=[], + training_set=str(tmp_path), + test_set=str(test_file), + training_set_metadata=metadata, + ) + assert meta[DATA_TEST_PARQUET_FP] == str(test_file) + + +class TestHDF5PreprocessorPrepareProcessedData: + def test_raises_on_no_dataset_or_training_set(self): + p = HDF5Preprocessor() + with pytest.raises(ValueError, match="One of `dataset` or `training_set` must be not None"): + p.prepare_processed_data(features=[], training_set_metadata={"key": "val"}) + + def test_raises_on_empty_metadata(self, tmp_path): + fake_hdf5 = tmp_path / "data.hdf5" + fake_hdf5.write_bytes(b"") + p = HDF5Preprocessor() + with pytest.raises(ValueError, match="training_set_metadata must not be None"): + p.prepare_processed_data(features=[], dataset=str(fake_hdf5), training_set_metadata=None) + + def test_raises_on_empty_dict_metadata(self, tmp_path): + p = HDF5Preprocessor() + with pytest.raises(ValueError, match="training_set_metadata must not be None"): + p.prepare_processed_data(features=[], dataset="/any/path.hdf5", training_set_metadata={}) diff --git a/tests/ludwig/data/test_format_registry.py b/tests/ludwig/data/test_format_registry.py new file mode 100644 index 0000000..722a5db --- /dev/null +++ b/tests/ludwig/data/test_format_registry.py @@ -0,0 +1,36 @@ +"""Tests for format detection.""" + +from ludwig.data.format_registry import detect_format, detect_format_from_dataset + + +class TestDetectFormat: + def test_csv(self): + assert detect_format("data.csv") == "csv" + + def test_parquet(self): + assert detect_format("data.parquet") == "parquet" + + def test_json(self): + assert detect_format("data.json") == "json" + + def test_hdf5(self): + assert detect_format("data.hdf5") == "hdf5" + + def test_unknown(self): + assert detect_format("data.xyz") is None + + def test_case_insensitive(self): + assert detect_format("DATA.CSV") == "csv" + + +class TestDetectFormatFromDataset: + def test_dataframe(self): + import pandas as pd + + assert detect_format_from_dataset(pd.DataFrame()) == "df" + + def test_dict(self): + assert detect_format_from_dataset({"col": [1, 2]}) == "dict" + + def test_string_path(self): + assert detect_format_from_dataset("data.csv") == "csv" diff --git a/tests/ludwig/data/test_lazy_cache.py b/tests/ludwig/data/test_lazy_cache.py new file mode 100644 index 0000000..54a276a --- /dev/null +++ b/tests/ludwig/data/test_lazy_cache.py @@ -0,0 +1,361 @@ +"""Tests for lazy media caching utilities. + +Covers: +- ``_cache_audio_column_to_disk`` in ``ludwig.features.audio_feature`` +- ``_cache_image_column_to_disk`` in ``ludwig.features.image_feature`` +- ``resolve_lazy_cache_dir`` / ``get_default_lazy_cache_dir`` in ``ludwig.data.lazy_utils`` +""" + +from __future__ import annotations + +import importlib +import os +import wave +from pathlib import Path + +import numpy as np +import pytest + +from ludwig.data.lazy_utils import resolve_lazy_cache_dir + +# --------------------------------------------------------------------------- +# Conditional skip markers +# --------------------------------------------------------------------------- + +_TORCH_AVAILABLE = importlib.util.find_spec("torch") is not None +_PIL_AVAILABLE = importlib.util.find_spec("PIL") is not None + +requires_torch = pytest.mark.skipif(not _TORCH_AVAILABLE, reason="torch not installed") +requires_pil = pytest.mark.skipif(not _PIL_AVAILABLE, reason="Pillow not installed") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _write_wav(path: str, num_samples: int = 1000, num_channels: int = 1, sample_rate: int = 16_000) -> None: + """Write a minimal valid WAV file to *path*.""" + with wave.open(path, "w") as wf: + wf.setnchannels(num_channels) + wf.setsampwidth(2) # 16-bit + wf.setframerate(sample_rate) + data = (np.zeros(num_samples * num_channels, dtype=np.int16)).tobytes() + wf.writeframes(data) + + +def _write_png(path: str, width: int = 8, height: int = 8) -> None: + """Write a minimal valid PNG file to *path*.""" + from PIL import Image as PILImage + + PILImage.fromarray(np.zeros((height, width, 3), dtype=np.uint8)).save(path, format="PNG") + + +# --------------------------------------------------------------------------- +# Audio caching tests +# --------------------------------------------------------------------------- + + +@requires_torch +class TestCacheAudioToDisk: + """Tests for ``_cache_audio_column_to_disk``.""" + + def test_reuses_existing_hf_path(self, tmp_path: Path) -> None: + """If audio dict has a 'path' pointing to an existing WAV, reuse it.""" + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + wav_path = str(tmp_path / "existing.wav") + _write_wav(wav_path) + + entry = {"array": np.zeros(16_000, dtype=np.float32), "sampling_rate": 16_000, "path": wav_path} + result = _cache_audio_column_to_disk([entry], tmp_path, "audio") + + assert result == [wav_path] + # No extra files should have been created in cache_dir besides existing.wav + wav_files = list(tmp_path.glob("audio_*.wav")) + assert len(wav_files) == 0 + + def test_writes_new_wav_for_dict_without_path(self, tmp_path: Path) -> None: + """Audio dict without existing 'path' gets written to cache_dir.""" + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + entry = {"array": np.zeros(8_000, dtype=np.float32), "sampling_rate": 8_000} + result = _cache_audio_column_to_disk([entry], tmp_path, "audio") + + assert len(result) == 1 + dest = result[0] + assert os.path.isfile(dest) + assert dest.endswith(".wav") + + def test_writes_new_wav_for_dict_with_nonexistent_path(self, tmp_path: Path) -> None: + """Audio dict whose 'path' does not exist on disk triggers a new WAV write.""" + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + entry = { + "array": np.zeros(8_000, dtype=np.float32), + "sampling_rate": 8_000, + "path": "/nonexistent/path/audio.wav", + } + result = _cache_audio_column_to_disk([entry], tmp_path, "audio") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + assert result[0] != "/nonexistent/path/audio.wav" + + def test_idempotent_does_not_overwrite(self, tmp_path: Path) -> None: + """Running cache twice does not overwrite existing files.""" + + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + entry = {"array": np.zeros(8_000, dtype=np.float32), "sampling_rate": 16_000} + result1 = _cache_audio_column_to_disk([entry], tmp_path, "audio") + mtime_before = os.path.getmtime(result1[0]) + + result2 = _cache_audio_column_to_disk([entry], tmp_path, "audio") + mtime_after = os.path.getmtime(result2[0]) + + assert result1 == result2 + assert mtime_before == mtime_after + + def test_tensor_input_cached(self, tmp_path: Path) -> None: + """torch.Tensor input is written to a WAV file.""" + import torch + + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + waveform = torch.zeros(1, 8_000) + result = _cache_audio_column_to_disk([waveform], tmp_path, "audio") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + + def test_1d_tensor_input_cached(self, tmp_path: Path) -> None: + """1-D torch.Tensor is promoted to (1, samples) before saving.""" + import torch + + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + waveform = torch.zeros(8_000) # shape (8000,) — no channel dim + result = _cache_audio_column_to_disk([waveform], tmp_path, "audio") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + + def test_multiple_entries_produce_ordered_paths(self, tmp_path: Path) -> None: + """Multiple entries produce one path per entry in order.""" + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + n = 5 + entries = [{"array": np.zeros(4_000, dtype=np.float32), "sampling_rate": 16_000} for _ in range(n)] + result = _cache_audio_column_to_disk(entries, tmp_path, "audio") + + assert len(result) == n + for path in result: + assert os.path.isfile(path) + + def test_invalid_entry_type_raises(self, tmp_path: Path) -> None: + """An unrecognised entry type raises ValueError.""" + from ludwig.features.audio_feature import _cache_audio_column_to_disk + + with pytest.raises(ValueError, match="unrecognised type"): + _cache_audio_column_to_disk(["not_a_dict_or_tensor"], tmp_path, "audio") + + +# --------------------------------------------------------------------------- +# Image caching tests +# --------------------------------------------------------------------------- + + +@requires_pil +class TestCacheImageToDisk: + """Tests for ``_cache_image_column_to_disk``.""" + + def test_reuses_pil_filename(self, tmp_path: Path) -> None: + """PIL Image with .filename pointing to existing file → reuse that path.""" + from PIL import Image as PILImage + + from ludwig.features.image_feature import _cache_image_column_to_disk + + png_path = str(tmp_path / "existing.png") + _write_png(png_path) + + img = PILImage.open(png_path) + # PIL sets .filename on images opened from disk + result = _cache_image_column_to_disk([img], tmp_path, "image") + + assert result == [png_path] + # No cached copies should have been written + cached = list(tmp_path.glob("image_*.png")) + assert len(cached) == 0 + + def test_writes_png_for_pil_without_filename(self, tmp_path: Path) -> None: + """In-memory PIL Image (no filename) → written to cache_dir as PNG.""" + from PIL import Image as PILImage + + from ludwig.features.image_feature import _cache_image_column_to_disk + + img = PILImage.fromarray(np.zeros((8, 8, 3), dtype=np.uint8)) + result = _cache_image_column_to_disk([img], tmp_path, "image") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + assert result[0].endswith(".png") + + def test_bytes_input_cached(self, tmp_path: Path) -> None: + """Raw bytes image input is decoded and saved as PNG.""" + import io + + from PIL import Image as PILImage + + from ludwig.features.image_feature import _cache_image_column_to_disk + + buf = io.BytesIO() + PILImage.fromarray(np.zeros((8, 8, 3), dtype=np.uint8)).save(buf, format="PNG") + raw_bytes = buf.getvalue() + + result = _cache_image_column_to_disk([raw_bytes], tmp_path, "image") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + + def test_numpy_array_cached(self, tmp_path: Path) -> None: + """A numpy (H, W, 3) array is saved as PNG.""" + from ludwig.features.image_feature import _cache_image_column_to_disk + + arr = np.zeros((8, 8, 3), dtype=np.uint8) + result = _cache_image_column_to_disk([arr], tmp_path, "image") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + + def test_numpy_chw_array_cached(self, tmp_path: Path) -> None: + """A numpy (3, H, W) channel-first array is transposed and saved.""" + from ludwig.features.image_feature import _cache_image_column_to_disk + + arr = np.zeros((3, 8, 8), dtype=np.uint8) + result = _cache_image_column_to_disk([arr], tmp_path, "image") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + + def test_hf_dict_with_bytes_cached(self, tmp_path: Path) -> None: + """HuggingFace-style dict with 'bytes' key is decoded and saved.""" + import io + + from PIL import Image as PILImage + + from ludwig.features.image_feature import _cache_image_column_to_disk + + buf = io.BytesIO() + PILImage.fromarray(np.zeros((8, 8, 3), dtype=np.uint8)).save(buf, format="PNG") + entry = {"bytes": buf.getvalue(), "path": None} + + result = _cache_image_column_to_disk([entry], tmp_path, "image") + + assert len(result) == 1 + assert os.path.isfile(result[0]) + + def test_hf_dict_with_existing_path_reused(self, tmp_path: Path) -> None: + """HuggingFace-style dict with a valid 'path' → reuse that path.""" + from ludwig.features.image_feature import _cache_image_column_to_disk + + png_path = str(tmp_path / "hf_cached.png") + _write_png(png_path) + + entry = {"bytes": None, "path": png_path} + result = _cache_image_column_to_disk([entry], tmp_path, "image") + + assert result == [png_path] + + def test_idempotent(self, tmp_path: Path) -> None: + """Running cache twice does not overwrite existing files.""" + from PIL import Image as PILImage + + from ludwig.features.image_feature import _cache_image_column_to_disk + + img = PILImage.fromarray(np.zeros((8, 8, 3), dtype=np.uint8)) + result1 = _cache_image_column_to_disk([img], tmp_path, "image") + mtime_before = os.path.getmtime(result1[0]) + + result2 = _cache_image_column_to_disk([img], tmp_path, "image") + mtime_after = os.path.getmtime(result2[0]) + + assert result1 == result2 + assert mtime_before == mtime_after + + def test_multiple_entries(self, tmp_path: Path) -> None: + """Multiple entries produce one path per entry in order.""" + from PIL import Image as PILImage + + from ludwig.features.image_feature import _cache_image_column_to_disk + + imgs = [PILImage.fromarray(np.zeros((8, 8, 3), dtype=np.uint8)) for _ in range(4)] + result = _cache_image_column_to_disk(imgs, tmp_path, "image") + + assert len(result) == 4 + for path in result: + assert os.path.isfile(path) + + def test_invalid_entry_type_raises(self, tmp_path: Path) -> None: + """An unrecognised entry type raises ValueError.""" + from ludwig.features.image_feature import _cache_image_column_to_disk + + with pytest.raises(ValueError, match="unrecognised type"): + _cache_image_column_to_disk([12345], tmp_path, "image") + + +# --------------------------------------------------------------------------- +# resolve_lazy_cache_dir / get_default_lazy_cache_dir tests +# --------------------------------------------------------------------------- + + +class TestResolveLazyCacheDir: + """Tests for ``resolve_lazy_cache_dir`` and ``get_default_lazy_cache_dir``.""" + + def test_uses_explicit_param(self, tmp_path: Path) -> None: + """When cache_dir_param is given, the resolved path is inside it.""" + explicit = str(tmp_path / "my_cache") + result = resolve_lazy_cache_dir(explicit, "audio_feat") + + assert result == Path(explicit) / "audio_feat" + assert result.is_dir() + + def test_falls_back_to_default(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """When cache_dir_param is None, the default root is used. + + We monkeypatch ``Path.home`` inside the lazy_utils module so the test + does not pollute the real ``~/.cache`` and does not require a module + reload (which would invalidate other imported names like ``LazyColumn``). + """ + import ludwig.data.lazy_utils as lu + + monkeypatch.setattr(lu, "get_default_lazy_cache_dir", lambda: tmp_path) + result = lu.resolve_lazy_cache_dir(None, "my_feat") + + assert result == tmp_path / "my_feat" + assert result.is_dir() + + def test_creates_directory(self, tmp_path: Path) -> None: + """resolve_lazy_cache_dir creates the directory if it does not exist.""" + nested = str(tmp_path / "does" / "not" / "exist") + result = resolve_lazy_cache_dir(nested, "feat") + + assert result.is_dir() + + def test_get_default_lazy_cache_dir_creates_root(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """get_default_lazy_cache_dir creates a 'lazy_media' directory under home.""" + import ludwig.data.lazy_utils as lu + + # Redirect Path.home() inside the module so we don't pollute ~/.cache + monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path)) + root = lu.get_default_lazy_cache_dir() + + assert root.is_dir() + assert root.name == "lazy_media" + + def test_feature_name_used_as_subdirectory(self, tmp_path: Path) -> None: + """The feature name becomes the leaf directory under cache_dir_param.""" + result = resolve_lazy_cache_dir(str(tmp_path), "my_audio_feature") + + assert result.name == "my_audio_feature" + assert result.parent == tmp_path diff --git a/tests/ludwig/data/test_lazy_utils.py b/tests/ludwig/data/test_lazy_utils.py new file mode 100644 index 0000000..dc44785 --- /dev/null +++ b/tests/ludwig/data/test_lazy_utils.py @@ -0,0 +1,153 @@ +"""Unit tests for LazyColumn. + +Verifies that LazyColumn: +1. Decodes samples on demand (not upfront) — peak memory is bounded. +2. Returns correctly stacked numpy arrays for batch indices. +3. Handles all numpy index forms (int, list, slice, boolean mask). +4. Runs decode in parallel (ThreadPoolExecutor) and produces the same result as sequential decode. +5. Integrates transparently with PandasDataset.get(). +""" + +import numpy as np + +from ludwig.data.lazy_utils import is_lazy_column, LazyColumn + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_paths(n: int) -> np.ndarray: + """Return fake "paths" — just stringified integers for testing.""" + return np.array([str(i) for i in range(n)], dtype=object) + + +def _decode_int_path(path: str) -> np.ndarray: + """Decode: parse int, return a (4,) float array filled with that int.""" + val = float(path) + return np.full((4,), val, dtype=np.float32) + + +# --------------------------------------------------------------------------- +# Basic interface +# --------------------------------------------------------------------------- + + +class TestLazyColumnInterface: + def setup_method(self): + self.paths = _make_paths(10) + self.col = LazyColumn(self.paths, _decode_int_path) + + def test_len(self): + assert len(self.col) == 10 + + def test_is_lazy_column(self): + assert is_lazy_column(self.col) + assert not is_lazy_column(np.zeros(10)) + + def test_repr(self): + assert "LazyColumn" in repr(self.col) + assert "n=10" in repr(self.col) + + def test_shape(self): + # Only batch dimension is known before decode + assert self.col.shape == (10,) + + def test_dtype(self): + assert self.col.dtype == object + + +class TestLazyColumnIndexing: + def setup_method(self): + self.paths = _make_paths(20) + self.col = LazyColumn(self.paths, _decode_int_path) + + def test_integer_index(self): + result = self.col[5] + assert isinstance(result, np.ndarray) + np.testing.assert_array_equal(result, np.full((4,), 5.0, dtype=np.float32)) + + def test_list_of_indices(self): + result = self.col[[0, 3, 7]] + assert result.shape == (3, 4) + np.testing.assert_array_almost_equal(result[:, 0], [0.0, 3.0, 7.0]) + + def test_numpy_integer_array(self): + idx = np.array([1, 2, 4]) + result = self.col[idx] + assert result.shape == (3, 4) + np.testing.assert_array_almost_equal(result[:, 0], [1.0, 2.0, 4.0]) + + def test_slice(self): + result = self.col[2:5] + assert result.shape == (3, 4) + np.testing.assert_array_almost_equal(result[:, 0], [2.0, 3.0, 4.0]) + + def test_boolean_mask(self): + mask = np.array([i % 2 == 0 for i in range(20)]) + result = self.col[mask] + assert result.shape == (10, 4) + expected = np.array([0, 2, 4, 6, 8, 10, 12, 14, 16, 18], dtype=np.float32) + np.testing.assert_array_almost_equal(result[:, 0], expected) + + def test_single_element_list(self): + result = self.col[[3]] + assert result.shape == (1, 4) + + +class TestLazyColumnCorrectness: + """Verify decoded values match a reference sequential implementation.""" + + def test_matches_sequential_decode(self): + paths = _make_paths(20) + col = LazyColumn(paths, _decode_int_path, max_workers=4) + indices = list(range(20)) + result = col[indices] + expected = np.stack([_decode_int_path(p) for p in paths[indices]]) + np.testing.assert_array_almost_equal(result, expected) + + def test_single_worker_matches_multi(self): + paths = _make_paths(16) + col_1 = LazyColumn(paths, _decode_int_path, max_workers=1) + col_4 = LazyColumn(paths, _decode_int_path, max_workers=4) + idx = list(range(16)) + np.testing.assert_array_equal(col_1[idx], col_4[idx]) + + def test_random_batch_selection(self): + rng = np.random.default_rng(42) + paths = _make_paths(32) + col = LazyColumn(paths, _decode_int_path) + idx = rng.choice(32, size=16, replace=False) + result = col[idx] + expected = np.stack([_decode_int_path(p) for p in paths[idx]]) + np.testing.assert_array_almost_equal(result, expected) + + +class TestLazyColumnPandasDatasetIntegration: + """Test that LazyColumn integrates with PandasDataset.get().""" + + def test_get_returns_numpy_array(self): + """PandasDataset.get() calls dataset[col][indices] — works for LazyColumn.""" + paths = _make_paths(32) + col = LazyColumn(paths, _decode_int_path) + # Simulate what PandasDataset.get() does: + idx = [0, 5, 10, 31] + result = col[idx] + assert isinstance(result, np.ndarray) + assert result.shape == (4, 4) + + def test_decode_is_lazy(self): + """Verify decode function is not called during construction.""" + call_count = {"n": 0} + + def counting_decode(path: str) -> np.ndarray: + call_count["n"] += 1 + return np.zeros(4, dtype=np.float32) + + paths = _make_paths(20) + col = LazyColumn(paths, counting_decode) + # Construction must not have called decode + assert call_count["n"] == 0 + # Accessing 5 samples triggers exactly 5 decode calls + _ = col[[0, 1, 2, 3, 4]] + assert call_count["n"] == 5 diff --git a/tests/ludwig/data/test_multimodal_collator.py b/tests/ludwig/data/test_multimodal_collator.py new file mode 100644 index 0000000..d15efdd --- /dev/null +++ b/tests/ludwig/data/test_multimodal_collator.py @@ -0,0 +1,125 @@ +"""Tests for MultimodalCollator.""" + +from unittest.mock import MagicMock + +import pytest +import torch + +from ludwig.data.multimodal_collator import MultimodalCollator + + +def _make_processor(output_key="input_ids"): + """Build a mock processor that returns a dict with one tensor under output_key.""" + + def call(text, images, return_tensors, padding, **kwargs): + batch_size = len(text) + result = {output_key: torch.zeros(batch_size, 10, dtype=torch.long)} + return result + + proc = MagicMock(side_effect=call) + tokenizer = MagicMock() + tokenizer.pad_token_id = 0 + + def tokenize(texts, return_tensors, padding, **kwargs): + return {"input_ids": torch.ones(len(texts), 5, dtype=torch.long)} + + tokenizer.side_effect = tokenize + proc.tokenizer = tokenizer + return proc + + +def _make_examples(n: int = 3, include_labels: bool = False) -> list[dict]: + examples = [] + for i in range(n): + ex = {"image": f"img_{i}.jpg", "text": f"text {i}"} + if include_labels: + ex["labels"] = f"label {i}" + examples.append(ex) + return examples + + +class TestMultimodalCollatorBasic: + def test_returns_dict(self): + proc = _make_processor() + collator = MultimodalCollator(proc) + result = collator(_make_examples(2)) + assert isinstance(result, dict) + + def test_calls_processor_with_texts_and_images(self): + proc = _make_processor() + collator = MultimodalCollator(proc) + examples = _make_examples(3) + collator(examples) + call_kwargs = proc.call_args[1] + assert call_kwargs["text"] == ["text 0", "text 1", "text 2"] + assert call_kwargs["images"] == ["img_0.jpg", "img_1.jpg", "img_2.jpg"] + + def test_no_labels_by_default(self): + proc = _make_processor() + collator = MultimodalCollator(proc) + result = collator(_make_examples(2, include_labels=False)) + assert "labels" not in result + + def test_max_length_passed_to_processor(self): + proc = _make_processor() + collator = MultimodalCollator(proc, max_length=64) + collator(_make_examples(2)) + call_kwargs = proc.call_args[1] + assert call_kwargs["max_length"] == 64 + assert call_kwargs["truncation"] is True + + def test_custom_keys(self): + proc = _make_processor() + collator = MultimodalCollator(proc, image_key="img", text_key="caption") + examples = [{"img": "a.jpg", "caption": "hello"}, {"img": "b.jpg", "caption": "world"}] + collator(examples) + call_kwargs = proc.call_args[1] + assert call_kwargs["images"] == ["a.jpg", "b.jpg"] + assert call_kwargs["text"] == ["hello", "world"] + + +class TestMultimodalCollatorLabels: + def test_labels_added_when_present(self): + proc = _make_processor() + collator = MultimodalCollator(proc) + result = collator(_make_examples(2, include_labels=True)) + assert "labels" in result + + def test_labels_shape_matches_tokenizer_output(self): + proc = _make_processor() + collator = MultimodalCollator(proc) + result = collator(_make_examples(3, include_labels=True)) + # tokenizer mock returns shape (n, 5) + assert result["labels"].shape == (3, 5) + + def test_mixed_labels_raises_value_error(self): + """Some examples have labels, some don't → should raise ValueError.""" + proc = _make_processor() + collator = MultimodalCollator(proc) + examples = _make_examples(3, include_labels=True) + examples[1].pop("labels") # remove one label + with pytest.raises(ValueError, match="missing"): + collator(examples) + + def test_labels_pad_replaced_with_minus100(self): + proc = _make_processor() + # Tokenizer returns 0 for padding; collator should replace with -100 + collator = MultimodalCollator(proc) + result = collator(_make_examples(2, include_labels=True)) + # All tokens are 1 (from mock), none replaced → no -100 in this case + assert (result["labels"] != 0).all() + + def test_device_resolution_without_input_ids_key(self): + """Collator must not KeyError when processor emits pixel_values instead of input_ids.""" + proc = _make_processor(output_key="pixel_values") + collator = MultimodalCollator(proc) + # Should complete without raising KeyError + result = collator(_make_examples(2, include_labels=True)) + assert "labels" in result + + def test_no_tokenizer_raises_value_error(self): + proc = _make_processor() + del proc.tokenizer # remove tokenizer attribute + collator = MultimodalCollator(proc) + with pytest.raises((ValueError, AttributeError)): + collator(_make_examples(2, include_labels=True)) diff --git a/tests/ludwig/data/test_negative_sampling.py b/tests/ludwig/data/test_negative_sampling.py new file mode 100644 index 0000000..b73508b --- /dev/null +++ b/tests/ludwig/data/test_negative_sampling.py @@ -0,0 +1,25 @@ +import pandas as pd + +from ludwig.data.negative_sampling import negative_sample + + +def test_negative_sample(): + df = pd.DataFrame( + { + "user_id": [1, 1, 2, 2, 3], + "item_id": ["a", "b", "b", "c", "a"], + "label": [1, 1, 1, 1, 1], + } + ) + + df_with_samples = negative_sample(df, "user_id", "item_id", "label") + + assert 9 <= len(df_with_samples) <= 10 + assert df_with_samples["label"].sum() == 5 + + # Check data types + assert df_with_samples["user_id"].dtype == "int64" + assert df_with_samples["item_id"].dtype == "object" + + # Check that the negative samples are unique user-item pairs + assert len(df_with_samples) == len(df_with_samples.drop_duplicates(["user_id", "item_id"])) diff --git a/tests/ludwig/data/test_postprocessing.py b/tests/ludwig/data/test_postprocessing.py new file mode 100644 index 0000000..425aa8f --- /dev/null +++ b/tests/ludwig/data/test_postprocessing.py @@ -0,0 +1,31 @@ +import torch + +from ludwig.data.postprocessing import convert_dict_to_df + + +def test_convert_dict_to_df(): + d = { + "binary_C82EB": { + "predictions": torch.tensor([True, True, True, False]), + "probabilities": torch.tensor([[0.4777, 0.5223], [0.4482, 0.5518], [0.4380, 0.5620], [0.5059, 0.4941]]), + }, + "category_1491D": { + "predictions": ["NkNUG", "NkNUG", "NkNUG", "NkNUG"], + "probabilities": torch.tensor( + [ + [0.1058, 0.4366, 0.1939, 0.2637], + [0.0816, 0.4807, 0.1978, 0.2399], + [0.0907, 0.4957, 0.1829, 0.2308], + [0.0728, 0.5015, 0.1900, 0.2357], + ] + ), + }, + "num_7B25F": {"predictions": torch.tensor([2.0436, 2.1158, 2.1222, 2.1964])}, + } + + df = convert_dict_to_df(d) + + assert df.shape == (4, 5) + # Check that all elements in nested lists are stored in each row + assert all(len(row) == 2 for row in df["binary_C82EB_probabilities"]) + assert all(len(row) == 4 for row in df["category_1491D_probabilities"]) diff --git a/tests/ludwig/data/test_prefetch_batcher.py b/tests/ludwig/data/test_prefetch_batcher.py new file mode 100644 index 0000000..1bcabcd --- /dev/null +++ b/tests/ludwig/data/test_prefetch_batcher.py @@ -0,0 +1,333 @@ +"""Unit tests for RandomAccessBatcher prefetch mode and PandasDataset auto-prefetch. + +Covers: + - Prefetch produces identical batches to the sync path + - All samples are covered across one and multiple epochs + - set_epoch correctly resets state and re-starts the producer + - last_batch() is accurate before and after exhaustion + - Empty dataset terminates immediately + - PandasDataset auto-enables prefetch when lazy columns are present + - Audio lazy columns use a small max_workers to avoid CPU over-subscription +""" + +import os +from unittest.mock import patch + +import numpy as np +import pytest + +from ludwig.data.batcher.random_access import RandomAccessBatcher +from ludwig.data.dataset.pandas import PandasDataset +from ludwig.data.lazy_utils import LazyColumn +from ludwig.data.sampler import DistributedSampler + +# ───────────────────────────────────────────────────────────────────────────── +# Helpers +# ───────────────────────────────────────────────────────────────────────────── + + +class _SimpleDataset: + """Minimal dataset backed by a single numpy array for batcher testing.""" + + def __init__(self, n_samples: int, feature_name: str = "feat"): + self.features = {feature_name: {"name": feature_name, "type": "number"}} + self._data = {feature_name: np.arange(n_samples, dtype=np.float32)} + self._n = n_samples + self.feature_name = feature_name + + def get(self, col, idx): + return self._data[col][idx] + + def __len__(self): + return self._n + + +def _collect_batches(batcher) -> list[np.ndarray]: + batches = [] + while not batcher.last_batch(): + batches.append(batcher.next_batch()) + return batches + + +def _make_batcher(n: int, batch_size: int, prefetch_size: int = 0, shuffle: bool = False) -> RandomAccessBatcher: + ds = _SimpleDataset(n) + sampler = DistributedSampler(n, shuffle=shuffle, random_seed=0) + return RandomAccessBatcher(ds, sampler, batch_size=batch_size, prefetch_size=prefetch_size) + + +# ───────────────────────────────────────────────────────────────────────────── +# Correctness +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("prefetch_size", [0, 1, 2, 4]) +def test_prefetch_yields_all_samples(prefetch_size): + """Every sample must appear exactly once per epoch regardless of prefetch depth.""" + n, batch_size = 100, 16 + batcher = _make_batcher(n, batch_size, prefetch_size=prefetch_size) + batches = _collect_batches(batcher) + + values = np.concatenate([b["feat"] for b in batches]) + assert len(values) == n + assert set(values.tolist()) == set(range(n)) + + +@pytest.mark.parametrize("prefetch_size", [1, 2, 4]) +def test_prefetch_matches_sync_order(prefetch_size): + """With shuffle=False, prefetch must return batches in the same order as sync.""" + n, batch_size = 50, 8 + sync_batcher = _make_batcher(n, batch_size, prefetch_size=0, shuffle=False) + pre_batcher = _make_batcher(n, batch_size, prefetch_size=prefetch_size, shuffle=False) + + sync_values = np.concatenate([b["feat"] for b in _collect_batches(sync_batcher)]) + pre_values = np.concatenate([b["feat"] for b in _collect_batches(pre_batcher)]) + + np.testing.assert_array_equal(sync_values, pre_values) + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_prefetch_exact_batch_sizes(prefetch_size): + """All batches except possibly the last must be exactly batch_size samples.""" + n, batch_size = 55, 16 + batcher = _make_batcher(n, batch_size, prefetch_size=prefetch_size) + batches = _collect_batches(batcher) + + for b in batches[:-1]: + assert len(b["feat"]) == batch_size + # Last batch may be smaller + assert 0 < len(batches[-1]["feat"]) <= batch_size + + +# ───────────────────────────────────────────────────────────────────────────── +# last_batch() accuracy +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_last_batch_false_while_data_remains(prefetch_size): + batcher = _make_batcher(20, 4, prefetch_size=prefetch_size) + assert not batcher.last_batch() + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_last_batch_true_after_exhaustion(prefetch_size): + batcher = _make_batcher(8, 4, prefetch_size=prefetch_size) + _collect_batches(batcher) + assert batcher.last_batch() + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_next_batch_raises_on_exhaustion(prefetch_size): + batcher = _make_batcher(4, 4, prefetch_size=prefetch_size) + _collect_batches(batcher) + with pytest.raises(StopIteration): + batcher.next_batch() + + +# ───────────────────────────────────────────────────────────────────────────── +# set_epoch +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("prefetch_size", [0, 2, 4]) +def test_set_epoch_allows_multiple_passes(prefetch_size): + """set_epoch must reset state so the full dataset is available again.""" + n, batch_size, n_epochs = 30, 8, 3 + batcher = _make_batcher(n, batch_size, prefetch_size=prefetch_size) + all_samples = [] + for epoch in range(n_epochs): + batcher.set_epoch(epoch, batch_size) + epoch_samples = np.concatenate([b["feat"] for b in _collect_batches(batcher)]) + assert len(epoch_samples) == n, f"epoch {epoch}: got {len(epoch_samples)}, expected {n}" + all_samples.append(epoch_samples) + + # All epochs must cover the full dataset + for epoch_samples in all_samples: + assert set(epoch_samples.tolist()) == set(range(n)) + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_set_epoch_batch_size_change(prefetch_size): + """set_epoch must correctly apply a new batch_size for the next epoch.""" + batcher = _make_batcher(40, 8, prefetch_size=prefetch_size) + _collect_batches(batcher) # exhaust epoch 0 + + new_batch_size = 4 + batcher.set_epoch(1, new_batch_size) + batches = _collect_batches(batcher) + assert batcher.batch_size == new_batch_size + assert all(len(b["feat"]) == new_batch_size for b in batches) + assert sum(len(b["feat"]) for b in batches) == 40 + + +# ───────────────────────────────────────────────────────────────────────────── +# Edge cases +# ───────────────────────────────────────────────────────────────────────────── + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_empty_dataset_last_batch_immediately(prefetch_size): + """A dataset with 0 samples must signal last_batch() True immediately.""" + batcher = _make_batcher(0, 4, prefetch_size=prefetch_size) + assert batcher.last_batch() + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_single_sample_dataset(prefetch_size): + """Dataset with exactly 1 sample must produce exactly 1 batch.""" + batcher = _make_batcher(1, 4, prefetch_size=prefetch_size) + batches = _collect_batches(batcher) + assert len(batches) == 1 + assert len(batches[0]["feat"]) == 1 + + +@pytest.mark.parametrize("prefetch_size", [0, 2]) +def test_batch_size_larger_than_dataset(prefetch_size): + """When batch_size > n_samples, exactly one batch with all samples is produced.""" + n, batch_size = 5, 32 + batcher = _make_batcher(n, batch_size, prefetch_size=prefetch_size) + batches = _collect_batches(batcher) + assert len(batches) == 1 + assert len(batches[0]["feat"]) == n + + +# ───────────────────────────────────────────────────────────────────────────── +# PandasDataset auto-prefetch +# ───────────────────────────────────────────────────────────────────────────── + + +def test_pandas_dataset_auto_prefetch_for_lazy_columns(): + """PandasDataset must auto-enable prefetch when lazy columns are present.""" + n = 20 + paths = np.array([f"/fake/{i}.wav" for i in range(n)], dtype=object) + decode_fn = lambda p: np.zeros((8, 23), dtype=np.float32) + decode_fn.__name__ = "mock_decode" + + proc_col = "audio" + features = {proc_col: {"name": "audio_feat", "type": "audio"}} + training_set_metadata = { + "audio_feat": { + "lazy": True, + "reshape": None, + "lazy_audio_params": { + "audio_feature_dict": {"type": "fbank"}, + "feature_dim": 8, + "max_length": 23, + "padding_value": 0.0, + "normalization_type": None, + }, + } + } + + with patch("ludwig.features.audio_feature.AudioFeatureMixin._make_lazy_decode_fn", return_value=decode_fn): + ds = PandasDataset( + {proc_col: paths}, + features, + data_cache_fp=None, + training_set_metadata=training_set_metadata, + ) + + assert ds._has_lazy_columns(), "Dataset with audio paths should have lazy columns" + + # initialize_batcher should auto-enable prefetch (prefetch_size > 0) + with ds.initialize_batcher(batch_size=4, should_shuffle=False) as batcher: + assert batcher._prefetch_size > 0, f"Expected prefetch_size > 0 for lazy dataset, got {batcher._prefetch_size}" + batches = _collect_batches(batcher) + + assert sum(len(b[proc_col]) for b in batches) == n + + +def test_pandas_dataset_no_auto_prefetch_for_eager_columns(): + """PandasDataset must NOT enable prefetch for purely eager (pre-decoded) columns.""" + n = 20 + proc_col = "num" + features = {proc_col: {"name": "num_feat", "type": "number"}} + training_set_metadata = {"num_feat": {"lazy": False}} + ds = PandasDataset( + {proc_col: np.arange(n, dtype=np.float32)}, + features, + data_cache_fp=None, + training_set_metadata=training_set_metadata, + ) + + assert not ds._has_lazy_columns() + + with ds.initialize_batcher(batch_size=4, should_shuffle=False) as batcher: + assert batcher._prefetch_size == 0, ( + f"Expected prefetch_size == 0 for eager dataset, got {batcher._prefetch_size}" + ) + + +def test_pandas_dataset_explicit_prefetch_override(): + """Callers can override auto-prefetch by passing prefetch_size explicitly.""" + n = 10 + proc_col = "num" + features = {proc_col: {"name": "num_feat", "type": "number"}} + training_set_metadata = {"num_feat": {"lazy": False}} + ds = PandasDataset( + {proc_col: np.arange(n, dtype=np.float32)}, + features, + data_cache_fp=None, + training_set_metadata=training_set_metadata, + ) + + # Explicit prefetch_size=3 must be respected even on an eager dataset + with ds.initialize_batcher(batch_size=4, should_shuffle=False, prefetch_size=3) as batcher: + assert batcher._prefetch_size == 3 + + +# ───────────────────────────────────────────────────────────────────────────── +# Audio max_workers: avoid CPU over-subscription +# ───────────────────────────────────────────────────────────────────────────── + + +def test_audio_lazy_column_max_workers_capped(): + """LazyColumn created for audio must use max_workers ≤ cpu_count // torch_threads. + + The FBANK decode uses PyTorch's internal thread pool. Using many workers + in parallel creates CPU over-subscription and can be up to 5× slower than + a single worker. This test verifies the cap is respected. + """ + import torch + + n = 10 + paths = np.array([f"/fake/{i}.wav" for i in range(n)], dtype=object) + decode_fn = lambda p: np.zeros((8, 23), dtype=np.float32) + decode_fn.__name__ = "mock_decode" + + proc_col = "audio" + features = {proc_col: {"name": "audio_feat", "type": "audio"}} + training_set_metadata = { + "audio_feat": { + "lazy": True, + "reshape": None, + "lazy_audio_params": { + "audio_feature_dict": {"type": "fbank"}, + "feature_dim": 8, + "max_length": 23, + "padding_value": 0.0, + "normalization_type": None, + }, + } + } + + with patch("ludwig.features.audio_feature.AudioFeatureMixin._make_lazy_decode_fn", return_value=decode_fn): + ds = PandasDataset( + {proc_col: paths}, + features, + data_cache_fp=None, + training_set_metadata=training_set_metadata, + ) + + lazy_col = ds.dataset[proc_col] + assert isinstance(lazy_col, LazyColumn), "Expected LazyColumn for audio" + + cpu_count = os.cpu_count() or 4 + torch_threads = max(1, torch.get_num_threads()) + max_safe_workers = max(1, cpu_count // torch_threads) + + assert lazy_col._max_workers <= max_safe_workers, ( + f"Audio LazyColumn max_workers={lazy_col._max_workers} exceeds safe limit " + f"{max_safe_workers} (cpu_count={cpu_count}, torch_threads={torch_threads}). " + "Over-subscribing CPUs causes severe FBANK decode slowdowns." + ) diff --git a/tests/ludwig/data/test_preprocessing.py b/tests/ludwig/data/test_preprocessing.py new file mode 100644 index 0000000..3668513 --- /dev/null +++ b/tests/ludwig/data/test_preprocessing.py @@ -0,0 +1,9 @@ +from ludwig.data.preprocessing import is_input_feature +from tests.integration_tests.utils import text_feature + + +def test_is_input_feature(): + # Adds encoder when output_feature=False + assert is_input_feature(text_feature(output_feature=False)) is True + # Adds decoder when output_feature=True + assert is_input_feature(text_feature(output_feature=True)) is False diff --git a/tests/ludwig/data/test_preprocessing_progress.py b/tests/ludwig/data/test_preprocessing_progress.py new file mode 100644 index 0000000..da397f0 --- /dev/null +++ b/tests/ludwig/data/test_preprocessing_progress.py @@ -0,0 +1,305 @@ +import itertools +from unittest.mock import MagicMock, patch + +import pandas as pd +import pytest + +from ludwig.backend import LOCAL_BACKEND +from ludwig.callbacks import Callback +from ludwig.constants import COLUMN, MISSING_VALUE_STRATEGY, NAME, PREPROCESSING, PROC_COLUMN, TYPE +from ludwig.data.dataframe.pandas import PandasEngine +from ludwig.data.preprocessing import build_data +from ludwig.data.preprocessing_progress import get_total_partitions, PreprocessingProgressTracker + + +class ProgressCollector(Callback): + def __init__(self): + self.values = [] + + def on_preprocess_progress(self, progress, **kwargs): + self.values.append(progress) + + +# --------------------------------------------------------------------------- +# Pandas / local backend +# --------------------------------------------------------------------------- + + +def test_tracker_fires_progress_callbacks(): + """Each map_partitions call fires a callback synchronously (no sleep needed). + + With total=3: increments fire 1/3, 2/3, 3/3=1.0; stop() fires 1.0 again. + Intermediate (strictly < 1.0) count = total - 1 = 2. + """ + collector = ProgressCollector() + n = 3 + tracker = PreprocessingProgressTracker(total=n, callbacks=[collector], use_ray=False) + tracker.start() + + engine = PandasEngine() + series = pd.Series([1, 2, 3]) + for _ in range(n): + engine.map_partitions(series, lambda s: s, progress_tracker=tracker) + + tracker.stop() + + intermediate = [v for v in collector.values if v < 1.0] + assert len(intermediate) == n - 1, f"Expected {n - 1} intermediate values, got {intermediate}" + assert collector.values[-1] == 1.0 + + +def test_tracker_exact_fractions(): + """Progress values should be 1/N, 2/N, ..., N/N = 1.0.""" + collector = ProgressCollector() + n = 5 + tracker = PreprocessingProgressTracker(total=n, callbacks=[collector], use_ray=False) + tracker.start() + + engine = PandasEngine() + series = pd.Series(range(10)) + for _ in range(n): + engine.map_partitions(series, lambda s: s, progress_tracker=tracker) + + tracker.stop() + + # Collect distinct values before the final 1.0 + unique = sorted({v for v in collector.values if v < 1.0}) + expected = [k / n for k in range(1, n)] + assert unique == pytest.approx(expected), f"Unexpected progress steps: {unique}" + assert collector.values[-1] == 1.0 + + +def test_tracker_no_callbacks_does_not_crash(): + tracker = PreprocessingProgressTracker(total=2, callbacks=[], use_ray=False) + tracker.start() + + engine = PandasEngine() + series = pd.Series([1, 2, 3]) + engine.map_partitions(series, lambda s: s, progress_tracker=tracker) + engine.map_partitions(series, lambda s: s, progress_tracker=tracker) + + tracker.stop() + + +def test_map_partitions_without_tracker_unchanged(): + engine = PandasEngine() + series = pd.Series([1, 2, 3]) + result = engine.map_partitions(series, lambda s: s * 2) + pd.testing.assert_series_equal(result, series * 2) + + +def test_get_total_partitions_pandas(): + cols = {"a": pd.Series([1, 2, 3]), "b": pd.Series([4, 5, 6])} + assert get_total_partitions(cols, use_ray=False) == 1 + + +def test_progress_monotonically_increases(): + collector = ProgressCollector() + tracker = PreprocessingProgressTracker(total=5, callbacks=[collector], use_ray=False) + tracker.start() + + engine = PandasEngine() + series = pd.Series(range(10)) + for _ in range(5): + engine.map_partitions(series, lambda s: s, progress_tracker=tracker) + + tracker.stop() + + # All values should be non-decreasing + for a, b in itertools.pairwise(collector.values): + assert b >= a, f"Progress went backwards: {a} -> {b}" + assert collector.values[-1] == 1.0 + + +def test_progress_capped_at_one_when_overcounted(): + """If a feature calls map_partitions more times than the denominator, progress stays <= 1.0.""" + collector = ProgressCollector() + tracker = PreprocessingProgressTracker(total=2, callbacks=[collector], use_ray=False) + tracker.start() + + engine = PandasEngine() + series = pd.Series([1, 2, 3]) + # 4 calls against a denominator of 2 + for _ in range(4): + engine.map_partitions(series, lambda s: s, progress_tracker=tracker) + + tracker.stop() + + assert all(v <= 1.0 for v in collector.values), f"Progress exceeded 1.0: {collector.values}" + assert collector.values[-1] == 1.0 + + +def test_start_stop_without_increments(): + """stop() always fires 1.0 even if no increments happened.""" + collector = ProgressCollector() + tracker = PreprocessingProgressTracker(total=5, callbacks=[collector], use_ray=False) + tracker.start() + tracker.stop() + + assert collector.values[-1] == 1.0 + + +# --------------------------------------------------------------------------- +# Ray backend (mocked — no real Ray cluster needed) +# --------------------------------------------------------------------------- + + +def _make_mock_actor(total): + """Returns a mock Ray actor that simulates the _ProgressActor interface.""" + state = {"completed": 0, "total": total} + + actor = MagicMock() + # increment.remote() increments synchronously in the mock + actor.increment.remote.side_effect = lambda: state.__setitem__("completed", state["completed"] + 1) + # get_completed.remote() returns a future-like that ray.get() resolves + actor.get_completed.remote.side_effect = lambda: state["completed"] + actor.get_total.remote.side_effect = lambda: state["total"] + return actor, state + + +def test_ray_tracker_fires_via_poll(): + """Ray backend: poll thread fires callbacks; stop() drains actor then fires 1.0.""" + collector = ProgressCollector() + + mock_actor, state = _make_mock_actor(total=4) + + with ( + patch("ludwig.data.preprocessing_progress._make_ray_actor", return_value=mock_actor), + patch("ray.get", side_effect=lambda fut, **kw: fut), + patch("ray.kill"), + ): + tracker = PreprocessingProgressTracker(total=4, callbacks=[collector], use_ray=True) + tracker.start() + + # Simulate 4 partition completions: actor incremented directly + for _ in range(4): + state["completed"] += 1 + + tracker.stop() + + assert collector.values[-1] == 1.0 + + +def test_ray_stop_drains_actor_queue(): + """stop() calls get_completed.remote() before firing 1.0, ensuring actor queue is drained.""" + collector = ProgressCollector() + mock_actor, state = _make_mock_actor(total=2) + + drain_calls = [] + + def mock_ray_get(fut, **kw): + drain_calls.append(fut) + return fut # fut is already the value in our mock + + with ( + patch("ludwig.data.preprocessing_progress._make_ray_actor", return_value=mock_actor), + patch("ray.get", side_effect=mock_ray_get), + patch("ray.kill"), + ): + tracker = PreprocessingProgressTracker(total=2, callbacks=[collector], use_ray=True) + tracker.start() + state["completed"] = 2 + tracker.stop() + + # stop() must have called get_completed.remote() to drain the queue + assert mock_actor.get_completed.remote.called + assert collector.values[-1] == 1.0 + + +# --------------------------------------------------------------------------- +# Large-dataset simulation (pandas, stress) +# --------------------------------------------------------------------------- + + +def test_large_feature_count_pandas(): + """Simulate 50 features × 1 pandas partition: all 50 intermediate values emitted.""" + collector = ProgressCollector() + n = 50 + tracker = PreprocessingProgressTracker(total=n, callbacks=[collector], use_ray=False) + tracker.start() + + engine = PandasEngine() + series = pd.Series(range(1000)) + for _ in range(n): + engine.map_partitions(series, lambda s: s * 2, progress_tracker=tracker) + + tracker.stop() + + intermediate = [v for v in collector.values if v < 1.0] + # The nth increment fires 1.0 directly, so strictly-intermediate count = n - 1 + assert len(intermediate) == n - 1, f"Expected {n - 1} intermediate ticks, got {len(intermediate)}" + assert collector.values[-1] == 1.0 + # Strictly increasing + for a, b in itertools.pairwise(intermediate): + assert b > a + + +# --------------------------------------------------------------------------- +# Regression: feature-level increment (build_data loop) +# --------------------------------------------------------------------------- + + +def test_progress_fires_for_features_not_calling_map_partitions(): + """Regression test for issue #4195. + + Most feature types (category, binary, number without normalization) use + map_objects or direct series operations rather than map_partitions. The + old implementation incremented the counter only inside the map_partitions + monkey-patch, so those features produced zero progress callbacks. + + The fix moves the increment to build_data's feature loop: one tick after + each add_feature_data() call, regardless of which engine operation the + feature uses internally. + + This test verifies the fix by patching add_feature_data to never call + map_partitions. With the old code the counter stays at 0 and only stop()'s + forced 1.0 is emitted; with the new code every feature fires a callback. + """ + n_features = 5 + feature_names = [f"feat_{i}" for i in range(n_features)] + + feature_configs = [ + {NAME: name, TYPE: "number", COLUMN: name, PROC_COLUMN: f"{name}_proc"} for name in feature_names + ] + # Minimal preprocessing metadata — fill_with_const with no outlier strategy + # so that handle_missing_values/handle_outliers are no-ops. + training_set_metadata = { + name: { + PREPROCESSING: { + MISSING_VALUE_STRATEGY: "fill_with_const", + "computed_fill_value": 0.0, + } + } + for name in feature_names + } + input_cols = {name: pd.Series([1.0, 2.0, 3.0, 4.0, 5.0]) for name in feature_names} + + collector = ProgressCollector() + tracker = PreprocessingProgressTracker(total=n_features, callbacks=[collector], use_ray=False) + tracker.start() + + # Patch add_feature_data to do nothing — simulating features that never + # call map_partitions (binary, category, ...). + mock_feature_type = MagicMock() + mock_feature_type.add_feature_data.return_value = None + + with patch("ludwig.data.preprocessing.get_from_registry", return_value=mock_feature_type): + build_data(input_cols, feature_configs, training_set_metadata, LOCAL_BACKEND, False, tracker) + + tracker.stop() + + intermediate = [v for v in collector.values if v < 1.0] + + # OLD code: 0 intermediate callbacks (map_partitions never called → counter + # never incremented → only stop()'s forced 1.0 is present). + # NEW code: n_features - 1 intermediate callbacks (one per feature, the + # last one fires 1.0 directly and is not counted as intermediate). + assert len(intermediate) == n_features - 1, ( + f"Expected {n_features - 1} intermediate callbacks (one per feature). " + f"Got {collector.values!r}. " + "This fails with the old map_partitions-only implementation." + ) + assert collector.values[-1] == 1.0 + # Strictly increasing + for a, b in itertools.pairwise(collector.values): + assert b >= a, f"Progress went backwards: {a} -> {b}" diff --git a/tests/ludwig/data/test_ray_data.py b/tests/ludwig/data/test_ray_data.py new file mode 100644 index 0000000..f18bcfe --- /dev/null +++ b/tests/ludwig/data/test_ray_data.py @@ -0,0 +1,395 @@ +import os +import shutil +from unittest import mock + +import numpy as np +import pandas as pd +import pytest + +# Skip these tests if Ray is not installed +ray = pytest.importorskip("ray") +dask = pytest.importorskip("dask") + +from ludwig.data.dataset.ray import RayDataset, RayDatasetBatcher, RayDatasetShardBatcher, read_remote_parquet # noqa + +# Mark the entire module as distributed +pytestmark = [pytest.mark.distributed, pytest.mark.distributed_d] + + +def test_prefetch_batches_value(): + """Regression test: both Ray batcher async readers must use prefetch_batches > 1. + + prefetch_batches=1 starves the GPU because only one batch is in flight at a time. + The fix sets it to 4, which keeps more batches queued so the GPU stays busy. + See https://github.com/ludwig-ai/ludwig/issues/4142 + """ + import inspect + import re + + src_batcher = inspect.getsource(RayDatasetBatcher._create_async_reader) + src_shard = inspect.getsource(RayDatasetShardBatcher._create_async_reader) + + def _get_prefetch_value(src: str) -> int: + match = re.search(r"prefetch_batches\s*=\s*(\d+)", src) + assert match, "prefetch_batches kwarg not found in async reader source" + return int(match.group(1)) + + assert _get_prefetch_value(src_batcher) > 1, ( + "RayDatasetBatcher uses prefetch_batches=1, which starves the GPU. Increase it." + ) + assert _get_prefetch_value(src_shard) > 1, ( + "RayDatasetShardBatcher uses prefetch_batches=1, which starves the GPU. Increase it." + ) + + +def test_train_fn_passes_device_to_remote_trainer(): + """Regression test: train_fn must pass device= to RemoteTrainer so the trainer's self.device + matches the Ray-assigned GPU rather than falling back to get_torch_device(). + + Without this, metrics_to_device() and batch-to-device tensors may disagree with the model's + actual placement when running inside a Ray Train worker. + See https://github.com/ludwig-ai/ludwig/issues/4142 + """ + import inspect + + from ludwig.backend.ray import train_fn + + src = inspect.getsource(train_fn) + # The RemoteTrainer() call must forward device= so Trainer.__init__ doesn't + # silently override it with get_torch_device(). + assert "RemoteTrainer(model=model, device=" in src, ( + "train_fn must pass device= to RemoteTrainer to avoid device mismatch. " + "See https://github.com/ludwig-ai/ludwig/issues/4142" + ) + + +def test_progress_bar_does_not_call_rt_report_per_batch(): + """Regression test: LudwigProgressBar must not call rt.report() on every training batch. + + Each rt.report() call requires a round-trip through the Ray GCS (~1.9 s). With hundreds of + batches per run this completely dominates wall-clock time. The fix silently suppresses the + tqdm bar inside Ray workers instead of reporting per-batch progress via rt.report(). + """ + from ludwig.progress_bar import LudwigProgressBar + + with mock.patch("ludwig.progress_bar.rt") as mock_rt: + pbar = LudwigProgressBar(report_to_ray=True, config={"total": 10, "desc": "test"}, is_coordinator=True) + for _ in range(10): + pbar.update(1) + pbar.close() + + mock_rt.report.assert_not_called() + + +def test_async_reader_error(): + """Test that RayDatasetBatcher handles a dataset that produces no batches. + + When iter_batches yields nothing, the batcher should end up with last_batch=True. + """ + mock_dataset = mock.Mock() + # iter_batches yields nothing (empty dataset) + mock_dataset.iter_batches.return_value = iter([]) + + features = { + "num1": {"name": "num1", "type": "number"}, + "bin1": {"name": "bin1", "type": "binary"}, + } + training_set_metadata = { + "num1": {}, + "bin1": {}, + } + + batcher = RayDatasetBatcher( + dataset=mock_dataset, + features=features, + training_set_metadata=training_set_metadata, + batch_size=64, + samples_per_epoch=100, + ) + # With no data to read, the batcher should immediately signal last batch + assert batcher.last_batch() + + +@pytest.fixture(scope="module") +def parquet_file(ray_cluster_2cpu) -> str: + """Write a multi-file parquet dataset to the cwd. + + Returns: + The path to the parquet dataset. + """ + # The data needs to be written to a multi-file parquet format, otherwise the issue doesn't repro. To do this, we + # partitition a test dataframe with dask and then write to file. + df = pd.DataFrame({"col1": list(range(1000)), "col2": list(range(1000))}) + df = dask.dataframe.from_pandas(df, chunksize=100) + + # Typically we would write test data to a temporary directory, but the issue this was set up to test only happens + # when using relative filepaths. + cwd = os.getcwd() + filepath = os.path.join(cwd, "data.training.parquet") + df.to_parquet(filepath, engine="pyarrow") + + yield filepath + + # Clean up the data + shutil.rmtree(filepath) + + +@pytest.fixture(scope="module", params=["absolute", "relative"]) +def parquet_filepath(parquet_file: str, request: "pytest.FixtureRequest") -> str: + """Convert a filepath in the CWD to either an absolute or relative path. + + Args: + parquet_file: Absolute path to a parquet file in the CWD + request: pytest request fixture with the fixture parameters + + Returns: + Either the absolute or relative path of the parquet file. + """ + filepath_type = request.param + return parquet_file if filepath_type == "absolute" else os.path.basename(parquet_file) + + +def test_read_remote_parquet(parquet_filepath: str): + """Test for the fix to https://github.com/ludwig-ai/ludwig/issues/3440. + + Parquet file reads will fail with `pyarrow.lib.ArrowInvalid` under the following conditions: + 1) The Parquet data is in multi-file format + 2) A relative filepath is passed to the read function + 3) A filesystem object is passed to the read function + + The issue can be resolved by either: + 1) Passing an absolute filepath + 2) Not passing a filesystem object + """ + read_remote_parquet(parquet_filepath) + + +# --------------------------------------------------------------------------- +# Lazy decode tests +# --------------------------------------------------------------------------- + + +def _make_ray_dataset(df: pd.DataFrame, features: dict, training_set_metadata: dict) -> RayDataset: + """Construct a RayDataset directly from a pandas DataFrame for testing.""" + ds = RayDataset.__new__(RayDataset) + ds.ds = ray.data.from_pandas(df) + ds.features = features + ds.training_set_metadata = training_set_metadata + ds.data_cache_fp = None + ds.data_parquet_fp = None + return ds + + +def test_with_lazy_decode_audio(ray_cluster_2cpu, tmp_path): + """_with_lazy_decode must replace path strings with decoded numpy arrays for audio features. + + Before the fix, the Ray backend passed raw path strings to workers; the batcher then tried + to np.stack strings which either hung or produced garbage. After the fix, map_batches + decode transforms run inside each worker and return proper numpy arrays. + + The decode function is mocked here so the test has no soundfile dependency. + The real audio decode pipeline is exercised by test_ray_audio_basic (integration test). + """ + from unittest.mock import patch + + proc_col = "audio_proc_col" + feature_name = "audio_0" + feature_dim = 8 + max_length = 5 + + dummy_array = np.zeros((feature_dim, max_length), dtype=np.float32) + + def _mock_make_lazy_decode_fn(**kwargs): + def decode(path): + return dummy_array + + decode.__name__ = "audio_lazy_decode" + return decode + + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + training_set_metadata = { + feature_name: { + "lazy": True, + "reshape": None, + "lazy_audio_params": { + "audio_feature_dict": {"type": "fbank"}, + "feature_dim": feature_dim, + "max_length": max_length, + "padding_value": 0.0, + "normalization_type": None, + }, + } + } + + df = pd.DataFrame({proc_col: ["/fake/path/a.wav", "/fake/path/b.wav"]}) + ray_ds = _make_ray_dataset(df, features, training_set_metadata) + + # Before: raw dataset has path strings, not arrays. + raw_batch = ray_ds.ds.take_batch(batch_size=2, batch_format="pandas") + assert isinstance(raw_batch[proc_col].iloc[0], str), "Expected path string in raw dataset" + + # After: _with_lazy_decode adds a map_batches transform that decodes paths to arrays. + with patch("ludwig.features.audio_feature.AudioFeatureMixin._make_lazy_decode_fn", _mock_make_lazy_decode_fn): + decoded_ds = ray_ds._with_lazy_decode(ray_ds.ds) + + decoded_batch = decoded_ds.take_batch(batch_size=2, batch_format="pandas") + first = decoded_batch[proc_col].iloc[0] + assert isinstance(first, np.ndarray), f"Expected numpy array after decode, got {type(first)}" + assert first.shape == (feature_dim, max_length), f"Expected shape ({feature_dim}, {max_length}), got {first.shape}" + + +def test_with_lazy_decode_image(ray_cluster_2cpu, tmp_path): + """_with_lazy_decode must replace path strings with decoded numpy arrays for image features. + + Same root cause as audio: without decode transforms, workers receive file paths instead + of tensors, causing hangs or crashes in the batcher. + """ + PIL = pytest.importorskip("PIL.Image") + + # Write a tiny PNG (12×12 RGB). + img_path = str(tmp_path / "test.png") + img = PIL.fromarray(np.zeros((12, 12, 3), dtype=np.uint8)) + img.save(img_path) + + proc_col = "image_proc_col" + feature_name = "image_0" + h, w, c = 12, 12, 3 + + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "image"}} + training_set_metadata = { + feature_name: { + "lazy": True, + "reshape": None, + "lazy_image_params": { + "img_width": w, + "img_height": h, + "should_resize": False, + "num_channels": c, + "resize_method": "interpolate", + "user_specified_num_channels": True, + "standardize_image": "pixel_normalization", + "channel_class_map": [], + "default_image_shape": [c, h, w], + }, + } + } + + df = pd.DataFrame({proc_col: [img_path, img_path]}) + ray_ds = _make_ray_dataset(df, features, training_set_metadata) + + # Before: raw dataset has path strings. + raw_batch = ray_ds.ds.take_batch(batch_size=2, batch_format="pandas") + assert isinstance(raw_batch[proc_col].iloc[0], str), "Expected path string in raw dataset" + + # After: _with_lazy_decode adds a map_batches transform that decodes paths to arrays. + decoded_ds = ray_ds._with_lazy_decode(ray_ds.ds) + decoded_batch = decoded_ds.take_batch(batch_size=2, batch_format="pandas") + first = decoded_batch[proc_col].iloc[0] + assert isinstance(first, np.ndarray), f"Expected numpy array after decode, got {type(first)}" + assert first.ndim == 3, f"Expected 3-D image array (C × H × W), got shape {first.shape}" + assert first.shape == (c, h, w), f"Expected shape {(c, h, w)}, got {first.shape}" + + +def test_with_lazy_decode_non_lazy_passthrough(ray_cluster_2cpu): + """_with_lazy_decode must not touch features that are not lazy.""" + proc_col = "num_col" + feature_name = "num_0" + + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "number"}} + training_set_metadata = {feature_name: {"lazy": False}} + + df = pd.DataFrame({proc_col: [1.0, 2.0, 3.0]}) + ray_ds = _make_ray_dataset(df, features, training_set_metadata) + + decoded_ds = ray_ds._with_lazy_decode(ray_ds.ds) + + # Dataset should be identical — no transforms added. + batch = decoded_ds.take_batch(batch_size=3, batch_format="pandas") + assert list(batch[proc_col]) == [1.0, 2.0, 3.0] + + +def test_with_lazy_decode_missing_params_warns(ray_cluster_2cpu, caplog): + """_with_lazy_decode must warn when lazy=True but lazy_audio_params is absent. + + If training_set_metadata has lazy=True but the decode params were not saved + (e.g. stale cache from an older Ludwig version), the feature should be skipped + with a clear warning rather than silently passing path strings to workers. + """ + import logging + + proc_col = "audio_proc" + feature_name = "audio_0" + + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + # lazy=True but no lazy_audio_params — simulates a stale / manually-crafted metadata dict + training_set_metadata = {feature_name: {"lazy": True}} + + df = pd.DataFrame({proc_col: ["/fake/a.wav", "/fake/b.wav"]}) + ray_ds = _make_ray_dataset(df, features, training_set_metadata) + + with caplog.at_level(logging.WARNING, logger="ludwig.data.dataset.ray"): + decoded_ds = ray_ds._with_lazy_decode(ray_ds.ds) + + assert any("lazy_audio_params" in msg for msg in caplog.messages), ( + "_with_lazy_decode should warn about missing lazy_audio_params, but no warning was logged" + ) + # Dataset unchanged — still path strings (no transform added) + batch = decoded_ds.take_batch(batch_size=2, batch_format="pandas") + assert isinstance(batch[proc_col].iloc[0], str) + + +def test_lazy_decode_batcher_produces_tensors(ray_cluster_2cpu, tmp_path): + """End-to-end: RayDatasetBatcher with lazy audio must produce numpy arrays, not path strings. + + Exercises the full pipeline: _with_lazy_decode → materialize → iter_batches → + cast_as_tensor_dtype → _prepare_batch. Verifies that the decoded arrays have the + correct shape so the model would receive valid tensors. + """ + from unittest.mock import patch + + proc_col = "audio_proc" + feature_name = "audio_0" + feature_dim = 8 + max_length = 5 + + dummy_array = np.zeros((feature_dim, max_length), dtype=np.float32) + + def _mock_make_lazy_decode_fn(**kwargs): + def decode(path): + return dummy_array + + decode.__name__ = "audio_lazy_decode" + return decode + + features = {proc_col: {"name": feature_name, "column": feature_name, "type": "audio"}} + training_set_metadata = { + feature_name: { + "lazy": True, + "reshape": None, + "lazy_audio_params": { + "audio_feature_dict": {"type": "fbank"}, + "feature_dim": feature_dim, + "max_length": max_length, + "padding_value": 0.0, + "normalization_type": None, + }, + } + } + + n_samples = 10 + df = pd.DataFrame({proc_col: [f"/fake/path_{i}.wav" for i in range(n_samples)]}) + ray_ds = _make_ray_dataset(df, features, training_set_metadata) + + with patch("ludwig.features.audio_feature.AudioFeatureMixin._make_lazy_decode_fn", _mock_make_lazy_decode_fn): + with ray_ds.initialize_batcher(batch_size=4, should_shuffle=False) as batcher: + batches = [] + while not batcher.last_batch(): + batches.append(batcher.next_batch()) + + assert len(batches) > 0, "No batches produced" + for batch in batches: + arr = batch[proc_col] + assert isinstance(arr, np.ndarray), f"Expected numpy array, got {type(arr)}" + assert arr.ndim == 3, f"Expected (batch, feature_dim, max_length), got shape {arr.shape}" + assert arr.shape[1] == feature_dim + assert arr.shape[2] == max_length diff --git a/tests/ludwig/data/test_split.py b/tests/ludwig/data/test_split.py new file mode 100644 index 0000000..68873b8 --- /dev/null +++ b/tests/ludwig/data/test_split.py @@ -0,0 +1,396 @@ +from datetime import datetime, timedelta +from itertools import combinations +from random import randrange +from unittest.mock import Mock + +import numpy as np +import pandas as pd +import pytest + +from ludwig.data.dataframe.pandas import PandasEngine +from ludwig.data.split import get_splitter + +try: + from ludwig.data.dataframe.dask import DaskEngine +except ImportError: + DaskEngine = Mock + + +def test_make_divisions_ensure_minimum_rows(): + from ludwig.data.split import _make_divisions_ensure_minimum_rows + + # Constraints are satisfied, the function should make no change to divisions. + divisions = _make_divisions_ensure_minimum_rows((70, 80), 100, min_val_rows=3, min_test_rows=3) + assert divisions[0] == 70 + assert divisions[1] == 80 + # Constraints are satisfied, the function should make no change to divisions. + divisions = _make_divisions_ensure_minimum_rows((20, 22), 25, min_val_rows=0, min_test_rows=0) + assert divisions[0] == 20 + assert divisions[1] == 22 + # The number of rows in validation set is too small. + divisions = _make_divisions_ensure_minimum_rows((17, 19), 25, min_val_rows=3, min_test_rows=3) + assert divisions[0] == 16 + assert divisions[1] == 19 + # The number of rows in validation and test sets are both too small. + divisions = _make_divisions_ensure_minimum_rows((20, 22), 25, min_val_rows=3, min_test_rows=3) + assert divisions[0] == 19 + assert divisions[1] == 22 + + +@pytest.mark.parametrize( + ("df_engine",), + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d]), + ], +) +def test_random_split(df_engine, ray_cluster_2cpu): + nrows = 100 + npartitions = 10 + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=npartitions) + + probs = (0.7, 0.1, 0.2) + split_params = { + "type": "random", + "probabilities": probs, + } + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend, random_seed=42) + + assert len(splits) == 3 + for split, p in zip(splits, probs): + if isinstance(df_engine, DaskEngine): + # Dask splitting is not exact, so apply soft constraint here + assert np.isclose(len(split), int(nrows * p), atol=5) + else: + assert len(split) == int(nrows * p) + + # Test determinism + def compute(dfs): + return [df.compute() if isinstance(backend.df_engine, DaskEngine) else df for df in dfs] + + splits = compute(splits) + splits2 = compute(splitter.split(df, backend, random_seed=7)) + for s1, s2 in zip(splits, splits2): + assert not s1.equals(s2) + + splits3 = compute(splitter.split(df, backend, random_seed=42)) + for s1, s3 in zip(splits, splits3): + assert s1.equals(s3) + + +@pytest.mark.parametrize( + ("df_engine",), + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d]), + ], +) +def test_random_split_zero_probability_for_test_produces_no_zombie(df_engine, ray_cluster_2cpu): + nrows = 102 + npartitions = 10 + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=npartitions) + + probs = (0.7, 0.3, 0.0) + split_params = { + "type": "random", + "probabilities": probs, + } + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend, random_seed=42) + + assert len(splits[-1]) == 0 + + +@pytest.mark.parametrize( + ("df_engine",), + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d]), + ], +) +def test_fixed_split(df_engine, ray_cluster_2cpu): + nrows = 100 + npartitions = 10 + thresholds = [60, 80, 100] + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + + def get_split(v): + if v < thresholds[0]: + return 0 + if thresholds[0] <= v < thresholds[1]: + return 1 + return 2 + + df["split_col"] = df["C"].map(get_split).astype(np.int8) + + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=npartitions) + + split_params = { + "type": "fixed", + "column": "split_col", + } + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend) + + assert len(splits) == 3 + + last_t = 0 + for split, t in zip(splits, thresholds): + if isinstance(df_engine, DaskEngine): + split = split.compute() + + assert np.all(split["C"] < t) + assert np.all(split["C"] >= last_t) + last_t = t + + +@pytest.mark.parametrize( + ("df_engine", "nrows", "atol"), + [ + pytest.param(PandasEngine(), 100, 1, id="pandas"), + # Splitting with a distributed engine becomes more accurate with more rows. + pytest.param( + DaskEngine(_use_ray=False), 10000, 10, id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d] + ), + ], +) +@pytest.mark.parametrize( + "class_probs", + [ + pytest.param(np.array([0.33, 0.33, 0.34]), id="balanced"), + pytest.param(np.array([0.6, 0.2, 0.2]), id="imbalanced"), + ], +) +def test_stratify_split(df_engine, nrows, atol, class_probs, ray_cluster_2cpu): + npartitions = 10 + thresholds = np.cumsum((class_probs * nrows).astype(int)) + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + + def get_category(v): + if v < thresholds[0]: + return 0 + if thresholds[0] <= v < thresholds[1]: + return 1 + return 2 + + df["category"] = df.index.map(get_category).astype(np.int8) + + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=npartitions) + + probs = (0.7, 0.1, 0.2) + split_params = { + "type": "stratify", + "column": "category", + "probabilities": probs, + } + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend, random_seed=42) + assert len(splits) == 3 + + ratios = class_probs * nrows + for split, p in zip(splits, probs): + if isinstance(df_engine, DaskEngine): + split = split.compute() + for idx, r in enumerate(ratios): + actual = np.sum(split["category"] == idx) + expected = int(r * p) + assert np.isclose(actual, expected, atol=atol) + + # Test determinism + splits2 = splitter.split(df, backend, random_seed=7) + for s1, s2 in zip(splits, splits2): + if isinstance(df_engine, DaskEngine): + s1 = s1.compute() + s2 = s2.compute() + assert not s1.equals(s2) + + splits3 = splitter.split(df, backend, random_seed=42) + for s1, s3 in zip(splits, splits3): + if isinstance(df_engine, DaskEngine): + s1 = s1.compute() + s3 = s3.compute() + assert s1.equals(s3) + + +@pytest.mark.parametrize( + ("df_engine", "atol"), + [ + pytest.param(PandasEngine(), 1, id="pandas"), + pytest.param( + DaskEngine(_use_ray=False), 10, id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d] + ), + ], +) +def test_single_occurrence_stratified_split(df_engine, atol, ray_cluster_2cpu): + nrows = 1000 + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 2)), columns=["A", "B"]) + # create 4 classes, where two of them each occurs once in the dataframe. + df["category"] = (nrows // 2 - 1) * [0, 1] + [2, 3] + + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=10) + + probs = (0.7, 0.1, 0.2) + split_params = { + "type": "stratify", + "column": "category", + "probabilities": probs, + } + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend, random_seed=42) + assert len(splits) == 3 + + ratios = np.array([0.499, 0.499, 0.001, 0.001]) * nrows + for split, p in zip(splits, probs): + if isinstance(df_engine, DaskEngine): + split = split.compute() + for idx, r in enumerate(ratios): + actual = np.sum(split["category"] == idx) + expected = int(r * p) + assert np.isclose(actual, expected, atol=atol) + + +@pytest.mark.parametrize( + ("df_engine",), + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d]), + ], +) +@pytest.mark.parametrize("format", ["str", "datetime"]) +def test_datetime_split(format, df_engine, ray_cluster_2cpu): + nrows = 100 + npartitions = 10 + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + + def random_date(*args, **kwargs): + start = datetime.strptime("1/1/1990 1:30 PM", "%m/%d/%Y %I:%M %p") + end = datetime.strptime("1/1/2022 4:50 AM", "%m/%d/%Y %I:%M %p") + delta = end - start + int_delta = (delta.days * 24 * 60 * 60) + delta.seconds + random_second = randrange(int_delta) + t = start + timedelta(seconds=random_second) + return str(t) if format == "str" else t + + df["date_col"] = df["C"].map(random_date) + + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=npartitions) + + probs = (0.7, 0.1, 0.2) + split_params = { + "type": "datetime", + "column": "date_col", + "probabilities": probs, + } + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend) + + assert len(splits) == 3 + + min_datestr = "1990-01-01 00:00:00" + for split, p in zip(splits, probs): + if isinstance(df_engine, DaskEngine): + # Dask splitting is not exact, so apply soft constraint here + split = split.compute() + assert len(split) >= 1 + # Dask splitting is not exact, so we can potentially apply soft constraint. However, this can also be flaky: + # https://github.com/ludwig-ai/ludwig/actions/runs/4590907163/jobs/8106746310?pr=3315. + # assert np.isclose(len(split), int(nrows * p), atol=15) + else: + assert len(split) == int(nrows * p) + + assert np.all(split["date_col"] > min_datestr) + min_datestr = split["date_col"].max() + + +@pytest.mark.parametrize( + ("df_engine",), + [ + pytest.param(PandasEngine(), id="pandas"), + pytest.param(DaskEngine(_use_ray=False), id="dask", marks=[pytest.mark.distributed, pytest.mark.distributed_d]), + ], +) +def test_hash_split(df_engine, ray_cluster_2cpu): + nrows = 100 + npartitions = 10 + + df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + df["id"] = np.arange(0, 100) + + if isinstance(df_engine, DaskEngine): + df = df_engine.df_lib.from_pandas(df, npartitions=npartitions) + + probabilities = [0.8, 0.1, 0.1] + split_params = {"type": "hash", "column": "id", "probabilities": probabilities} + splitter = get_splitter(**split_params) + + backend = Mock() + backend.df_engine = df_engine + splits = splitter.split(df, backend) + assert len(splits) == 3 + if isinstance(df_engine, DaskEngine): + splits = [split.compute() for split in splits] + + # IDs should not overlap between splits + assert all([set(split1["id"]).isdisjoint(set(split2["id"])) for split1, split2 in combinations(splits, 2)]) + + for split, p in zip(splits, probabilities): + # Should be approximately the same size as the desired proportion + assert nrows * p - 5 <= len(split["id"]) <= nrows * p + 5 + + # Need to ensure deterministic splitting even as we append data + df2 = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=["A", "B", "C"]) + df2["id"] = np.arange(100, 200) + + nrows *= 2 + + df = df_engine.df_lib.concat([df, df2]) + + splits2 = splitter.split(df, backend) + assert len(splits2) == 3 + if isinstance(df_engine, DaskEngine): + splits2 = [split.compute() for split in splits2] + + # IDs should not overlap between splits + assert all([set(split1["id"]).isdisjoint(set(split2["id"])) for split1, split2 in combinations(splits2, 2)]) + + for split1, split2, p in zip(splits, splits2, probabilities): + ids1 = set(split1["id"].values.tolist()) + ids2 = set(split2["id"].values.tolist()) + + assert nrows * p - 10 <= len(ids2) <= nrows * p + 10 + + # All elements from the first round of splitting are in the same split, even after appending + # more rows + assert ids1.issubset(ids2) diff --git a/tests/ludwig/data/test_split_utils.py b/tests/ludwig/data/test_split_utils.py new file mode 100644 index 0000000..379f54a --- /dev/null +++ b/tests/ludwig/data/test_split_utils.py @@ -0,0 +1,51 @@ +"""Tests for split utilities.""" + +import numpy as np + +from ludwig.data.split_utils import get_split_indices, stratified_split_indices + + +class TestGetSplitIndices: + def test_default_proportions(self): + splits = get_split_indices(1000) + train = (splits == 0).sum() + val = (splits == 1).sum() + test = (splits == 2).sum() + assert abs(train - 700) < 10 + assert abs(val - 100) < 10 + assert abs(test - 200) < 10 + + def test_custom_proportions(self): + splits = get_split_indices(1000, probabilities=(0.8, 0.1, 0.1)) + train = (splits == 0).sum() + assert abs(train - 800) < 10 + + def test_reproducible(self): + s1 = get_split_indices(100, random_seed=42) + s2 = get_split_indices(100, random_seed=42) + assert np.array_equal(s1, s2) + + def test_different_seeds(self): + s1 = get_split_indices(100, random_seed=42) + s2 = get_split_indices(100, random_seed=99) + assert not np.array_equal(s1, s2) + + +class TestStratifiedSplitIndices: + def test_maintains_distribution(self): + labels = np.array([0] * 500 + [1] * 500) + splits = stratified_split_indices(labels) + + train_0 = ((splits == 0) & (labels == 0)).sum() + train_1 = ((splits == 0) & (labels == 1)).sum() + # Both classes should have ~70% in training + assert abs(train_0 - 350) < 20 + assert abs(train_1 - 350) < 20 + + def test_imbalanced(self): + labels = np.array([0] * 900 + [1] * 100) + splits = stratified_split_indices(labels) + + # Minority class should still be split proportionally + test_1 = ((splits == 2) & (labels == 1)).sum() + assert test_1 >= 10 # At least some minority in test diff --git a/tests/ludwig/data/test_statistics.py b/tests/ludwig/data/test_statistics.py new file mode 100644 index 0000000..fe2f093 --- /dev/null +++ b/tests/ludwig/data/test_statistics.py @@ -0,0 +1,135 @@ +"""Unit tests for WelfordAccumulator (online mean/variance/min/max). + +These tests ensure: +1. Single-pass statistics match numpy batch statistics exactly (within floating point). +2. Shard-merge is commutative and associative. +3. Edge cases (single sample, constant series, negative values) are handled correctly. +""" + +import math + +import numpy as np + +from ludwig.data.statistics import welford_from_array, welford_stats_match_numpy, WelfordAccumulator + + +class TestWelfordSingleSeries: + def test_matches_numpy_uniform(self): + xs = np.random.default_rng(0).uniform(-10, 10, size=1000) + acc = welford_from_array(xs) + assert welford_stats_match_numpy(acc, xs) + + def test_matches_numpy_normal(self): + xs = np.random.default_rng(1).normal(5.0, 2.0, size=500) + acc = welford_from_array(xs) + assert welford_stats_match_numpy(acc, xs) + + def test_matches_numpy_single_element(self): + acc = WelfordAccumulator() + acc.update(42.0) + assert acc.count == 1 + assert math.isclose(float(acc.mean), 42.0) + assert float(acc.std) == 0.0 + assert math.isclose(float(acc.min), 42.0) + assert math.isclose(float(acc.max), 42.0) + + def test_constant_series_zero_variance(self): + acc = WelfordAccumulator() + for _ in range(100): + acc.update(7.0) + assert math.isclose(float(acc.mean), 7.0) + assert float(acc.std) < 1e-10 + assert float(acc.variance) < 1e-10 + + def test_min_max(self): + xs = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0] + acc = welford_from_array(xs) + assert math.isclose(float(acc.min), 1.0) + assert math.isclose(float(acc.max), 9.0) + + def test_negative_values(self): + xs = np.linspace(-100, -1, 50) + acc = welford_from_array(xs) + assert welford_stats_match_numpy(acc, xs) + + def test_result_dict_keys(self): + acc = welford_from_array([1.0, 2.0, 3.0]) + result = acc.result() + assert set(result.keys()) == {"count", "mean", "std", "variance", "min", "max"} + assert result["count"] == 3 + + def test_update_batch(self): + xs = np.arange(1.0, 101.0) + acc = WelfordAccumulator() + acc.update_batch(xs) + assert welford_stats_match_numpy(acc, xs) + + +class TestWelfordMerge: + def test_merge_two_equal_shards(self): + rng = np.random.default_rng(42) + xs = rng.normal(0, 1, size=200) + half = len(xs) // 2 + acc_a = welford_from_array(xs[:half]) + acc_b = welford_from_array(xs[half:]) + merged = WelfordAccumulator.merge(acc_a, acc_b) + assert welford_stats_match_numpy(merged, xs) + + def test_merge_unequal_shards(self): + rng = np.random.default_rng(7) + xs = rng.exponential(2.0, size=300) + acc_a = welford_from_array(xs[:50]) + acc_b = welford_from_array(xs[50:]) + merged = WelfordAccumulator.merge(acc_a, acc_b) + assert welford_stats_match_numpy(merged, xs) + + def test_merge_with_empty(self): + xs = [1.0, 2.0, 3.0] + acc = welford_from_array(xs) + empty = WelfordAccumulator() + assert WelfordAccumulator.merge(acc, empty).count == 3 + assert WelfordAccumulator.merge(empty, acc).count == 3 + + def test_merge_all(self): + rng = np.random.default_rng(99) + xs = rng.standard_normal(600) + shards = [welford_from_array(xs[i * 100 : (i + 1) * 100]) for i in range(6)] + merged = WelfordAccumulator.merge_all(shards) + assert welford_stats_match_numpy(merged, xs) + + def test_merge_commutativity(self): + xs1 = np.array([1.0, 2.0, 3.0]) + xs2 = np.array([4.0, 5.0]) + ab = WelfordAccumulator.merge(welford_from_array(xs1), welford_from_array(xs2)) + ba = WelfordAccumulator.merge(welford_from_array(xs2), welford_from_array(xs1)) + assert math.isclose(float(ab.mean), float(ba.mean)) + assert math.isclose(float(ab.std), float(ba.std)) + + def test_merge_associativity(self): + a = welford_from_array([1.0, 2.0]) + b = welford_from_array([3.0, 4.0]) + c = welford_from_array([5.0]) + ab_c = WelfordAccumulator.merge(WelfordAccumulator.merge(a, b), c) + a_bc = WelfordAccumulator.merge(a, WelfordAccumulator.merge(b, c)) + assert math.isclose(float(ab_c.mean), float(a_bc.mean), rel_tol=1e-12) + assert math.isclose(float(ab_c.std), float(a_bc.std), rel_tol=1e-12) + + +class TestWelfordEdgeCases: + def test_empty_accumulator_defaults(self): + acc = WelfordAccumulator() + assert acc.count == 0 + assert float(acc.mean) == 0.0 + assert float(acc.std) == 0.0 + + def test_large_values(self): + xs = np.array([1e15, 2e15, 3e15]) + acc = welford_from_array(xs) + assert welford_stats_match_numpy(acc, xs, rtol=1e-6) + + def test_two_elements(self): + acc = WelfordAccumulator() + acc.update(0.0) + acc.update(2.0) + assert math.isclose(float(acc.mean), 1.0) + assert math.isclose(float(acc.std), math.sqrt(2.0)) diff --git a/tests/ludwig/data/test_types.py b/tests/ludwig/data/test_types.py new file mode 100644 index 0000000..c608bc1 --- /dev/null +++ b/tests/ludwig/data/test_types.py @@ -0,0 +1,90 @@ +"""Tests for typed metadata classes.""" + +from ludwig.data.types import ( + CategoryMetadata, + NumberMetadata, + TextMetadata, + TrainingSetMetadata, +) + + +class TestNumberMetadata: + def test_from_dict(self): + d = {"mean": 5.0, "std": 2.0, "min": 0.0, "max": 10.0} + meta = NumberMetadata.from_dict(d) + assert meta.mean == 5.0 + assert meta.std == 2.0 + + def test_to_dict(self): + meta = NumberMetadata(mean=5.0, std=2.0) + d = meta.to_dict() + assert d["mean"] == 5.0 + assert "min" not in d # None values excluded + + def test_ignores_unknown_keys(self): + d = {"mean": 5.0, "unknown_key": "value"} + meta = NumberMetadata.from_dict(d) + assert meta.mean == 5.0 + + def test_ple_bin_edges(self): + meta = NumberMetadata(ple_bin_edges=[0.0, 0.25, 0.5, 0.75, 1.0]) + assert len(meta.ple_bin_edges) == 5 + + +class TestCategoryMetadata: + def test_roundtrip(self): + meta = CategoryMetadata(idx2str=["a", "b", "c"], vocab_size=3) + d = meta.to_dict() + meta2 = CategoryMetadata.from_dict(d) + assert meta2.idx2str == ["a", "b", "c"] + assert meta2.vocab_size == 3 + + +class TestTextMetadata: + def test_defaults(self): + meta = TextMetadata() + assert meta.vocab_size == 0 + assert meta.pad_idx == 0 + + +class TestTrainingSetMetadata: + def test_dict_like_access(self): + meta = TrainingSetMetadata() + meta["feature1"] = {"mean": 5.0} + assert meta["feature1"] == {"mean": 5.0} + assert "feature1" in meta + + def test_get_with_default(self): + meta = TrainingSetMetadata() + assert meta.get("missing", "default") == "default" + + def test_from_dict(self): + d = { + "age": {"mean": 30.0, "std": 10.0}, + "income": {"idx2str": [">50K", "<=50K"]}, + "data_train_parquet_fp": "/path/to/train.parquet", + } + meta = TrainingSetMetadata.from_dict(d) + assert meta["age"]["mean"] == 30.0 + assert meta.data_train_parquet_fp == "/path/to/train.parquet" + + def test_to_dict(self): + meta = TrainingSetMetadata() + meta["age"] = {"mean": 30.0} + meta.data_train_parquet_fp = "/path/to/train.parquet" + d = meta.to_dict() + assert d["age"] == {"mean": 30.0} + assert d["data_train_parquet_fp"] == "/path/to/train.parquet" + + def test_skips_hdf5_paths(self): + d = {"data_train_hdf5_fp": "/old/path.hdf5", "feature1": {"mean": 1.0}} + meta = TrainingSetMetadata.from_dict(d) + assert meta.get("data_train_hdf5_fp") is None + assert meta["feature1"]["mean"] == 1.0 + + def test_keys_and_items(self): + meta = TrainingSetMetadata() + meta["f1"] = {"a": 1} + meta["f2"] = {"b": 2} + assert set(meta.keys()) == {"f1", "f2"} + assert len(list(meta.items())) == 2 diff --git a/tests/ludwig/datasets/__init__.py b/tests/ludwig/datasets/__init__.py new file mode 100644 index 0000000..58771ff --- /dev/null +++ b/tests/ludwig/datasets/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== diff --git a/tests/ludwig/datasets/download_all_datasets.py b/tests/ludwig/datasets/download_all_datasets.py new file mode 100644 index 0000000..dc5a666 --- /dev/null +++ b/tests/ludwig/datasets/download_all_datasets.py @@ -0,0 +1,28 @@ +#! /usr/bin/env python +# +# Lists and downloads all datasets, including Kaggle datasets, into ./download_datasets + +# You must have valid kaggle credentials in your environment, a few GB of disk space, and good internet bandwidth. +# Also, for each dataset associated with a Kaggle competition you'll need to sign in to Kaggle and accept the terms of +# the competition. +# +from ludwig import datasets + + +def download_all_datasets(): + """Downloads all datasets to ./downloaded_datasets.""" + dataset_names = datasets.list_datasets() + + print("Datasets: ") + for name in dataset_names: + print(f" {name}") + print("Downloading all datasets") + + # Download All Datasets + for dataset_name in dataset_names: + print(f"Downloading {dataset_name}") + datasets.download_dataset(dataset_name, "./downloaded_datasets") + + +if __name__ == "__main__": + download_all_datasets() diff --git a/tests/ludwig/datasets/mnist/test_mnist_workflow.py b/tests/ludwig/datasets/mnist/test_mnist_workflow.py new file mode 100644 index 0000000..0076433 --- /dev/null +++ b/tests/ludwig/datasets/mnist/test_mnist_workflow.py @@ -0,0 +1,65 @@ +import gzip +import os +import shutil +from unittest import mock + +import ludwig.datasets +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetState + + +def test_download_mnist_dataset(tmpdir): + train_image_archive_filename = os.path.join(tmpdir, "train-images-idx3-ubyte") + train_image_handle = open(train_image_archive_filename, "w+b") + train_image_handle.write(b"This binary string will be written as training mage data") + train_image_handle.close() + with open(train_image_archive_filename, "rb") as f_in: + with gzip.open(train_image_archive_filename + ".gz", "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + train_labels_archive_filename = os.path.join(tmpdir, "train-labels-idx1-ubyte") + train_labels_handle = open(train_labels_archive_filename, "w") + train_labels_handle.write("0") + train_labels_handle.close() + with open(train_labels_archive_filename, "rb") as f_in: + with gzip.open(train_labels_archive_filename + ".gz", "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + test_image_archive_filename = os.path.join(tmpdir, "t10k-images-idx3-ubyte") + test_image_handle = open(test_image_archive_filename, "w+b") + test_image_handle.write(b"This binary string will be written as test mage data") + test_image_handle.close() + with open(test_image_archive_filename, "rb") as f_in: + with gzip.open(test_image_archive_filename + ".gz", "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + test_labels_archive_filename = os.path.join(tmpdir, "t10k-labels-idx1-ubyte") + test_labels_handle = open(test_labels_archive_filename, "w") + test_labels_handle.write("0") + test_labels_handle.close() + with open(test_labels_archive_filename, "rb") as f_in: + with gzip.open(test_labels_archive_filename + ".gz", "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + + download_urls = [ + "file://" + train_image_archive_filename + ".gz", + "file://" + train_labels_archive_filename + ".gz", + "file://" + test_image_archive_filename + ".gz", + "file://" + test_labels_archive_filename + ".gz", + ] + + config = DatasetConfig( + version=1.0, + name="mnist", + download_urls=download_urls, + ) + + ludwig.datasets._get_dataset_configs.cache_clear() + with mock.patch("ludwig.datasets._load_dataset_config", return_value=config): + dataset = ludwig.datasets.get_dataset("mnist", cache_dir=tmpdir) + assert not dataset.state == DatasetState.DOWNLOADED + assert not dataset.state == DatasetState.TRANSFORMED + dataset.download() + + assert dataset.state == DatasetState.DOWNLOADED + ludwig.datasets._get_dataset_configs.cache_clear() diff --git a/tests/ludwig/datasets/model_configs/train_all_model_configs.py b/tests/ludwig/datasets/model_configs/train_all_model_configs.py new file mode 100644 index 0000000..590af3d --- /dev/null +++ b/tests/ludwig/datasets/model_configs/train_all_model_configs.py @@ -0,0 +1,148 @@ +#! /usr/bin/env python +# +# Trains a ludwig model for every dataset which has a default_model_config. +# You must have valid kaggle credentials in your environment, a few GB of disk space, and good internet bandwidth. +# Also, for each dataset associated with a Kaggle competition you'll need to sign in to Kaggle and accept the terms of +# the competition. +# +import multiprocessing +import time +from dataclasses import dataclass + +import pandas as pd + +from ludwig import datasets, visualize +from ludwig.api import LudwigModel +from ludwig.globals import LUDWIG_VERSION +from ludwig.utils.misc_utils import get_commit_hash + + +@dataclass +class TrainingResults: + """Results of a training run for a dataset.""" + + ludwig_version: str + ludwig_commit: str | None + dataset_version: str + dataset_name: str + has_config: bool + output_directory: str | None = None + splits: str | None = None + metric: str | None = None + performance: float | None = None + load_time: float | None = None + train_time: float | None = None + eval_time: float | None = None + + +def _train_dataset_process(dataset_name, results_queue): + """Runs each train job in a new process.""" + load_start_time = time.time() + dataset = datasets.get_dataset(dataset_name) + config = dataset.default_model_config + df = dataset.load() + load_end_time = time.time() + if "split" not in df: + df["split"] = 0 + available_splits = sorted(df.split.unique()) + results = TrainingResults( + LUDWIG_VERSION, + get_commit_hash(), + dataset.version, + dataset.name, + config is not None, + splits=" ".join([str(s) for s in available_splits]), + load_time=load_end_time - load_start_time, + ) + if config: + dataset.export(".") + print(f"Training {dataset_name}") + + # Train model on config + train_start_time = time.time() + model = LudwigModel(config) + train_stats, _, output_directory = model.train(dataset=df, model_name=dataset_name) + + # If dataset has a test split with labels, evaluate on test set. If not, evaluate on training set. + evaluate_start_time = time.time() + eval_stats, _, _ = model.evaluate( + df, + split=2 if 2 in available_splits else 0, + collect_predictions=False, + collect_overall_stats=True, + ) + evaluate_end_time = time.time() + + # Visualize learning curve + visualize.learning_curves([train_stats], model_names=[dataset_name], output_directory=output_directory) + + results.output_directory = output_directory + + # Get metric for first output feature + first_of_name = config["output_features"][0]["name"] + stats = eval_stats[first_of_name] + if "accuracy" in stats: + results.metric = "accuracy" + results.performance = stats["accuracy"] + elif "root_mean_squared_error" in stats: + results.metric = "root_mean_squared_error" + results.performance = stats["root_mean_squared_error"] + elif "mean_squared_error" in stats: + results.metric = "mean_squared_error" + results.performance = stats["mean_squared_error"] + elif "mean_absolute_error" in stats: + results.metric = "mean_absolute_error" + results.performance = stats["mean_absolute_error"] + elif "loss" in stats: + results.metric = "loss" + results.performance = stats["loss"] + results.train_time = evaluate_start_time - train_start_time + results.eval_time = evaluate_end_time - evaluate_start_time + print(f"Trained {dataset_name} in {evaluate_end_time - load_start_time:.2f} seconds") + results_queue.put(results) + + +def train_all_datasets(): + # Maps dataset name to current running process. + max_processes = 4 + running_processes = {} + accumulated_results = [] + # As each process completes it pushes its results onto the results_queue. + results_queue = multiprocessing.Queue() + for dataset_name in datasets.list_datasets(): + if len(running_processes) >= max_processes: + # Block until a subprocess completes + next_results = results_queue.get() + accumulated_results.append(next_results) + process = running_processes[next_results.dataset_name] + process.join() + del running_processes[next_results.dataset_name] + process = multiprocessing.Process(target=_train_dataset_process, args=[dataset_name, results_queue]) + running_processes[dataset_name] = process + process.start() + while len(running_processes) > 0: + if len(running_processes) < 4: + remaining_datasets = ", ".join(sorted(running_processes.keys())) + print(f"Finishing up, waiting for {len(running_processes)} to complete ({remaining_datasets})") + else: + print(f"Finishing up, waiting for {len(running_processes)} to complete") + # Block until a subprocess completes, clear it out, + next_results = results_queue.get() + accumulated_results.append(next_results) + process = running_processes[next_results.dataset_name] + process.join() + del running_processes[next_results.dataset_name] + results_df = pd.DataFrame(accumulated_results) + with pd.option_context( + "display.max_rows", None, "display.max_columns", None, "display.precision", 3, "display.width", 120 + ): + results_to_display = results_df[results_df["has_config"]].copy() + results_to_display = results_to_display.drop( + columns=["dataset_version", "output_directory", "ludwig_version", "ludwig_commit", "has_config"] + ) + print(results_to_display) + results_df.to_csv("train_all_model_configs_results.csv", index=False) + + +if __name__ == "__main__": + train_all_datasets() diff --git a/tests/ludwig/datasets/test_dataset_configs.py b/tests/ludwig/datasets/test_dataset_configs.py new file mode 100644 index 0000000..ca05cf8 --- /dev/null +++ b/tests/ludwig/datasets/test_dataset_configs.py @@ -0,0 +1,37 @@ +import ludwig.datasets +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetLoader +from tests.integration_tests.utils import private_test + + +@private_test +def test_get_config_and_load(tmpdir): + yosemite_config = ludwig.datasets._get_dataset_config("yosemite") + assert isinstance(yosemite_config, DatasetConfig) + + yosemite_dataset = ludwig.datasets.get_dataset("yosemite", cache_dir=tmpdir) + assert isinstance(yosemite_dataset, DatasetLoader) + df = yosemite_dataset.load() + assert df is not None + assert len(df) == 18721 # Expected number of rows in Yosemite temperatures dataset. + + # DISABLED: Flaky for tests, probably due to the dataset size. + # # Test loading dataset without 'split' and 'Unnamed: 0' columns in config. + # twitter_bots_config = ludwig.datasets._get_dataset_config("twitter_bots") + # assert isinstance(twitter_bots_config, DatasetConfig) + + # twitter_bots_dataset = ludwig.datasets.get_dataset("twitter_bots", cache_dir=tmpdir) + # assert isinstance(twitter_bots_dataset, DatasetLoader) + # df = twitter_bots_dataset.load() + # assert df is not None + # assert len(df.columns) == 22 # Expected number of columns in Twitter bots dataset including split column. + + +def test_get_config_kaggle(tmpdir): + twitter_bots_config = ludwig.datasets._get_dataset_config("twitter_bots") + assert isinstance(twitter_bots_config, DatasetConfig) + + twitter_bots_dataset = ludwig.datasets.get_dataset("twitter_bots", cache_dir=tmpdir) + # Twitter bots dataset is large, so we won't load it in this unit test. + assert isinstance(twitter_bots_dataset, DatasetLoader) + assert twitter_bots_dataset.is_kaggle_dataset diff --git a/tests/ludwig/datasets/test_dataset_links.py b/tests/ludwig/datasets/test_dataset_links.py new file mode 100644 index 0000000..a5f77ce --- /dev/null +++ b/tests/ludwig/datasets/test_dataset_links.py @@ -0,0 +1,49 @@ +#! /usr/bin/env python +# +# Checks all dataset download links (just those with URLs, not including kaggle datasets).""" +# +import logging +from concurrent.futures import as_completed, ThreadPoolExecutor + +import pytest +import requests + +import ludwig + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@pytest.mark.slow +def test_links(): + # Iterate through all datasets, ensure links are valid and reachable. + all_datasets = ludwig.datasets.list_datasets() + + tasks = {} + with ThreadPoolExecutor(max_workers=10) as executor: + for dataset_name in all_datasets: + config = ludwig.datasets._get_dataset_config(dataset_name) + download_urls = [config.download_urls] if isinstance(config.download_urls, str) else config.download_urls + for url in download_urls: + future = executor.submit(_check_url, dataset_name, url) + tasks[future] = (dataset_name, url) + + failures = [] + for future in as_completed(tasks): + dataset_name, url = tasks[future] + error = future.result() + if error: + failures.append(error) + + assert not failures, "Failed URLs:\n" + "\n".join(failures) + + +def _check_url(dataset_name, url): + logger.info(f"Checking {dataset_name}: {url}") + try: + response = requests.head(url, timeout=30) + if not response.ok: + return f"Failed to download {dataset_name} from {url} (status {response.status_code})" + except requests.RequestException as e: + return f"Failed to download {dataset_name} from {url} ({e})" + return None diff --git a/tests/ludwig/datasets/test_datasets.py b/tests/ludwig/datasets/test_datasets.py new file mode 100644 index 0000000..af8d3f5 --- /dev/null +++ b/tests/ludwig/datasets/test_datasets.py @@ -0,0 +1,252 @@ +import importlib +import importlib.util +import io +import os +import uuid +from unittest import mock + +import pandas as pd +import pytest + +import ludwig.datasets +from ludwig.api import LudwigModel +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetState +from tests.integration_tests.utils import private_test + +SUPPORTED_UNCOMPRESSED_FILETYPES = ["json", "jsonl", "tsv", "csv"] + + +def test_load_csv_dataset(tmpdir): + input_df = pd.DataFrame( + {"name": ["Raphael", "Donatello"], "mask": ["red", "purple"], "weapon": ["sai", "bo staff"], "split": [0, 1]} + ) + + extracted_filename = "input.csv" + compression_opts = dict(method="zip", archive_name=extracted_filename) + + archive_filename = os.path.join(tmpdir, "archive.zip") + input_df.to_csv(archive_filename, index=False, compression=compression_opts) + + config = DatasetConfig( + version=1.0, + name="fake_csv_dataset", + download_urls=["file://" + archive_filename], + ) + + ludwig.datasets._get_dataset_configs.cache_clear() + with mock.patch("ludwig.datasets._load_dataset_config", return_value=config): + dataset = ludwig.datasets.get_dataset("fake_csv_dataset", cache_dir=tmpdir) + + assert not dataset.state == DatasetState.DOWNLOADED + assert not dataset.state == DatasetState.TRANSFORMED + + output_df = dataset.load() + pd.testing.assert_frame_equal(input_df, output_df) + + assert dataset.state == DatasetState.TRANSFORMED + ludwig.datasets._get_dataset_configs.cache_clear() + + +@pytest.mark.parametrize("f_type", SUPPORTED_UNCOMPRESSED_FILETYPES) +def test_multifile_join_dataset(tmpdir, f_type): + if f_type != "jsonl": + train_df = pd.DataFrame( + {"name": ["Raphael", "Donatello"], "mask": ["red", "purple"], "weapon": ["sai", "bo staff"]} + ) + + test_df = pd.DataFrame({"name": ["Jack", "Bob"], "mask": ["green", "yellow"], "weapon": ["knife", "gun"]}) + + val_df = pd.DataFrame({"name": ["Tom"], "mask": ["pink"], "weapon": ["stick"]}) + else: + train_df = pd.DataFrame([{"name": "joe"}, {"mask": "green"}, {"weapon": "stick"}]) + test_df = pd.DataFrame([{"name": "janice"}, {"mask": "black"}, {"weapon": "gun"}]) + val_df = pd.DataFrame([{"name": "sara"}, {"mask": "pink"}, {"weapon": "gun"}]) + + # filetypes = ['json', 'tsv', 'jsonl'] + train_filename = "train." + f_type + test_filename = "test." + f_type + val_filename = "val." + f_type + train_filepath = os.path.join(tmpdir, train_filename) + test_filepath = os.path.join(tmpdir, test_filename) + val_filepath = os.path.join(tmpdir, val_filename) + + if f_type == "json": + train_df.to_json(train_filepath) + test_df.to_json(test_filepath) + val_df.to_json(val_filepath) + elif f_type == "jsonl": + train_df.to_json(train_filepath, orient="records", lines=True) + test_df.to_json(test_filepath, orient="records", lines=True) + val_df.to_json(val_filepath, orient="records", lines=True) + elif f_type == "tsv": + train_df.to_csv(train_filepath, sep="\t") + test_df.to_csv(test_filepath, sep="\t") + val_df.to_csv(val_filepath, sep="\t") + else: + train_df.to_csv(train_filepath) + test_df.to_csv(test_filepath) + val_df.to_csv(val_filepath) + + config = DatasetConfig( + version=1.0, + name="fake_multifile_dataset", + download_urls=["file://" + train_filepath, "file://" + test_filepath, "file://" + val_filepath], + train_filenames=train_filename, + validation_filenames=val_filename, + test_filenames=test_filename, + ) + + ludwig.datasets._get_dataset_configs.cache_clear() + with mock.patch("ludwig.datasets._load_dataset_config", return_value=config): + dataset = ludwig.datasets.get_dataset("fake_multifile_dataset", cache_dir=tmpdir) + + assert not dataset.state == DatasetState.DOWNLOADED + assert not dataset.state == DatasetState.TRANSFORMED + + output_df = dataset.load() + assert output_df.shape[0] == train_df.shape[0] + test_df.shape[0] + val_df.shape[0] + + assert dataset.state == DatasetState.TRANSFORMED + ludwig.datasets._get_dataset_configs.cache_clear() + + +@pytest.mark.parametrize( + "include_competitions,include_data_modalities", [(True, True), (True, False), (False, True), (False, False)] +) +def test_get_datasets_info(include_competitions, include_data_modalities): + dataset_output_features = ludwig.datasets.get_datasets_output_features( + include_competitions=include_competitions, include_data_modalities=include_data_modalities + ) + + assert len(dataset_output_features) > 1 + assert isinstance(dataset_output_features, dict) + assert dataset_output_features["twitter_bots"].get("name", None) + assert dataset_output_features["twitter_bots"].get("output_features", None) + assert isinstance(dataset_output_features["twitter_bots"]["output_features"], list) + assert dataset_output_features["twitter_bots"]["output_features"][0].get("name", None) + assert dataset_output_features["twitter_bots"]["output_features"][0].get("type", None) + + if include_competitions: + assert dataset_output_features["titanic"].get("name", None) + else: + assert dataset_output_features.get("titanic", None) is None + + if include_data_modalities: + data_modalities = dataset_output_features["twitter_bots"].get("data_modalities", None) + assert data_modalities + assert len(data_modalities) >= 1 + else: + assert dataset_output_features["twitter_bots"].get("data_modalities", None) is None + + dataset_output_features = ludwig.datasets.get_datasets_output_features(dataset="twitter_bots") + assert len(dataset_output_features["output_features"]) == 1 + assert dataset_output_features["name"] == "twitter_bots" + + +def test_get_dataset_buffer(): + buffer = ludwig.datasets.get_buffer("iris") + + assert isinstance(buffer, io.BytesIO) + + +def test_train_dataset_uri(tmpdir): + input_df = pd.DataFrame( + { + "input": ["a", "b", "a", "b", "a", "b", "c", "c", "a", "b"], + "output": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "split": [0, 0, 0, 0, 0, 0, 0, 1, 2, 2], + } + ) + + extracted_filename = "input.csv" + compression_opts = dict(method="zip", archive_name=extracted_filename) + + archive_filename = os.path.join(tmpdir, "archive.zip") + input_df.to_csv(archive_filename, index=False, compression=compression_opts) + + dataset_name = f"fake_csv_dataset_{uuid.uuid4().hex}" + config = DatasetConfig( + version=1.0, + name=dataset_name, + download_urls=["file://" + archive_filename], + ) + + model_config = { + "input_features": [{"name": "input", "type": "category"}], + "output_features": [{"name": "output", "type": "number"}], + "preprocessing": {"split": {"type": "fixed"}}, + "combiner": {"type": "concat", "fc_size": 14}, + "trainer": {"batch_size": 8, "epochs": 1}, + } + + ludwig.datasets._get_dataset_configs.cache_clear() + with mock.patch("ludwig.datasets._load_dataset_config", return_value=config): + with mock.patch("ludwig.datasets.loaders.dataset_loader.get_default_cache_location", return_value=str(tmpdir)): + model = LudwigModel(model_config, backend="local") + + results = model.train(dataset=f"ludwig://{dataset_name}") + proc_result = results.preprocessed_data + train_df1 = proc_result.training_set.to_df() + val_df1 = proc_result.validation_set.to_df() + test_df1 = proc_result.test_set.to_df() + + assert len(train_df1) == 7 + assert len(val_df1) == 1 + assert len(test_df1) == 2 + + results = model.train( + training_set=f"ludwig://{dataset_name}", + validation_set=f"ludwig://{dataset_name}", + test_set=f"ludwig://{dataset_name}", + ) + proc_result_split = results.preprocessed_data + train_df2 = proc_result_split.training_set.to_df() + val_df2 = proc_result_split.validation_set.to_df() + test_df2 = proc_result_split.test_set.to_df() + + assert len(train_df2) == 7 + assert len(val_df2) == 1 + assert len(test_df2) == 2 + + sort_col = train_df1.columns[-1] + + def sort_df(df): + return df.sort_values(by=[sort_col]).reset_index(drop=True) + + assert sort_df(train_df1).equals(sort_df(train_df2)) + assert sort_df(val_df1).equals(sort_df(val_df2)) + assert sort_df(test_df1).equals(sort_df(test_df2)) + + ludwig.datasets._get_dataset_configs.cache_clear() + + +@private_test +@pytest.mark.parametrize("dataset_name, size", [("code_alpaca", 20000), ("consumer_complaints", 38000)]) +def test_ad_hoc_dataset_download(tmpdir, dataset_name, size): + dataset_config = ludwig.datasets._get_dataset_config(dataset_name) + assert isinstance(dataset_config, DatasetConfig) + + ludwig_dataset = ludwig.datasets.get_dataset(dataset_name, cache_dir=tmpdir) + df = ludwig_dataset.load() + assert df is not None + assert len(df) >= size + + +@pytest.mark.skipif(not importlib.util.find_spec("datasets"), reason="huggingface datasets not installed") +@pytest.mark.xfail(reason="HuggingFace datasets library no longer supports loading datasets via scripts") +def test_hf_dataset_loading(): + import datasets + + loader = ludwig.datasets.get_dataset("hugging_face") + data = loader.load("JeremyAlain/123_test", "data_0") + hf_data = datasets.load_dataset(path="JeremyAlain/123_test", name="data_0") + + assert len(data) == hf_data["train"].num_rows + + train, val, test = loader.load("neil-code/dialogsum-test", None, split=True) + hf_data = datasets.load_dataset(path="neil-code/dialogsum-test") + + assert len(train) == hf_data["train"].num_rows + assert len(val) == hf_data["validation"].num_rows + assert len(test) == hf_data["test"].num_rows diff --git a/tests/ludwig/datasets/test_model_configs.py b/tests/ludwig/datasets/test_model_configs.py new file mode 100644 index 0000000..d050f45 --- /dev/null +++ b/tests/ludwig/datasets/test_model_configs.py @@ -0,0 +1,31 @@ +import ludwig.datasets + + +def test_default_model_config(tmpdir): + titanic_configs = ludwig.datasets.model_configs_for_dataset("titanic") + assert len(titanic_configs) > 0 + + titanic = ludwig.datasets.get_dataset("titanic", cache_dir=tmpdir) + assert titanic.default_model_config is not None + + assert titanic.default_model_config == titanic_configs["default"] + + +def test_best_model_config(tmpdir): + higgs_configs = ludwig.datasets.model_configs_for_dataset("higgs") + assert len(higgs_configs) > 0 + + higgs = ludwig.datasets.get_dataset("higgs", cache_dir=tmpdir) + assert higgs.default_model_config is not None + assert higgs.best_model_config is not None + + assert higgs.default_model_config == higgs_configs["default"] + assert higgs.best_model_config == higgs_configs["best"] + + +def test_dataset_has_no_model_configs(tmpdir): + bbc_news_configs = ludwig.datasets.model_configs_for_dataset("bbcnews") + assert len(bbc_news_configs) == 0 + + bbcnews = ludwig.datasets.get_dataset("bbcnews", cache_dir=tmpdir) + assert bbcnews.default_model_config is None diff --git a/tests/ludwig/datasets/test_smoke_regression.py b/tests/ludwig/datasets/test_smoke_regression.py new file mode 100644 index 0000000..1612bf1 --- /dev/null +++ b/tests/ludwig/datasets/test_smoke_regression.py @@ -0,0 +1,377 @@ +"""Regression tests for issues discovered during the Ludwig dataset smoke-test campaign. + +Each test is a direct regression guard for a specific root-cause failure that was +found, fixed, and must never regress. The test names describe the exact symptom. + +Issues covered +-------------- +1. select_columns breaks TranslationLoader (custom loaders read intermediate columns) +2. TranslationLoader splits a nested dict column into per-language flat columns +3. Europarl language-pair loaders exist for all supported pairs +4. MASSIVE intent/scenario dataset IDs use lowercase snake_case, not CamelCase +5. MTOP domain/intent supports all 6 expected languages +6. MassiveScenarioClassification does not exist; correct ID is massive_scenario +7. Dataset configs: germeval18 uses column "binary", not "label" +8. STS17 cross-lingual subsample order is "en-de", not "de-en" +9. smoke_results.json cleanup: stale entries for deleted configs are removed +""" + +from __future__ import annotations + +import glob +import json +import os + +import pandas as pd +import pytest +import yaml + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +CONFIGS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "..", "ludwig", "datasets", "configs") +RESULTS_FILE = os.path.join(os.path.dirname(__file__), "..", "..", "..", "scripts", "smoke_results.json") + + +def load_config(name: str) -> dict: + path = os.path.join(CONFIGS_DIR, f"{name}.yaml") + with open(path) as f: + return yaml.safe_load(f) + + +def all_config_names() -> set[str]: + return {os.path.basename(f)[:-5] for f in glob.glob(os.path.join(CONFIGS_DIR, "*.yaml"))} + + +# --------------------------------------------------------------------------- +# 1. select_columns must be disabled for custom loaders +# --------------------------------------------------------------------------- + + +class TestSelectColumnsCustomLoader: + """Regression: select_columns=[de, en] was applied BEFORE TranslationLoader + ran, stripping the 'translation' dict column and producing an empty DataFrame. + The fix detects has_custom_loader and disables select_columns for them. + """ + + def test_has_custom_loader_flag_for_translation_datasets(self): + """Every config with a non-HFLoader loader must be detectable as custom.""" + for name in all_config_names(): + cfg = load_config(name) + loader_spec = cfg.get("loader", "") + has_custom = bool(loader_spec and loader_spec != "hugging_face.HFLoader") + if has_custom: + # The loader must reference a real class in translation_loader or misc_loaders + assert "." in loader_spec, f"{name}: loader spec must be module.ClassName, got {loader_spec!r}" + + def test_wmt_t2t_de_en_uses_translation_loader(self): + cfg = load_config("wmt_t2t_de_en") + assert cfg["loader"] != "hugging_face.HFLoader", "wmt_t2t_de_en must use a TranslationLoader subclass" + + def test_europarl_configs_use_translation_loader(self): + europarl_names = [n for n in all_config_names() if n.startswith("europarl_")] + assert len(europarl_names) >= 13, f"Expected ≥13 europarl configs, found {len(europarl_names)}" + for name in europarl_names: + cfg = load_config(name) + loader = cfg.get("loader", "") + assert "TranslationLoader" in loader or "Loader" in loader, ( + f"{name}: expected a TranslationLoader subclass, got {loader!r}" + ) + + +# --------------------------------------------------------------------------- +# 2. TranslationLoader: verify the loader class hierarchy +# --------------------------------------------------------------------------- + + +@pytest.mark.skipif( + not __import__("importlib").util.find_spec("torch"), + reason="torch not installed", +) +class TestTranslationLoaderClasses: + def test_europarl_loader_classes_importable(self): + from ludwig.datasets.loaders.translation_loader import ( + EuroparlBgEnLoader, + EuroparlCsEnLoader, + EuroparlDaEnLoader, + EuroparlDeEnLoader, + EuroparlElEnLoader, + EuroparlEnEsLoader, + EuroparlEnFrLoader, + EuroparlEnItLoader, + EuroparlEnNlLoader, + EuroparlEnPlLoader, + EuroparlEnPtLoader, + EuroparlEnRoLoader, + EuroparlEnSvLoader, + TranslationLoader, + ) + + for cls in [ + EuroparlBgEnLoader, + EuroparlCsEnLoader, + EuroparlDaEnLoader, + EuroparlDeEnLoader, + EuroparlElEnLoader, + EuroparlEnEsLoader, + EuroparlEnFrLoader, + EuroparlEnItLoader, + EuroparlEnNlLoader, + EuroparlEnPlLoader, + EuroparlEnPtLoader, + EuroparlEnRoLoader, + EuroparlEnSvLoader, + ]: + assert issubclass(cls, TranslationLoader), f"{cls.__name__} must be a TranslationLoader subclass" + + def test_translation_loader_has_source_and_target_lang(self): + from ludwig.datasets.loaders.translation_loader import EuroparlDeEnLoader + + assert hasattr(EuroparlDeEnLoader, "source_lang") + assert hasattr(EuroparlDeEnLoader, "target_lang") + assert EuroparlDeEnLoader.source_lang == "de" + assert EuroparlDeEnLoader.target_lang == "en" + + def test_translation_loader_transform_expands_nested_dict(self): + """TranslationLoader._transform must split a nested translation dict into flat columns.""" + from ludwig.datasets.loaders.translation_loader import EuroparlDeEnLoader + + raw = pd.DataFrame({"translation": [{"de": "Hallo", "en": "Hello"}, {"de": "Welt", "en": "World"}]}) + + import tempfile + + with tempfile.TemporaryDirectory() as tmpdir: + PatchedCls = type( + "_TestEuroparlDeEnLoader", + (EuroparlDeEnLoader,), + {"processed_dataset_dir": property(lambda self, d=tmpdir: d)}, + ) + instance = object.__new__(PatchedCls) + result = instance._transform(raw) + + assert "de" in result.columns, f"Expected 'de' column after transform, got {list(result.columns)}" + assert "en" in result.columns, f"Expected 'en' column after transform, got {list(result.columns)}" + assert list(result["de"]) == ["Hallo", "Welt"] + assert list(result["en"]) == ["Hello", "World"] + + +# --------------------------------------------------------------------------- +# 3. Europarl configs — one config per language pair +# --------------------------------------------------------------------------- + + +class TestEuroparlConfigs: + EXPECTED_PAIRS = [ + "europarl_bg_en", + "europarl_cs_en", + "europarl_da_en", + "europarl_de_en", + "europarl_el_en", + "europarl_en_es", + "europarl_en_fr", + "europarl_en_it", + "europarl_en_nl", + "europarl_en_pl", + "europarl_en_pt", + "europarl_en_ro", + "europarl_en_sv", + ] + + def test_all_expected_europarl_configs_exist(self): + existing = all_config_names() + for name in self.EXPECTED_PAIRS: + assert name in existing, f"Missing Europarl config: {name}" + + def test_europarl_configs_have_two_text_output_columns(self): + for name in self.EXPECTED_PAIRS: + cfg = load_config(name) + col_names = [c["name"] for c in cfg.get("columns", [])] + out_names = [f["name"] for f in cfg.get("output_features", [])] + assert len(col_names) >= 2, f"{name}: expected ≥2 columns" + assert len(out_names) == 1, f"{name}: expected 1 output feature" + + +# --------------------------------------------------------------------------- +# 4 & 6. MASSIVE dataset ID correctness +# --------------------------------------------------------------------------- + + +class TestMassiveDatasetIDs: + def test_massive_intent_configs_use_correct_dataset_id(self): + """Regression: new configs incorrectly used 'mteb/MassiveIntentClassification'.""" + intent_configs = [n for n in all_config_names() if n.startswith("mteb_massive_intent_")] + assert len(intent_configs) >= 5, "Expected at least 5 MASSIVE intent configs" + for name in intent_configs: + cfg = load_config(name) + assert cfg["huggingface_dataset_id"] == "mteb/massive_intent", ( + f"{name}: wrong dataset ID {cfg['huggingface_dataset_id']!r}; " + "must be 'mteb/massive_intent' (lowercase snake_case)" + ) + + def test_massive_scenario_configs_use_correct_dataset_id(self): + """Regression: 'mteb/MassiveScenarioClassification' does not exist on the Hub.""" + scenario_configs = [n for n in all_config_names() if n.startswith("mteb_massive_scenario_")] + assert len(scenario_configs) >= 5, "Expected at least 5 MASSIVE scenario configs" + for name in scenario_configs: + cfg = load_config(name) + assert cfg["huggingface_dataset_id"] == "mteb/massive_scenario", ( + f"{name}: wrong dataset ID {cfg['huggingface_dataset_id']!r}; " + "must be 'mteb/massive_scenario' (lowercase snake_case)" + ) + + def test_massive_intent_covers_51_languages(self): + intent_configs = [n for n in all_config_names() if n.startswith("mteb_massive_intent_")] + assert len(intent_configs) == 51, f"Expected 51 MASSIVE intent configs, got {len(intent_configs)}" + + def test_massive_scenario_covers_51_languages(self): + scenario_configs = [n for n in all_config_names() if n.startswith("mteb_massive_scenario_")] + assert len(scenario_configs) == 51, f"Expected 51 MASSIVE scenario configs, got {len(scenario_configs)}" + + +# --------------------------------------------------------------------------- +# 5. MTOP domain/intent: all 6 languages present +# --------------------------------------------------------------------------- + + +class TestMTOPConfigs: + LANGUAGES = ["en", "de", "fr", "es", "hi", "th"] + + def test_mtop_domain_all_languages(self): + for lang in self.LANGUAGES: + name = f"mteb_mtop_domain_{lang}" + assert name in all_config_names(), f"Missing MTOP domain config: {name}" + cfg = load_config(name) + assert cfg["huggingface_dataset_id"] == "mteb/MTOPDomainClassification" + assert cfg["huggingface_subsample"] == lang + + def test_mtop_intent_all_languages(self): + intent_cfgs = { + "en": "mteb_mtop_intent_en", + "de": "mteb_mtop_intent_de2", + "fr": "mteb_mtop_intent_fr2", + "es": "mteb_mtop_intent_es2", + "hi": "mteb_mtop_intent_hi2", + "th": "mteb_mtop_intent_th2", + } + for lang, cfg_name in intent_cfgs.items(): + assert cfg_name in all_config_names(), f"Missing MTOP intent config: {cfg_name}" + cfg = load_config(cfg_name) + assert cfg["huggingface_dataset_id"] == "mteb/MTOPIntentClassification" + assert cfg["huggingface_subsample"] == lang + + +# --------------------------------------------------------------------------- +# 7. germeval18: output column is "binary", not "label" +# --------------------------------------------------------------------------- + + +class TestGermeval18Config: + def test_output_column_is_binary(self): + cfg = load_config("germeval18") + out_cols = {f["name"] for f in cfg.get("output_features", [])} + assert "binary" in out_cols, ( + "germeval18 must use column 'binary' as output (philschmid/germeval18 has no 'label' column)" + ) + assert "label" not in out_cols, "germeval18 must not reference 'label' — that column does not exist" + + def test_germeval18_uses_philschmid_dataset(self): + cfg = load_config("germeval18") + assert "philschmid" in cfg.get("huggingface_dataset_id", ""), "germeval18 must use philschmid/germeval18" + + +# --------------------------------------------------------------------------- +# 8. STS17: subsample order is "en-de", not "de-en" +# --------------------------------------------------------------------------- + + +class TestSTS17SubsampleOrder: + def test_sts17_de_uses_en_de_subsample(self): + cfg = load_config("mteb_sts17_de") + subsample = cfg.get("huggingface_subsample", "") + assert subsample == "en-de", f"mteb_sts17_de must use subsample 'en-de' (not 'de-en'); got {subsample!r}" + + def test_sts17_es_uses_es_en_subsample(self): + cfg = load_config("mteb_sts17_es") + subsample = cfg.get("huggingface_subsample", "") + # es-en or en-es — just confirm it's not plain "es" or "en" + assert "-" in subsample, f"mteb_sts17_es subsample must be a language pair, got {subsample!r}" + + +# --------------------------------------------------------------------------- +# 9. smoke_results.json: no stale entries for deleted configs +# --------------------------------------------------------------------------- + + +class TestSmokeResultsConsistency: + def test_no_stale_results_for_missing_configs(self): + if not os.path.exists(RESULTS_FILE): + pytest.skip("smoke_results.json not found") + + with open(RESULTS_FILE) as f: + results = json.load(f) + + existing_configs = all_config_names() + stale = [r["name"] for r in results if r["name"] not in existing_configs] + assert stale == [], ( + f"smoke_results.json contains {len(stale)} stale entries for configs that no longer exist: {stale[:10]}" + ) + + def test_all_results_have_required_fields(self): + if not os.path.exists(RESULTS_FILE): + pytest.skip("smoke_results.json not found") + + with open(RESULTS_FILE) as f: + results = json.load(f) + + for entry in results: + assert "name" in entry, f"Result entry missing 'name': {entry}" + assert "status" in entry, f"Result entry for {entry.get('name')} missing 'status'" + + +# --------------------------------------------------------------------------- +# General config schema validation +# --------------------------------------------------------------------------- + + +class TestConfigSchema: + def test_all_configs_have_required_fields(self): + for name in all_config_names(): + if name in self._NON_DATASET_CONFIGS: + continue + cfg = load_config(name) + assert "version" in cfg, f"{name}: missing 'version'" + assert "name" in cfg, f"{name}: missing 'name'" + assert cfg["name"] == name, f"{name}: 'name' field {cfg['name']!r} does not match filename" + + # Loader-reference stubs that are not actual dataset configs + _NON_DATASET_CONFIGS = {"hugging_face"} + + def test_hf_configs_have_dataset_id(self): + for name in all_config_names(): + if name in self._NON_DATASET_CONFIGS: + continue + cfg = load_config(name) + loader = cfg.get("loader", "hugging_face.HFLoader") + # Configs using direct download or Kaggle are not HF configs + uses_direct_download = bool( + cfg.get("download_urls") or cfg.get("kaggle_dataset_id") or cfg.get("kaggle_competition") + ) + if uses_direct_download: + continue + if "HFLoader" in loader or not loader: + has_id = bool(cfg.get("huggingface_dataset_id")) + has_legacy_id = bool(cfg.get("hf_dataset_id")) # legacy field name + assert has_id or has_legacy_id, f"{name}: HF loader config missing 'huggingface_dataset_id'" + + def test_output_features_reference_declared_columns(self): + for name in all_config_names(): + cfg = load_config(name) + col_names = {c["name"] for c in cfg.get("columns", [])} + if not col_names: + continue # some legacy configs have no columns list + for out_feat in cfg.get("output_features", []): + out_name = out_feat["name"] + assert out_name in col_names, ( + f"{name}: output feature '{out_name}' not in declared columns {sorted(col_names)}" + ) diff --git a/tests/ludwig/datasets/titanic/test_titanic_workflow.py b/tests/ludwig/datasets/titanic/test_titanic_workflow.py new file mode 100644 index 0000000..e1b1580 --- /dev/null +++ b/tests/ludwig/datasets/titanic/test_titanic_workflow.py @@ -0,0 +1,110 @@ +import os +import zipfile +from shutil import copy +from unittest import mock + +import pandas as pd + +import ludwig.datasets +from ludwig.datasets.dataset_config import DatasetConfig +from ludwig.datasets.loaders.dataset_loader import DatasetState + + +def test_download_titanic_dataset(tmpdir): + titanic_train_df = pd.DataFrame( + { + "passenger_id": [1216, 699, 234], + "pclass": [3, 3, 4], + "name": ["sai bo", "bo staff", "tae kwan nic"], + "sex": ["female", "male", "male"], + "age": [38, 28, 18], + "sibsp": [0, 1, 0], + "parch": [1, 1, 2], + "ticket": [335432, 315089, 322472], + "fare": [7.7333, 8.6625, 9.8765], + "cabin": [1, 2, 4], + "embarked": ["C", "Q", "S"], + "boat": [0, 0, 0], + "body": [0, 1, 0], + "home.dest": ["Croatia", "Italy", "Sweden"], + "survived": [0, 1, 0], + } + ) + + titanic_test_df = pd.DataFrame( + { + "passenger_id": [1216, 699, 234], + "pclass": [3, 3, 4], + "name": ["mo bo", "bo bo bo", "Rafael Nadal"], + "sex": ["female", "male", "male"], + "age": [28, 18, 30], + "sibsp": [0, 1, 0], + "parch": [1, 1, 2], + "ticket": [335412, 215089, 922472], + "fare": [17.7333, 18.6625, 19.8765], + "cabin": [2, 2, 1], + "embarked": ["Q", "Q", "C"], + "boat": [0, 0, 0], + "body": [0, 1, 0], + "home.dest": ["Sweden", "Slovenia", "Italy"], + "survived": [0, 1, 0], + } + ) + + train_fname = os.path.join(tmpdir, "train.csv") + titanic_train_df.to_csv(train_fname, index=False) + + test_fname = os.path.join(tmpdir, "test.csv") + titanic_test_df.to_csv(test_fname, index=False) + + archive_filename = os.path.join(tmpdir, "titanic.zip") + with zipfile.ZipFile(archive_filename, "w") as z: + z.write(train_fname, "train.csv") + z.write(test_fname, "test.csv") + + config = DatasetConfig( + version=1.0, + name="titanic", + kaggle_competition="titanic", + archive_filenames="titanic.zip", + # Normally we would verify the zip file, but in this test the zip file is created every time and contains the + # creation dates of the csv files so its digest will be different every time the test is run. + sha256={ + "test.csv": "348c49a95fe099fcc3b9142c82fb6becb87edc0f4d2c69c485e0dce4af8625e0", + "train.csv": "483556c465414fd78deb02b25f39a0de844b0728c1ef0505df0e5b3e40fec995", + }, + train_filenames="train.csv", + test_filenames="test.csv", + ) + + def download_files(competition_name, path): + assert competition_name == "titanic" + copy(archive_filename, path) + + ludwig.datasets._get_dataset_configs.cache_clear() + with mock.patch("ludwig.datasets._load_dataset_config", return_value=config): + with mock.patch("ludwig.datasets.kaggle.create_kaggle_client") as mock_kaggle_cls: + mock_kaggle_api = mock.MagicMock() + mock_kaggle_api.competition_download_files = download_files + mock_kaggle_cls.return_value = mock_kaggle_api + + dataset = ludwig.datasets.get_dataset("titanic", cache_dir=tmpdir) + assert not dataset.state == DatasetState.DOWNLOADED + + dataset.download() + assert dataset.state == DatasetState.DOWNLOADED + mock_kaggle_api.authenticate.assert_called_once() + + assert not dataset.state == DatasetState.TRANSFORMED + dataset.extract() + # Normally we would verify before extracting, but in this test the zip file is created on each run and + # changes between test runs. Instead we verify the extracted .csv files. + dataset.verify() + dataset.transform() + assert dataset.state == DatasetState.TRANSFORMED + + output_train_df, output_test_df, output_val_df = dataset.load(split=True) + assert len(output_train_df) == len(titanic_train_df) + assert len(output_test_df) == len(titanic_test_df) + assert len(output_val_df) == 0 + ludwig.datasets._get_dataset_configs.cache_clear() diff --git a/tests/ludwig/decoders/test_image_decoder.py b/tests/ludwig/decoders/test_image_decoder.py new file mode 100644 index 0000000..2f48591 --- /dev/null +++ b/tests/ludwig/decoders/test_image_decoder.py @@ -0,0 +1,41 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, HIDDEN, LOGITS +from ludwig.decoders.image_decoders import UNetDecoder +from ludwig.encoders.image.base import UNetEncoder +from ludwig.utils.misc_utils import set_random_seed +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("height,width,num_channels,num_classes", [(224, 224, 1, 2), (224, 224, 3, 8)]) +@pytest.mark.parametrize("batch_size", [4, 1]) +def test_unet_decoder(height, width, num_channels, num_classes, batch_size): + # make repeatable + set_random_seed(RANDOM_SEED) + + unet_encoder = UNetEncoder(height=height, width=width, num_channels=num_channels) + inputs = torch.rand(batch_size, num_channels, height, width) + encoder_outputs = unet_encoder(inputs) + assert encoder_outputs[ENCODER_OUTPUT].shape[1:] == unet_encoder.output_shape + assert len(encoder_outputs[ENCODER_OUTPUT_STATE]) == 4 + + hidden = torch.reshape(encoder_outputs[ENCODER_OUTPUT], [batch_size, -1]) + + unet_decoder = UNetDecoder(hidden.size(dim=1), height, width, 1, num_classes) + combiner_outputs = { + HIDDEN: hidden, + ENCODER_OUTPUT_STATE: encoder_outputs[ENCODER_OUTPUT_STATE].copy(), # create a copy + } + + output = unet_decoder(combiner_outputs, target=None) + + assert list(output[LOGITS].size()) == [batch_size, num_classes, height, width] + + # check for parameter updating + target = torch.randn(output[LOGITS].shape) + combiner_outputs[ENCODER_OUTPUT_STATE] = encoder_outputs[ENCODER_OUTPUT_STATE] # restore state + fpc, tpc, upc, not_updated = check_module_parameters_updated(unet_decoder, (combiner_outputs, None), target) + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/decoders/test_llm_decoders.py b/tests/ludwig/decoders/test_llm_decoders.py new file mode 100644 index 0000000..6408efd --- /dev/null +++ b/tests/ludwig/decoders/test_llm_decoders.py @@ -0,0 +1,69 @@ +import pytest +import torch + +from ludwig.constants import BACKEND, BASE_MODEL, GENERATION, INPUT_FEATURES, MODEL_TYPE, OUTPUT_FEATURES +from ludwig.decoders.llm_decoders import TextExtractorDecoder +from ludwig.schema.model_config import ModelConfig +from tests.integration_tests.utils import text_feature + +TEST_MODEL_NAME = "hf-internal-testing/tiny-random-GPTJForCausalLM" + + +def test_text_extractor_decoder(): + max_new_tokens = 4 + + input_features = [ + { + "name": "Question", + "type": "text", + "encoder": {"type": "passthrough"}, + } + ] + output_features = [text_feature(output_feature=True, name="Answer", decoder={"type": "text_extractor"})] + + config = { + MODEL_TYPE: "llm", + BASE_MODEL: TEST_MODEL_NAME, + GENERATION: { + "temperature": 0.1, + "top_p": 0.75, + "top_k": 40, + "num_beams": 4, + "max_new_tokens": max_new_tokens, + }, + INPUT_FEATURES: input_features, + OUTPUT_FEATURES: output_features, + BACKEND: "local", + } + + config = ModelConfig.from_dict(config) + decoder_config = config.output_features[0].decoder + + decoder = TextExtractorDecoder(32, decoder_config) + + inputs = [ + torch.tensor([1, 1, 1, 2, 2, 2, 2]), # baseline + torch.tensor([1, 1, 1, 2]), # too short; test padding + torch.tensor([1, 1, 1, 1, 2, 2, 2]), # test different input length + ] + input_lengths = [3, 3, 4] + + # tests happy path + outputs = decoder.forward(inputs, input_lengths, max_new_tokens) + assert outputs["predictions"].shape == (3, max_new_tokens) + # Create a Boolean mask for elements equal to 0 or 2 (padding or output) + mask = (outputs["predictions"] == 0) | (outputs["predictions"] == 2) + assert mask.all() + + # test overly long generation fails without updated max_new_tokens + inputs.append(torch.tensor([1, 1, 1, 2, 2, 2, 2, 2])) # too long; test downstream failure) + input_lengths.append(3) + with pytest.raises(ValueError): + outputs = decoder.forward(inputs, input_lengths, max_new_tokens) + + # test overly long generation succeeds with new max_new_tokens + new_max_new_tokens = 5 + outputs = decoder.forward(inputs, input_lengths, new_max_new_tokens) + assert outputs["predictions"].shape == (4, new_max_new_tokens) + mask = (outputs["predictions"] == 0) | (outputs["predictions"] == 2) + assert mask.all() diff --git a/tests/ludwig/decoders/test_sequence_decoder.py b/tests/ludwig/decoders/test_sequence_decoder.py new file mode 100644 index 0000000..6abfe66 --- /dev/null +++ b/tests/ludwig/decoders/test_sequence_decoder.py @@ -0,0 +1,133 @@ +import pytest +import torch + +from ludwig.constants import HIDDEN, LOGITS +from ludwig.decoders.sequence_decoders import ( + LSTMDecoder, + RNNDecoder, + SequenceGeneratorDecoder, + SequenceLSTMDecoder, + SequenceRNNDecoder, +) +from ludwig.utils.misc_utils import set_random_seed +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("cell_type", ["rnn", "gru"]) +@pytest.mark.parametrize("num_layers", [1, 2]) +@pytest.mark.parametrize("batch_size", [20, 1]) +def test_rnn_decoder(cell_type, num_layers, batch_size): + hidden_size = 256 + vocab_size = 50 + + input = torch.randint(vocab_size, size=(batch_size,)) + initial_hidden = torch.zeros(num_layers, batch_size, hidden_size) + rnn_decoder = RNNDecoder(hidden_size, vocab_size, cell_type, num_layers=num_layers) + + output = rnn_decoder(input, initial_hidden) + + assert len(output) == 2 + assert list(output[0].size()) == [batch_size, 1, vocab_size] + assert list(output[1].size()) == [num_layers, batch_size, hidden_size] + + +@pytest.mark.parametrize("num_layers", [1, 2]) +@pytest.mark.parametrize("batch_size", [20, 1]) +def test_lstm_decoder(num_layers, batch_size): + hidden_size = 256 + vocab_size = 50 + + input = torch.randint(vocab_size, size=(batch_size,)) + initial_hidden = torch.zeros(num_layers, batch_size, hidden_size) + initial_cell_state = torch.zeros(num_layers, batch_size, hidden_size) + lstm_decoder = LSTMDecoder(hidden_size, vocab_size, num_layers=num_layers) + + output = lstm_decoder(input, initial_hidden, initial_cell_state) + + assert len(output) == 3 + assert list(output[0].size()) == [batch_size, 1, vocab_size] + assert list(output[1].size()) == [num_layers, batch_size, hidden_size] + assert list(output[2].size()) == [num_layers, batch_size, hidden_size] + + +@pytest.mark.parametrize("cell_type", ["rnn", "gru"]) +@pytest.mark.parametrize("num_layers", [1, 2]) +@pytest.mark.parametrize("batch_size", [20, 1]) +def test_sequence_rnn_decoder(cell_type, num_layers, batch_size): + hidden_size = 256 + vocab_size = 50 + max_sequence_length = 10 + + # make repeatable + set_random_seed(RANDOM_SEED) + + combiner_outputs = {HIDDEN: torch.rand([batch_size, hidden_size])} + sequence_rnn_decoder = SequenceRNNDecoder( + hidden_size, vocab_size, max_sequence_length, cell_type, num_layers=num_layers + ) + + output = sequence_rnn_decoder(combiner_outputs, target=None) + + assert list(output.size()) == [batch_size, max_sequence_length, vocab_size] + + # check for parameter updating + target = torch.randn(output.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(sequence_rnn_decoder, (combiner_outputs, None), target) + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize("num_layers", [1, 2]) +@pytest.mark.parametrize("batch_size", [20, 1]) +def test_sequence_lstm_decoder(num_layers, batch_size): + hidden_size = 256 + vocab_size = 50 + max_sequence_length = 10 + + # make repeatable + set_random_seed(RANDOM_SEED) + + combiner_outputs = {HIDDEN: torch.rand([batch_size, hidden_size])} + sequence_lstm_decoder = SequenceLSTMDecoder(hidden_size, vocab_size, max_sequence_length, num_layers=num_layers) + + output = sequence_lstm_decoder(combiner_outputs, target=None) + + assert list(output.size()) == [batch_size, max_sequence_length, vocab_size] + + # check for parameter updating + target = torch.randn(output.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + sequence_lstm_decoder, (combiner_outputs, None), target + ) + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +@pytest.mark.parametrize("cell_type", ["rnn", "gru", "lstm"]) +@pytest.mark.parametrize("num_layers", [1, 2]) +@pytest.mark.parametrize("batch_size", [20, 1]) +def test_sequence_generator_decoder(cell_type, num_layers, batch_size): + hidden_size = 256 + vocab_size = 50 + max_sequence_length = 10 + + # make repeatable + set_random_seed(RANDOM_SEED) + + combiner_outputs = {HIDDEN: torch.rand([batch_size, hidden_size])} + sequence_rnn_decoder = SequenceGeneratorDecoder( + input_size=hidden_size, + vocab_size=vocab_size, + max_sequence_length=max_sequence_length, + cell_type=cell_type, + num_layers=num_layers, + ) + + output = sequence_rnn_decoder(combiner_outputs, target=None) + + assert list(output[LOGITS].size()) == [batch_size, max_sequence_length, vocab_size] + + # check for parameter updating + target = torch.randn(output[LOGITS].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(sequence_rnn_decoder, (combiner_outputs, None), target) + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/decoders/test_sequence_decoder_utils.py b/tests/ludwig/decoders/test_sequence_decoder_utils.py new file mode 100644 index 0000000..91a7fac --- /dev/null +++ b/tests/ludwig/decoders/test_sequence_decoder_utils.py @@ -0,0 +1,72 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT_STATE, HIDDEN +from ludwig.decoders import sequence_decoder_utils +from ludwig.modules.reduction_modules import SequenceReducer + + +@pytest.mark.parametrize("num_layers", [1, 2]) +def test_get_rnn_init_state_uses_hidden(num_layers): + batch_size = 16 + sequence_length = 32 + state_size = 64 + combiner_outputs = {} + combiner_outputs[HIDDEN] = torch.rand([batch_size, sequence_length, state_size]) + + # With sequence reduction. + result = sequence_decoder_utils.get_rnn_init_state(combiner_outputs, SequenceReducer(reduce_mode="sum"), num_layers) + assert list(result.size()) == [num_layers, batch_size, state_size] + + # Without sequence reduction. + with pytest.raises(ValueError): + sequence_decoder_utils.get_rnn_init_state(combiner_outputs, SequenceReducer(reduce_mode="none"), num_layers) + + +@pytest.mark.parametrize("num_layers", [1, 2]) +def test_get_rnn_init_state_prefers_encoder_output_state(num_layers): + batch_size = 16 + state_size = 64 + combiner_outputs = {} + combiner_outputs[HIDDEN] = torch.rand([batch_size, state_size]) + combiner_outputs[ENCODER_OUTPUT_STATE] = torch.rand([batch_size, state_size * 2]) + + result = sequence_decoder_utils.get_rnn_init_state(combiner_outputs, SequenceReducer(reduce_mode="sum"), num_layers) + + assert list(result.size()) == [num_layers, batch_size, state_size * 2] + + +@pytest.mark.parametrize("num_layers", [1, 2]) +def test_get_lstm_init_state_uses_hidden(num_layers): + batch_size = 16 + sequence_length = 32 + state_size = 64 + combiner_outputs = {} + combiner_outputs[HIDDEN] = torch.rand([batch_size, sequence_length, state_size]) + + # With sequence reduction. + decoder_hidden_state, decoder_cell_state = sequence_decoder_utils.get_lstm_init_state( + combiner_outputs, SequenceReducer(reduce_mode="sum"), num_layers + ) + assert list(decoder_hidden_state.size()) == [num_layers, batch_size, state_size] + assert list(decoder_cell_state.size()) == [num_layers, batch_size, state_size] + + # Without sequence reduction. + with pytest.raises(ValueError): + sequence_decoder_utils.get_lstm_init_state(combiner_outputs, SequenceReducer(reduce_mode="none"), num_layers) + + +@pytest.mark.parametrize("num_layers", [1, 2]) +def test_get_lstm_init_state_prefers_encoder_output_state(num_layers): + batch_size = 16 + state_size = 64 + combiner_outputs = {} + combiner_outputs[HIDDEN] = torch.rand([batch_size, state_size]) + combiner_outputs[ENCODER_OUTPUT_STATE] = torch.rand([batch_size, state_size * 2]) + + decoder_hidden_state, decoder_cell_state = sequence_decoder_utils.get_lstm_init_state( + combiner_outputs, SequenceReducer(reduce_mode="sum"), num_layers + ) + + assert list(decoder_hidden_state.size()) == [num_layers, batch_size, state_size * 2] + assert list(decoder_cell_state.size()) == [num_layers, batch_size, state_size * 2] diff --git a/tests/ludwig/decoders/test_sequence_tagger.py b/tests/ludwig/decoders/test_sequence_tagger.py new file mode 100644 index 0000000..0a239f0 --- /dev/null +++ b/tests/ludwig/decoders/test_sequence_tagger.py @@ -0,0 +1,34 @@ +import pytest +import torch + +from ludwig.constants import HIDDEN, LOGITS +from ludwig.decoders.sequence_tagger import SequenceTaggerDecoder +from ludwig.utils.misc_utils import set_random_seed +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("use_attention", [True, False]) +@pytest.mark.parametrize("use_bias", [True, False]) +def test_sequence_tagger(use_attention, use_bias): + # make repeatable + set_random_seed(RANDOM_SEED) + + batch_size = 20 + combiner_output_state_size = 100 + vocab_size = 150 + max_sequence_length = 30 + decoder_inputs = {HIDDEN: torch.rand(batch_size, max_sequence_length, combiner_output_state_size)} + tagger_decoder = SequenceTaggerDecoder( + combiner_output_state_size, vocab_size, max_sequence_length, use_attention=use_attention, use_bias=use_bias + ) + + outputs = tagger_decoder(decoder_inputs) + + assert outputs[LOGITS].size()[1:] == tagger_decoder.output_shape + + # check for parameter updating + target = torch.randn(outputs[LOGITS].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(tagger_decoder, (decoder_inputs,), target) + assert upc == tpc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/distributed/__init__.py b/tests/ludwig/distributed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/distributed/test_accelerate_strategy.py b/tests/ludwig/distributed/test_accelerate_strategy.py new file mode 100644 index 0000000..4e8d5c9 --- /dev/null +++ b/tests/ludwig/distributed/test_accelerate_strategy.py @@ -0,0 +1,62 @@ +"""Tests for AccelerateStrategy.""" + +import torch + + +class TestAccelerateStrategy: + def test_import_and_instantiate(self): + from ludwig.distributed.accelerate import AccelerateStrategy + + strategy = AccelerateStrategy() + assert strategy.size() >= 1 + assert strategy.rank() >= 0 + assert strategy.local_rank() >= 0 + + def test_is_available(self): + from ludwig.distributed.accelerate import AccelerateStrategy + + assert AccelerateStrategy.is_available() + + def test_barrier_no_hang(self): + from ludwig.distributed.accelerate import AccelerateStrategy + + strategy = AccelerateStrategy() + strategy.barrier() # Should not hang in single-process mode + + def test_broadcast_object(self): + from ludwig.distributed.accelerate import AccelerateStrategy + + strategy = AccelerateStrategy() + result = strategy.broadcast_object({"key": "value"}) + assert result == {"key": "value"} + + def test_allreduce(self): + from ludwig.distributed.accelerate import AccelerateStrategy + + strategy = AccelerateStrategy() + t = torch.tensor([1.0, 2.0, 3.0]) + result = strategy.allreduce(t) + assert torch.allclose(result, t) # Single process: identity + + def test_registered_in_strategies(self): + from ludwig.distributed import STRATEGIES + + assert "accelerate" in STRATEGIES + + def test_init_dist_strategy(self): + from ludwig.distributed import init_dist_strategy + + strategy = init_dist_strategy("accelerate") + assert strategy.size() >= 1 + + def test_context_managers(self): + from ludwig.distributed.accelerate import AccelerateStrategy + + strategy = AccelerateStrategy() + model = torch.nn.Linear(10, 10) + + with strategy.prepare_model_update(model, should_step=True): + pass + + with strategy.prepare_optimizer_update(torch.optim.SGD(model.parameters(), lr=0.01)): + pass diff --git a/tests/ludwig/encoders/__init__.py b/tests/ludwig/encoders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/encoders/test_bag_encoders.py b/tests/ludwig/encoders/test_bag_encoders.py new file mode 100644 index 0000000..5df7683 --- /dev/null +++ b/tests/ludwig/encoders/test_bag_encoders.py @@ -0,0 +1,43 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.bag_encoders import BagEmbedWeightedEncoder +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 +DEVICE = get_torch_device() + + +@pytest.mark.parametrize("dropout", [0, 0.9]) +@pytest.mark.parametrize("num_fc_layers", [0, 2]) +@pytest.mark.parametrize("vocab", [["a", "b", "c", "d", "e", "f", "g", "h"]]) +@pytest.mark.parametrize("embedding_size", [10]) +@pytest.mark.parametrize("representation", ["dense", "sparse"]) +def test_set_encoder(vocab: list[str], embedding_size: int, representation: str, num_fc_layers: int, dropout: float): + # make repeatable + torch.manual_seed(RANDOM_SEED) + + bag_encoder = BagEmbedWeightedEncoder( + vocab=vocab, + representation=representation, + embedding_size=embedding_size, + num_fc_layers=num_fc_layers, + dropout=dropout, + ).to(DEVICE) + inputs = torch.randint(0, 9, size=(2, len(vocab))).to(DEVICE) + outputs = bag_encoder(inputs)[ENCODER_OUTPUT] + assert outputs.shape[1:] == bag_encoder.output_shape + + # check for parameter updating + target = torch.randn(outputs.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(bag_encoder, (inputs,), target) + + if dropout == 0: + assert upc == tpc, f"Not all parameters updated. Parameters not updated: {not_updated}.\nModule: {bag_encoder}" + else: + # given random seed and configuration, non-zero dropout can take various values + assert (upc == tpc) or (upc == 0), ( + f"Not all parameterss updated. Parameters not updated: {not_updated}.\nModule: {bag_encoder}" + ) diff --git a/tests/ludwig/encoders/test_category_encoders.py b/tests/ludwig/encoders/test_category_encoders.py new file mode 100644 index 0000000..ff24e8d --- /dev/null +++ b/tests/ludwig/encoders/test_category_encoders.py @@ -0,0 +1,69 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.category_encoders import CategoricalEmbedEncoder, CategoricalSparseEncoder +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 +DEVICE = get_torch_device() + + +@pytest.mark.parametrize("trainable", [True, False]) +@pytest.mark.parametrize("vocab", [["red", "orange", "yellow", "green", "blue", "violet"], ["a", "b", "c"]]) +@pytest.mark.parametrize("embedding_size", [4, 6, 10]) +def test_categorical_dense_encoder(vocab: list[str], embedding_size: int, trainable: bool): + # make repeatable + torch.manual_seed(RANDOM_SEED) + + dense_encoder = CategoricalEmbedEncoder( + vocab=vocab, + embedding_size=embedding_size, + embeddings_trainable=trainable, + ).to(DEVICE) + inputs = torch.randint(len(vocab), (10,)).to(DEVICE) # Chooses 10 items from vocab with replacement. + inputs = torch.unsqueeze(inputs, 1) + outputs = dense_encoder(inputs)[ENCODER_OUTPUT] + # In dense mode, the embedding size should be less than or equal to vocab size. + assert outputs.shape[-1] == min(embedding_size, len(vocab)) + # Ensures output shape matches encoder expected output shape. + assert outputs.shape[1:] == dense_encoder.output_shape + + # check for parameter updating + target = torch.randn(outputs.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(dense_encoder, (inputs,), target) + + if trainable: + assert fpc == 0, "Embedding layer should be trainable, but found to be frozen." + else: + assert fpc == 1, "Embedding layer should be frozen, but found to be trainable." + + assert upc == tpc, f"Not all parameters updated. Parameters not updated: {not_updated}.\nModule: {dense_encoder}" + + +@pytest.mark.parametrize("trainable", [True, False]) +@pytest.mark.parametrize("vocab", [["red", "orange", "yellow", "green", "blue", "violet"], ["a", "b", "c"]]) +def test_categorical_sparse_encoder(vocab: list[str], trainable: bool): + # make repeatable + torch.manual_seed(RANDOM_SEED) + + sparse_encoder = CategoricalSparseEncoder(vocab=vocab, embeddings_trainable=trainable).to(DEVICE) + inputs = torch.randint(len(vocab), (10,)).to(DEVICE) # Chooses 10 items from vocab with replacement. + inputs = torch.unsqueeze(inputs, 1) + outputs = sparse_encoder(inputs)[ENCODER_OUTPUT] + # In sparse mode, embedding_size will always be equal to vocab size. + assert outputs.shape[-1] == len(vocab) + # Ensures output shape matches encoder expected output shape. + assert outputs.shape[1:] == sparse_encoder.output_shape + + # check for parameter updating + target = torch.randn(outputs.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(sparse_encoder, (inputs,), target) + + if trainable: + assert fpc == 0, "Embedding layer should be trainable, but found to be frozen." + else: + assert fpc == 1, "Embedding layer should be frozen, but found to be trainable." + + assert upc == tpc, f"Not all parameters updated. Parameters not updated: {not_updated}.\nModule: {sparse_encoder}" diff --git a/tests/ludwig/encoders/test_date_encoders.py b/tests/ludwig/encoders/test_date_encoders.py new file mode 100644 index 0000000..a2c74be --- /dev/null +++ b/tests/ludwig/encoders/test_date_encoders.py @@ -0,0 +1,50 @@ +import logging + +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.date_encoders import DateEmbed, DateWave +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 +DEVICE = get_torch_device() + +logger = logging.getLogger(__name__) + + +def test_date_embed(): + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup encoder to test + date_embed = DateEmbed().to(DEVICE) + inputs = torch.tensor( + [[2022, 6, 25, 5, 176, 9, 30, 59, 34259], [2022, 6, 25, 5, 176, 9, 30, 59, 34259]], dtype=torch.int32 + ).to(DEVICE) + outputs = date_embed(inputs) + assert outputs[ENCODER_OUTPUT].size()[1:] == date_embed.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(date_embed, (inputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +def test_date_wave(): + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup encoder to test + date_embed = DateWave().to(DEVICE) + inputs = torch.tensor( + [[2022, 6, 25, 5, 176, 9, 30, 59, 34259], [2022, 6, 25, 5, 176, 9, 30, 59, 34259]], dtype=torch.int32 + ).to(DEVICE) + outputs = date_embed(inputs) + assert outputs[ENCODER_OUTPUT].size()[1:] == date_embed.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(date_embed, (inputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/encoders/test_generic_encoders.py b/tests/ludwig/encoders/test_generic_encoders.py new file mode 100644 index 0000000..2e06db6 --- /dev/null +++ b/tests/ludwig/encoders/test_generic_encoders.py @@ -0,0 +1,30 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.generic_encoders import DenseEncoder, PassthroughEncoder + + +@pytest.mark.parametrize("input_size", [1, 2, 10]) +@pytest.mark.parametrize("categorical", [True, False]) +def test_generic_passthrough_encoder(input_size: int, categorical: bool): + passthrough_encoder = PassthroughEncoder(input_size) + # Passthrough encoder allows categorical input feature (int), dense encoder's input must be float. + if categorical: + inputs = torch.randint(10, (10, input_size)) + else: + inputs = torch.rand((10, input_size)) + outputs = passthrough_encoder(inputs) + # Ensures output shape matches encoder expected output shape. + assert outputs[ENCODER_OUTPUT].shape[1:] == passthrough_encoder.output_shape + + +@pytest.mark.parametrize("input_size", [1, 2, 10]) +@pytest.mark.parametrize("num_layers", [1, 3, 6]) +@pytest.mark.parametrize("output_size", [1, 2, 10, 256]) +def test_generic_dense_encoder(input_size: int, num_layers: int, output_size: int): + dense_encoder = DenseEncoder(input_size, num_layers=num_layers, output_size=output_size) + inputs = torch.rand((10, input_size)) + outputs = dense_encoder(inputs) + # Ensures output shape matches encoder expected output shape. + assert outputs[ENCODER_OUTPUT].shape[1:] == dense_encoder.output_shape diff --git a/tests/ludwig/encoders/test_h3_encoders.py b/tests/ludwig/encoders/test_h3_encoders.py new file mode 100644 index 0000000..911ece9 --- /dev/null +++ b/tests/ludwig/encoders/test_h3_encoders.py @@ -0,0 +1,80 @@ +import logging + +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders import h3_encoders +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 +DEVICE = get_torch_device() + +logger = logging.getLogger(__name__) + + +def test_h3_embed(): + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup encoder to test + embed = h3_encoders.H3Embed().to(DEVICE) + inputs = torch.tensor( + [ + [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7], + [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7], + ], + dtype=torch.int32, + ).to(DEVICE) + outputs = embed(inputs) + assert outputs[ENCODER_OUTPUT].size()[1:] == embed.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(embed, (inputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +def test_h3_weighted_sum(): + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup encoder to test + embed = h3_encoders.H3WeightedSum().to(DEVICE) + inputs = torch.tensor( + [ + [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7], + [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7], + ], + dtype=torch.int32, + ).to(DEVICE) + outputs = embed(inputs) + assert outputs[ENCODER_OUTPUT].size()[1:] == embed.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(embed, (inputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" + + +def test_h3_rnn_embed(): + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup encoder to test + embed = h3_encoders.H3RNN().to(DEVICE) + inputs = torch.tensor( + [ + [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7], + [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7], + ], + dtype=torch.int32, + ).to(DEVICE) + outputs = embed(inputs) + assert outputs[ENCODER_OUTPUT].size()[1:] == embed.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(embed, (inputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/encoders/test_image_encoders.py b/tests/ludwig/encoders/test_image_encoders.py new file mode 100644 index 0000000..59fb019 --- /dev/null +++ b/tests/ludwig/encoders/test_image_encoders.py @@ -0,0 +1,583 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.image.base import MLPMixerEncoder, Stacked2DCNN, UNetEncoder +from ludwig.encoders.image.torchvision import ( + TVAlexNetEncoder, + TVConvNeXtEncoder, + TVDenseNetEncoder, + TVEfficientNetEncoder, + TVGoogLeNetEncoder, + TVInceptionV3Encoder, + TVMaxVitEncoder, + TVMNASNetEncoder, + TVMobileNetV2Encoder, + TVMobileNetV3Encoder, + TVRegNetEncoder, + TVResNetEncoder, + TVResNeXtEncoder, + TVShuffleNetV2Encoder, + TVSqueezeNetEncoder, + TVSwinTransformerEncoder, + TVVGGEncoder, + TVViTEncoder, + TVWideResNetEncoder, +) +from ludwig.utils.image_utils import torchvision_model_registry +from ludwig.utils.misc_utils import set_random_seed +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("height,width,num_conv_layers,num_channels", [(224, 224, 5, 3)]) +def test_stacked2d_cnn(height: int, width: int, num_conv_layers: int, num_channels: int): + # make repeatable + set_random_seed(RANDOM_SEED) + + stacked_2d_cnn = Stacked2DCNN( + height=height, width=width, num_conv_layers=num_conv_layers, num_channels=num_channels + ) + inputs = torch.rand(2, num_channels, height, width) + outputs = stacked_2d_cnn(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == stacked_2d_cnn.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(stacked_2d_cnn, (inputs,), target) + + assert tpc == upc, f"Not all expected parameters updated. Parameters not updated {not_updated}." + + +@pytest.mark.parametrize("height,width,num_channels", [(224, 224, 3)]) +def test_mlp_mixer_encoder(height: int, width: int, num_channels: int): + # make repeatable + set_random_seed(RANDOM_SEED) + + mlp_mixer = MLPMixerEncoder(height=height, width=width, num_channels=num_channels) + inputs = torch.rand(2, num_channels, height, width) + outputs = mlp_mixer(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == mlp_mixer.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(mlp_mixer, (inputs,), target) + + assert tpc == upc, f"Not all expected parameters updated. Parameters not updated {not_updated}." + + +@pytest.mark.parametrize("height,width,num_channels", [(224, 224, 1), (224, 224, 3)]) +def test_unet_encoder(height: int, width: int, num_channels: int): + # make repeatable + set_random_seed(RANDOM_SEED) + + unet_encoder = UNetEncoder(height=height, width=width, num_channels=num_channels) + inputs = torch.rand(2, num_channels, height, width) + outputs = unet_encoder(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == unet_encoder.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(unet_encoder, (inputs,), target) + + assert tpc == upc, f"Not all expected parameters updated. Parameters not updated {not_updated}." + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [v.variant_id for v in torchvision_model_registry["alexnet"].values()]) +def test_tv_alexnet_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVAlexNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["convnext"].values())).variant_id]) +def test_tv_convnext_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVConvNeXtEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["densenet"].values())).variant_id]) +def test_tv_densenet_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVDenseNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +# test only model variants that do not require large amount of memory +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["efficientnet"].values())).variant_id]) +def test_tv_efficientnet_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVEfficientNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [v.variant_id for v in torchvision_model_registry["googlenet"].values()]) +def test_tv_googlenet_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVGoogLeNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [v.variant_id for v in torchvision_model_registry["inceptionv3"].values()]) +def test_tv_inceptionv3_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVInceptionV3Encoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["maxvit"].values())).variant_id]) +def test_tv_maxvit_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVMaxVitEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["mnasnet"].values())).variant_id]) +def test_tv_mnasnet_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVMNASNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [v.variant_id for v in torchvision_model_registry["mobilenetv2"].values()]) +def test_tv_mobilenetv2_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVMobileNetV2Encoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["mobilenetv3"].values())).variant_id]) +def test_tv_mobilenetv3_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVMobileNetV3Encoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +# test only model variants that do not require large amount of memory +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["regnet"].values())).variant_id]) +def test_tv_regnet_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVRegNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["resnet"].values())).variant_id]) +def test_tv_resnet_torch_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVResNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["resnext"].values())).variant_id]) +def test_tv_resnext_encoder( + model_variant: int, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVResNeXtEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["shufflenet_v2"].values())).variant_id]) +def test_tv_shufflenet_v2_encoder( + model_variant: str, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVShuffleNetV2Encoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [v.variant_id for v in torchvision_model_registry["squeezenet"].values()]) +def test_tv_squeezenet_encoder( + model_variant: str, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVSqueezeNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize( + "model_variant", [next(iter(torchvision_model_registry["swin_transformer"].values())).variant_id] +) +def test_tv_swin_transformer_encoder( + model_variant: str, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVSwinTransformerEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["vgg"].values())).variant_id]) +def test_tv_vgg_encoder( + model_variant: int | str, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVVGGEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +# test only VIT model variants that do not require large amount of memory +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["vit"].values())).variant_id]) +def test_tv_vit_encoder( + model_variant: str, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVViTEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape + + +@pytest.mark.parametrize( + "trainable,saved_weights_in_checkpoint,use_pretrained", + [(True, True, False), (False, False, False)], + ids=["trainable", "frozen"], +) +@pytest.mark.parametrize("model_variant", [next(iter(torchvision_model_registry["wide_resnet"].values())).variant_id]) +def test_tv_wide_resnet_encoder( + model_variant: str, + use_pretrained: bool, + saved_weights_in_checkpoint: bool, + trainable: bool, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + pretrained_model = TVWideResNetEncoder( + model_variant=model_variant, + use_pretrained=use_pretrained, + saved_weights_in_checkpoint=saved_weights_in_checkpoint, + trainable=trainable, + ) + inputs = torch.rand(2, *pretrained_model.input_shape) + outputs = pretrained_model(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape diff --git a/tests/ludwig/encoders/test_llm_encoders.py b/tests/ludwig/encoders/test_llm_encoders.py new file mode 100644 index 0000000..7a9ff0a --- /dev/null +++ b/tests/ludwig/encoders/test_llm_encoders.py @@ -0,0 +1,247 @@ +import copy + +import pytest +import torch +import torch.nn as nn +from transformers import AutoConfig, PreTrainedModel + +from ludwig.encoders.text_encoders import LLMEncoder +from ludwig.schema.encoders.text_encoders import LLMEncoderConfig +from ludwig.schema.llms.peft import AdaloraConfig, BaseAdapterConfig, IA3Config, LoraConfig +from ludwig.utils.llm_utils import get_context_len + +# Mapping of adapter types to test against and their respective config objects. +ADAPTER_CONFIG_MAP = { + "adalora": AdaloraConfig, + "ia3": IA3Config, + "lora": LoraConfig, +} + + +@pytest.fixture() +def encoder_config() -> LLMEncoderConfig: + """Create a baseline LLMEncoderConfig. + + Returns: + A baseline LLMEncoderConfig with a small model, no adapter, and no quantization + """ + return LLMEncoderConfig( + type="llm", + max_sequence_length=256, + base_model="HuggingFaceH4/tiny-random-LlamaForCausalLM", + adapter=None, + quantization=None, + ) + + +@pytest.fixture() +def model_config(encoder_config): + return AutoConfig.from_pretrained(encoder_config.base_model) + + +class WrapperModule(nn.Module): + def __init__(self, encoder: LLMEncoder): + super().__init__() + self.encoder = encoder + + +class TestLLMEncoder: + def create_encoder_config_with_adapter( + self, encoder_config: LLMEncoderConfig, adapter: str, **kwargs + ) -> BaseAdapterConfig: + """Create a config for the requested adapter. + + Args: + adapter: name of the adapter + + Returns: + A config object for the requested adapter. If any keyword args are passed, they will be used to initialize + the config. + """ + new_config = copy.deepcopy(encoder_config) + new_config.adapter = ADAPTER_CONFIG_MAP[adapter](**kwargs) + return new_config + + def adapter_param_name_prefix(self, adapter: str) -> str: + """Get the PEFT paramter name prefix for a given adapter type. + + Args: + adapter: A valid config value for `adapter.type` + + Returns: + The PEFT-applied prefix for the adapter's parameter names. + + Raises: + KeyError: raised when the provided adapter name is not valid for LLMEncoder. + """ + return LLMEncoder.ADAPTER_PARAM_NAME_PREFIX[adapter] + + def test_init(self, encoder_config: LLMEncoderConfig, model_config): + # Test initializing without an adapter + encoder = LLMEncoder(encoder_config=encoder_config) + + assert encoder.model_name == encoder_config.base_model + assert isinstance(encoder.model, PreTrainedModel) + # Check adapter was not initialized + for k in ADAPTER_CONFIG_MAP: + prefix = self.adapter_param_name_prefix(k) + assert all(map(lambda k: prefix not in k, encoder.state_dict().keys())) + assert encoder.input_shape == torch.Size([encoder_config.max_sequence_length]) + assert encoder.output_shape == torch.Size([encoder_config.max_sequence_length, model_config.hidden_size]) + + # The final layer must not be trainable because it is not used + last_module = list(encoder.model.modules())[-1] + assert all(not p.requires_grad for p in last_module.parameters()) + + # Test that max sequence length falls back to the context length when too large + context_len = get_context_len(model_config) + cl_config = copy.deepcopy(encoder_config) + cl_config.max_sequence_length = context_len + 1 + + encoder = LLMEncoder(encoder_config=cl_config) + + assert encoder.model_name == encoder_config.base_model + assert isinstance(encoder.model, PreTrainedModel) + # Check adapter was not initialized + for k in ADAPTER_CONFIG_MAP: + prefix = self.adapter_param_name_prefix(k) + assert all(map(lambda k: prefix not in k, encoder.state_dict().keys())) + assert encoder.input_shape == torch.Size([context_len]) + assert encoder.output_shape == torch.Size([context_len, model_config.hidden_size]) + + # The final layer must not be trainable because it is not used + last_module = list(encoder.model.modules())[-1] + assert all(not p.requires_grad for p in last_module.parameters()) + + @pytest.mark.parametrize("adapter", list(ADAPTER_CONFIG_MAP.keys())) + def test_init_with_adapter(self, encoder_config: LLMEncoderConfig, adapter: str, model_config): + from peft import PeftModel + + encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter) + encoder = LLMEncoder(encoder_config=encoder_config_with_adapter) + prefix = self.adapter_param_name_prefix(adapter) + + # The adapter should not be initialized until `prepare_for_training` is called + assert not isinstance(encoder.model, PeftModel) + assert not any(map(lambda k: prefix in k, encoder.state_dict().keys())) + + assert encoder.model_name == encoder_config.base_model + assert encoder.input_shape == torch.Size([encoder_config.max_sequence_length]) + assert encoder.output_shape == torch.Size([encoder_config.max_sequence_length, model_config.hidden_size]) + + # The final layer must not be trainable because it is not used + last_module = list(encoder.model.modules())[-1] + assert all(not p.requires_grad for p in last_module.parameters()) + + @pytest.mark.parametrize("adapter", list(ADAPTER_CONFIG_MAP.keys())) + def test_prepare_for_training(self, encoder_config: LLMEncoderConfig, adapter: str): + from peft import PeftModel + + encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter) + encoder = LLMEncoder(encoder_config=encoder_config_with_adapter) + prefix = self.adapter_param_name_prefix(adapter) + + # The adapter should not be initialized until `prepare_for_training` is called + assert not isinstance(encoder.model, PeftModel) + assert not any(map(lambda k: prefix in k, encoder.state_dict().keys())) + + # Initialize the adapter + encoder.prepare_for_training() + + # At this point, the adapter should be initialized and the state dict should contain adapter parameters + assert isinstance(encoder.model, PeftModel) + assert any(map(lambda k: prefix in k, encoder.state_dict().keys())) + + def test_save_to_state_dict(self, encoder_config: LLMEncoderConfig, tmpdir): + # With no adapter, the state dict should only contain the model parameters + encoder = LLMEncoder(encoder_config=encoder_config) + # Check adapter was not initialized + for k in ADAPTER_CONFIG_MAP: + prefix = self.adapter_param_name_prefix(k) + assert all(map(lambda k: prefix not in k, encoder.state_dict().keys())) + + @pytest.mark.parametrize("adapter", list(ADAPTER_CONFIG_MAP.keys())) + def test_save_to_state_dict_adapter(self, encoder_config: LLMEncoderConfig, adapter: str, tmpdir): + # With an adapter, the state dict should only contain adapter parameters + encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter) + encoder = LLMEncoder(encoder_config=encoder_config_with_adapter) + prefix = self.adapter_param_name_prefix(adapter) + # Initialize the adapters + encoder.prepare_for_training() + assert all(map(lambda k: prefix in k, encoder.state_dict().keys())) + + @pytest.mark.parametrize("wrap", [False, True], ids=["no_wrapper", "with_wrapper"]) + def test_load_from_state_dict(self, encoder_config: LLMEncoderConfig, wrap: bool): + def weights_init(m): + """Reinitialize the weights of a torch module.""" + if hasattr(m, "weight") and m.weight.ndim > 1: + torch.nn.init.xavier_uniform_(m.weight.data) + + # Create two encoders from the same config + encoder1 = LLMEncoder(encoder_config=encoder_config) + encoder2 = LLMEncoder(encoder_config=encoder_config) + + if wrap: + encoder1 = WrapperModule(encoder1) + encoder2 = WrapperModule(encoder2) + + # Reinitialize the weights of one encoder so the two are not identical + encoder2.apply(weights_init) + + # Ensure that the weights are different + encoder1_sd = encoder1.state_dict() + encoder2_sd = encoder2.state_dict() + assert any(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), encoder1_sd.keys())) + + # Load the weights of encoder1 back into encoder2 and ensure the weights are equal + encoder2.load_state_dict(encoder1_sd) + encoder2_sd = encoder2.state_dict() + assert all(map(lambda k: torch.equal(encoder1_sd[k], encoder2_sd[k]), encoder1_sd.keys())) + + @pytest.mark.parametrize("wrap", [False, True], ids=["no_wrapper", "with_wrapper"]) + @pytest.mark.parametrize("adapter", list(ADAPTER_CONFIG_MAP.keys())) + def test_load_from_state_dict_adapter(self, encoder_config: LLMEncoderConfig, adapter: str, wrap: bool): + def weights_init(m): + """Reinitialize the weights of a torch module.""" + if hasattr(m, "weight") and m.weight.ndim > 1: + torch.nn.init.xavier_uniform_(m.weight.data) + + prefix = self.adapter_param_name_prefix(adapter) + + # Update the config with an adapter + encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter) + + # Create two encoders from the same config + encoder1 = LLMEncoder(encoder_config=encoder_config_with_adapter) + encoder2 = LLMEncoder(encoder_config=encoder_config_with_adapter) + + # Initialize the adapters + encoder1.prepare_for_training() + encoder2.prepare_for_training() + + if wrap: + encoder1 = WrapperModule(encoder1) + encoder2 = WrapperModule(encoder2) + + encoder2.apply(weights_init) + + encoder1_sd = encoder1.state_dict() + encoder2_sd = encoder2.state_dict() + adapter_keys = [k for k in encoder1_sd.keys() if prefix in k and "weight" in k] + model_keys = [k for k in encoder1_sd.keys() if prefix not in k] + + # The LoRA weights should no longer be equal + assert all(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), adapter_keys)) + + # The remaining weights should also no longer be equal + assert all(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), model_keys)) + + # Load the weights of encoder1 back into encoder2 + encoder2.load_state_dict(encoder1_sd) + encoder2_sd = encoder2.state_dict() + + # The LoRA weights should now be equal again + assert all(map(lambda k: torch.equal(encoder1_sd[k], encoder2_sd[k]), adapter_keys)) + + # The remaining weights should still be unequal + assert all(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), model_keys)) diff --git a/tests/ludwig/encoders/test_mamba_hybrid.py b/tests/ludwig/encoders/test_mamba_hybrid.py new file mode 100644 index 0000000..7c6eed3 --- /dev/null +++ b/tests/ludwig/encoders/test_mamba_hybrid.py @@ -0,0 +1,173 @@ +"""Phase 6.6.2 — Mamba-2 + Jamba hybrid encoder unit tests.""" + +from __future__ import annotations + +import pytest +import torch + +from ludwig.encoders.mamba_hybrid import _Mamba2Block, JambaEncoder, Mamba2Encoder + + +class TestMamba2Block: + def test_shape_preserved(self): + torch.manual_seed(0) + block = _Mamba2Block(d_model=32, num_heads=4) + x = torch.randn(2, 16, 32) + out = block(x) + assert out.shape == x.shape + + def test_residual_init(self): + """A freshly-initialised block with zero input path output is ~= identity on x via the residual connection. + + The Mamba-2 block isn't exactly identity at init, but the residual makes sure the output is 'close' to the input + rather than random. + """ + torch.manual_seed(0) + block = _Mamba2Block(d_model=32, num_heads=4, dropout=0.0) + block.eval() + x = torch.randn(2, 8, 32) + out = block(x) + # Output has the same statistics as the input — not an identity, but residual-anchored. + assert out.shape == x.shape + assert not torch.isnan(out).any() + + def test_d_model_divisibility_check(self): + with pytest.raises(ValueError, match="must be divisible"): + _Mamba2Block(d_model=30, num_heads=4) + + def test_backward(self): + block = _Mamba2Block(d_model=16, num_heads=4) + x = torch.randn(2, 8, 16, requires_grad=True) + out = block(x) + out.sum().backward() + assert x.grad is not None + assert block.log_alpha.grad is not None + + +class TestMamba2Encoder: + def test_forward_shapes_with_embedding(self): + encoder = Mamba2Encoder( + should_embed=True, + vocab=list(range(50)), + embedding_size=64, + d_model=32, + n_layers=2, + num_heads=4, + output_size=24, + ) + inputs = torch.randint(0, 50, (3, 10)) + out = encoder(inputs)["encoder_output"] + assert out.shape == (3, 24) + + def test_forward_shapes_without_embedding(self): + encoder = Mamba2Encoder( + should_embed=False, + d_model=32, + n_layers=2, + num_heads=4, + output_size=16, + reduce_output="mean", + ) + inputs = torch.randn(2, 8, 32) + out = encoder(inputs)["encoder_output"] + assert out.shape == (2, 16) + + def test_reduce_output_last(self): + encoder = Mamba2Encoder( + should_embed=False, d_model=16, n_layers=2, num_heads=4, output_size=16, reduce_output="last" + ) + inputs = torch.randn(2, 5, 16) + out = encoder(inputs)["encoder_output"] + assert out.shape == (2, 16) + + +class TestJambaEncoder: + def test_interleaving_pattern(self): + import torch.nn as nn + + encoder = JambaEncoder( + should_embed=False, d_model=16, n_layers=8, attention_every_k=4, num_heads=4, ffn_size=32, output_size=16 + ) + # Every 4th position (indices 3 and 7) is attention. + types = [type(layer).__name__ for layer in encoder.layers] + attention_positions = [i for i, t in enumerate(types) if t == "TransformerEncoderLayer"] + assert attention_positions == [3, 7], f"expected attention at indices [3, 7], got {attention_positions}" + # The remaining layers should be SSM blocks. + ssm_positions = [i for i, t in enumerate(types) if t == "_Mamba2Block"] + assert ssm_positions == [0, 1, 2, 4, 5, 6] + _ = nn # silence unused import when running this test in isolation + + def test_forward_shape(self): + encoder = JambaEncoder( + should_embed=False, + d_model=32, + n_layers=4, + attention_every_k=2, # alternate SSM / attention + num_heads=4, + ffn_size=64, + output_size=20, + ) + inputs = torch.randn(2, 6, 32) + out = encoder(inputs)["encoder_output"] + assert out.shape == (2, 20) + + def test_backward(self): + encoder = JambaEncoder( + should_embed=False, d_model=16, n_layers=4, attention_every_k=2, num_heads=4, ffn_size=32, output_size=16 + ) + x = torch.randn(2, 6, 16, requires_grad=True) + out = encoder(x)["encoder_output"] + out.sum().backward() + assert x.grad is not None + + +class TestSchemaRegistration: + def test_mamba2_encoder_config_parses(self): + from ludwig.schema.encoders.mamba_hybrid import Mamba2EncoderConfig + + cfg = Mamba2EncoderConfig.model_validate({"type": "mamba2", "d_model": 128, "n_layers": 6, "num_heads": 4}) + assert cfg.type == "mamba2" + assert cfg.d_model == 128 + assert cfg.num_heads == 4 + + def test_jamba_encoder_config_parses(self): + from ludwig.schema.encoders.mamba_hybrid import JambaEncoderConfig + + cfg = JambaEncoderConfig.model_validate( + {"type": "jamba", "d_model": 128, "n_layers": 8, "attention_every_k": 4, "ffn_size": 512} + ) + assert cfg.type == "jamba" + assert cfg.attention_every_k == 4 + assert cfg.ffn_size == 512 + + def test_full_model_config_with_mamba2(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + "input_features": [ + {"name": "seq", "type": "sequence", "encoder": {"type": "mamba2", "d_model": 64, "n_layers": 2}} + ], + "output_features": [{"name": "y", "type": "binary"}], + } + ) + assert cfg.input_features[0].encoder.type == "mamba2" + assert cfg.input_features[0].encoder.d_model == 64 + + def test_full_model_config_with_jamba(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + "input_features": [ + { + "name": "seq", + "type": "sequence", + "encoder": {"type": "jamba", "d_model": 64, "n_layers": 4, "attention_every_k": 2}, + } + ], + "output_features": [{"name": "y", "type": "binary"}], + } + ) + assert cfg.input_features[0].encoder.type == "jamba" + assert cfg.input_features[0].encoder.attention_every_k == 2 diff --git a/tests/ludwig/encoders/test_number_encoders.py b/tests/ludwig/encoders/test_number_encoders.py new file mode 100644 index 0000000..ac6cbe0 --- /dev/null +++ b/tests/ludwig/encoders/test_number_encoders.py @@ -0,0 +1,121 @@ +"""Tests for PLE and Periodic number encoders.""" + +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.number_encoders import PeriodicEncoder, PLEEncoder + +BATCH_SIZE = 16 + + +class TestPLEEncoder: + def test_output_shape(self): + encoder = PLEEncoder(num_bins=32, output_size=128) + encoder.set_bin_edges(torch.linspace(0, 1, 33).tolist()) + inputs = torch.randn(BATCH_SIZE, 1) + output = encoder(inputs) + assert ENCODER_OUTPUT in output + assert output[ENCODER_OUTPUT].shape == (BATCH_SIZE, 128) + + def test_output_shape_1d_input(self): + encoder = PLEEncoder(num_bins=16, output_size=64) + encoder.set_bin_edges(torch.linspace(0, 10, 17).tolist()) + inputs = torch.randn(BATCH_SIZE) + output = encoder(inputs) + assert output[ENCODER_OUTPUT].shape == (BATCH_SIZE, 64) + + def test_gradient_flow(self): + encoder = PLEEncoder(num_bins=16, output_size=64) + encoder.set_bin_edges(torch.linspace(0, 1, 17).tolist()) + inputs = torch.randn(BATCH_SIZE, 1, requires_grad=True) + output = encoder(inputs) + loss = output[ENCODER_OUTPUT].sum() + loss.backward() + assert inputs.grad is not None + # Check projection weights got gradients + assert encoder.projection.weight.grad is not None + + def test_ple_interpolation_values(self): + """Test that PLE values are correct for known bin edges and inputs.""" + encoder = PLEEncoder(num_bins=4, output_size=4) + encoder.set_bin_edges([0.0, 0.25, 0.5, 0.75, 1.0]) + + # Input exactly at bin edges should produce known patterns + inputs = torch.tensor([0.0, 0.25, 0.5, 0.75, 1.0]).unsqueeze(-1) + output = encoder(inputs) + # Just verify output is valid (detailed interpolation values depend on projection weights) + assert output[ENCODER_OUTPUT].shape == (5, 4) + assert torch.isfinite(output[ENCODER_OUTPUT]).all() + + def test_properties(self): + encoder = PLEEncoder(num_bins=32, output_size=128) + assert encoder.input_shape == torch.Size([1]) + assert encoder.output_shape == torch.Size([128]) + + def test_default_bin_edges(self): + """Encoder should work even before set_bin_edges is called (uses default linspace).""" + encoder = PLEEncoder(num_bins=8, output_size=32) + inputs = torch.randn(BATCH_SIZE, 1) + output = encoder(inputs) + assert output[ENCODER_OUTPUT].shape == (BATCH_SIZE, 32) + + def test_set_bin_edges_deduplication(self): + """Duplicate bin edges should be handled gracefully.""" + encoder = PLEEncoder(num_bins=4, output_size=16) + # Duplicates at edges (common when data has many identical values) + encoder.set_bin_edges([0.0, 0.0, 0.5, 0.5, 1.0]) + inputs = torch.tensor([0.25, 0.75]).unsqueeze(-1) + output = encoder(inputs) + assert torch.isfinite(output[ENCODER_OUTPUT]).all() + + +class TestPeriodicEncoder: + def test_output_shape(self): + encoder = PeriodicEncoder(num_frequencies=32, output_size=128) + inputs = torch.randn(BATCH_SIZE, 1) + output = encoder(inputs) + assert ENCODER_OUTPUT in output + assert output[ENCODER_OUTPUT].shape == (BATCH_SIZE, 128) + + def test_output_shape_1d_input(self): + encoder = PeriodicEncoder(num_frequencies=16, output_size=64) + inputs = torch.randn(BATCH_SIZE) + output = encoder(inputs) + assert output[ENCODER_OUTPUT].shape == (BATCH_SIZE, 64) + + def test_gradient_flow(self): + encoder = PeriodicEncoder(num_frequencies=16, output_size=64) + inputs = torch.randn(BATCH_SIZE, 1, requires_grad=True) + output = encoder(inputs) + loss = output[ENCODER_OUTPUT].sum() + loss.backward() + assert inputs.grad is not None + assert encoder.frequencies.grad is not None + assert encoder.phases.grad is not None + assert encoder.projection.weight.grad is not None + + def test_properties(self): + encoder = PeriodicEncoder(num_frequencies=32, output_size=128) + assert encoder.input_shape == torch.Size([1]) + assert encoder.output_shape == torch.Size([128]) + + def test_sigma_affects_initialization(self): + """Different sigma values should produce different frequency distributions.""" + torch.manual_seed(42) + enc1 = PeriodicEncoder(num_frequencies=64, sigma=0.1) + torch.manual_seed(42) + enc2 = PeriodicEncoder(num_frequencies=64, sigma=10.0) + # Frequencies should differ by the sigma factor + assert not torch.allclose(enc1.frequencies, enc2.frequencies) + + def test_periodic_output_is_bounded_before_projection(self): + """Sin outputs should be in [-1, 1] range before projection.""" + encoder = PeriodicEncoder(num_frequencies=32, output_size=32) + # Override projection to identity to check raw periodic features + encoder.projection = torch.nn.Identity() + encoder._output_size = 32 + inputs = torch.randn(BATCH_SIZE, 1) * 100 # large inputs + output = encoder(inputs) + # Raw periodic features are sin() values, should be in [-1, 1] + assert output[ENCODER_OUTPUT].min() >= -1.0 + assert output[ENCODER_OUTPUT].max() <= 1.0 diff --git a/tests/ludwig/encoders/test_phase_1_5.py b/tests/ludwig/encoders/test_phase_1_5.py new file mode 100644 index 0000000..648bf86 --- /dev/null +++ b/tests/ludwig/encoders/test_phase_1_5.py @@ -0,0 +1,540 @@ +"""Comprehensive tests for Phase 1.5: Encoder Modernization. + +Tests cover: +- 1.5.1 Image encoder consolidation + CLIP/DINOv2/SigLIP +- 1.5.2 Audio encoders (Wav2Vec2, Whisper, HuBERT) +- 1.5.3 Text encoder cleanup +- 1.5.4 Sequence encoder modernization (attention pooling, Mamba) +- 1.5.5 Category & number encoder enhancements +- 1.5.6 Date & H3 encoder cleanup +- 1.5.7 Activation function expansion +""" + +import pytest +import torch +import torch.nn as nn + +# Force CPU for all tests to avoid device mismatch issues +DEVICE = torch.device("cpu") + + +# ============================================================================ +# 1.5.7 — Activation Function Tests +# ============================================================================ +class TestActivationExpansion: + """Test that all new activations are registered and functional.""" + + def test_activation_count(self): + from ludwig.utils.torch_utils import activations + + assert len(activations) >= 24, f"Expected >= 24 activations, got {len(activations)}" + + @pytest.mark.parametrize( + "name", + [ + "relu", + "elu", + "leakyRelu", + "tanh", + "sigmoid", + "softmax", + "logSigmoid", + "gelu", + "silu", + "swish", + "mish", + "selu", + "prelu", + "relu6", + "hardswish", + "hardsigmoid", + "softplus", + "celu", + "swiglu", + "geglu", + "reglu", + "sparsemax", + "entmax15", + None, + ], + ) + def test_activation_instantiates(self, name): + from ludwig.utils.torch_utils import get_activation + + act = get_activation(name) + assert isinstance(act, nn.Module) + + @pytest.mark.parametrize("name", ["gelu", "silu", "mish", "selu", "relu6", "softplus", "celu"]) + def test_standard_activation_forward(self, name): + from ludwig.utils.torch_utils import get_activation + + act = get_activation(name) + x = torch.randn(4, 16) + out = act(x) + assert out.shape == x.shape + + @pytest.mark.parametrize("name", ["swiglu", "geglu", "reglu"]) + def test_glu_variant_forward(self, name): + """GLU variants split input in half, so output dim is half of input dim.""" + from ludwig.utils.torch_utils import get_activation + + act = get_activation(name) + x = torch.randn(4, 32) # Must be even for chunk(2) + out = act(x) + assert out.shape == (4, 16), f"GLU output should be half input dim, got {out.shape}" + + def test_sparsemax_forward(self): + from ludwig.utils.torch_utils import get_activation + + act = get_activation("sparsemax") + x = torch.randn(4, 10) + out = act(x) + assert out.shape == x.shape + # Sparsemax output should be non-negative and sum to ~1 + assert (out >= -1e-6).all() + + def test_entmax15_forward(self): + from ludwig.utils.torch_utils import get_activation + + act = get_activation("entmax15") + x = torch.randn(4, 10) + out = act(x) + assert out.shape == x.shape + assert (out >= -1e-6).all() + + def test_schema_includes_new_activations(self): + from ludwig.schema.utils import ActivationOptions + + field = ActivationOptions() + # Check field_info for allowed values — this depends on schema_utils internals + # Just verify it doesn't error + assert field is not None + + +# ============================================================================ +# 1.5.5 — Category & Number Encoder Tests +# ============================================================================ +class TestCategoryEncoderEnhancements: + def test_target_encoder_forward(self): + from ludwig.encoders.category_encoders import CategoricalTargetEncoder + + enc = CategoricalTargetEncoder(vocab=["a", "b", "c", "d"], output_size=8) + x = torch.tensor([0, 1, 2, 3]) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert ENCODER_OUTPUT in out + assert out[ENCODER_OUTPUT].shape == (4, 8) + + def test_target_encoder_output_shape(self): + from ludwig.encoders.category_encoders import CategoricalTargetEncoder + + enc = CategoricalTargetEncoder(vocab=["x", "y", "z"], output_size=16) + assert enc.output_shape == torch.Size([16]) + + def test_hash_encoder_forward(self): + from ludwig.encoders.category_encoders import CategoricalHashEncoder + + enc = CategoricalHashEncoder(vocab=["a"] * 100, num_hash_buckets=32, embedding_size=64) + x = torch.tensor([0, 50, 99, 10]) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert out[ENCODER_OUTPUT].shape == (4, 64) + + def test_hash_encoder_hashing(self): + """Hash encoder should map large indices to bounded bucket range.""" + from ludwig.encoders.category_encoders import CategoricalHashEncoder + + enc = CategoricalHashEncoder(vocab=["a"] * 10, num_hash_buckets=8, embedding_size=4) + x = torch.tensor([0, 100, 999]) # Values beyond vocab size + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert out[ENCODER_OUTPUT].shape == (3, 4) + + def test_hash_encoder_output_shape(self): + from ludwig.encoders.category_encoders import CategoricalHashEncoder + + enc = CategoricalHashEncoder(vocab=["a"], num_hash_buckets=16, embedding_size=32) + assert enc.output_shape == torch.Size([32]) + + def test_target_encoder_schema(self): + from ludwig.schema.encoders.category_encoders import CategoricalTargetEncoderConfig + + cfg = CategoricalTargetEncoderConfig.model_validate({"type": "target"}) + assert cfg.type == "target" + + def test_hash_encoder_schema(self): + from ludwig.schema.encoders.category_encoders import CategoricalHashEncoderConfig + + cfg = CategoricalHashEncoderConfig.model_validate({"type": "hash"}) + assert cfg.type == "hash" + + +class TestBinsEncoder: + def test_bins_encoder_forward(self): + from ludwig.encoders.number_encoders import BinsEncoder + + enc = BinsEncoder(input_size=1, num_bins=16, output_size=64) + x = torch.randn(8, 1) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert out[ENCODER_OUTPUT].shape == (8, 64) + + def test_bins_encoder_set_bin_edges(self): + from ludwig.encoders.number_encoders import BinsEncoder + + enc = BinsEncoder(input_size=1, num_bins=4, output_size=32) + enc.set_bin_edges([0.0, 0.25, 0.5, 0.75, 1.0]) + x = torch.tensor([[0.1], [0.3], [0.6], [0.9]]) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert out[ENCODER_OUTPUT].shape == (4, 32) + + def test_bins_encoder_output_shape(self): + from ludwig.encoders.number_encoders import BinsEncoder + + enc = BinsEncoder(input_size=1, num_bins=8, output_size=128) + assert enc.output_shape == torch.Size([128]) + + def test_bins_encoder_schema(self): + from ludwig.schema.encoders.number_encoders import BinsEncoderConfig + + cfg = BinsEncoderConfig.model_validate({"type": "bins"}) + assert cfg.type == "bins" + assert cfg.num_bins == 32 + + +# ============================================================================ +# 1.5.6 — Date & H3 Encoder Tests +# ============================================================================ +class TestDateEncoderCleanup: + @staticmethod + def _make_date_vector(batch_size, device): + """Create a realistic date input vector: [year, month, day, weekday, yearday, hour, min, sec, sec_of_day].""" + return torch.tensor( + [ + [2024, 3, 15, 4, 75, 10, 30, 45, 37845], + ] + * batch_size, + dtype=torch.int, + device=device, + ) + + def test_date_embed_forward(self): + from ludwig.encoders.date_encoders import DateEmbed + + enc = DateEmbed(encoder_config=None).to(DEVICE) + x = self._make_date_vector(4, DEVICE) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert ENCODER_OUTPUT in out + + def test_date_wave_forward(self): + from ludwig.encoders.date_encoders import DateWave + + enc = DateWave(encoder_config=None).to(DEVICE) + x = self._make_date_vector(4, DEVICE) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert ENCODER_OUTPUT in out + + def test_date_encoder_base_exists(self): + from ludwig.encoders.date_encoders import DateEncoderBase + + assert DateEncoderBase is not None + + def test_shared_constants(self): + """Constants should be importable from ludwig.constants.""" + from ludwig.constants import DATE_VECTOR_LENGTH, H3_VECTOR_LENGTH, MAX_H3_RESOLUTION + + assert DATE_VECTOR_LENGTH == 9 + assert MAX_H3_RESOLUTION == 15 + assert H3_VECTOR_LENGTH == MAX_H3_RESOLUTION + 4 + + +class TestH3Encoders: + def test_h3_embed_forward(self): + from ludwig.constants import H3_VECTOR_LENGTH + from ludwig.encoders.h3_encoders import H3Embed + + enc = H3Embed(encoder_config=None).to(DEVICE) + # H3 vector: [mode(0-5), edge(0-6), resolution(0-15), base_cell(0-121), cell0..cell14(0-6)] + x = torch.zeros(4, H3_VECTOR_LENGTH, dtype=torch.long, device=DEVICE) + x[:, 0] = 1 # mode + x[:, 1] = 0 # edge + x[:, 2] = 5 # resolution + x[:, 3] = 10 # base_cell + x[:, 4:] = 3 # cells + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert ENCODER_OUTPUT in out + + +# ============================================================================ +# 1.5.4 — Sequence Encoder Modernization Tests +# ============================================================================ +class TestAttentionPooling: + def test_attention_pooling_forward(self): + from ludwig.modules.reduction_modules import AttentionPooling + + pool = AttentionPooling(input_size=64).to(DEVICE) + x = torch.randn(4, 10, 64, device=DEVICE) + out = pool(x) + assert out.shape == (4, 64) + + def test_attention_pooling_with_mask(self): + from ludwig.modules.reduction_modules import AttentionPooling + + pool = AttentionPooling(input_size=32).to(DEVICE) + x = torch.randn(2, 8, 32, device=DEVICE) + mask = torch.ones(2, 8, dtype=torch.bool, device=DEVICE) + mask[0, 5:] = False + out = pool(x, mask=mask) + assert out.shape == (2, 32) + + def test_attention_pooling_in_reducer(self): + from ludwig.modules.reduction_modules import SequenceReducer + + reducer = SequenceReducer(reduce_mode="attention_pooling", encoding_size=64).to(DEVICE) + x = torch.randn(4, 10, 64, device=DEVICE) + out = reducer(x) + assert out.shape == (4, 64) + + +class TestMambaEncoder: + def test_mamba_encoder_forward(self): + from ludwig.encoders.sequence_encoders import MambaEncoder + + enc = MambaEncoder( + vocab=["a", "b", "c", "d", "e", "", ""], + max_sequence_length=16, + embedding_size=32, + d_model=32, + n_layers=2, + d_conv=4, + expand_factor=2, + reduce_output="mean", + ).to(DEVICE) + x = torch.randint(0, 7, (4, 16), device=DEVICE) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert ENCODER_OUTPUT in out + assert out[ENCODER_OUTPUT].shape[0] == 4 + + def test_mamba_encoder_no_reduce(self): + from ludwig.encoders.sequence_encoders import MambaEncoder + + enc = MambaEncoder( + vocab=["a", "b", "c", "", ""], + max_sequence_length=8, + embedding_size=16, + d_model=16, + n_layers=1, + reduce_output=None, + ).to(DEVICE) + x = torch.randint(0, 5, (2, 8), device=DEVICE) + out = enc(x) + from ludwig.constants import ENCODER_OUTPUT + + assert out[ENCODER_OUTPUT].dim() == 3 # [batch, seq, hidden] + + def test_mamba_encoder_schema(self): + from ludwig.schema.encoders.sequence_encoders import MambaEncoderConfig + + cfg = MambaEncoderConfig.model_validate({"type": "mamba"}) + assert cfg.type == "mamba" + assert cfg.n_layers >= 1 + + +# ============================================================================ +# 1.5.1 — Image Encoder Tests +# ============================================================================ +class TestTorchVisionConsolidation: + """Test that TorchVision encoders still work after consolidation.""" + + def test_tv_base_has_softmax_removal_strategies(self): + from ludwig.encoders.image.torchvision import TVBaseEncoder + + assert hasattr(TVBaseEncoder, "_softmax_removal") + + def test_tv_encoder_subclass_is_thin(self): + """Each TV encoder subclass should only set class attributes, no _remove_softmax_layer override.""" + from ludwig.encoders.image.torchvision import TVAlexNetEncoder, TVResNetEncoder + + # These should NOT override _remove_softmax_layer anymore + assert "_remove_softmax_layer" not in TVResNetEncoder.__dict__ + assert "_remove_softmax_layer" not in TVAlexNetEncoder.__dict__ + + def test_tv_resnet_schema(self): + from ludwig.schema.encoders.image.torchvision import TVResNetEncoderConfig + + cfg = TVResNetEncoderConfig.model_validate({"type": "resnet", "model_variant": 50}) + assert cfg.type == "resnet" + + +class TestPretrainedImageEncoders: + """Test CLIP, DINOv2, SigLIP encoder registration and schema.""" + + def test_clip_schema(self): + from ludwig.schema.encoders.image.pretrained import CLIPImageEncoderConfig + + cfg = CLIPImageEncoderConfig.model_validate({"type": "clip"}) + assert cfg.type == "clip" + assert "clip" in cfg.pretrained_model_name_or_path + + def test_dinov2_schema(self): + from ludwig.schema.encoders.image.pretrained import DINOv2ImageEncoderConfig + + cfg = DINOv2ImageEncoderConfig.model_validate({"type": "dinov2"}) + assert cfg.type == "dinov2" + assert "dinov2" in cfg.pretrained_model_name_or_path + + def test_siglip_schema(self): + from ludwig.schema.encoders.image.pretrained import SigLIPImageEncoderConfig + + cfg = SigLIPImageEncoderConfig.model_validate({"type": "siglip"}) + assert cfg.type == "siglip" + assert "siglip" in cfg.pretrained_model_name_or_path + + def test_pretrained_encoders_registered(self): + from ludwig.constants import IMAGE + from ludwig.encoders.registry import get_encoder_registry + + reg = get_encoder_registry() + for name in ["clip", "dinov2", "siglip"]: + assert name in reg[IMAGE], f"{name} not registered for IMAGE" + + +# ============================================================================ +# 1.5.2 — Audio Encoder Tests +# ============================================================================ +class TestAudioEncoders: + """Test pretrained audio encoder registration and schema.""" + + def test_wav2vec2_schema(self): + from ludwig.schema.encoders.audio_encoders import Wav2Vec2EncoderConfig + + cfg = Wav2Vec2EncoderConfig.model_validate({"type": "wav2vec2"}) + assert cfg.type == "wav2vec2" + + def test_whisper_schema(self): + from ludwig.schema.encoders.audio_encoders import WhisperEncoderConfig + + cfg = WhisperEncoderConfig.model_validate({"type": "whisper"}) + assert cfg.type == "whisper" + + def test_hubert_schema(self): + from ludwig.schema.encoders.audio_encoders import HuBERTEncoderConfig + + cfg = HuBERTEncoderConfig.model_validate({"type": "hubert"}) + assert cfg.type == "hubert" + + def test_audio_encoders_registered(self): + from ludwig.constants import AUDIO + from ludwig.encoders.registry import get_encoder_registry + + reg = get_encoder_registry() + for name in ["wav2vec2", "whisper", "hubert"]: + assert name in reg[AUDIO], f"{name} not registered for AUDIO" + + +# ============================================================================ +# 1.5.3 — Text Encoder Cleanup Tests +# ============================================================================ +class TestTextEncoderCleanup: + def test_dead_encoders_removed(self): + """TransformerXL, CTRL, and FlauBERT should be removed.""" + from ludwig.constants import TEXT + from ludwig.encoders.registry import get_encoder_registry + + reg = get_encoder_registry() + text_encoders = reg.get(TEXT, {}) + assert "transformer_xl" not in text_encoders + assert "ctrl" not in text_encoders + assert "flaubert" not in text_encoders + + def test_kept_encoders_exist(self): + """BERT, RoBERTa, DistilBERT should still be registered.""" + from ludwig.constants import TEXT + from ludwig.encoders.registry import get_encoder_registry + + reg = get_encoder_registry() + text_encoders = reg.get(TEXT, {}) + for name in [ + "bert", + "roberta", + "distilbert", + "albert", + "electra", + "deberta", + "xlnet", + "gpt2", + "t5", + "mt5", + "auto_transformer", + "tf_idf", + ]: + assert name in text_encoders, f"{name} should still be registered for TEXT" + + def test_encoder_docstrings(self): + """All text encoders should have docstrings after cleanup.""" + import ludwig.encoders.text_encoders as te + + for name in ["BERTEncoder", "RoBERTaEncoder", "DistilBERTEncoder", "AutoTransformerEncoder", "TfIdfEncoder"]: + cls = getattr(te, name, None) + if cls is not None: + assert cls.__doc__ is not None, f"{name} should have a docstring" + assert len(cls.__doc__) > 50, f"{name} docstring too short" + + +# ============================================================================ +# Integration: Registry Completeness +# ============================================================================ +class TestRegistryCompleteness: + def test_all_new_encoders_in_registry(self): + from ludwig.constants import AUDIO, CATEGORY, IMAGE, NUMBER, SEQUENCE + from ludwig.encoders.registry import get_encoder_registry + + reg = get_encoder_registry() + + expected = { + IMAGE: ["clip", "dinov2", "siglip"], + AUDIO: ["wav2vec2", "whisper", "hubert"], + CATEGORY: ["target", "hash"], + NUMBER: ["bins"], + SEQUENCE: ["mamba"], + } + + for feat, names in expected.items(): + for name in names: + assert name in reg[feat], f"Encoder '{name}' not found in {feat} registry" + + def test_all_new_schema_configs_loadable(self): + """All new encoder configs should be loadable with just their type.""" + configs = [ + ("ludwig.schema.encoders.image.pretrained", "CLIPImageEncoderConfig", "clip"), + ("ludwig.schema.encoders.image.pretrained", "DINOv2ImageEncoderConfig", "dinov2"), + ("ludwig.schema.encoders.image.pretrained", "SigLIPImageEncoderConfig", "siglip"), + ("ludwig.schema.encoders.audio_encoders", "Wav2Vec2EncoderConfig", "wav2vec2"), + ("ludwig.schema.encoders.audio_encoders", "WhisperEncoderConfig", "whisper"), + ("ludwig.schema.encoders.audio_encoders", "HuBERTEncoderConfig", "hubert"), + ("ludwig.schema.encoders.category_encoders", "CategoricalTargetEncoderConfig", "target"), + ("ludwig.schema.encoders.category_encoders", "CategoricalHashEncoderConfig", "hash"), + ("ludwig.schema.encoders.number_encoders", "BinsEncoderConfig", "bins"), + ] + + import importlib + + for module_path, cls_name, type_val in configs: + mod = importlib.import_module(module_path) + cls = getattr(mod, cls_name) + cfg = cls.model_validate({"type": type_val}) + assert cfg.type == type_val, f"Config {cls_name} type mismatch" diff --git a/tests/ludwig/encoders/test_sequence_encoders.py b/tests/ludwig/encoders/test_sequence_encoders.py new file mode 100644 index 0000000..c0404c8 --- /dev/null +++ b/tests/ludwig/encoders/test_sequence_encoders.py @@ -0,0 +1,56 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.sequence_encoders import ( + SequenceEmbedEncoder, + SequencePassthroughEncoder, + StackedRNN, + StackedTransformer, +) +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +DEVICE = get_torch_device() +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("reduce_output", ["mean", "last", "concat", None]) +def test_sequence_passthrough_encoder(reduce_output: str): + batch_size = 10 + sequence_length = 32 + sequence_passthrough_encoder = SequencePassthroughEncoder( + reduce_output=reduce_output, max_sequence_length=sequence_length, encoding_size=8 + ).to(DEVICE) + inputs = torch.rand(batch_size, sequence_length, 8).to(DEVICE) + outputs = sequence_passthrough_encoder(inputs) + # SequencePassthroughEncoder does not implement output_shape, expect output to match input shape after reduce. + assert outputs[ENCODER_OUTPUT].shape[1:] == sequence_passthrough_encoder.reduce_sequence.output_shape + + +@pytest.mark.parametrize( + "encoder_type", + [SequenceEmbedEncoder, StackedRNN, StackedTransformer], +) +@pytest.mark.parametrize("reduce_output", ["mean", "last", "concat", None]) +@pytest.mark.parametrize("vocab_size", [2, 1024]) # Uses vocabularies smaller than (and larger than) embedding size. +def test_sequence_encoders(encoder_type: type, reduce_output: str, vocab_size: int): + # make repeatable + torch.manual_seed(RANDOM_SEED) + + batch_size = 10 + sequence_length = 32 + sequence_encoder = encoder_type( + vocab=list(range(1, vocab_size + 1)), max_sequence_length=sequence_length, reduce_output=reduce_output + ).to(DEVICE) + inputs = torch.randint(2, (batch_size, sequence_length)).to(DEVICE) + outputs = sequence_encoder(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == sequence_encoder.output_shape + + # check for parameter updating + target = torch.randn(outputs[ENCODER_OUTPUT].shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(sequence_encoder, (inputs,), target) + + assert upc == tpc, ( + f"Not all parameters updated. Parameters not updated: {not_updated}.\nModule: {sequence_encoder}" + ) diff --git a/tests/ludwig/encoders/test_set_encoders.py b/tests/ludwig/encoders/test_set_encoders.py new file mode 100644 index 0000000..f3eee6a --- /dev/null +++ b/tests/ludwig/encoders/test_set_encoders.py @@ -0,0 +1,41 @@ +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.encoders.set_encoders import SetSparseEncoder +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 +DEVICE = get_torch_device() + + +@pytest.mark.parametrize("num_fc_layers", [0, 2]) +@pytest.mark.parametrize("vocab", [["a", "b", "c", "d", "e", "f", "g", "h"]]) +@pytest.mark.parametrize("embedding_size", [10]) +@pytest.mark.parametrize("representation", ["sparse"]) +def test_set_encoder( + vocab: list[str], + embedding_size: int, + representation: str, + num_fc_layers: int, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + # setup encoder to test + set_encoder = SetSparseEncoder( + vocab=vocab, + representation=representation, + embedding_size=embedding_size, + num_fc_layers=num_fc_layers, + ).to(DEVICE) + inputs = torch.randint(0, 2, size=(2, len(vocab))).bool().to(DEVICE) + outputs = set_encoder(inputs)[ENCODER_OUTPUT] + assert outputs.shape[1:] == set_encoder.output_shape + + # check for parameter updating + target = torch.randn(outputs.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(set_encoder, (inputs,), target) + assert tpc == upc, f"Failed to update parameters. Parameters not updated: {not_updated}" diff --git a/tests/ludwig/encoders/test_text_encoders.py b/tests/ludwig/encoders/test_text_encoders.py new file mode 100644 index 0000000..98e05c2 --- /dev/null +++ b/tests/ludwig/encoders/test_text_encoders.py @@ -0,0 +1,314 @@ +import json +import os +from unittest import mock + +import pytest +import torch + +import ludwig.schema.encoders.utils as schema_encoders_utils +from ludwig.api import LudwigModel +from ludwig.constants import ENCODER, ENCODER_OUTPUT, MODEL_ECD, NAME, TEXT, TRAINER +from ludwig.encoders import text_encoders +from ludwig.error import ConfigValidationError +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.data_utils import load_json +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated +from tests.integration_tests.utils import ( + category_feature, + clear_huggingface_cache, + generate_data, + HF_ENCODERS, + HF_ENCODERS_SHORT, + LocalTestBackend, + text_feature, +) + +DEVICE = get_torch_device() +RANDOM_SEED = 1919 + + +def _load_pretrained_hf_model_no_weights( + modelClass: type, + pretrained_model_name_or_path: str | os.PathLike | None, + **pretrained_kwargs, +): + """Loads a HF model architecture without loading the weights.""" + from transformers import AutoConfig, AutoModel + + config = AutoConfig.from_pretrained(pretrained_model_name_or_path) + return AutoModel.from_config(config), False + + +def get_mismatched_config_params(ludwig_results_dir, ludwig_model): + saved_config_dict = load_json(os.path.join(ludwig_results_dir, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME)) + saved_config_obj = ModelConfig.from_dict(saved_config_dict) + + mismatches = [] + for input_feature_config in saved_config_obj.input_features.to_list(): + feature_name = input_feature_config[NAME] + encoder_config_from_file = input_feature_config[ENCODER] + encoder_config_from_model = ludwig_model.model.input_features.get(feature_name).encoder_obj.config.to_dict() + for k, v in encoder_config_from_model.items(): + # Skip saved_weights_in_checkpoint because this value is not yet set when the global config + # is modified with the final encoder config. + if k == "saved_weights_in_checkpoint": + continue + + if encoder_config_from_file[k] != v: + mismatch = { + "feature_name": feature_name, + "param_name": k, + "val_from_file": encoder_config_from_file[k], + "val_from_model": v, + } + mismatches.append(mismatch) + return mismatches + + +# Use a curated subset of HF encoders that are compatible with transformers 5.x. +# Full encoder-schema coverage is tested by test_encoder_names_constant_synced_with_schema. +_HF_ENCODERS_E2E = ["albert", "bert", "distilbert", "electra", "roberta", "auto_transformer"] + + +@pytest.mark.parametrize("encoder_name", _HF_ENCODERS_E2E) +def test_hf_ludwig_model_e2e(tmpdir, csv_filename, encoder_name): + """Tests HuggingFace encoders end-to-end. + + This test validates the following: + 1. Encoder config defaults are compatible with Ludwig experiments. + 2. Ludwig correctly updates the encoder config with the parameters introduced by the HF encoder. + 3. Ludwig correctly loads checkpoints containing HF encoder weights. + """ + input_features = [ + text_feature( + encoder={ + "vocab_size": 30, + "min_len": 1, + "type": encoder_name, + } + ) + ] + output_features = [category_feature(decoder={"vocab_size": 2})] + rel_path = generate_data(input_features, output_features, csv_filename) + + if encoder_name == "auto_transformer": + # need to explciitly set the pretrained model name for auto_transformer + input_features[0][ENCODER]["pretrained_model_name_or_path"] = ( + "hf-internal-testing/tiny-bert-for-token-classification" + ) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1}, + } + model = LudwigModel(config=config, backend=LocalTestBackend()) + + with mock.patch( + "ludwig.encoders.text_encoders.load_pretrained_hf_model_with_hub_fallback", + side_effect=_load_pretrained_hf_model_no_weights, + ): + # Validates that the defaults associated with the encoder are compatible with Ludwig training. + _, _, _, results_dir = model.experiment(dataset=rel_path, output_directory=tmpdir) + + # Validate that the saved config reflects the parameters introduced by the HF encoder. + # This ensures that the config updates after initializing the encoder. + mismatched_config_params = get_mismatched_config_params(results_dir, model) + if len(mismatched_config_params) > 0: + raise AssertionError( + f"Config parameters mismatched with encoder parameters: " + f"{json.dumps(mismatched_config_params, indent=4)}" + ) + + # Validate the model can be loaded. + # This ensures that the config reflects the internal architecture of the encoder. + LudwigModel.load(os.path.join(results_dir, MODEL_FILE_NAME)) + clear_huggingface_cache() + + +@pytest.mark.parametrize("reduce_output", [None, "last", "sum", "mean", "max", "concat"]) +@pytest.mark.parametrize("encoder_name", HF_ENCODERS_SHORT) +def test_hf_ludwig_model_reduce_options(tmpdir, csv_filename, encoder_name, reduce_output): + input_features = [ + text_feature( + preprocessing={ + "max_sequence_length": 10, + }, + encoder={ + "vocab_size": 30, + "min_len": 1, + "type": encoder_name, + "reduce_output": reduce_output, + }, + ) + ] + output_features = [category_feature(decoder={"vocab_size": 2})] + rel_path = generate_data(input_features, output_features, csv_filename) + + if encoder_name == "auto_transformer": + # need to explciitly set the pretrained model name for auto_transformer + input_features[0][ENCODER]["pretrained_model_name_or_path"] = ( + "hf-internal-testing/tiny-bert-for-token-classification" + ) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1}, + } + + try: + ModelConfig.from_dict(config) + except ConfigValidationError as e: + pytest.skip(e.message) + + model = LudwigModel( + config=config, + backend=LocalTestBackend(), + ) + + # Validates that the defaults associated with the encoder are compatible with Ludwig training. + with mock.patch( + "ludwig.encoders.text_encoders.load_pretrained_hf_model_with_hub_fallback", + side_effect=_load_pretrained_hf_model_no_weights, + ): + model.train( + dataset=rel_path, + output_directory=tmpdir, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + + clear_huggingface_cache() + + +@pytest.mark.parametrize( + "pretrained_model_name_or_path", + [ + "hf-internal-testing/tiny-random-OPTModel", + "hf-internal-testing/tiny-random-BertModel", + "hf-internal-testing/tiny-random-DistilBertModel", + ], +) +def test_hf_ludwig_model_auto_transformers(tmpdir, csv_filename, pretrained_model_name_or_path): + """Tests different AutoModel types to ensure our wrapper handles them correctly. + + This is needed because different PretrainedModel implemetnations have different input / output signatures. + """ + input_features = [ + text_feature( + preprocessing={ + "max_sequence_length": 10, + }, + encoder={ + "vocab_size": 30, + "min_len": 1, + "type": "auto_transformer", + "pretrained_model_name_or_path": pretrained_model_name_or_path, + }, + ) + ] + output_features = [category_feature(decoder={"vocab_size": 2})] + rel_path = generate_data(input_features, output_features, csv_filename) + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1}, + } + model = LudwigModel(config=config, backend=LocalTestBackend()) + + # Validates that the defaults associated with the encoder are compatible with Ludwig training. + with mock.patch( + "ludwig.encoders.text_encoders.load_pretrained_hf_model_with_hub_fallback", + side_effect=_load_pretrained_hf_model_no_weights, + ): + model.train(dataset=rel_path, output_directory=tmpdir) + + +@pytest.mark.parametrize("trainable", [True, False]) +def test_distilbert_param_updates(trainable: bool): + max_sequence_length = 20 + distil_bert_encoder = text_encoders.DistilBERTEncoder( + use_pretrained=False, + max_sequence_length=max_sequence_length, + trainable=trainable, + ).to(DEVICE) + + # send a random input through the model with its initial weights + inputs = torch.rand((2, max_sequence_length)).type(distil_bert_encoder.input_dtype).to(DEVICE) + outputs = distil_bert_encoder(inputs) + + # perform a backward pass to update the model params + target = torch.randn(outputs[ENCODER_OUTPUT].shape).to(DEVICE) + check_module_parameters_updated(distil_bert_encoder, (inputs,), target) + + # send the same input through the model again. should be different if trainable, else the same + outputs2 = distil_bert_encoder(inputs) + + encoder_output1 = outputs[ENCODER_OUTPUT] + encoder_output2 = outputs2[ENCODER_OUTPUT] + + if trainable: + # Outputs should be different if the model was updated + assert not torch.equal(encoder_output1, encoder_output2) + else: + # Outputs should be the same if the model wasn't updated + assert torch.equal(encoder_output1, encoder_output2) + + +@pytest.mark.parametrize("encoder_name", HF_ENCODERS) +def test_encoder_names_constant_synced_with_schema(encoder_name): + """Ensures that each value in the HF_ENCODERS constant is represented by an equivalent schema object.""" + schema_encoders_utils.get_encoder_cls(MODEL_ECD, TEXT, encoder_name) + + +@pytest.mark.parametrize("vocab_size", [20]) +def test_tfidf_encoder(vocab_size: int): + # make repeatable + torch.manual_seed(RANDOM_SEED) + + batch_size = 10 + sequence_length = 32 + vocab = [str(i) for i in range(1, vocab_size + 1)] + str2idf = dict.fromkeys(vocab, 1) + text_encoder = text_encoders.TfIdfEncoder( + max_sequence_length=sequence_length, + str2idf=str2idf, + vocab=vocab, + vocab_size=vocab_size, + ).to(DEVICE) + + assert len(text_encoder.output_shape) == 1 + assert text_encoder.output_shape[0] == vocab_size + assert len(list(text_encoder.parameters())) == 0 + + inputs = torch.randint(2, (batch_size, sequence_length)).to(DEVICE) + outputs = text_encoder(inputs) + assert outputs[ENCODER_OUTPUT].shape[1:] == text_encoder.output_shape + + +def test_hf_auto_transformer_use_pretrained(): + """This test ensures that use_pretrained is always True when using the auto_transformer text encoder even if a + user explicitly sets it to False.""" + config = { + "input_features": [ + text_feature( + encoder={ + "type": "auto_transformer", + "use_pretrained": False, + "pretrained_model_name_or_path": "hf-internal-testing/tiny-random-bloom", + }, + ) + ], + "output_features": [category_feature(decoder={"vocab_size": 2})], + } + + model = LudwigModel(config=config, backend=LocalTestBackend()) + assert model.config_obj.input_features[0].encoder.use_pretrained diff --git a/tests/ludwig/encoders/test_timm_encoder.py b/tests/ludwig/encoders/test_timm_encoder.py new file mode 100644 index 0000000..1a82408 --- /dev/null +++ b/tests/ludwig/encoders/test_timm_encoder.py @@ -0,0 +1,50 @@ +import pytest +import torch + +timm = pytest.importorskip("timm", reason="timm not installed") + +from ludwig.encoders.image.timm import ( # noqa: E402 + TimmCAFormerEncoder, + TimmConvFormerEncoder, + TimmEncoder, + TimmPoolFormerEncoder, +) + + +@pytest.mark.parametrize( + "encoder_cls,model_name", + [ + (TimmEncoder, "resnetv2_50"), + (TimmCAFormerEncoder, "caformer_s18"), + (TimmConvFormerEncoder, "convformer_s18"), + (TimmPoolFormerEncoder, "poolformerv2_s12"), + ], + ids=["timm_resnet", "caformer", "convformer", "poolformer"], +) +def test_timm_encoder_forward(encoder_cls, model_name): + encoder = encoder_cls(model_name=model_name, use_pretrained=False, trainable=True) + + # Get the expected input shape from the encoder + input_shape = encoder.input_shape # (C, H, W) + batch = torch.randn(2, *input_shape) + + output = encoder(batch) + assert "encoder_output" in output + + out_tensor = output["encoder_output"] + assert out_tensor.shape[0] == 2 + assert out_tensor.shape[1:] == encoder.output_shape + + +@pytest.mark.parametrize("trainable", [True, False]) +def test_timm_encoder_trainable(trainable): + encoder = TimmCAFormerEncoder(model_name="caformer_s18", use_pretrained=False, trainable=trainable) + + for p in encoder.model.parameters(): + assert p.requires_grad == trainable + + +def test_timm_encoder_output_shape_property(): + encoder = TimmEncoder(model_name="caformer_s18", use_pretrained=False) + assert len(encoder.output_shape) == 1 + assert encoder.output_shape[0] > 0 diff --git a/tests/ludwig/evaluation/test_evaluation.py b/tests/ludwig/evaluation/test_evaluation.py new file mode 100644 index 0000000..d5d7d15 --- /dev/null +++ b/tests/ludwig/evaluation/test_evaluation.py @@ -0,0 +1,81 @@ +import os + +import pandas as pd +import yaml + +from ludwig.api import LudwigModel + + +def test_eval_steps_determinism(): + # Force CPU to avoid CUBLAS errors with tiny random LLM models on GPU. + old_val = os.environ.get("CUDA_VISIBLE_DEVICES") + os.environ["CUDA_VISIBLE_DEVICES"] = "" + try: + _run_eval_steps_determinism() + finally: + if old_val is None: + os.environ.pop("CUDA_VISIBLE_DEVICES", None) + else: + os.environ["CUDA_VISIBLE_DEVICES"] = old_val + + +def _run_eval_steps_determinism(): + df = pd.DataFrame( + { + "in": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t"], + "out": [i for i in range(20)], + "split": ([0] * 10) + ([2] * 10), + } + ) + config = yaml.safe_load(""" + model_type: llm + base_model: hf-internal-testing/tiny-random-GPT2LMHeadModel + + input_features: + - name: in + type: text + + output_features: + - name: out + type: text + + prompt: + template: >- + {in} + + generation: + temperature: null + do_sample: False + max_new_tokens: 64 + + preprocessing: + split: + type: fixed + column: split + + trainer: + type: finetune + epochs: 1 + batch_size: 1 + eval_batch_size: 2 + learning_rate: 0.00001 + gradient_clipping: + clipglobalnorm: 1.0 + + backend: + type: local + """) + model = LudwigModel(config=config) + model.train(df) + results1 = model.evaluate(df) + + model.config_obj.trainer.eval_steps = 4 + results2 = model.evaluate(df) + results3 = model.evaluate(df) + + for k in results1[0]["out"]: + # The core assertion: repeated evaluations with the same eval_steps + # setting must produce identical results (determinism). + assert results2[0]["out"][k] == results3[0]["out"][k], ( + f"Metric '{k}' differs between repeated evaluations: {results2[0]['out'][k]} vs {results3[0]['out'][k]}" + ) diff --git a/tests/ludwig/explain/test_captum.py b/tests/ludwig/explain/test_captum.py new file mode 100644 index 0000000..36f4732 --- /dev/null +++ b/tests/ludwig/explain/test_captum.py @@ -0,0 +1,60 @@ +import torch + +from ludwig.explain.captum import get_token_attributions + + +def test_get_token_attributions(): + feature_name = "text_8D824" + input_ids = torch.tensor([[1, 5, 6, 4, 4, 4, 6, 0, 2], [1, 4, 5, 6, 4, 4, 6, 5, 0]], dtype=torch.int8) + model = type("Model", (), {})() + model.training_set_metadata = { + feature_name: { + "idx2str": [ + "", + "", + "", + "", + "oypszb", + "yscnrkzw", + "llcgslcvzr", + ] + } + } + token_attributions = torch.tensor( + [ + [-0.1289, -0.3222, -0.4931, -0.2914, -0.2891, -0.2871, -0.4118, -0.4647, 0.0000], + # zero norm should not lead to division by zero + [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], + ], + dtype=torch.float64, + ) + + toks_and_attrs = get_token_attributions(model, feature_name, input_ids, token_attributions) + + # assert equality up to 4 decimal places + assert [[(ta[0], round(ta[1], 4)) for ta in tas] for tas in toks_and_attrs] == [ + [ + # normalized attributions + ("", -0.1289), + ("yscnrkzw", -0.3222), + ("llcgslcvzr", -0.4931), + ("oypszb", -0.2914), + ("oypszb", -0.2891), + ("oypszb", -0.2871), + ("llcgslcvzr", -0.4118), + ("", -0.4647), + ("", 0.0), + ], + [ + # zero norm should retain original zero attributions + ("", 0.0), + ("oypszb", 0.0), + ("yscnrkzw", 0.0), + ("llcgslcvzr", 0.0), + ("oypszb", 0.0), + ("oypszb", 0.0), + ("llcgslcvzr", 0.0), + ("yscnrkzw", 0.0), + ("", 0.0), + ], + ] diff --git a/tests/ludwig/explain/test_util.py b/tests/ludwig/explain/test_util.py new file mode 100644 index 0000000..f52f2d3 --- /dev/null +++ b/tests/ludwig/explain/test_util.py @@ -0,0 +1,93 @@ +import logging +import os + +import pandas as pd +import torch + +from ludwig.api import LudwigModel +from ludwig.constants import NAME +from ludwig.explain.util import get_absolute_module_key_from_submodule, replace_layer_with_copy +from tests.integration_tests.utils import binary_feature, generate_data, LocalTestBackend, text_feature + + +def test_get_absolute_module_key_from_submodule(): + class ParentModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.child_module_1 = ChildModule() + self.child_module_2 = ChildModule() + + class ChildModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear = torch.nn.Linear(10, 10) + + # the expected module names are those that are relative to the parent module, i.e. "child_module_1.linear.weight" + parent_module = ParentModule() + expected_module_names = set() + for parent_param_name, _ in parent_module.named_parameters(): + expected_module_names.add(parent_param_name) + + # incorrect module names are those that are relative to the child module, not the parent module, + # i.e. "linear.weight" and "linear.bias" + incorrect_param_names = set() + for child_param_name, _ in parent_module.child_module_1.named_parameters(): + incorrect_param_names.add(child_param_name) + + module_names_child_1 = set(get_absolute_module_key_from_submodule(parent_module, parent_module.child_module_1)) + module_names_child_2 = set(get_absolute_module_key_from_submodule(parent_module, parent_module.child_module_2)) + + # check that the module names are not equivalent to the incorrect module names + assert set.isdisjoint(module_names_child_1, incorrect_param_names) + assert set.isdisjoint(module_names_child_2, incorrect_param_names) + + # check that the module names are disjoint from one another because they are relative to the parent module + assert set.isdisjoint(module_names_child_1, module_names_child_2) + + # check that the union of the two sets is equal to the expected module names + assert set.union(module_names_child_1, module_names_child_2) == expected_module_names + + +def test_replace_layer_with_copy(tmpdir): + text_feature_1 = text_feature() + text_feature_2 = text_feature(tied=text_feature_1["name"]) + input_features = [text_feature_1, text_feature_2] + output_features = [binary_feature()] + + csv_filename = os.path.join(tmpdir, "training.csv") + generate_data(input_features, output_features, csv_filename, num_examples=200) + df = pd.read_csv(csv_filename) + config = { + "input_features": input_features, + "output_features": output_features, + "trainer": { + "epochs": 1, + }, + } + model = LudwigModel(config, logging_level=logging.WARNING, backend=LocalTestBackend()) + model.train(df) + + input_feature_module = model.model.input_features.get(text_feature_2[NAME]) + target_layer = input_feature_module.encoder_obj.get_embedding_layer() + + data_ptrs_before = {} + for param_name, param in input_feature_module.named_parameters(): + data_ptrs_before[param_name] = param.data_ptr() + + keys_to_copy = get_absolute_module_key_from_submodule(input_feature_module, target_layer) + replace_layer_with_copy(input_feature_module, target_layer) + + data_ptrs_after = {} + for param_name, param in input_feature_module.named_parameters(): + data_ptrs_after[param_name] = param.data_ptr() + + # Check that the data pointers are different for the copied keys and that they are the same for the rest. + for param_name, _ in input_feature_module.named_parameters(): + if param_name in keys_to_copy: + assert data_ptrs_before[param_name] != data_ptrs_after[param_name], ( + f"Data pointers should be different for copied key {param_name}" + ) + else: + assert data_ptrs_before[param_name] == data_ptrs_after[param_name], ( + f"Data pointers should be the same for non-copied key {param_name}" + ) diff --git a/tests/ludwig/features/__init__.py b/tests/ludwig/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/features/test_audio_feature.py b/tests/ludwig/features/test_audio_feature.py new file mode 100644 index 0000000..54248e1 --- /dev/null +++ b/tests/ludwig/features/test_audio_feature.py @@ -0,0 +1,96 @@ +import os +from random import choice +from string import ascii_lowercase, ascii_uppercase, digits + +import pandas as pd +import pytest +import torch + +from ludwig.backend import LOCAL_BACKEND +from ludwig.constants import BFILL, ENCODER_OUTPUT, PROC_COLUMN +from ludwig.features.audio_feature import AudioFeatureMixin, AudioInputFeature +from ludwig.schema.features.audio_feature import AudioInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.utils import audio_feature, category_feature, generate_data + +BATCH_SIZE = 2 +SEQ_SIZE = 20 +AUDIO_W_SIZE = 16 + +CHARS = ascii_uppercase + ascii_lowercase + digits +VOCAB = ["".join(choice(CHARS) for _ in range(2)) for _ in range(256)] +DEVICE = get_torch_device() + + +@pytest.mark.parametrize("encoder", ["rnn", "stacked_cnn", "parallel_cnn", "stacked_parallel_cnn", "rnn", "cnnrnn"]) +def test_audio_input_feature(encoder: str) -> None: + audio_config = { + "name": "audio_feature", + "type": "audio", + "preprocessing": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_filter_bands": 80, + "audio_file_length_limit_in_s": 3.0, + }, + "encoder": { + "type": encoder, + "should_embed": False, + "vocab": VOCAB, + "max_sequence_length": SEQ_SIZE, + "embedding_size": AUDIO_W_SIZE, + }, + } + + audio_config, _ = load_config_with_kwargs(AudioInputFeatureConfig, audio_config) + audio_input_feature = AudioInputFeature(audio_config).to(DEVICE) + + audio_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE, AUDIO_W_SIZE], dtype=torch.float32).to(DEVICE) + encoder_output = audio_input_feature(audio_tensor) + assert encoder_output[ENCODER_OUTPUT].shape[1:] == audio_input_feature.output_shape + + +@pytest.mark.parametrize("feature_type", ["raw", "stft", "stft_phase", "group_delay", "fbank"]) +def test_add_feature_data(feature_type, tmpdir): + preprocessing_params = { + "audio_file_length_limit_in_s": 3.0, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": "per_file", + "type": feature_type, + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_fft_points": None, + "window_type": "hamming", + "num_filter_bands": 80, + } + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + audio_feature_config = audio_feature(audio_dest_folder, preprocessing=preprocessing_params) + data_df_path = generate_data( + [audio_feature_config], + [category_feature(vocab_size=5, reduce_input="sum")], + os.path.join(tmpdir, "data.csv"), + num_examples=10, + ) + data_df = pd.read_csv(data_df_path) + metadata = { + audio_feature_config["name"]: AudioFeatureMixin.get_feature_meta( + {}, data_df[audio_feature_config["name"]], preprocessing_params, LOCAL_BACKEND, True + ) + } + + proc_df = {} + AudioFeatureMixin.add_feature_data( + feature_config=audio_feature_config, + input_df=data_df, + proc_df=proc_df, + metadata=metadata, + preprocessing_parameters=preprocessing_params, + backend=LOCAL_BACKEND, + skip_save_processed_input=False, + ) + + assert len(proc_df[audio_feature_config[PROC_COLUMN]]) == 10 diff --git a/tests/ludwig/features/test_bag_feature.py b/tests/ludwig/features/test_bag_feature.py new file mode 100644 index 0000000..3e8b266 --- /dev/null +++ b/tests/ludwig/features/test_bag_feature.py @@ -0,0 +1,44 @@ +from random import choice +from string import ascii_lowercase, ascii_uppercase, digits + +import pytest +import torch + +from ludwig.constants import ENCODER, ENCODER_OUTPUT +from ludwig.features.bag_feature import BagInputFeature +from ludwig.schema.features.bag_feature import BagInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +SEQ_SIZE = 20 +BAG_W_SIZE = 256 +EMBEDDING_SIZE = 5 + +CHARS = ascii_uppercase + ascii_lowercase + digits +VOCAB = ["".join(choice(CHARS) for _ in range(2)) for _ in range(256)] +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def bag_config(): + return { + "name": "bag_feature", + "type": "bag", + "encoder": { + "max_len": 5, + "vocab_size": 10, + "embedding_size": EMBEDDING_SIZE, + "vocab": VOCAB, + }, + } + + +@pytest.mark.parametrize("encoder", ["embed"]) +def test_bag_input_feature(bag_config: dict, encoder: str) -> None: + bag_config[ENCODER].update({"type": encoder}) + bag_config, _ = load_config_with_kwargs(BagInputFeatureConfig, bag_config) + bag_input_feature = BagInputFeature(bag_config).to(DEVICE) + bag_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE, BAG_W_SIZE], dtype=torch.float32).to(DEVICE) + encoder_output = bag_input_feature(bag_tensor) + assert encoder_output[ENCODER_OUTPUT].shape[1:][1:] == bag_input_feature.output_shape diff --git a/tests/ludwig/features/test_binary_feature.py b/tests/ludwig/features/test_binary_feature.py new file mode 100644 index 0000000..685ae37 --- /dev/null +++ b/tests/ludwig/features/test_binary_feature.py @@ -0,0 +1,91 @@ +import pytest +import torch + +from ludwig.constants import ENCODER, ENCODER_OUTPUT +from ludwig.features.binary_feature import BinaryInputFeature, BinaryOutputFeature +from ludwig.schema.features.binary_feature import BinaryInputFeatureConfig, BinaryOutputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +BINARY_W_SIZE = 1 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def binary_config(): + return { + "name": "binary_feature", + "type": "binary", + } + + +@pytest.mark.parametrize("encoder", ["passthrough", "dense"]) +def test_binary_input_feature(binary_config: dict, encoder: str): + binary_config.update({ENCODER: {"type": encoder}}) + binary_config, _ = load_config_with_kwargs(BinaryInputFeatureConfig, binary_config) + binary_input_feature = BinaryInputFeature(binary_config).to(DEVICE) + + binary_tensor = binary_input_feature.create_sample_input(batch_size=BATCH_SIZE).to(DEVICE) + assert binary_tensor.shape == torch.Size([BATCH_SIZE]) + assert binary_tensor.dtype == torch.bool + + encoder_output = binary_input_feature(binary_tensor) + + assert encoder_output[ENCODER_OUTPUT].shape[1:] == binary_input_feature.output_shape + + +def test_binary_output_feature(): + binary_output_config = { + "name": "binary_feature", + "type": "binary", + "input_size": BINARY_W_SIZE, + "decoder": { + "type": "regressor", + "input_size": 1, + }, + "loss": { + "type": "binary_weighted_cross_entropy", + "positive_class_weight": 1, + "robust_lambda": 0, + "confidence_penalty": 0, + }, + } + binary_output_config, _ = load_config_with_kwargs(BinaryOutputFeatureConfig, binary_output_config) + binary_output_feature = BinaryOutputFeature(binary_output_config, {}).to(DEVICE) + combiner_outputs = dict() + combiner_outputs["combiner_output"] = torch.randn([BATCH_SIZE, BINARY_W_SIZE], dtype=torch.float32).to(DEVICE) + + binary_output = binary_output_feature(combiner_outputs, {}) + + assert "last_hidden" in binary_output + assert "logits" in binary_output + assert binary_output["logits"].size() == torch.Size([BATCH_SIZE]) + + +def test_binary_output_feature_without_positive_class_weight(): + binary_output_config = { + "name": "binary_feature", + "type": "binary", + "input_size": BINARY_W_SIZE, + "decoder": { + "type": "regressor", + "input_size": 1, + }, + "loss": { + "type": "binary_weighted_cross_entropy", + "positive_class_weight": None, + "robust_lambda": 0, + "confidence_penalty": 0, + }, + } + binary_output_config, _ = load_config_with_kwargs(BinaryOutputFeatureConfig, binary_output_config) + binary_output_feature = BinaryOutputFeature(binary_output_config, {}).to(DEVICE) + combiner_outputs = {} + combiner_outputs["combiner_output"] = torch.randn([BATCH_SIZE, BINARY_W_SIZE], dtype=torch.float32).to(DEVICE) + + binary_output = binary_output_feature(combiner_outputs, {}) + + assert "last_hidden" in binary_output + assert "logits" in binary_output + assert binary_output["logits"].size() == torch.Size([BATCH_SIZE]) diff --git a/tests/ludwig/features/test_category_feature.py b/tests/ludwig/features/test_category_feature.py new file mode 100644 index 0000000..698a5ca --- /dev/null +++ b/tests/ludwig/features/test_category_feature.py @@ -0,0 +1,56 @@ +from copy import deepcopy + +import pytest +import torch + +from ludwig.constants import ENCODER, ENCODER_OUTPUT, TYPE +from ludwig.features.category_feature import CategoryInputFeature +from ludwig.schema.features.category_feature import ECDCategoryInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def category_config(): + return { + "name": "category_column_name", + "type": "category", + "tied": None, + "encoder": { + "embedding_size": 256, + "embeddings_on_cpu": False, + "pretrained_embeddings": None, + "embeddings_trainable": True, + "dropout": 0.0, + "vocab": ["a", "b", "c"], + "embedding_initializer": None, + }, + } + + +@pytest.mark.parametrize("encoder", ["dense", "sparse"]) +def test_category_input_feature( + category_config: dict, + encoder: str, +) -> None: + # setup image input feature definition + category_def = deepcopy(category_config) + category_def[ENCODER][TYPE] = encoder + + # pickup any other missing parameters + defaults = ECDCategoryInputFeatureConfig(name="foo").to_dict() + category_def = merge_dict(defaults, category_def) + + # ensure no exceptions raised during build + category_config, _ = load_config_with_kwargs(ECDCategoryInputFeatureConfig, category_def) + input_feature_obj = CategoryInputFeature(category_config).to(DEVICE) + + # check one forward pass through input feature + input_tensor = torch.randint(0, 3, size=(BATCH_SIZE,), dtype=torch.int32).to(DEVICE) + + encoder_output = input_feature_obj(input_tensor) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) diff --git a/tests/ludwig/features/test_date_feature.py b/tests/ludwig/features/test_date_feature.py new file mode 100644 index 0000000..511d0b0 --- /dev/null +++ b/tests/ludwig/features/test_date_feature.py @@ -0,0 +1,182 @@ +from copy import deepcopy +from datetime import date, datetime, UTC +from typing import Any + +import pytest +import torch +from dateutil.parser import parse + +from ludwig.constants import ENCODER_OUTPUT, FILL_WITH_CONST, MISSING_VALUE_STRATEGY +from ludwig.features import date_feature +from ludwig.features.date_feature import DateInputFeature +from ludwig.schema.features.date_feature import DateInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.types import FeatureConfigDict +from ludwig.utils.date_utils import create_vector_from_datetime_obj +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +DATE_W_SIZE = 9 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def date_config(): + return {"name": "date_column_name", "type": "date"} + + +def test_date_input_feature(date_config: FeatureConfigDict): + # setup image input feature definition + feature_def = deepcopy(date_config) + + # pickup any other missing parameters + defaults = DateInputFeatureConfig(name="foo").to_dict() + set_def = merge_dict(defaults, feature_def) + + # ensure no exceptions raised during build + feature_config, _ = load_config_with_kwargs(DateInputFeatureConfig, set_def) + input_feature_obj = DateInputFeature(feature_config).to(DEVICE) + + # check one forward pass through input feature + input_tensor = input_feature_obj.create_sample_input(batch_size=BATCH_SIZE).to(DEVICE) + assert input_tensor.shape == torch.Size((BATCH_SIZE, DATE_W_SIZE)) + assert input_tensor.dtype == torch.int32 + + encoder_output = input_feature_obj(input_tensor) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) + + +@pytest.mark.parametrize( + "date_str,datetime_format,expected_list", + [ + ("2012-02-26T13:51:50.417-07:00", None, [2012, 2, 26, 6, 57, 13, 51, 50, 49910]), + ("2022-06-25 09:30:59", None, [2022, 6, 25, 5, 176, 9, 30, 59, 34259]), + ("2022-06-25", None, [2022, 6, 25, 5, 176, 0, 0, 0, 0]), + ], +) +def test_date_to_list(date_str, datetime_format, expected_list): + preprocessing_parameters = None + assert ( + date_feature.DateInputFeature.date_to_list(date_str, datetime_format, preprocessing_parameters) == expected_list + ) + + +@pytest.fixture(scope="module") +def reference_date_list() -> list[int]: + return create_vector_from_datetime_obj(datetime.fromtimestamp(1691600953.443032, tz=UTC).replace(tzinfo=None)) + + +@pytest.fixture(scope="module") +def fill_value() -> str: + return "1970-01-01 00:00:00" + + +@pytest.fixture(scope="module") +def fill_value_list(fill_value: str) -> list[int]: + return create_vector_from_datetime_obj(parse(fill_value)) + + +@pytest.mark.parametrize( + "timestamp,datetime_format,expected_list", + [ + pytest.param(1691600953.443032, None, "reference_date_list", id="float-s"), + pytest.param(1691600953443.032, None, "reference_date_list", id="float-ms"), + pytest.param(1691600953, None, "reference_date_list", id="int-s"), + pytest.param(1691600953443, None, "reference_date_list", id="int-ms"), + pytest.param(1691600953.443032, "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="float-s-fmt"), + pytest.param(1691600953443.032, "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="float-ms-fmt"), + pytest.param(1691600953, "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="int-s-fmt"), + pytest.param(1691600953443, "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="int-ms-fmt"), + pytest.param("1691600953.443032", None, "reference_date_list", id="string[float]-s"), + pytest.param("1691600953443.0032", None, "reference_date_list", id="string[float]-ms"), + pytest.param("1691600953", None, "reference_date_list", id="string[int]-s"), + pytest.param("1691600953443", None, "reference_date_list", id="string[int]-ms"), + pytest.param("1691600953.443032", "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="string[float]-s-fmt"), + pytest.param("1691600953443.0032", "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="string[float]-ms-fmt"), + pytest.param("1691600953", "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="string[int]-s-fmt"), + pytest.param("1691600953443", "%d/%m/%y %H:%M:%S.%f", "reference_date_list", id="string[int]-ms-fmt"), + pytest.param("foo", None, "fill_value_list", id="string error"), + pytest.param([1691600953.443032], None, "fill_value_list", id="list error"), + pytest.param(None, None, "fill_value_list", id="NoneType error"), + ], +) +def test_date_to_list_numeric(timestamp: Any, datetime_format: str, expected_list: list[int], fill_value: str, request): + """Test that numeric datetime formats are converted correctly. + + Currently, we support int, float, and string representations of POSIX timestamps in seconds and milliseconds. Valid + timestamps should be converted to datetime lists by `luwdig.utils.date_utils.create_vector_from_datetime_object`. + If a string format is provided, it should be ignored. + + Args: + timestamp: Input to be converted to a date vector + datetime_format: Optional format string, should be ignored under the hood with these timestamps. + expected_list: The expected output of `DateFeatureMixin.date_to_list` + fill_value: Date to be used as fallback + request: pytest request fixture + """ + expected_result = request.getfixturevalue(expected_list) + + # The default fill value is `datetime.now`, for testing we override this to be a constant. + preprocessing_parameters = {MISSING_VALUE_STRATEGY: FILL_WITH_CONST, "fill_value": fill_value} + + # No exception should ever be raised from `date_to_list` due to a parsing error. The expected behavior is to fall + # back to the fill value. + dt = date_feature.DateInputFeature.date_to_list(timestamp, datetime_format, preprocessing_parameters) + assert dt == expected_result + + +def test_date_to_list__DatetimeObjectFromParsedJSON(): + preprocessing_parameters = None + datetime_obj = datetime.fromisoformat("2022-06-25") + assert date_feature.DateInputFeature.date_to_list(datetime_obj, None, preprocessing_parameters) == [ + 2022, + 6, + 25, + 5, + 176, + 0, + 0, + 0, + 0, + ] + + +def test_date_to_list__UsesFillValueOnInvalidDate(): + preprocessing_parameters = {"fill_value": "2013-02-26"} + invalid_date_str = "2012abc-02" + datetime_format = None + assert date_feature.DateInputFeature.date_to_list(invalid_date_str, datetime_format, preprocessing_parameters) == [ + 2013, + 2, + 26, + 1, + 57, + 0, + 0, + 0, + 0, + ] + + +@pytest.fixture(scope="module") +def date_obj(): + return date.fromisoformat("2022-06-25") + + +@pytest.fixture(scope="module") +def date_obj_vec(): + return create_vector_from_datetime_obj(datetime.fromisoformat("2022-06-25")) + + +def test_date_object_to_list(date_obj, date_obj_vec, fill_value): + """Test support for datetime.date object conversion. + + Args: + date_obj: Date object to convert into a vector + date_obj_vector: Expected vector version of `date_obj` + """ + computed_date_vec = date_feature.DateInputFeature.date_to_list( + date_obj, None, preprocessing_parameters={MISSING_VALUE_STRATEGY: FILL_WITH_CONST, "fill_value": fill_value} + ) + assert computed_date_vec == date_obj_vec diff --git a/tests/ludwig/features/test_feature_utils.py b/tests/ludwig/features/test_feature_utils.py new file mode 100644 index 0000000..c0fd14a --- /dev/null +++ b/tests/ludwig/features/test_feature_utils.py @@ -0,0 +1,63 @@ +import numpy as np +import pytest +import torch + +from ludwig.features import feature_utils + + +def test_ludwig_feature_dict(): + feature_dict = feature_utils.LudwigFeatureDict() + + to_module = torch.nn.Module() + type_module = torch.nn.Module() + + feature_dict.set("to", to_module) + feature_dict.set("type", type_module) + + assert iter(feature_dict) is not None + assert next(feature_dict) is not None + assert len(feature_dict) == 2 + assert feature_dict.keys() == ["to", "type"] + assert feature_dict.items() == [("to", to_module), ("type", type_module)] + assert feature_dict.get("to"), to_module + + feature_dict.update({"to_empty": torch.nn.Module()}) + + assert len(feature_dict) == 3 + assert [key for key in feature_dict] == ["to", "type", "to_empty"] + + +def test_ludwig_feature_dict_with_periods(): + feature_dict = feature_utils.LudwigFeatureDict() + + to_module = torch.nn.Module() + + feature_dict.set("to.", to_module) + + assert feature_dict.keys() == ["to."] + assert feature_dict.items() == [("to.", to_module)] + assert feature_dict.get("to.") == to_module + + +@pytest.mark.parametrize("sequence_type", [list, tuple, np.array]) +def test_compute_token_probabilities(sequence_type): + inputs = sequence_type( + [ + [0.1, 0.2, 0.7], + [0.3, 0.4, 0.3], + [0.6, 0.3, 0.2], + ] + ) + + token_probabilities = feature_utils.compute_token_probabilities(inputs) + assert np.allclose(token_probabilities, [0.7, 0.4, 0.6]) + + +def test_compute_sequence_probability(): + inputs = np.array([0.7, 0.4, 0.6]) + + sequence_probability = feature_utils.compute_sequence_probability( + inputs, max_sequence_length=2, return_log_prob=False + ) + + assert np.allclose(sequence_probability, [0.28]) # 0.7 * 0.4 diff --git a/tests/ludwig/features/test_h3_feature.py b/tests/ludwig/features/test_h3_feature.py new file mode 100644 index 0000000..4d820e0 --- /dev/null +++ b/tests/ludwig/features/test_h3_feature.py @@ -0,0 +1,68 @@ +from ludwig.features import h3_feature + + +def test_h3_to_list(): + assert h3_feature.H3FeatureMixin.h3_to_list(0) == [0, 0, 0, 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7] + assert h3_feature.H3FeatureMixin.h3_to_list(576495936675512319) == [ + 1, + 0, + 0, + 0, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + ] + assert h3_feature.H3FeatureMixin.h3_to_list(102576495936675512319) == [ + 1, + 7, + 8, + 71, + 2, + 7, + 1, + 2, + 2, + 6, + 1, + 6, + 7, + 7, + 7, + 7, + 7, + 7, + 7, + ] + assert h3_feature.H3FeatureMixin.h3_to_list(50102576495936675512319) == [ + 2, + 0, + 14, + 102, + 7, + 0, + 3, + 5, + 0, + 5, + 5, + 0, + 5, + 7, + 7, + 7, + 7, + 7, + 7, + ] diff --git a/tests/ludwig/features/test_image_feature.py b/tests/ludwig/features/test_image_feature.py new file mode 100644 index 0000000..881c7ac --- /dev/null +++ b/tests/ludwig/features/test_image_feature.py @@ -0,0 +1,314 @@ +from copy import deepcopy + +import pytest +import torch + +from ludwig.constants import ( + BFILL, + CROP_OR_PAD, + ENCODER, + ENCODER_OUTPUT, + ENCODER_OUTPUT_STATE, + INTERPOLATE, + LOGITS, + TYPE, +) +from ludwig.features.image_feature import _ImagePreprocessing, ImageInputFeature, ImageOutputFeature +from ludwig.schema.features.image_feature import ImageInputFeatureConfig, ImageOutputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.utils import image_feature + +BATCH_SIZE = 2 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def image_config(): + return { + "name": "image_column_name", + "type": "image", + "tied": None, + "encoder": { + "type": "stacked_cnn", + "conv_layers": None, + "num_conv_layers": None, + "filter_size": 3, + "num_filters": 256, + "strides": (1, 1), + "padding": "valid", + "dilation_rate": (1, 1), + "conv_use_bias": True, + "conv_weights_initializer": "xavier_uniform", + "conv_bias_initializer": "zeros", + "conv_norm": None, + "conv_norm_params": None, + "conv_activation": "relu", + "conv_dropout": 0, + "pool_function": "max", + "pool_size": (2, 2), + "pool_strides": None, + "fc_layers": None, + "num_fc_layers": 1, + "output_size": 16, + "fc_use_bias": True, + "fc_weights_initializer": "xavier_uniform", + "fc_bias_initializer": "zeros", + "fc_norm": None, + "fc_norm_params": None, + "fc_activation": "relu", + "fc_dropout": 0, + }, + "preprocessing": { + "height": 28, + "width": 28, + "num_channels": 1, + "scaling": "pixel_normalization", + }, + } + + +@pytest.mark.parametrize( + "encoder, height, width, num_channels", + [ + ("stacked_cnn", 28, 28, 3), + ("stacked_cnn", 28, 28, 1), + ("mlp_mixer", 32, 32, 3), + ], +) +def test_image_input_feature(image_config: dict, encoder: str, height: int, width: int, num_channels: int) -> None: + # setup image input feature definition + image_def = deepcopy(image_config) + image_def[ENCODER][TYPE] = encoder + image_def[ENCODER]["height"] = height + image_def[ENCODER]["width"] = width + image_def[ENCODER]["num_channels"] = num_channels + + # pickup any other missing parameters + defaults = ImageInputFeatureConfig(name="foo").to_dict() + set_def = merge_dict(defaults, image_def) + + # ensure no exceptions raised during build + image_config, _ = load_config_with_kwargs(ImageInputFeatureConfig, set_def) + input_feature_obj = ImageInputFeature(image_config).to(DEVICE) + + # check one forward pass through input feature + input_tensor = torch.rand(size=(BATCH_SIZE, num_channels, height, width), dtype=torch.float32).to(DEVICE) + + encoder_output = input_feature_obj(input_tensor) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) + + # todo: remove code + # # test for parameter updates + # before = [(x[0], x[1].clone()) for x in input_feature_obj.named_parameters()] + # loss_function = torch.nn.MSELoss() + # optimizer = torch.optim.SGD(input_feature_obj.parameters(), lr=0.1) + # target_tensor = torch.ones(encoder_output['encoder_output'].shape, dtype=torch.float32) + # + # # do parameter update + # loss = loss_function(encoder_output['encoder_output'], target_tensor) + # loss.backward() + # optimizer.step() + # + # after = [(x[0], x[1].clone()) for x in input_feature_obj.named_parameters()] + # + # # check for parameter update + # for b, a in zip(before, after): + # if not (b[1] != a[1]).any(): + # raise RuntimeError( + # f'no parameter update for {a[0]}' + # ) + + +@pytest.mark.parametrize( + "encoder, decoder, height, width, num_channels, num_classes", + [ + ("unet", "unet", 128, 128, 3, 2), + ("unet", "unet", 32, 32, 3, 7), + ], +) +def test_image_output_feature( + encoder: str, + decoder: str, + height: int, + width: int, + num_channels: int, + num_classes: int, +) -> None: + # setup image input feature definition + input_feature_def = image_feature( + folder=".", + encoder={ + "type": encoder, + "height": height, + "width": width, + "num_channels": num_channels, + }, + ) + # create image input feature object + feature_cls = ImageInputFeature + schema_cls = ImageInputFeatureConfig + input_config = schema_cls.from_dict(input_feature_def) + input_feature_obj = feature_cls(input_config).to(DEVICE) + + # check one forward pass through input feature + input_tensor = torch.rand(size=(BATCH_SIZE, num_channels, height, width), dtype=torch.float32).to(DEVICE) + + encoder_output = input_feature_obj(input_tensor) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) + if encoder == "unet": + assert len(encoder_output[ENCODER_OUTPUT_STATE]) == 4 + + hidden = torch.reshape(encoder_output[ENCODER_OUTPUT], [BATCH_SIZE, -1]) + + # setup image output feature definition + output_feature_def = image_feature( + folder=".", + decoder={ + "type": decoder, + "height": height, + "width": width, + "num_channels": num_channels, + "num_classes": num_classes, + }, + input_size=hidden.size(dim=1), + ) + # create image output feature object + feature_cls = ImageOutputFeature + schema_cls = ImageOutputFeatureConfig + output_config = schema_cls.from_dict(output_feature_def) + output_feature_obj = feature_cls(output_config, {}).to(DEVICE) + + combiner_outputs = { + "combiner_output": hidden, + ENCODER_OUTPUT_STATE: encoder_output[ENCODER_OUTPUT_STATE], + } + + image_output = output_feature_obj(combiner_outputs, {}) + + assert LOGITS in image_output + assert image_output[LOGITS].size() == torch.Size([BATCH_SIZE, num_classes, height, width]) + + +def test_image_preproc_module_bad_num_channels(): + metadata = { + "preprocessing": { + "missing_value_strategy": BFILL, + "in_memory": True, + "resize_method": "interpolate", + "scaling": "pixel_normalization", + "num_processes": 1, + "infer_image_num_channels": True, + "infer_image_dimensions": True, + "infer_image_max_height": 256, + "infer_image_max_width": 256, + "infer_image_sample_size": 100, + "height": 12, + "width": 12, + "num_channels": 2, + "num_classes": 0, + "channel_class_map": [], + }, + "reshape": (2, 12, 12), + } + module = _ImagePreprocessing(metadata) + + with pytest.raises(ValueError): + module(torch.rand(2, 3, 10, 10)) + + +@pytest.mark.parametrize("resize_method", [INTERPOLATE, CROP_OR_PAD]) +@pytest.mark.parametrize(["num_channels", "num_channels_expected"], [(1, 3), (3, 1)]) +def test_image_preproc_module_list_of_tensors(resize_method, num_channels, num_channels_expected): + metadata = { + "preprocessing": { + "missing_value_strategy": BFILL, + "in_memory": True, + "resize_method": resize_method, + "scaling": "pixel_normalization", + "num_processes": 1, + "infer_image_num_channels": True, + "infer_image_dimensions": True, + "infer_image_max_height": 256, + "infer_image_max_width": 256, + "infer_image_sample_size": 100, + "height": 12, + "width": 12, + "num_channels": num_channels_expected, + "num_classes": 0, + "channel_class_map": [], + }, + "reshape": (num_channels_expected, 12, 12), + } + module = _ImagePreprocessing(metadata) + + res = module([torch.rand(num_channels, 25, 25), torch.rand(num_channels, 10, 10)]) + + assert res.shape == torch.Size((2, num_channels_expected, 12, 12)) + + +@pytest.mark.parametrize("resize_method", [INTERPOLATE, CROP_OR_PAD]) +@pytest.mark.parametrize(["num_channels", "num_channels_expected"], [(1, 3), (3, 1)]) +def test_image_preproc_module_tensor(resize_method, num_channels, num_channels_expected): + metadata = { + "preprocessing": { + "missing_value_strategy": BFILL, + "in_memory": True, + "resize_method": resize_method, + "scaling": "pixel_normalization", + "num_processes": 1, + "infer_image_num_channels": True, + "infer_image_dimensions": True, + "infer_image_max_height": 256, + "infer_image_max_width": 256, + "infer_image_sample_size": 100, + "height": 12, + "width": 12, + "num_channels": num_channels_expected, + "num_classes": 0, + "channel_class_map": [], + }, + "reshape": (num_channels_expected, 12, 12), + } + module = _ImagePreprocessing(metadata) + + res = module(torch.rand(2, num_channels, 10, 10)) + + assert res.shape == torch.Size((2, num_channels_expected, 12, 12)) + + +@pytest.mark.parametrize(["height", "width"], [(224, 224), (32, 32)]) +def test_image_preproc_module_class_map(height, width): + metadata = { + "preprocessing": { + "num_processes": 1, + "resize_method": CROP_OR_PAD, + "infer_image_num_channels": True, + "infer_image_dimensions": True, + "infer_image_max_height": height, + "infer_image_max_width": width, + "infer_image_sample_size": 100, + "infer_image_num_classes": True, + "height": height, + "width": width, + "num_channels": 3, + "num_classes": 8, + "channel_class_map": [ + [40, 40, 40], + [40, 40, 41], + [40, 41, 40], + [40, 41, 41], + [41, 40, 40], + [41, 40, 41], + [41, 41, 40], + [41, 41, 41], + ], + }, + } + module = _ImagePreprocessing(metadata) + + res = module(torch.randint(40, 42, (2, 3, height, width))) + + assert res.shape == torch.Size((2, height, width)) + assert torch.all(res.ge(0)) and torch.all(res.le(7)) diff --git a/tests/ludwig/features/test_number_feature.py b/tests/ludwig/features/test_number_feature.py new file mode 100644 index 0000000..17bbb63 --- /dev/null +++ b/tests/ludwig/features/test_number_feature.py @@ -0,0 +1,55 @@ +from copy import deepcopy + +import numpy as np +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.features.number_feature import _OutlierReplacer, NumberInputFeature +from ludwig.schema.features.number_feature import ECDNumberInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def number_config(): + return {"name": "number_column_name", "type": "number"} + + +def test_number_input_feature( + number_config: dict, +) -> None: + # setup image input feature definition + number_def = deepcopy(number_config) + + # pickup any other missing parameters + defaults = ECDNumberInputFeatureConfig(name="foo").to_dict() + set_def = merge_dict(defaults, number_def) + + # ensure no exceptions raised during build + number_config, _ = load_config_with_kwargs(ECDNumberInputFeatureConfig, set_def) + input_feature_obj = NumberInputFeature(number_config).to(DEVICE) + + # check one forward pass through input feature + input_tensor = input_feature_obj.create_sample_input(batch_size=BATCH_SIZE) + assert input_tensor.shape == torch.Size([BATCH_SIZE]) + assert input_tensor.dtype == torch.float32 + + encoder_output = input_feature_obj(input_tensor) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) + + +def test_outlier_replacer(): + replacer = _OutlierReplacer( + {"mean": 50, "std": 30, "preprocessing": {"outlier_threshold": 2.0, "computed_outlier_fill_value": 42}} + ) + + t = torch.from_numpy(np.array([10, 20, 1000, -500, 80], dtype=np.float32)) + t_out_expected = torch.from_numpy(np.array([10, 20, 42, 42, 80], dtype=np.float32)) + + t_out = replacer(t) + assert torch.equal(t_out, t_out_expected) diff --git a/tests/ludwig/features/test_sequence_features.py b/tests/ludwig/features/test_sequence_features.py new file mode 100644 index 0000000..b64718f --- /dev/null +++ b/tests/ludwig/features/test_sequence_features.py @@ -0,0 +1,336 @@ +import numpy as np +import pytest +import torch + +try: + import torchtext +except ImportError: + torchtext = None + +from ludwig.constants import ENCODER_OUTPUT, LAST_HIDDEN, LOGITS, SEQUENCE, TEXT, TYPE +from ludwig.features.sequence_feature import _SequencePreprocessing, SequenceInputFeature, SequenceOutputFeature +from ludwig.features.text_feature import TextInputFeature, TextOutputFeature +from ludwig.schema.features.sequence_feature import SequenceInputFeatureConfig, SequenceOutputFeatureConfig +from ludwig.schema.features.text_feature import ECDTextInputFeatureConfig, ECDTextOutputFeatureConfig +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.utils import ENCODERS, sequence_feature + +DEVICE = get_torch_device() +BATCH_SIZE = 8 +SEQ_SIZE = 6 +VOCAB_SIZE = 64 + + +@pytest.fixture(scope="module") +def input_sequence() -> tuple[torch.Tensor, list]: + # generates a realistic looking synthetic sequence tensor, i.e. + # each sequence will have non-zero tokens at the beginning with + # trailing zero tokens, including a max length token with a single + # zero token at the end. Example: + # [ + # [3, 5, 6, 0, 0, 0], + # [10, 11, 12, 13, 14, 0], # max length sequence + # [32, 0, 0, 0, 0, 0] # minimum length sequence + # ] + input_tensor = torch.zeros([BATCH_SIZE, SEQ_SIZE], dtype=torch.int32).to(DEVICE) + sequence_lengths = np.random.randint(1, SEQ_SIZE, size=BATCH_SIZE) + for i in range(input_tensor.shape[0]): + input_tensor[i, : sequence_lengths[i]] = torch.tensor( + np.random.randint(2, VOCAB_SIZE, size=sequence_lengths[i]) + ) + + # emulate idx2str structure + idx2str = ["", ""] + [str(i) for i in range(2, VOCAB_SIZE)] + + return input_tensor, idx2str + + +@pytest.mark.parametrize("encoder", ENCODERS) +@pytest.mark.parametrize("sequence_type", [SEQUENCE, TEXT]) +def test_sequence_input_feature(input_sequence: tuple, encoder: str, sequence_type: str): + # test assumes "sequence data" has been tokenized and converted to + # numeric representation. Focus of this test is primarily on + # integration with encoder with correctly sized encoder tensor and + # required properties are present + + input_sequence, idx2str = input_sequence + + # setup input sequence feature definition + # use sequence_feature() to generate baseline + # sequence definition and then augment with + # pre-processing metadata parameters + input_feature_def = sequence_feature( + encoder={ + "type": encoder, + "max_len": SEQ_SIZE, + # augment with emulated pre-processing metadata + "max_sequence_length": SEQ_SIZE, + "vocab": idx2str, + } + ) + input_feature_def[TYPE] = sequence_type + + # create sequence input feature object + feature_cls = SequenceInputFeature if sequence_type == SEQUENCE else TextInputFeature + schema_cls = SequenceInputFeatureConfig if sequence_type == SEQUENCE else ECDTextInputFeatureConfig + sequence_config = schema_cls.from_dict(input_feature_def) + input_feature_obj = feature_cls(sequence_config).to(DEVICE) + + # confirm dtype property + assert input_feature_obj.input_dtype == torch.int32 + + # confirm input_shape property + assert input_feature_obj.input_shape == (SEQ_SIZE,) + + # confirm output_shape property default output shape + # from sequence_feature() function + encoder_output = input_feature_obj(input_sequence) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) + + +@pytest.mark.parametrize("sequence_type", [SEQUENCE, TEXT]) +def test_sequence_output_feature(sequence_type: str): + output_feature_def = sequence_feature( + decoder={ + "type": "generator", + "max_len": SEQ_SIZE, + "max_sequence_length": SEQ_SIZE, + "vocab_size": VOCAB_SIZE, + }, + input_size=VOCAB_SIZE, + ) + output_feature_def[TYPE] = sequence_type + + feature_cls = SequenceOutputFeature if sequence_type == SEQUENCE else TextOutputFeature + schema_cls = SequenceOutputFeatureConfig if sequence_type == SEQUENCE else ECDTextOutputFeatureConfig + sequence_config = schema_cls.from_dict(output_feature_def) + output_feature_obj = feature_cls(sequence_config, {}).to(DEVICE) + combiner_outputs = { + "combiner_output": torch.randn([BATCH_SIZE, SEQ_SIZE, VOCAB_SIZE], dtype=torch.float32).to(DEVICE) + } + + text_output = output_feature_obj(combiner_outputs, {}) + + assert LAST_HIDDEN in text_output + assert LOGITS in text_output + assert text_output[LOGITS].size() == torch.Size([BATCH_SIZE, SEQ_SIZE, VOCAB_SIZE]) + + +def test_sequence_preproc_module_bad_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "dutch_lemmatize", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": {"": 0, "": 1, "": 2, "": 3, "▁hell": 4, "o": 5, "▁world": 6}, + } + + with pytest.raises(ValueError): + _SequencePreprocessing(metadata) + + +def test_sequence_preproc_module_space_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "space", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": { + "": 0, + "": 1, + "": 2, + "": 3, + "hello": 4, + "world": 5, + "paleontology": 6, + }, + } + module = _SequencePreprocessing(metadata) + + res = module([" paleontology", "unknown", "hello world hello", "hello world hello world "]) + + assert torch.allclose( + res, torch.tensor([[1, 6, 0, 2, 2, 2], [1, 3, 0, 2, 2, 2], [1, 4, 5, 4, 0, 2], [1, 4, 5, 4, 5, 0]]) + ) + + +def test_text_preproc_module_space_punct_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "space_punct", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": { + "": 0, + "": 1, + "": 2, + "": 3, + "this": 4, + "sentence": 5, + "has": 6, + "punctuation": 7, + ",": 8, + ".": 9, + }, + } + module = _SequencePreprocessing(metadata) + + res = module(["punctuation", ",,,,", "this... this... punctuation", "unknown"]) + + assert torch.allclose( + res, torch.tensor([[1, 7, 0, 2, 2, 2], [1, 8, 8, 8, 8, 0], [1, 4, 9, 9, 9, 4], [1, 3, 0, 2, 2, 2]]) + ) + + +@pytest.mark.skipif( + torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 12, 0), + reason="requires torchtext 0.12.0 or higher", +) +def test_sequence_preproc_module_sentencepiece_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "sentencepiece", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": { + "": 0, + "": 1, + "": 2, + "": 3, + "▁hell": 4, + "o": 5, + "▁world": 6, + "▁pale": 7, + "ont": 8, + "ology": 9, + }, + } + module = _SequencePreprocessing(metadata) + + res = module(["paleontology", "unknown", "hello world hello", "hello world hello world"]) + + assert torch.allclose( + res, torch.tensor([[1, 7, 8, 9, 0, 2], [1, 3, 3, 3, 0, 2], [1, 4, 5, 6, 4, 5], [1, 4, 5, 6, 4, 5]]) + ) + + +@pytest.mark.skipif( + torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 12, 0), + reason="requires torchtext 0.12.0 or higher", +) +def test_sequence_preproc_module_clip_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "clip", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": { + "": 0, + "": 1, + "": 2, + "": 3, + "hello": 4, + "world": 5, + "pale": 7, + "ontology": 8, + }, + } + module = _SequencePreprocessing(metadata) + + res = module(["paleontology", "unknown", "hello world hello", "hello world hello world"]) + + assert torch.allclose( + res, torch.tensor([[1, 7, 8, 0, 2, 2], [1, 3, 0, 2, 2, 2], [1, 4, 5, 4, 0, 2], [1, 4, 5, 4, 5, 0]]) + ) + + +@pytest.mark.skipif( + torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 12, 0), + reason="requires torchtext 0.12.0 or higher", +) +def test_sequence_preproc_module_gpt2bpe_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "gpt2bpe", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": { + "": 0, + "": 1, + "": 2, + "": 3, + "hello": 4, + "Ġworld": 5, + "Ġhello": 7, + "p": 8, + "ale": 9, + "ont": 10, + "ology": 11, + }, + } + module = _SequencePreprocessing(metadata) + + res = module(["paleontology", "unknown", "hello world hello", "hello world hello world"]) + + assert torch.allclose( + res, torch.tensor([[1, 8, 9, 10, 11, 0], [1, 3, 0, 2, 2, 2], [1, 4, 5, 7, 0, 2], [1, 4, 5, 7, 5, 0]]) + ) + + +@pytest.mark.skipif( + torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 13, 0), + reason="requires torchtext 0.13.0 or higher", +) +def test_sequence_preproc_module_bert_tokenizer(): + metadata = { + "preprocessing": { + "lowercase": True, + "tokenizer": "bert", + "unknown_symbol": "", + "padding_symbol": "", + "computed_fill_value": "", + }, + "max_sequence_length": SEQ_SIZE, + "str2idx": { + "": 0, + "": 1, + "": 2, + "": 3, + "hello": 4, + "world": 5, + "pale": 7, + "##ont": 8, + "##ology": 9, + }, + } + module = _SequencePreprocessing(metadata) + + res = module(["paleontology", "unknown", "hello world hello", "hello world hello world"]) + + assert torch.allclose( + res, torch.tensor([[1, 7, 8, 9, 0, 2], [1, 3, 0, 2, 2, 2], [1, 4, 5, 4, 0, 2], [1, 4, 5, 4, 5, 0]]) + ) diff --git a/tests/ludwig/features/test_set_feature.py b/tests/ludwig/features/test_set_feature.py new file mode 100644 index 0000000..83907b6 --- /dev/null +++ b/tests/ludwig/features/test_set_feature.py @@ -0,0 +1,61 @@ +from copy import deepcopy + +import pytest +import torch + +from ludwig.constants import ENCODER, ENCODER_OUTPUT +from ludwig.features.set_feature import SetInputFeature +from ludwig.schema.features.set_feature import SetInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.misc_utils import merge_dict +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def set_config(): + return { + "name": "set_column_name", + "type": "set", + "tied": None, + "encoder": { + "type": "embed", + "vocab": ["a", "b", "c"], + "representation": "dense", + "embedding_size": 50, + "embeddings_trainable": True, + "pretrained_embeddings": None, + "embeddings_on_cpu": False, + "fc_layers": None, + "num_fc_layers": 0, + "use_bias": True, + "weights_initializer": "uniform", + "bias_initializer": "zeros", + "norm": None, + "norm_params": None, + "activation": "relu", + "dropout": 0.0, + "reduce_output": "sum", + }, + } + + +def test_set_input_feature(set_config: dict) -> None: + # setup image input feature definition + set_def = deepcopy(set_config) + + # pickup any other missing parameters + defaults = SetInputFeatureConfig(name="foo").to_dict() + set_def = merge_dict(defaults, set_def) + + # ensure no exceptions raised during build + set_config, _ = load_config_with_kwargs(SetInputFeatureConfig, set_def) + input_feature_obj = SetInputFeature(set_config).to(DEVICE) + + # check one forward pass through input feature + input_tensor = torch.randint(0, 2, size=(BATCH_SIZE, len(set_def[ENCODER]["vocab"])), dtype=torch.int64).to(DEVICE) + + encoder_output = input_feature_obj(input_tensor) + assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape) diff --git a/tests/ludwig/features/test_text_feature.py b/tests/ludwig/features/test_text_feature.py new file mode 100644 index 0000000..c3574ba --- /dev/null +++ b/tests/ludwig/features/test_text_feature.py @@ -0,0 +1,141 @@ +import pandas as pd +import pytest +import torch +from transformers import AutoTokenizer + +from ludwig.backend import LocalBackend +from ludwig.constants import IGNORE_INDEX_TOKEN_ID, LOGITS, PREDICTIONS, PROBABILITIES +from ludwig.features import text_feature + +TEST_MODEL_NAME = "hf-internal-testing/tiny-random-OPTForCausalLM" + + +def test_backwards_compatibility(): + # Tests that legacy level-based metadata keys are supported. + metadata = { + "SibSp": { + "char_idx2str": ["", "", "", "", "0", "1", "2", "4", "3", "8", "5"], + "char_max_sequence_length": 3, + "char_pad_idx": 2, + "char_pad_symbol": "", + "char_str2freq": { + "0": 608, + "1": 209, + "2": 28, + "3": 16, + "4": 18, + "5": 5, + "8": 7, + "": 0, + "": 0, + "": 0, + "": 0, + }, + "char_str2idx": { + "0": 4, + "1": 5, + "2": 6, + "3": 8, + "4": 7, + "5": 10, + "8": 9, + "": 0, + "": 2, + "": 1, + "": 3, + }, + "char_unk_symbol": "", + "char_vocab_size": 11, + "preprocessing": { + "char_most_common": 70, + "char_sequence_length_limit": 1024, + "char_tokenizer": "characters", + "char_vocab_file": None, + "computed_fill_value": "", + "fill_value": "", + "lowercase": True, + "missing_value_strategy": "fill_with_const", + "padding": "right", + "padding_symbol": "", + "pretrained_model_name_or_path": None, + "unknown_symbol": "", + "word_most_common": 20000, + "word_sequence_length_limit": 256, + "word_tokenizer": "space_punct", + "word_vocab_file": None, + }, + "word_idx2str": ["", "", "", "", "0", "1", "2", "4", "3", "8", "5"], + "word_max_sequence_length": 3, + "word_pad_idx": 2, + "word_pad_symbol": "", + "word_str2freq": { + "0": 608, + "1": 209, + "2": 28, + "3": 16, + "4": 18, + "5": 5, + "8": 7, + "": 0, + "": 0, + "": 0, + "": 0, + }, + "word_str2idx": { + "0": 4, + "1": 5, + "2": 6, + "3": 8, + "4": 7, + "5": 10, + "8": 9, + "": 0, + "": 2, + "": 1, + "": 3, + }, + "word_unk_symbol": "", + "word_vocab_size": 11, + } + } + + column = pd.core.series.Series(["hello world", "hello world"]) + + feature_data = text_feature.TextInputFeature.feature_data( + column, metadata["SibSp"], metadata["SibSp"]["preprocessing"], LocalBackend() + ) + + assert list(feature_data[0]) == [1, 3, 3] + assert list(feature_data[1]) == [1, 3, 3] + + +@pytest.mark.parametrize("vocab_size", [8]) +@pytest.mark.parametrize( + "targets", + [ + ([78, 79, 504, 76, 397, 84, 0], [" first she 18 yearman our"]), + ([IGNORE_INDEX_TOKEN_ID, IGNORE_INDEX_TOKEN_ID, IGNORE_INDEX_TOKEN_ID, 76, 397, 84, 0], [" yearman our"]), + ], +) +@pytest.mark.parametrize("predictions", [[78, 79, 504, 76, 397, 84, 0]]) +def test_get_decoded_targets_and_predictions(vocab_size, targets, predictions): + tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME) + + # Scenario 1: Prediction and target tensors have the same length, so nothing should change + targets, decoded_texts_gt = targets + targets = torch.tensor([targets]) + predictions = { + PREDICTIONS: torch.tensor([predictions], dtype=torch.int64), + PROBABILITIES: torch.randn(1, 7, vocab_size).to(torch.float32), + LOGITS: torch.randn(1, 7, vocab_size).to(torch.float32), + } + ( + decoded_targets, + decoded_predictions, + ) = text_feature.get_decoded_targets_and_predictions(targets, predictions, tokenizer) + + assert isinstance(decoded_targets, list) + assert isinstance(decoded_predictions, list) + assert all(isinstance(x, str) for x in decoded_targets) + assert all(isinstance(x, str) for x in decoded_predictions) + assert decoded_targets == decoded_predictions == decoded_texts_gt diff --git a/tests/ludwig/features/test_timeseries_feature.py b/tests/ludwig/features/test_timeseries_feature.py new file mode 100644 index 0000000..dca1edf --- /dev/null +++ b/tests/ludwig/features/test_timeseries_feature.py @@ -0,0 +1,42 @@ +import pytest +import torch + +from ludwig.constants import ENCODER, ENCODER_OUTPUT, TYPE +from ludwig.features.timeseries_feature import TimeseriesInputFeature +from ludwig.schema.features.timeseries_feature import TimeseriesInputFeatureConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils.torch_utils import get_torch_device + +SEQ_SIZE = 2 +TIMESERIES_W_SIZE = 1 +MAX_LEN = 7 +EMBEDDING_SIZE = 5 +DEVICE = get_torch_device() + + +@pytest.fixture(scope="module") +def timeseries_config(): + return { + "name": "timeseries_12", + "type": "timeseries", + "encoder": { + "max_len": MAX_LEN, + "embedding_size": EMBEDDING_SIZE, + "max_sequence_length": SEQ_SIZE, + "output_size": 8, + "state_size": 8, + "num_filters": 8, + "hidden_size": 8, + }, + } + + +@pytest.mark.parametrize("encoder", ["rnn", "stacked_cnn", "parallel_cnn"]) +def test_timeseries_input_feature(timeseries_config: dict, encoder: str) -> None: + timeseries_config[ENCODER][TYPE] = encoder + + timeseries_config, _ = load_config_with_kwargs(TimeseriesInputFeatureConfig, timeseries_config) + timeseries_input_feature = TimeseriesInputFeature(timeseries_config).to(DEVICE) + timeseries_tensor = torch.randn([SEQ_SIZE, TIMESERIES_W_SIZE], dtype=torch.float32).to(DEVICE) + encoder_output = timeseries_input_feature(timeseries_tensor) + assert encoder_output[ENCODER_OUTPUT].shape[1:] == timeseries_input_feature.output_shape diff --git a/tests/ludwig/hyperopt/test_hyperopt.py b/tests/ludwig/hyperopt/test_hyperopt.py new file mode 100644 index 0000000..9086aa0 --- /dev/null +++ b/tests/ludwig/hyperopt/test_hyperopt.py @@ -0,0 +1,168 @@ +import pytest + +from ludwig.constants import INPUT_FEATURES, NAME, OUTPUT_FEATURES, TYPE +from ludwig.hyperopt.utils import log_warning_if_all_grid_type_parameters, substitute_parameters +from ludwig.schema.model_config import ModelConfig + +BASE_CONFIG = { + INPUT_FEATURES: [{NAME: "title", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "summary", TYPE: "text"}], +} + + +def _get_config(): + return { + "input_features": [{"name": "Date received", "type": "category"}], + "output_features": [{"name": "Product", "type": "category"}], + "hyperopt": { + "goal": "minimize", + "metric": "loss", + "executor": { + "type": "ray", + "scheduler": { + "type": "async_hyperband", + "max_t": 3600, + "time_attr": "time_total_s", + "grace_period": 72, + "reduction_factor": 5, + }, + "num_samples": 10, + "time_budget_s": 3600, + "cpu_resources_per_trial": 1, + }, + "parameters": {"trainer.learning_rate": {"space": "choice", "categories": [0.005, 0.01, 0.02, 0.025]}}, + "search_alg": {"type": "variant_generator"}, + "output_feature": "Product", + }, + } + + +@pytest.mark.parametrize( + "parameters, expected", + [ + ( + { + "combiner.type": "tabnet", + "combiner.fc_layers": [{"output_size": 64}, {"output_size": 32}], + "trainer.learning_rate": 0.1, + "trainer.batch_size": 256, + }, + { + **BASE_CONFIG, + "combiner": {"type": "tabnet", "fc_layers": [{"output_size": 64}, {"output_size": 32}]}, + "trainer": {"learning_rate": 0.1, "batch_size": 256}, + }, + ), + ( + { + "title.encoder.type": "bert", + "summary.decoder.reduce_input": "sum", + "trainer.learning_rate": 0.1, + "trainer.batch_size": 256, + }, + { + INPUT_FEATURES: [{NAME: "title", TYPE: "text", "encoder": {"type": "bert"}}], + OUTPUT_FEATURES: [{NAME: "summary", TYPE: "text", "decoder": {"reduce_input": "sum"}}], + "trainer": {"learning_rate": 0.1, "batch_size": 256}, + }, + ), + ( + { + ".": { + "combiner": {"type": "concat", "num_fc_layers": 2}, + "trainer": {"learning_rate_scaling": "linear"}, + }, + "trainer.learning_rate": 0.1, + }, + { + **BASE_CONFIG, + "combiner": {"type": "concat", "num_fc_layers": 2}, + "trainer": {"learning_rate_scaling": "linear", "learning_rate": 0.1}, + }, + ), + ( + { + ".": { + "combiner": {"type": "concat", "num_fc_layers": 2}, + "trainer": {"learning_rate_scaling": "linear"}, + }, + "trainer": { + "learning_rate": 0.1, + "batch_size": 256, + }, + }, + { + **BASE_CONFIG, + "combiner": {"type": "concat", "num_fc_layers": 2}, + "trainer": {"learning_rate_scaling": "linear", "learning_rate": 0.1, "batch_size": 256}, + }, + ), + ], + ids=["flat", "features", "nested", "multi-nested"], +) +def test_substitute_parameters(parameters, expected): + actual_config = substitute_parameters(BASE_CONFIG, parameters) + assert actual_config == expected + + +def test_grid_search_more_than_one_sample(): + """Test logs a user warning indicating that duplicate trials will be created because all of the parameters in + the search space are of type grid_search and the number of samples is greater than 1.""" + with pytest.warns(RuntimeWarning): + log_warning_if_all_grid_type_parameters( + { + "parameters": { + "trainer.learning_rate": {"space": "grid_search", "values": [0.001, 0.005, 0.1]}, + "defaults.text.encoder.type": {"space": "grid_search", "values": ["parallel_cnn", "stacked_cnn"]}, + }, + "executor": {"num_samples": 2}, + } + ) + + +@pytest.mark.parametrize( + "parameters, expected_num_samples", + [ + ( + { + "trainer.learning_rate": {"space": "grid_search", "values": [0.001, 0.005, 0.1]}, + "defaults.category.encoder.type": {"space": "grid_search", "values": ["dense", "sparse"]}, + }, + 1, + ), + ( + { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.0001, + "upper": 0.01, + }, + "defaults.category.encoder.type": {"space": "grid_search", "values": ["dense", "sparse"]}, + }, + 1, + ), + ( + { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.0001, + "upper": 0.01, + }, + }, + 10, + ), + ], + ids=["all_grid_search", "mixed", "no_grid_search"], +) +def test_default_num_samples(parameters, expected_num_samples): + """This test ensures that the default number of samples is set correctly when the user does not specify the + number of samples in the hyperopt config.""" + config = _get_config() + + # Override to set num_samples to None so we can test inference logic + config["hyperopt"]["executor"]["num_samples"] = None + config["hyperopt"]["parameters"] = parameters + + processed_config = ModelConfig.from_dict(config).to_dict() + + assert processed_config["hyperopt"]["executor"]["num_samples"] == expected_num_samples diff --git a/tests/ludwig/hyperopt/test_optuna_executor.py b/tests/ludwig/hyperopt/test_optuna_executor.py new file mode 100644 index 0000000..ecae569 --- /dev/null +++ b/tests/ludwig/hyperopt/test_optuna_executor.py @@ -0,0 +1,110 @@ +"""Tests for native Optuna executor.""" + +import pytest + +from ludwig.hyperopt.optuna_executor import _create_sampler, _suggest_params, OptunaExecutor + + +class TestOptunaExecutorInit: + def test_constructor_matches_registry_interface(self): + """OptunaExecutor must accept the same args as the executor registry passes.""" + executor = OptunaExecutor( + parameters={"trainer.learning_rate": {"space": "loguniform", "lower": 1e-5, "upper": 1e-1}}, + output_feature="label", + metric="accuracy", + goal="maximize", + split="validation", + num_samples=5, + sampler="tpe", + ) + assert executor.output_feature == "label" + assert executor.metric == "accuracy" + assert executor.goal == "maximize" + assert executor.num_samples == 5 + + def test_registered_in_executor_registry(self): + """OptunaExecutor must be available via executor_registry['optuna'].""" + from ludwig.hyperopt.execution import executor_registry + + assert "optuna" in executor_registry + cls = executor_registry["optuna"] + assert cls is OptunaExecutor + + def test_invalid_sampler_raises(self): + with pytest.raises(ValueError, match="Unknown sampler"): + _create_sampler("invalid") + + +class TestSuggestParams: + def test_uniform(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + params = _suggest_params(trial, {"x": {"space": "uniform", "lower": 0.0, "upper": 1.0}}) + assert 0.0 <= params["x"] <= 1.0 + + def test_loguniform(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + params = _suggest_params(trial, {"lr": {"space": "loguniform", "lower": 1e-5, "upper": 1e-1}}) + assert 1e-5 <= params["lr"] <= 1e-1 + + def test_int(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + params = _suggest_params(trial, {"n": {"space": "int", "lower": 1, "upper": 10}}) + assert isinstance(params["n"], int) + assert 1 <= params["n"] <= 10 + + def test_categorical(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + params = _suggest_params(trial, {"opt": {"space": "choice", "categories": ["adam", "sgd"]}}) + assert params["opt"] in ["adam", "sgd"] + + def test_grid_search(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + params = _suggest_params(trial, {"bs": {"space": "grid_search", "values": [32, 64, 128]}}) + assert params["bs"] in [32, 64, 128] + + def test_invalid_space_raises(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + with pytest.raises(ValueError, match="Unknown search space"): + _suggest_params(trial, {"x": {"space": "unknown", "lower": 0, "upper": 1}}) + + def test_multiple_params(self): + import optuna + + study = optuna.create_study() + trial = study.ask() + params = _suggest_params( + trial, + { + "lr": {"space": "loguniform", "lower": 1e-5, "upper": 1e-1}, + "bs": {"space": "int", "lower": 16, "upper": 256}, + "opt": {"space": "choice", "categories": ["adam", "sgd"]}, + }, + ) + assert "lr" in params + assert "bs" in params + assert "opt" in params + + +class TestSamplers: + @pytest.mark.parametrize("sampler_type", ["auto", "tpe", "random", "cmaes"]) + def test_create_sampler(self, sampler_type): + sampler = _create_sampler(sampler_type) + assert sampler is not None diff --git a/tests/ludwig/hyperopt/test_search_space_generator.py b/tests/ludwig/hyperopt/test_search_space_generator.py new file mode 100644 index 0000000..ab0ef18 --- /dev/null +++ b/tests/ludwig/hyperopt/test_search_space_generator.py @@ -0,0 +1,20 @@ +"""Tests for search space auto-generation.""" + +from ludwig.hyperopt.search_space_generator import generate_trainer_search_space + + +class TestGenerateTrainerSearchSpace: + def test_default_fields(self): + space = generate_trainer_search_space() + assert "trainer.learning_rate" in space + assert space["trainer.learning_rate"]["space"] == "loguniform" + + def test_custom_fields(self): + space = generate_trainer_search_space(tunable_fields=["learning_rate", "num_layers"]) + assert "trainer.learning_rate" in space + assert "combiner.num_layers" in space + assert "trainer.batch_size" not in space + + def test_batch_size_is_int(self): + space = generate_trainer_search_space(tunable_fields=["batch_size"]) + assert space["trainer.batch_size"]["space"] == "int" diff --git a/tests/ludwig/model_export/test_onnx_exporter.py b/tests/ludwig/model_export/test_onnx_exporter.py new file mode 100644 index 0000000..1acda08 --- /dev/null +++ b/tests/ludwig/model_export/test_onnx_exporter.py @@ -0,0 +1,33 @@ +import unittest +from unittest.mock import MagicMock, patch + +import pytest + +onnx = pytest.importorskip("onnx") + +from ludwig.api import LudwigModel # noqa: E402 +from ludwig.model_export.base_model_exporter import LudwigTorchWrapper # noqa: E402 +from ludwig.model_export.onnx_exporter import OnnxExporter # noqa: E402 + + +class TestOnnxExporter(unittest.TestCase): + @patch.object(LudwigModel, "load") + @patch.object(LudwigTorchWrapper, "eval") + @patch("torch.onnx") + def test_onnx_export( + self, + mock_onnx, + mock_ludwig_torch_wrapper_eval, + mock_ludwig_model_load, + ): + sample_model_path = MagicMock() + sample_export_path = MagicMock() + sample_output_model_name = MagicMock() + mock_ludwig_model_load.return_value = MagicMock() + mock_onnx.export.return_value = MagicMock() + onnx_exporter = OnnxExporter() + + onnx_exporter.export(sample_model_path, sample_export_path, sample_output_model_name) + + mock_ludwig_torch_wrapper_eval.assert_called_once() + mock_ludwig_model_load.assert_called_once() diff --git a/tests/ludwig/models/__init__.py b/tests/ludwig/models/__init__.py new file mode 100644 index 0000000..42715bb --- /dev/null +++ b/tests/ludwig/models/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== diff --git a/tests/ludwig/models/test_predictor.py b/tests/ludwig/models/test_predictor.py new file mode 100644 index 0000000..1f4ea6c --- /dev/null +++ b/tests/ludwig/models/test_predictor.py @@ -0,0 +1,139 @@ +"""Unit tests for Predictor.batch_collect_activations. + +Verifies the per-batch CPU-offload accumulation pattern that avoids OOM when +collecting activations from large models over many batches. +""" + +import contextlib +from unittest.mock import MagicMock + +import numpy as np +import pytest +import torch + + +def _make_predictor(batches, model_outputs_per_batch): + """Build a Predictor with mocked internals. + + Args: + batches: list of batch dicts to return from batcher.next_batch() + model_outputs_per_batch: list of dicts (one per batch) returned by _predict_on_inputs + """ + from ludwig.distributed.base import LocalStrategy + from ludwig.models.base import BaseModel + from ludwig.models.predictor import Predictor + + # Minimal mock BaseModel that satisfies isinstance check + mock_model = MagicMock(spec=BaseModel) + in_feat = MagicMock() + in_feat.feature_name = "text" + in_feat.proc_column = "text_proc" + mock_model.input_features = {"text": in_feat} + + mock_dist_model = MagicMock() + mock_dist_model.training = False + + predictor = Predictor.__new__(Predictor) + predictor._batch_size = 4 + predictor._distributed = LocalStrategy() + predictor.report_tqdm_to_ray = False + predictor.device = "cpu" + predictor.dist_model = mock_dist_model + predictor.model = mock_model + + # Build batcher mock that yields batches one by one + call_count = [0] + + def next_batch(): + b = batches[call_count[0]] + call_count[0] += 1 + return b + + def last_batch(): + return call_count[0] >= len(batches) + + batcher = MagicMock() + batcher.next_batch.side_effect = next_batch + batcher.last_batch.side_effect = last_batch + batcher.steps_per_epoch = len(batches) + + # Wrap initialize_batcher as a context manager + @contextlib.contextmanager + def init_batcher(*args, **kwargs): + yield batcher + + mock_model.metrics_to_device = MagicMock() + + # Patch dataset + predictor._dataset_mock = MagicMock() + predictor._dataset_mock.initialize_batcher = init_batcher + + # Patch _predict_on_inputs to return successive outputs + output_iter = iter(model_outputs_per_batch) + predictor._predict_on_inputs = lambda inputs: next(output_iter) + + return predictor + + +class TestBatchCollectActivationsCPUOffload: + def test_tensors_concatenated_across_batches(self): + batches = [{"text_proc": np.zeros(4)} for _ in range(3)] + per_batch = [ + {"hidden": torch.ones(4, 8)}, + {"hidden": torch.ones(4, 8) * 2}, + {"hidden": torch.ones(4, 8) * 3}, + ] + predictor = _make_predictor(batches, per_batch) + result = predictor.batch_collect_activations(layer_names=["hidden"], dataset=predictor._dataset_mock) + name, tensor = result[0] + assert name == "hidden" + assert tensor.shape == (12, 8) # 3 batches × 4 rows + + def test_batch1_values_in_concatenated_output(self): + batches = [{"text_proc": np.zeros(2)} for _ in range(2)] + per_batch = [ + {"out": torch.full((2, 3), 1.0)}, + {"out": torch.full((2, 3), 9.0)}, + ] + predictor = _make_predictor(batches, per_batch) + result = predictor.batch_collect_activations(layer_names=["out"], dataset=predictor._dataset_mock) + _, tensor = result[0] + assert tensor[0, 0].item() == pytest.approx(1.0) + assert tensor[2, 0].item() == pytest.approx(9.0) + + def test_output_tensors_are_on_cpu(self): + """Result tensors must be on CPU regardless of where they were produced.""" + batches = [{"text_proc": np.zeros(2)} for _ in range(2)] + per_batch = [ + {"layer": torch.ones(2, 4)}, # already CPU in test; accumulation must stay CPU + {"layer": torch.ones(2, 4) * 2}, + ] + predictor = _make_predictor(batches, per_batch) + result = predictor.batch_collect_activations(layer_names=["layer"], dataset=predictor._dataset_mock) + _, tensor = result[0] + assert tensor.device.type == "cpu" + + def test_non_tensor_values_collected_as_lists(self): + batches = [{"text_proc": np.zeros(2)} for _ in range(2)] + per_batch = [ + {"used_tokens": 10}, + {"used_tokens": 20}, + ] + predictor = _make_predictor(batches, per_batch) + result = predictor.batch_collect_activations(layer_names=["used_tokens"], dataset=predictor._dataset_mock) + name, values = result[0] + assert name == "used_tokens" + assert values == [10, 20] + + def test_bucketing_field_raises(self): + from ludwig.models.predictor import Predictor + + predictor = Predictor.__new__(Predictor) + predictor._distributed = MagicMock() + predictor._distributed.rank.return_value = 0 + with pytest.raises(ValueError, match="BucketedBatcher"): + predictor.batch_collect_activations( + layer_names=["x"], + dataset=MagicMock(), + bucketing_field="some_field", + ) diff --git a/tests/ludwig/models/test_trainable_image_layers.py b/tests/ludwig/models/test_trainable_image_layers.py new file mode 100644 index 0000000..bd9512e --- /dev/null +++ b/tests/ludwig/models/test_trainable_image_layers.py @@ -0,0 +1,90 @@ +import logging +import os + +import pytest +import torch +from torchvision.models import resnet18, ResNet18_Weights + +from ludwig.api import LudwigModel +from ludwig.data.dataset_synthesizer import cli_synthesize_dataset + + +# pytest fixture to do one time setup of required data +@pytest.fixture(scope="module") +def setup_data(tmp_path_factory): + # setup location for training data + data_dir = tmp_path_factory.mktemp("data", numbered=False) + train_fp = os.path.join(data_dir, "train.csv") + + # setup local cache to torchvision model to avoid multiple downloads + tv_cache = tmp_path_factory.mktemp("tv_cache", numbered=False) + + # describe synthetic data to create + feature_list = [ + {"name": "binary_output_feature", "type": "binary"}, + { + "name": "image", + "type": "image", + "destination_folder": os.path.join(data_dir, "images"), + "preprocessing": {"height": 600, "width": 600, "num_channels": 3}, + }, + ] + + # create synthetic data + cli_synthesize_dataset(10, feature_list, train_fp) + + return train_fp, str(tv_cache) + + +@pytest.mark.parametrize("trainable", [True, False]) +def test_trainable_torchvision_layers(setup_data, trainable): + # retrieve data setup from fixture + train_fp, tv_cache = setup_data + + config = { + "input_features": [ + { + "name": "image", + "type": "image", + "encoder": { + "type": "resnet", + "model_variant": 18, + "model_cache_dir": tv_cache, + "trainable": trainable, + }, + }, + ], + "output_features": [ + { + "name": "binary_output_feature", + "type": "binary", + } + ], + "trainer": { + "epochs": 2, + }, + } + + model = LudwigModel(config, logging_level=logging.INFO) + + _, _, output_dir = model.train(dataset=train_fp, skip_save_processed_input=True) + + # instantiate native torchvision to get original parameter values + os.environ["TORCH_HOME"] = tv_cache + tv_model = resnet18(weights=ResNet18_Weights.DEFAULT) + + # replace last layer to match image encoder setup + tv_model.fc = torch.nn.Identity() + + # compare Ludwig image encoder parameter against original native torchvision weights + # if trainable is True, parameters should differ, otherwise all parameters should be unchanged + if trainable: + for p1, p2 in zip( + model.model.input_features.get("image").encoder_obj.model.parameters(), tv_model.parameters() + ): + assert not torch.all(p1.cpu() == p2.cpu()) + else: + for p1, p2 in zip( + model.model.input_features.get("image").encoder_obj.model.parameters(), tv_model.parameters() + ): + assert torch.all(p1.cpu() == p2.cpu()) diff --git a/tests/ludwig/models/test_training_determinism.py b/tests/ludwig/models/test_training_determinism.py new file mode 100644 index 0000000..1959ea0 --- /dev/null +++ b/tests/ludwig/models/test_training_determinism.py @@ -0,0 +1,142 @@ +import logging +import os + +import numpy as np +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import BATCH_SIZE, EVAL_BATCH_SIZE, TRAINER +from ludwig.utils.numerical_test_utils import assert_all_finite + + +def _assert_stats_close(stats1, stats2, rtol=1e-2, atol=1e-2): + """Assert that two nested stats structures are approximately equal. + + CUDA floating-point operations may introduce non-deterministic differences across runs, so we use approximate + comparison instead of exact equality. + """ + if isinstance(stats1, dict): + assert set(stats1.keys()) == set(stats2.keys()) + for k in stats1: + _assert_stats_close(stats1[k], stats2[k], rtol=rtol, atol=atol) + elif isinstance(stats1, (list, tuple)): + assert len(stats1) == len(stats2) + for v1, v2 in zip(stats1, stats2): + _assert_stats_close(v1, v2, rtol=rtol, atol=atol) + elif isinstance(stats1, (int, float, np.integer, np.floating)): + np.testing.assert_allclose(float(stats1), float(stats2), rtol=rtol, atol=atol) + elif hasattr(stats1, "__dict__"): + _assert_stats_close(vars(stats1), vars(stats2), rtol=rtol, atol=atol) + else: + assert stats1 == stats2 + + +from tests.integration_tests.utils import ( # noqa: E402 + audio_feature, + bag_feature, + binary_feature, + category_feature, + date_feature, + generate_data, + h3_feature, + image_feature, + number_feature, + sequence_feature, + set_feature, + text_feature, + timeseries_feature, + vector_feature, +) + + +@pytest.mark.distributed +@pytest.mark.distributed_f +@pytest.mark.skip(reason="https://github.com/ludwig-ai/ludwig/issues/2686") +def test_training_determinism_ray_backend(csv_filename, tmpdir, ray_cluster_4cpu): + experiment_output_1, experiment_output_2 = train_twice("ray", csv_filename, tmpdir) + + eval_stats_1, train_stats_1, _, _ = experiment_output_1 + eval_stats_2, train_stats_2, _, _ = experiment_output_2 + + assert_all_finite(eval_stats_1) + assert_all_finite(eval_stats_2) + assert_all_finite(train_stats_1) + assert_all_finite(train_stats_2) + + np.testing.assert_equal(eval_stats_1, eval_stats_2) + np.testing.assert_equal(train_stats_1, train_stats_2) + + +def test_training_determinism_local_backend(csv_filename, tmpdir): + experiment_output_1, experiment_output_2 = train_twice("local", csv_filename, tmpdir) + + eval_stats_1, train_stats_1, _, _ = experiment_output_1 + eval_stats_2, train_stats_2, _, _ = experiment_output_2 + + assert_all_finite(eval_stats_1) + assert_all_finite(eval_stats_2) + assert_all_finite(train_stats_1) + assert_all_finite(train_stats_2) + + _assert_stats_close(eval_stats_1, eval_stats_2) + _assert_stats_close(train_stats_1, train_stats_2) + + +def train_twice(backend, csv_filename, tmpdir): + image_dest_folder = os.path.join(tmpdir, "generated_images") + audio_dest_folder = os.path.join(tmpdir, "generated_audio") + + # Configure features to be tested: + input_features = [ + binary_feature(), + number_feature(), + category_feature(encoder={"vocab_size": 10}), + sequence_feature(encoder={"vocab_size": 3}), + text_feature(encoder={"vocab_size": 3}), + vector_feature(), + timeseries_feature(), + date_feature(), + h3_feature(), + set_feature(encoder={"vocab_size": 3}), + bag_feature(encoder={"vocab_size": 3}), + image_feature(image_dest_folder), + audio_feature(audio_dest_folder), + ] + output_features = [ + binary_feature(), + number_feature(), + category_feature(decoder={"vocab_size": 10}), + ] + # NOTE: It's important that we set batch size and eval batch size explicitly to bypass all batch size tuning, which + # is non-deterministic, even with fixed random seeds. + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"epochs": 2, BATCH_SIZE: 128, EVAL_BATCH_SIZE: 2}, + } + + # Generate training data + training_data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=100) + + ludwig_model_1 = LudwigModel(config, logging_level=logging.ERROR, backend=backend) + ludwig_model_2 = LudwigModel(config, logging_level=logging.ERROR, backend=backend) + experiment_output_1 = ludwig_model_1.experiment( + dataset=training_data_csv_path, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + experiment_output_2 = ludwig_model_2.experiment( + dataset=training_data_csv_path, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + + return experiment_output_1, experiment_output_2 diff --git a/tests/ludwig/models/test_training_success.py b/tests/ludwig/models/test_training_success.py new file mode 100644 index 0000000..ec7e5e1 --- /dev/null +++ b/tests/ludwig/models/test_training_success.py @@ -0,0 +1,48 @@ +from contextlib import nullcontext as no_error_raised + +from ludwig.api import LudwigModel +from ludwig.constants import BINARY, TRAINER +from tests.integration_tests.utils import binary_feature, category_feature, generate_data + + +def generate_data_and_train(config, csv_filename): + # Generate training data + training_data_csv_path = generate_data(config["input_features"], config["output_features"], csv_filename) + + # Train Ludwig (Pythonic) model: + ludwig_model = LudwigModel(config) + + with no_error_raised(): + ludwig_model.experiment( + dataset=training_data_csv_path, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) + + +def test_category_passthrough_encoder(csv_filename): + input_features = [category_feature(), category_feature()] + output_features = [category_feature(output_feature=True)] + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: {"train_steps": 1}, + "defaults": {"category": {"encoder": {"type": "passthrough"}}}, + } + generate_data_and_train(config, csv_filename) + + +def test_binary_encoders(csv_filename): + config = { + "input_features": [ + {"name": "binary1", "type": BINARY, "encoder": {"type": "passthrough"}}, + {"name": "binary2", "type": BINARY, "encoder": {"type": "dense"}}, + ], + "output_features": [binary_feature(output_feature=True)], + TRAINER: {"train_steps": 1}, + } + generate_data_and_train(config, csv_filename) diff --git a/tests/ludwig/modules/__init__.py b/tests/ludwig/modules/__init__.py new file mode 100644 index 0000000..42715bb --- /dev/null +++ b/tests/ludwig/modules/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== diff --git a/tests/ludwig/modules/test_attention.py b/tests/ludwig/modules/test_attention.py new file mode 100644 index 0000000..3ea52a2 --- /dev/null +++ b/tests/ludwig/modules/test_attention.py @@ -0,0 +1,160 @@ +import pytest +import torch + +from ludwig.modules.attention_modules import ( + FeedForwardAttentionReducer, + MultiHeadSelfAttention, + TransformerBlock, + TransformerStack, +) +from ludwig.utils.misc_utils import set_random_seed +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("input_hidden_size", [128, 256]) +@pytest.mark.parametrize("input_seq_size", [10]) +@pytest.mark.parametrize("input_batch_size", [16]) +def test_feed_forward_attention_reducer(input_batch_size: int, input_seq_size: int, input_hidden_size: int): + # make repeatable + set_random_seed(RANDOM_SEED) + + # Generate synthetic data + current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32) + + # instantiate feed forward attention reducer + feed_forward_attention_reducer = FeedForwardAttentionReducer(input_hidden_size) + + result = feed_forward_attention_reducer(current_inputs) + + # ensure returned tensor is the correct shape + assert list(result.shape) == [input_batch_size, input_hidden_size] + + # check for parameter updating if fully connected layer is present + target = torch.randn(result.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + feed_forward_attention_reducer, + (current_inputs,), + target, + ) + assert upc == tpc, f"Some parameters not updated. These parameters not updated: {not_updated}" + + +@pytest.mark.parametrize("input_hidden_size", [128, 256]) +@pytest.mark.parametrize("input_seq_size", [1, 10]) +@pytest.mark.parametrize("input_batch_size", [16]) +def test_multihead_self_attention(input_batch_size: int, input_seq_size: int, input_hidden_size: int): + # make repeatable + set_random_seed(RANDOM_SEED) + + # Generate synthetic data + current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32) + + # instantiate feed forward attention reducer + multihead_self_attention = MultiHeadSelfAttention(input_hidden_size, input_hidden_size) + + result = multihead_self_attention(current_inputs) + + # ensure returned tensor is the correct shape + assert list(result.shape) == [input_batch_size, input_seq_size, input_hidden_size] + + # check for parameter updating if fully connected layer is present + target = torch.randn(result.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + multihead_self_attention, + (current_inputs,), + target, + ) + + # With F.scaled_dot_product_attention, all parameters receive gradients even with a single-token sequence. + assert upc == tpc, f"Some parameters not updated. These parameters not updated: {not_updated}" + + +# heads must be a divisor of input_hidden_size +@pytest.mark.parametrize( + "input_batch_size,input_seq_size,input_hidden_size,output_size,heads", + [ + (16, 10, 128, 64, 8), + (16, 20, 256, 128, 16), + (32, 10, 256, 256, 8), + ], + ids=["small", "medium", "large"], +) +def test_transformer_block( + input_batch_size: int, + input_seq_size: int, + input_hidden_size: int, + output_size: int, + heads: int, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + # Generate synthetic data + current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32) + + # instantiate feed forward attention reducer + transformer_block = TransformerBlock(input_hidden_size, input_seq_size, input_hidden_size, heads, output_size) + + result = transformer_block(current_inputs) + + # ensure returned tensor is the correct shape + assert list(result.shape) == [input_batch_size, input_seq_size, input_hidden_size] + + # check for parameter updating if fully connected layer is present + target = torch.randn(result.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + transformer_block, + (current_inputs,), + target, + ) + assert upc == tpc, f"Some parameters not updated. These parameters not updated: {not_updated}" + + +@pytest.mark.parametrize( + "input_batch_size,input_seq_size,input_hidden_size,output_size,heads,num_layers", + [ + (16, 10, 128, 64, 8, 1), + (16, 20, 256, 128, 16, 1), + (32, 10, 256, 256, 8, 4), + ], + ids=["single_layer_small", "single_layer_medium", "multi_layer"], +) +def test_transformer_stack( + input_batch_size: int, + input_seq_size: int, + input_hidden_size: int, + output_size: int, + heads: int, + num_layers: int, +): + # make repeatable + set_random_seed(RANDOM_SEED) + + # Generate synthetic data + current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32) + + # instantiate feed forward attention reducer + transformer_stack = TransformerStack( + input_hidden_size, + input_seq_size, + input_hidden_size, + heads, + output_size, + num_layers, + ) + + result = transformer_stack(current_inputs) + + # ensure returned tensor is the correct shape + assert list(result.shape) == [input_batch_size, input_seq_size, input_hidden_size] + + # check for parameter updating if fully connected layer is present + target = torch.randn(result.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated( + transformer_stack, + (current_inputs,), + target, + ) + assert upc == tpc, f"Some parameters not updated. These parameters not updated: {not_updated}" diff --git a/tests/ludwig/modules/test_batch_ensemble.py b/tests/ludwig/modules/test_batch_ensemble.py new file mode 100644 index 0000000..4445e26 --- /dev/null +++ b/tests/ludwig/modules/test_batch_ensemble.py @@ -0,0 +1,48 @@ +"""Tests for BatchEnsemble.""" + +import torch + +from ludwig.modules.batch_ensemble import BatchEnsembleLinear + + +class TestBatchEnsembleLinear: + def test_output_shape(self): + layer = BatchEnsembleLinear(128, 64, num_members=4) + x = torch.randn(16, 128) + out = layer(x) + assert out.shape == (16, 64) + + def test_train_vs_eval(self): + layer = BatchEnsembleLinear(32, 16, num_members=4) + x = torch.randn(8, 32) + + layer.train() + out_train = layer(x) + + layer.eval() + out_eval = layer(x) + + assert out_train.shape == out_eval.shape + + def test_gradient_flow(self): + layer = BatchEnsembleLinear(32, 16, num_members=4) + layer.train() + x = torch.randn(8, 32, requires_grad=True) + out = layer(x) + out.sum().backward() + assert x.grad is not None + assert layer.weight.grad is not None + assert layer.r.grad is not None + assert layer.s.grad is not None + + def test_per_member_params(self): + layer = BatchEnsembleLinear(32, 16, num_members=4) + assert layer.r.shape == (4, 32) + assert layer.s.shape == (4, 16) + + def test_no_bias(self): + layer = BatchEnsembleLinear(32, 16, num_members=2, bias=False) + assert layer.bias is None + x = torch.randn(4, 32) + out = layer(x) + assert out.shape == (4, 16) diff --git a/tests/ludwig/modules/test_contrastive_alignment.py b/tests/ludwig/modules/test_contrastive_alignment.py new file mode 100644 index 0000000..9983c65 --- /dev/null +++ b/tests/ludwig/modules/test_contrastive_alignment.py @@ -0,0 +1,152 @@ +"""Unit tests for ContrastiveAlignmentLoss (Phase 6.4.2).""" + +from __future__ import annotations + +import math + +import pytest +import torch + +from ludwig.modules.contrastive_alignment import ContrastiveAlignmentLoss + + +class TestContrastiveAlignmentLoss: + def test_shape_and_scalar(self): + torch.manual_seed(0) + loss_fn = ContrastiveAlignmentLoss({"a": 8, "b": 12}, projection_dim=16) + batch = { + "a": torch.randn(4, 8), + "b": torch.randn(4, 12), + } + loss = loss_fn(batch) + assert loss.ndim == 0 + assert torch.isfinite(loss) + + def test_perfect_alignment_near_zero_loss(self): + """When two feature embeddings are identical and projection is identity-ish, loss should be very small + because the diagonal dominates the similarity matrix.""" + torch.manual_seed(0) + # Use a large projection_dim and initialize projections to near-identity so paired + # examples map to near-identical points in the aligned space. + loss_fn = ContrastiveAlignmentLoss({"a": 16, "b": 16}, projection_dim=16, learnable_temperature=False) + with torch.no_grad(): + for name in ["a", "b"]: + loss_fn.projections[name].weight.copy_(torch.eye(16)) + loss_fn.projections[name].bias.zero_() + x = torch.randn(32, 16) + loss = loss_fn({"a": x, "b": x.clone()}) + # With identical features and an identity projection the contrastive loss is minimised. + assert loss.item() < 0.1 + + def test_misalignment_increases_loss(self): + """Pairing example i with i works better than random pairing.""" + torch.manual_seed(0) + loss_fn = ContrastiveAlignmentLoss({"a": 16, "b": 16}, projection_dim=16, learnable_temperature=False) + with torch.no_grad(): + for name in ["a", "b"]: + loss_fn.projections[name].weight.copy_(torch.eye(16)) + loss_fn.projections[name].bias.zero_() + x = torch.randn(32, 16) + + aligned = loss_fn({"a": x, "b": x.clone()}) + # Shuffle feature b so its positive is no longer at position i. + perm = torch.randperm(32) + misaligned = loss_fn({"a": x, "b": x[perm].clone()}) + assert misaligned.item() > aligned.item() + + def test_pair_symmetry(self): + """Swapping two feature names should leave the loss value unchanged (up to floating-point).""" + torch.manual_seed(0) + loss_fn = ContrastiveAlignmentLoss({"a": 8, "b": 8}, projection_dim=16, learnable_temperature=False) + with torch.no_grad(): + # Copy feature-a projection weights into feature-b so the two features become + # interchangeable. + loss_fn.projections["b"].weight.copy_(loss_fn.projections["a"].weight) + loss_fn.projections["b"].bias.copy_(loss_fn.projections["a"].bias) + x = torch.randn(16, 8) + y = torch.randn(16, 8) + ab = loss_fn({"a": x, "b": y}) + ba = loss_fn({"a": y, "b": x}) + assert torch.allclose(ab, ba, atol=1e-5) + + def test_three_features(self): + """Loss should accommodate any number >= 2 of features and average over pairs.""" + torch.manual_seed(0) + loss_fn = ContrastiveAlignmentLoss({"a": 4, "b": 5, "c": 6}, projection_dim=8) + embeddings = { + "a": torch.randn(4, 4), + "b": torch.randn(4, 5), + "c": torch.randn(4, 6), + } + loss = loss_fn(embeddings) + assert torch.isfinite(loss) + + def test_rejects_single_feature(self): + with pytest.raises(ValueError, match="at least 2 input features"): + ContrastiveAlignmentLoss({"only_one": 8}) + + def test_rejects_missing_feature_in_batch(self): + loss_fn = ContrastiveAlignmentLoss({"a": 4, "b": 4}, projection_dim=8) + with pytest.raises(KeyError, match="expected feature 'b'"): + loss_fn({"a": torch.randn(2, 4)}) + + def test_learnable_vs_fixed_temperature(self): + fixed = ContrastiveAlignmentLoss({"a": 4, "b": 4}, projection_dim=8, learnable_temperature=False) + learnable = ContrastiveAlignmentLoss({"a": 4, "b": 4}, projection_dim=8, learnable_temperature=True) + + assert not fixed.log_temperature.requires_grad + assert learnable.log_temperature.requires_grad + # Both should start at log(1/0.07). + expected = math.log(1.0 / 0.07) + assert abs(float(fixed.log_temperature) - expected) < 1e-5 + assert abs(float(learnable.log_temperature) - expected) < 1e-5 + + def test_backward_populates_encoder_grads(self): + """The loss gradient must flow into the per-feature encoder inputs so an upstream encoder is actually + updated during pre-alignment.""" + loss_fn = ContrastiveAlignmentLoss({"a": 8, "b": 8}, projection_dim=16) + a = torch.randn(4, 8, requires_grad=True) + b = torch.randn(4, 8, requires_grad=True) + loss = loss_fn({"a": a, "b": b}) + loss.backward() + assert a.grad is not None and torch.isfinite(a.grad).all() + assert b.grad is not None and torch.isfinite(b.grad).all() + + +class TestContrastivePretrainSchema: + def test_default_values(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + "input_features": [ + {"name": "a", "type": "number"}, + {"name": "b", "type": "number"}, + ], + "output_features": [{"name": "y", "type": "binary"}], + } + ) + assert cfg.trainer.contrastive_pretrain_epochs == 0 + assert cfg.trainer.contrastive_pretrain_temperature == 0.07 + assert cfg.trainer.contrastive_pretrain_projection_dim == 128 + + def test_explicit_values(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + "input_features": [ + {"name": "a", "type": "number"}, + {"name": "b", "type": "number"}, + ], + "output_features": [{"name": "y", "type": "binary"}], + "trainer": { + "contrastive_pretrain_epochs": 3, + "contrastive_pretrain_temperature": 0.1, + "contrastive_pretrain_projection_dim": 64, + }, + } + ) + assert cfg.trainer.contrastive_pretrain_epochs == 3 + assert cfg.trainer.contrastive_pretrain_temperature == 0.1 + assert cfg.trainer.contrastive_pretrain_projection_dim == 64 diff --git a/tests/ludwig/modules/test_convolutional_modules.py b/tests/ludwig/modules/test_convolutional_modules.py new file mode 100644 index 0000000..32c75d4 --- /dev/null +++ b/tests/ludwig/modules/test_convolutional_modules.py @@ -0,0 +1,514 @@ +from collections.abc import Callable + +import pytest +import torch + +from ludwig.modules.convolutional_modules import ( + Conv1DLayer, + Conv1DStack, + Conv2DLayer, + Conv2DLayerFixedPadding, + Conv2DStack, + ParallelConv1D, + ParallelConv1DStack, + ResNet, + ResNetBlock, + ResNetBlockLayer, + ResNetBottleneckBlock, +) +from ludwig.utils.image_utils import get_img_output_shape +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +BATCH_SIZE = 2 +SEQ_SIZE = 17 +HIDDEN_SIZE = 8 +NUM_FILTERS = 4 + +RANDOM_SEED = 1919 + + +### +# Helper function to compute expected output shape +# for Conv1D related layers +### +def expected_seq_size( + seq_size: int, # input max sequence length + padding: str, # conv1d padding: 'same' or 'valid' + kernel_size: int, # conv1d kernel size + stride: int, # conv1d stride + dilation: int, # conv1d dilation rate + pool_size: None | int, # pooling layer kernel size + pool_padding: str, # pooling layer padding: 'same' or 'valid' + pool_stride: int, # pooling layer stride +) -> int: + # output shape for the convolutional layer + output_seq_size = get_img_output_shape( + img_height=0, # img_height set to zero for 1D structure + img_width=seq_size, # img_width equates to max sequence length + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + ) + if pool_size is not None: + # pooling layer present, adjust expected output shape for pooling layer + output_seq_size = get_img_output_shape( + img_height=0, # img_height set to zero for 1D structure + img_width=output_seq_size[1], # img_width equates to max sequence length + kernel_size=pool_size, + stride=pool_stride, + padding=pool_padding, + dilation=1, # pooling layer only support unit dilation + ) + return output_seq_size[1] + + +### +# 1D Convolutional Tests +### +@pytest.mark.parametrize("pool_function", ["max", "mean"]) +@pytest.mark.parametrize( + "pool_size, pool_padding, pool_stride", + [(None, None, None), (3, "same", 1), (5, "same", 1), (3, "valid", 2), (5, "valid", 2)], +) +@pytest.mark.parametrize("dilation", [1, 2]) +@pytest.mark.parametrize("strides, padding", [(1, "same"), (1, "valid"), (2, "valid")]) +@pytest.mark.parametrize("kernel_size", [3, 5]) +def test_conv1d_layer( + kernel_size: int, + strides: int, + padding: str, + dilation: int, + pool_size: None | int, + pool_padding: str, + pool_stride: int, + pool_function: str, +) -> None: + # make test repeatable + torch.manual_seed(RANDOM_SEED) + + # setup synthetic tensor for test + input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32) + + conv1_layer = Conv1DLayer( + in_channels=HIDDEN_SIZE, + out_channels=NUM_FILTERS, + max_sequence_length=SEQ_SIZE, + kernel_size=kernel_size, + strides=strides, + padding=padding, + dilation=dilation, + pool_function=pool_function, + pool_size=pool_size, + pool_strides=pool_stride, + pool_padding=pool_padding, + ) + + out_tensor = conv1_layer(input) + + # check for correct output class + assert isinstance(out_tensor, torch.Tensor) + + # check for correct output shape + output_seq_size = expected_seq_size( + seq_size=SEQ_SIZE, + padding=padding, + kernel_size=kernel_size, + stride=strides, + dilation=dilation, + pool_size=pool_size, + pool_padding=pool_padding, + pool_stride=pool_stride, + ) + assert out_tensor.size() == (BATCH_SIZE, output_seq_size, NUM_FILTERS) + + +@pytest.mark.parametrize("dropout", [0, 0.5]) +@pytest.mark.parametrize( + "layers, num_layers", + [ + (None, None), # setup up default number of layers with default values + (None, 4), # setup of 4 layers with default values + ([{"num_filters": NUM_FILTERS - 2}, {"num_filters": NUM_FILTERS + 2}], None), # 2 custom layers + ], +) +def test_conv1d_stack(layers: None | list, num_layers: None | int, dropout: float) -> None: + # make test repeatable + torch.manual_seed(RANDOM_SEED) + + # setup synthetic input tensor for test + input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32) + + conv1_stack = Conv1DStack( + in_channels=HIDDEN_SIZE, + out_channels=NUM_FILTERS, + max_sequence_length=SEQ_SIZE, + layers=layers, + num_layers=num_layers, + default_num_filters=NUM_FILTERS, + default_dropout=dropout, + ) + + # check for correct stack formation + if layers is None: + assert len(conv1_stack.stack) == 6 if num_layers is None else num_layers + else: + # custom layer specification + assert len(conv1_stack.stack) == len(layers) + assert conv1_stack.stack[0].out_channels == NUM_FILTERS - 2 + assert conv1_stack.stack[1].out_channels == NUM_FILTERS + 2 + + # generate output tensor + out_tensor = conv1_stack(input) + + # check for correct output class + assert isinstance(out_tensor, torch.Tensor) + + assert out_tensor.size()[1:] == conv1_stack.output_shape[:] + + # check for correct output shape + last_module = conv1_stack.stack[-1] + output_seq_size = expected_seq_size( + seq_size=last_module.input_shape[0], + padding=last_module.padding, + kernel_size=last_module.kernel_size, + stride=last_module.stride, + dilation=last_module.dilation, + pool_size=last_module.pool_size, + pool_padding=last_module.pool_padding, + pool_stride=last_module.pool_strides, + ) + if layers is None: + # default stack setup + assert out_tensor.size() == (BATCH_SIZE, output_seq_size, NUM_FILTERS) + else: + # custom stack setup + assert out_tensor.size() == (BATCH_SIZE, output_seq_size, NUM_FILTERS + 2) + + # check for parameter updates + target = torch.randn(conv1_stack.output_shape) + _, tpc, upc, not_updated = check_module_parameters_updated(conv1_stack, (input,), target) + if dropout == 0: + # all trainable parameters should be updated + assert tpc == upc, ( + f"All parameter not updated. Parameters not updated: {not_updated}\nModule structure:\n{conv1_stack}" + ) + else: + # with specified config and random seed, non-zero dropout update parameter count could take different values + assert (tpc == upc) or (upc == 1), ( + f"All parameter not updated. Parameters not updated: {not_updated}\nModule structure:\n{conv1_stack}" + ) + + +@pytest.mark.parametrize( + "layers", + [ + None, # setup up default number of layers with default values + [{"filter_size": 3}, {"filter_size": 4}], # custom parallel layers + ], +) +def test_parallel_conv1d(layers: None | list) -> None: + input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32) + + parallel_conv1d = ParallelConv1D( + in_channels=HIDDEN_SIZE, + out_channels=NUM_FILTERS, + max_sequence_length=SEQ_SIZE, + layers=layers, + default_num_filters=NUM_FILTERS, + ) + + # check for correct stack formation + if layers is None: + assert len(parallel_conv1d.parallel_layers) == 4 + else: + # custom layer specification + assert len(parallel_conv1d.parallel_layers) == len(layers) + assert parallel_conv1d.parallel_layers[0].kernel_size == 3 + assert parallel_conv1d.parallel_layers[1].kernel_size == 4 + + # generate output tensor + out_tensor = parallel_conv1d(input) + + # check for correct output class + assert isinstance(out_tensor, torch.Tensor) + + # check for correct output shape + parallel_module = parallel_conv1d.parallel_layers[0] + output_seq_size = expected_seq_size( + seq_size=parallel_module.input_shape[0], + padding=parallel_module.padding, + kernel_size=parallel_module.kernel_size, + stride=parallel_module.stride, + dilation=parallel_module.dilation, + pool_size=parallel_module.pool_size, + pool_padding=parallel_module.pool_padding, + pool_stride=parallel_module.pool_strides, + ) + + assert out_tensor.size() == (BATCH_SIZE, output_seq_size, len(parallel_conv1d.parallel_layers) * NUM_FILTERS) + + +TEST_FILTER_SIZE0 = 7 +TEST_FILTER_SIZE1 = 5 + + +@pytest.mark.parametrize("dropout", [0, 0.99]) +@pytest.mark.parametrize( + "stacked_layers", + [ + None, # setup up default number of layers with default values + # custom stacked parallel layers + [ + [ # parallel_conv1d_stack.stack[0] + {"filter_size": 3}, + {"filter_size": 5}, + {"filter_size": TEST_FILTER_SIZE0}, + ], + [ # parallel_conv1d_stack.stack[1] + {"filter_size": 2}, + {"filter_size": 3}, + {"filter_size": 4}, + {"filter_size": TEST_FILTER_SIZE1}, + ], + ], + ], +) +def test_parallel_conv1d_stack(stacked_layers: None | list, dropout: float) -> None: + # make repeatable + torch.manual_seed(RANDOM_SEED) + + # setup synthetic input tensor for test + input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32) + + parallel_conv1d_stack = ParallelConv1DStack( + in_channels=HIDDEN_SIZE, + out_channels=NUM_FILTERS, + max_sequence_length=SEQ_SIZE, + stacked_layers=stacked_layers, + default_num_filters=NUM_FILTERS, + default_dropout=dropout, + ) + + # check for correct stack formation + if stacked_layers is None: + assert len(parallel_conv1d_stack.stack) == 3 + for i in range(len(parallel_conv1d_stack.stack)): + assert len(parallel_conv1d_stack.stack[i].parallel_layers) == 4 + else: + # spot check custom layer specification + assert len(parallel_conv1d_stack.stack) == len(stacked_layers) + assert len(parallel_conv1d_stack.stack[0].parallel_layers) == 3 + assert parallel_conv1d_stack.stack[0].parallel_layers[2].kernel_size == TEST_FILTER_SIZE0 + assert len(parallel_conv1d_stack.stack[1].parallel_layers) == 4 + assert parallel_conv1d_stack.stack[1].parallel_layers[3].kernel_size == TEST_FILTER_SIZE1 + + # generate output tensor + out_tensor = parallel_conv1d_stack(input) + + # check for correct output class + assert isinstance(out_tensor, torch.Tensor) + + # check output shape + assert out_tensor.size() == (BATCH_SIZE, *parallel_conv1d_stack.output_shape) + + # check for parameter updates + target = torch.randn(parallel_conv1d_stack.output_shape) + _, tpc, upc, not_updated = check_module_parameters_updated(parallel_conv1d_stack, (input,), target) + if dropout == 0: + # all trainable parameters should be updated + assert tpc == upc, ( + f"All parameter not updated. Parameters not updated: {not_updated}" + f"\nModule structure:\n{parallel_conv1d_stack}" + ) + else: + # With high dropout (0.99), most gradients are zeroed out. The exact number of updated + # parameters depends on the random seed and PyTorch version. + assert upc > 0, ( + f"No parameters updated with dropout={dropout}. Parameters not updated: {not_updated}" + f"\nModule structure:\n{parallel_conv1d_stack}" + ) + + +### +# 2D Convolutional Tests +### +@pytest.mark.parametrize( + ("img_height,img_width,in_channels,out_channels,pool_kernel_size,pool_stride,pool_padding,pool_dilation"), + [(224, 224, 3, 16, 2, 2, 0, 1)], +) +@pytest.mark.parametrize("stride,padding", [(1, "valid"), (1, "same"), (2, "valid")]) +@pytest.mark.parametrize("kernel_size", [1, 3, 5]) +@pytest.mark.parametrize("dilation", [1, 2]) +@pytest.mark.parametrize("norm", ["batch", "layer"]) +def test_conv2d_layer( + img_height: int, + img_width: int, + in_channels: int, + out_channels: int, + kernel_size: int, + stride: int, + padding: int | tuple[int] | str, + dilation: int | tuple[int], + norm: str, + pool_kernel_size: int | tuple[int], + pool_stride: int, + pool_padding: int | tuple[int] | str, + pool_dilation: int | tuple[int], +) -> None: + conv2d_layer = Conv2DLayer( + img_height=img_height, + img_width=img_width, + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + norm=norm, + pool_kernel_size=pool_kernel_size, + pool_stride=pool_stride, + pool_padding=pool_padding, + pool_dilation=pool_dilation, + ) + input_tensor = torch.rand(2, in_channels, img_height, img_width) + output_tensor = conv2d_layer(input_tensor) + assert output_tensor.shape[1:] == conv2d_layer.output_shape + + +@pytest.mark.parametrize("img_height,img_width", [(224, 224)]) +@pytest.mark.parametrize( + "layers,num_layers,first_in_channels", + [ + (None, None, 3), + (None, 5, 3), + ([{"out_channels": 8}], None, 3), + ([{"out_channels": 8, "in_channels": 3}], None, None), + ], +) +def test_conv2d_stack( + img_height: int, + img_width: int, + layers: list[dict] | None, + num_layers: int | None, + first_in_channels: int | None, +) -> None: + conv2d_stack = Conv2DStack( + img_height=img_height, + img_width=img_width, + layers=layers, + num_layers=num_layers, + first_in_channels=first_in_channels, + ) + input_tensor = torch.rand(2, 3, img_height, img_width) + output_tensor = conv2d_stack(input_tensor) + assert output_tensor.shape[1:] == conv2d_stack.output_shape + + +@pytest.mark.parametrize("img_height,img_width,in_channels", [(224, 224, 8)]) +@pytest.mark.parametrize("stride", [1, 3]) +@pytest.mark.parametrize("groups", [1, 8]) +def test_conv2d_layer_fixed_padding( + img_height: int, img_width: int, in_channels: int, stride: int, groups: int +) -> None: + conv2d_fixed_padding = Conv2DLayerFixedPadding( + img_height=img_height, img_width=img_width, in_channels=in_channels, stride=stride, groups=groups + ) + input_tensor = torch.rand(2, in_channels, img_height, img_width) + output_tensor = conv2d_fixed_padding(input_tensor) + assert output_tensor.shape[1:] == conv2d_fixed_padding.output_shape + + +@pytest.mark.parametrize("img_height,img_width,first_in_channels,out_channels", [(224, 224, 64, 64)]) +@pytest.mark.parametrize( + "projection_shortcut", + [None, Conv2DLayerFixedPadding(img_height=224, img_width=224, in_channels=64, out_channels=64)], +) +def test_resnet_block( + img_height: int, img_width: int, first_in_channels: int, out_channels: int, projection_shortcut: Callable +) -> None: + resnet_block = ResNetBlock( + img_height=img_height, + img_width=img_width, + first_in_channels=first_in_channels, + out_channels=out_channels, + projection_shortcut=projection_shortcut, + ) + input_tensor = torch.rand(2, first_in_channels, img_height, img_width) + output_tensor = resnet_block(input_tensor) + assert output_tensor.shape[1:] == resnet_block.output_shape + + +@pytest.mark.parametrize("img_height,img_width,first_in_channels,out_channels", [(224, 224, 64, 64)]) +@pytest.mark.parametrize( + "projection_shortcut", + [None, Conv2DLayerFixedPadding(img_height=224, img_width=224, in_channels=64, out_channels=256)], +) +def test_resnet_bottleneck_block( + img_height: int, img_width: int, first_in_channels: int, out_channels: int, projection_shortcut: Callable +) -> None: + resnet_block = ResNetBottleneckBlock( + img_height=img_height, + img_width=img_width, + first_in_channels=first_in_channels, + out_channels=out_channels, + projection_shortcut=projection_shortcut, + ) + input_tensor = torch.rand(2, first_in_channels, img_height, img_width) + output_tensor = resnet_block(input_tensor) + assert output_tensor.shape[1:] == resnet_block.output_shape + + +@pytest.mark.parametrize("img_height,img_width,first_in_channels,out_channels,num_blocks", [(224, 224, 3, 32, 3)]) +@pytest.mark.parametrize("is_bottleneck, block_fn", [(True, ResNetBottleneckBlock), (False, ResNetBlock)]) +def test_resnet_block_layer( + img_height: int, + img_width: int, + first_in_channels: int, + out_channels: int, + is_bottleneck: bool, + block_fn: ResNetBlock | ResNetBottleneckBlock, + num_blocks: int, +): + resnet_block_layer = ResNetBlockLayer( + img_height=img_height, + img_width=img_width, + first_in_channels=first_in_channels, + out_channels=out_channels, + is_bottleneck=is_bottleneck, + block_fn=block_fn, + num_blocks=num_blocks, + ) + input_tensor = torch.rand(2, first_in_channels, img_height, img_width) + output_tensor = resnet_block_layer(input_tensor) + assert output_tensor.shape[1:] == resnet_block_layer.output_shape + + +@pytest.mark.parametrize("img_height,img_width,first_in_channels,out_channels", [(224, 224, 3, 64)]) +@pytest.mark.parametrize("resnet_size", [18, 34, 50]) +def test_resnet( + img_height: int, + img_width: int, + first_in_channels: int, + out_channels: int, + resnet_size: int, +): + # make repeatable + torch.manual_seed(RANDOM_SEED) + + resnet = ResNet( + img_height=img_height, + img_width=img_width, + first_in_channels=first_in_channels, + out_channels=out_channels, + resnet_size=resnet_size, + ) + input_tensor = torch.rand(2, first_in_channels, img_height, img_width) + output_tensor = resnet(input_tensor) + assert output_tensor.shape[1:] == resnet.output_shape + + # check for parameter updates + target = torch.randn(output_tensor.shape) + fpc, tpc, upc, not_updated = check_module_parameters_updated(resnet, (input_tensor,), target) + # all trainable parameters should be updated + assert tpc == upc, f"All parameter not updated. Parameters not updated: {not_updated}\nModule structure:\n{resnet}" diff --git a/tests/ludwig/modules/test_dpo_loss.py b/tests/ludwig/modules/test_dpo_loss.py new file mode 100644 index 0000000..bd430d3 --- /dev/null +++ b/tests/ludwig/modules/test_dpo_loss.py @@ -0,0 +1,102 @@ +"""Tests for DPO loss function.""" + +import torch + +from ludwig.modules.dpo_loss import compute_token_log_probs, dpo_loss + + +class TestComputeTokenLogProbs: + def test_basic_shape(self): + logits = torch.randn(2, 10, 100) # batch=2, seq_len=10, vocab=100 + labels = torch.randint(0, 100, (2, 10)) + result = compute_token_log_probs(logits, labels) + assert result.shape == (2,) + + def test_ignores_minus_100(self): + logits = torch.randn(1, 5, 100) + labels = torch.tensor([[-100, -100, 3, 4, 5]]) + result = compute_token_log_probs(logits, labels) + assert torch.isfinite(result).all() + + def test_all_ignored_gives_zero(self): + logits = torch.randn(1, 5, 100) + labels = torch.full((1, 5), -100) + result = compute_token_log_probs(logits, labels) + assert result.item() == 0.0 + + +class TestDPOLoss: + def test_sigmoid_loss_basic(self): + batch, seq_len, vocab = 4, 20, 100 + chosen_logits = torch.randn(batch, seq_len, vocab) + rejected_logits = torch.randn(batch, seq_len, vocab) + chosen_labels = torch.randint(0, vocab, (batch, seq_len)) + rejected_labels = torch.randint(0, vocab, (batch, seq_len)) + + loss, chosen_rewards, rejected_rewards = dpo_loss( + chosen_logits, rejected_logits, chosen_labels, rejected_labels, beta=0.1 + ) + assert loss.shape == () + assert torch.isfinite(loss) + assert loss > 0 # DPO loss is always positive + + def test_ipo_loss(self): + batch, seq_len, vocab = 2, 10, 50 + chosen_logits = torch.randn(batch, seq_len, vocab) + rejected_logits = torch.randn(batch, seq_len, vocab) + chosen_labels = torch.randint(0, vocab, (batch, seq_len)) + rejected_labels = torch.randint(0, vocab, (batch, seq_len)) + + loss, _, _ = dpo_loss(chosen_logits, rejected_logits, chosen_labels, rejected_labels, beta=0.1, loss_type="ipo") + assert torch.isfinite(loss) + + def test_label_smoothing(self): + batch, seq_len, vocab = 2, 10, 50 + chosen_logits = torch.randn(batch, seq_len, vocab) + rejected_logits = torch.randn(batch, seq_len, vocab) + chosen_labels = torch.randint(0, vocab, (batch, seq_len)) + rejected_labels = torch.randint(0, vocab, (batch, seq_len)) + + loss_no_smooth, _, _ = dpo_loss( + chosen_logits, rejected_logits, chosen_labels, rejected_labels, beta=0.1, label_smoothing=0.0 + ) + loss_smooth, _, _ = dpo_loss( + chosen_logits, rejected_logits, chosen_labels, rejected_labels, beta=0.1, label_smoothing=0.1 + ) + # With smoothing, loss should be different + assert not torch.allclose(loss_no_smooth, loss_smooth) + + def test_rewards_are_detached(self): + batch, seq_len, vocab = 2, 10, 50 + chosen_logits = torch.randn(batch, seq_len, vocab, requires_grad=True) + rejected_logits = torch.randn(batch, seq_len, vocab, requires_grad=True) + chosen_labels = torch.randint(0, vocab, (batch, seq_len)) + rejected_labels = torch.randint(0, vocab, (batch, seq_len)) + + _, chosen_rewards, rejected_rewards = dpo_loss(chosen_logits, rejected_logits, chosen_labels, rejected_labels) + assert not chosen_rewards.requires_grad + assert not rejected_rewards.requires_grad + + def test_gradient_flow(self): + batch, seq_len, vocab = 2, 10, 50 + chosen_logits = torch.randn(batch, seq_len, vocab, requires_grad=True) + rejected_logits = torch.randn(batch, seq_len, vocab, requires_grad=True) + chosen_labels = torch.randint(0, vocab, (batch, seq_len)) + rejected_labels = torch.randint(0, vocab, (batch, seq_len)) + + loss, _, _ = dpo_loss(chosen_logits, rejected_logits, chosen_labels, rejected_labels) + loss.backward() + assert chosen_logits.grad is not None + assert rejected_logits.grad is not None + + def test_invalid_loss_type_raises(self): + import pytest + + with pytest.raises(ValueError, match="Unknown DPO loss type"): + dpo_loss( + torch.randn(1, 5, 10), + torch.randn(1, 5, 10), + torch.randint(0, 10, (1, 5)), + torch.randint(0, 10, (1, 5)), + loss_type="invalid", + ) diff --git a/tests/ludwig/modules/test_embedding_modules.py b/tests/ludwig/modules/test_embedding_modules.py new file mode 100644 index 0000000..e1b912b --- /dev/null +++ b/tests/ludwig/modules/test_embedding_modules.py @@ -0,0 +1,95 @@ +import pytest +import torch + +from ludwig.modules.embedding_modules import Embed, EmbedSequence, EmbedSet, EmbedWeighted, TokenAndPositionEmbedding +from ludwig.utils.torch_utils import get_torch_device + +DEVICE = get_torch_device() + + +@pytest.mark.parametrize("vocab", [["a", "b", "c"]]) +@pytest.mark.parametrize("embedding_size", [2]) +@pytest.mark.parametrize("representation", ["dense", "sparse"]) +def test_embed( + vocab: list[str], + embedding_size: int, + representation: str, +): + embed = Embed( + vocab=vocab, + embedding_size=embedding_size, + representation=representation, + ).to(DEVICE) + inputs = torch.randint(0, 2, size=(2, 1)).bool().to(DEVICE) + outputs = embed(inputs) + assert outputs.shape[1:] == embed.output_shape + + +@pytest.mark.parametrize("vocab", [["a", "b", "c", "d"]]) +@pytest.mark.parametrize("embedding_size", [3]) +@pytest.mark.parametrize("representation", ["dense", "sparse"]) +def test_embed_set( + vocab: list[str], + embedding_size: int, + representation: str, +): + embed = EmbedSet( + vocab=vocab, + embedding_size=embedding_size, + representation=representation, + ).to(DEVICE) + inputs = torch.randint(0, 2, size=(2, len(vocab))).bool().to(DEVICE) + outputs = embed(inputs) + assert outputs.shape[1:] == embed.output_shape + + +@pytest.mark.parametrize("vocab", [["a", "b", "c", "d", "e", "f", "g", "h"]]) +@pytest.mark.parametrize("embedding_size", [5, 10]) +@pytest.mark.parametrize("representation", ["dense", "sparse"]) +def test_embed_weighted( + vocab: list[str], + embedding_size: int, + representation: str, +): + embed_weighted = EmbedWeighted(vocab=vocab, embedding_size=embedding_size, representation=representation).to(DEVICE) + inputs = torch.randint(0, 2, size=(2, len(vocab))).bool().to(DEVICE) + outputs = embed_weighted(inputs) + assert outputs.shape[1:] == embed_weighted.output_shape + + +@pytest.mark.parametrize("vocab", [["a", "b", "c"]]) +@pytest.mark.parametrize("embedding_size", [2]) +@pytest.mark.parametrize("representation", ["dense", "sparse"]) +def test_embed_sequence( + vocab: list[str], + embedding_size: int, + representation: str, +): + embed = EmbedSequence( + vocab=vocab, + embedding_size=embedding_size, + max_sequence_length=10, + representation=representation, + ).to(DEVICE) + inputs = torch.randint(0, 2, size=(2, 10)).to(DEVICE) + outputs = embed(inputs) + assert outputs.shape[1:] == embed.output_shape + + +@pytest.mark.parametrize("vocab", [["a", "b", "c"]]) +@pytest.mark.parametrize("embedding_size", [10]) +@pytest.mark.parametrize("representation", ["dense", "sparse"]) +def test_token_and_position_embedding( + vocab: list[str], + embedding_size: int, + representation: str, +): + embed = TokenAndPositionEmbedding( + vocab=vocab, + embedding_size=embedding_size, + max_sequence_length=10, + representation=representation, + ).to(DEVICE) + inputs = torch.randint(0, 2, size=(2, 10)).to(DEVICE) + outputs = embed(inputs) + assert outputs.shape[1:] == embed.output_shape diff --git a/tests/ludwig/modules/test_encoder.py b/tests/ludwig/modules/test_encoder.py new file mode 100644 index 0000000..25412ac --- /dev/null +++ b/tests/ludwig/modules/test_encoder.py @@ -0,0 +1,330 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import random + +import numpy as np +import pytest +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.data.dataset_synthesizer import build_vocab +from ludwig.encoders.base import Encoder +from ludwig.encoders.image.base import MLPMixerEncoder, Stacked2DCNN +from ludwig.encoders.sequence_encoders import ( + ParallelCNN, + SequenceEmbedEncoder, + StackedCNN, + StackedCNNRNN, + StackedParallelCNN, + StackedRNN, +) +from ludwig.utils.torch_utils import get_torch_device +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +DROPOUT = 0.5 +DEVICE = get_torch_device() +RANDOM_SEED = 1919 + + +def create_encoder(encoder_type, **encoder_kwargs): + encoder = encoder_type(**encoder_kwargs) + return encoder + + +def _generate_image(image_size): + return np.random.randint(0, 1, image_size).astype(np.float32) + + +def generate_images(image_size, num_images): + return np.array([_generate_image(image_size) for _ in range(num_images)]) + + +def _generate_sentence(vocab_size, max_len): + sentence = np.zeros(max_len, dtype=np.int32) + random_length = random.randint(1, max_len) + sentence[:random_length] = [random.randint(0, vocab_size - 1) for _ in range(random_length)] + + return sentence + + +def generate_random_sentences(num_sentences=10, max_len=10, vocab_size=10): + # Generate some random text + vocab = build_vocab(vocab_size) + + text = np.array([_generate_sentence(vocab_size, max_len) for _ in range(num_sentences)]) + + return text, vocab + + +def encoder_test( + encoder, + input_data, + output_dtype, + output_shape, + output_data=None, +): + """Helper method to test different kinds of encoders. + + :param encoder: encoder object + :param input_data: data to encode + :param output_dtype: expected data type of the output (optional) + :param output_shape: expected shape of the encoder output (optional) + :param output_data: expected output data (optional) + :return: returns the encoder object for the caller to run extra checks + """ + encoder = encoder.to(DEVICE) + + # Run the encoder + input_data = torch.from_numpy(input_data).to(DEVICE) + + hidden = encoder(input_data)[ENCODER_OUTPUT] + + # Check output shape and type + assert hidden.dtype == output_dtype + assert list(hidden.shape) == output_shape + + if output_data is not None: + # todo the hidden output is actually a tensor. May need modification + assert np.allclose(hidden, output_data) + + +def test_image_encoders_stacked_2dcnn(): + # make repeatable + np.random.seed(RANDOM_SEED) + torch.manual_seed(RANDOM_SEED) + + # Test the resnet encoder for images + encoder_kwargs = {"num_conv_layers": 2, "num_filters": 16, "output_size": 28, "dropout": DROPOUT} + image_size = (3, 10, 10) + + encoder = create_encoder( + Stacked2DCNN, num_channels=image_size[0], height=image_size[1], width=image_size[2], **encoder_kwargs + ) + + assert encoder is not None + assert encoder.conv_stack_2d is not None + assert list(encoder.conv_stack_2d.output_shape) == [32, 1, 1] + assert len(encoder.fc_stack.layers) == 1 + assert encoder.conv_stack_2d.layers[0]["pool_kernel_size"] == 2 + assert encoder.conv_stack_2d.layers[0]["stride"] == 1 + assert encoder.conv_stack_2d.layers[0]["pool_stride"] == 2 + assert encoder.conv_stack_2d.layers[0]["norm"] is None + assert encoder.conv_stack_2d.layers[0]["activation"] == "relu" + assert encoder.conv_stack_2d.layers[0]["dropout"] == 0 + + output_shape = [1, 28] + input_image = generate_images(image_size, 1) + + encoder_test( + encoder=encoder, input_data=input_image, output_dtype=torch.float32, output_shape=output_shape, output_data=None + ) + + output_shape = [5, 28] + input_images = generate_images(image_size, 5) + + encoder_test( + encoder=encoder, + input_data=input_images, + output_dtype=torch.float32, + output_shape=output_shape, + output_data=None, + ) + + # test for parameter updates + # generate tensors for parameter update test + target = torch.rand(output_shape) + image_tensor = torch.rand(input_image.shape) + + # check for parameter updates + fpc, tpc, upc, not_updated = check_module_parameters_updated(encoder, (image_tensor,), target) + assert upc == tpc, ( + f"Not all trainable parameters updated. Parameters not updated: {not_updated}. Module structure\n{encoder}" + ) + + +def test_image_encoders_mlpmixer(): + # make repeatable + np.random.seed(RANDOM_SEED) + torch.manual_seed(RANDOM_SEED) + + # Test the resnet encoder for images + encoder_kwargs = { + "patch_size": 5, + "embed_size": 8, + "token_size": 32, + "channel_dim": 16, + "num_layers": 2, + "dropout": DROPOUT, + } + image_size = (3, 10, 10) + + output_shape = [1, 8] + input_image = generate_images(image_size, 1) + + encoder = create_encoder( + MLPMixerEncoder, num_channels=image_size[0], height=image_size[1], width=image_size[2], **encoder_kwargs + ) + encoder_test( + encoder=encoder, input_data=input_image, output_dtype=torch.float32, output_shape=output_shape, output_data=None + ) + + output_shape = [5, 8] + input_images = generate_images(image_size, 5) + + encoder_test( + encoder=encoder, + input_data=input_images, + output_dtype=torch.float32, + output_shape=output_shape, + output_data=None, + ) + + assert encoder is not None + assert encoder.mlp_mixer.__class__.__name__ == "MLPMixer" + assert len(encoder.mlp_mixer.mixer_blocks) == 2 + assert list(encoder.mlp_mixer.mixer_blocks[0].mlp1.output_shape) == [4] + assert encoder.mlp_mixer.patch_conv.__class__.__name__ == "Conv2d" + assert encoder.mlp_mixer.patch_conv.kernel_size == (5, 5) + + # test for parameter updates + # generate tensors for parameter update test + target = torch.rand(output_shape) + image_tensor = torch.rand(input_image.shape) + + # check for parameter updates + fpc, tpc, upc, not_updated = check_module_parameters_updated(encoder, (image_tensor,), target) + assert upc == tpc, ( + f"Not all trainable parameters updated. Parameters not updated: {not_updated}. Module structure\n{encoder}" + ) + + +def test_sequence_encoder_embed(): + num_sentences = 4 + embedding_size = 5 + max_len = 6 + + # make repeatable + np.random.seed(RANDOM_SEED) + torch.manual_seed(RANDOM_SEED) + + # Generate data + text, vocab = generate_random_sentences( + num_sentences=num_sentences, + max_len=max_len, + ) + + encoder_kwargs = {"embedding_size": embedding_size, "vocab": vocab} + + # Different values for reduce_output and the corresponding expected size + reduce_outputs = ["sum", None, "concat"] + output_shapes = [ + [num_sentences, embedding_size], + [num_sentences, max_len, embedding_size], + [num_sentences, max_len * embedding_size], + ] + + for reduce_output, output_shape in zip(reduce_outputs, output_shapes): + for trainable in [True, False]: + encoder_kwargs["reduce_output"] = reduce_output + encoder_kwargs["embeddings_trainable"] = trainable + encoder_kwargs["dropout"] = DROPOUT + encoder = create_encoder(SequenceEmbedEncoder, max_sequence_length=max_len, **encoder_kwargs) + + encoder_test( + encoder=encoder, + input_data=text, + output_dtype=torch.float32, + output_shape=output_shape, + output_data=None, + ) + + assert encoder.embed_sequence.dropout is not None + + # test for parameter updates + # generate tensors for parameter update test + target = torch.rand(output_shape) + + # check for parameter updates + fpc, tpc, upc, not_updated = check_module_parameters_updated( + encoder, (torch.tensor(text, dtype=torch.int32),), target + ) + assert upc == tpc, ( + f"Not all trainable parameters updated. Parameters not updated: {not_updated}." + f" Module structure\n{encoder}" + ) + + +@pytest.mark.parametrize("encoder_type", [ParallelCNN, StackedCNN, StackedParallelCNN, StackedRNN, StackedCNNRNN]) +@pytest.mark.parametrize("trainable", [True, False]) +@pytest.mark.parametrize("reduce_output", ["sum", "max"]) +def test_sequence_encoders(encoder_type: Encoder, trainable: bool, reduce_output: str): + num_sentences = 4 + embedding_size = 5 + max_len = 7 + output_size = 3 + + # make repeatable + np.random.seed(RANDOM_SEED) + torch.manual_seed(RANDOM_SEED) + + # Generate data + text, vocab = generate_random_sentences( + num_sentences=num_sentences, + max_len=max_len, + ) + + encoder_kwargs = { + "embedding_size": embedding_size, + "vocab": vocab, + "output_size": output_size, + "num_fc_layers": 1, + "filter_size": 3, + "num_filters": 8, + "state_size": output_size, + } + + # todo figure out the output size for parallel 1d conv + output_shape = [num_sentences, output_size] + + encoder_kwargs["embeddings_trainable"] = trainable + # Dropout is left at default (0) because with small inputs (4 sentences, max_len=7) + # high dropout causes vanishing gradients, making the parameter update check flaky. + encoder_kwargs["reduce_output"] = reduce_output + encoder = create_encoder(encoder_type, max_sequence_length=max_len, **encoder_kwargs) + + encoder_test( + encoder=encoder, input_data=text, output_dtype=torch.float32, output_shape=output_shape, output_data=None + ) + + assert isinstance(encoder, encoder_type) + + # test for parameter updates + # generate tensors for parameter update test + target = torch.rand(output_shape) + + # check for parameter updates + fpc, tpc, upc, not_updated = check_module_parameters_updated( + encoder, (torch.tensor(text, dtype=torch.int32),), target + ) + + if trainable: + assert fpc == 0, "Embedding layer expected to be trainable but found to be frozen" + else: + assert fpc == 1, "Embedding layer expected to be frozen, but found to be trainable." + + if trainable: + assert upc > 0, ( + f"No trainable parameters updated. Parameters not updated: {not_updated}. Module structure\n{encoder}" + ) diff --git a/tests/ludwig/modules/test_fully_connected_modules.py b/tests/ludwig/modules/test_fully_connected_modules.py new file mode 100644 index 0000000..ed1153b --- /dev/null +++ b/tests/ludwig/modules/test_fully_connected_modules.py @@ -0,0 +1,87 @@ +import pytest +import torch + +from ludwig.modules.fully_connected_modules import FCLayer, FCStack +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.torch_utils import get_torch_device + +BATCH_SIZE = 2 +DEVICE = get_torch_device() +RANDOM_SEED = 1919 + + +@pytest.mark.parametrize("input_size", [2, 3]) +@pytest.mark.parametrize("output_size", [3, 4]) +@pytest.mark.parametrize("activation", ["relu", "sigmoid", "tanh"]) +@pytest.mark.parametrize("dropout", [0.0, 0.6]) +@pytest.mark.parametrize("batch_size", [1, 2]) +@pytest.mark.parametrize("norm", [None, "layer", "batch", "ghost"]) +def test_fc_layer( + input_size: int, + output_size: int, + activation: str, + dropout: float, + batch_size: int, + norm: str | None, +): + set_random_seed(RANDOM_SEED) # make repeatable + fc_layer = FCLayer( + input_size=input_size, output_size=output_size, activation=activation, dropout=dropout, norm=norm + ).to(DEVICE) + input_tensor = torch.randn(batch_size, input_size, device=DEVICE) + output_tensor = fc_layer(input_tensor) + assert output_tensor.shape[1:] == fc_layer.output_shape + + +@pytest.mark.parametrize( + "first_layer_input_size,layers,num_layers", + [ + (2, None, 3), + (2, [{"output_size": 4}, {"output_size": 8}], None), + (2, [{"input_size": 2, "output_size": 4}, {"output_size": 8}], None), + ], +) +def test_fc_stack( + first_layer_input_size: int | None, + layers: list | None, + num_layers: int | None, +): + set_random_seed(RANDOM_SEED) + fc_stack = FCStack(first_layer_input_size=first_layer_input_size, layers=layers, num_layers=num_layers).to(DEVICE) + input_tensor = torch.randn(BATCH_SIZE, first_layer_input_size, device=DEVICE) + output_tensor = fc_stack(input_tensor) + assert output_tensor.shape[1:] == fc_stack.output_shape + + +def test_fc_stack_input_size_mismatch_fails(): + first_layer_input_size = 10 + layers = [{"input_size": 2, "output_size": 4}, {"output_size": 8}] + + fc_stack = FCStack( + first_layer_input_size=first_layer_input_size, + layers=layers, + ).to(DEVICE) + input_tensor = torch.randn(BATCH_SIZE, first_layer_input_size, device=DEVICE) + + with pytest.raises(RuntimeError): + fc_stack(input_tensor) + + +def test_fc_stack_no_layers_behaves_like_passthrough(): + first_layer_input_size = 10 + layers = None + num_layers = 0 + output_size = 15 + + fc_stack = FCStack( + first_layer_input_size=first_layer_input_size, + layers=layers, + num_layers=num_layers, + default_output_size=output_size, + ).to(DEVICE) + input_tensor = torch.randn(BATCH_SIZE, first_layer_input_size, device=DEVICE) + output_tensor = fc_stack(input_tensor) + + assert list(output_tensor.shape[1:]) == [first_layer_input_size] + assert output_tensor.shape[1:] == fc_stack.output_shape + assert torch.allclose(input_tensor, output_tensor) diff --git a/tests/ludwig/modules/test_gated_cross_attention.py b/tests/ludwig/modules/test_gated_cross_attention.py new file mode 100644 index 0000000..fc94af3 --- /dev/null +++ b/tests/ludwig/modules/test_gated_cross_attention.py @@ -0,0 +1,149 @@ +"""Unit tests for the Flamingo-style gated cross-attention module.""" + +from __future__ import annotations + +import pytest +import torch + +from ludwig.modules.gated_cross_attention import GatedCrossAttention + + +class TestGatedCrossAttention: + def test_identity_at_init(self): + """Zero-init gates must make the block an identity at step 0.""" + torch.manual_seed(0) + block = GatedCrossAttention(d_model=32, num_heads=4) + x = torch.randn(2, 7, 32) + kv = torch.randn(2, 5, 32) + + out = block(x, kv) + torch.testing.assert_close(out, x, atol=1e-6, rtol=1e-6) + + def test_non_identity_after_gate_nudge(self): + torch.manual_seed(0) + block = GatedCrossAttention(d_model=32, num_heads=4) + with torch.no_grad(): + block.attn_gate.fill_(1.0) + block.ffn_gate.fill_(1.0) + x = torch.randn(2, 7, 32) + kv = torch.randn(2, 5, 32) + + out = block(x, kv) + # Shape preserved... + assert out.shape == x.shape + # ...but values changed by a meaningful amount. + assert not torch.allclose(out, x, atol=1e-3) + + def test_different_kv_dim(self): + """kv_dim != d_model should project keys/values to d_model.""" + block = GatedCrossAttention(d_model=32, num_heads=4, kv_dim=64) + x = torch.randn(2, 4, 32) + kv = torch.randn(2, 6, 64) + out = block(x, kv) + assert out.shape == x.shape + + def test_key_padding_mask(self): + """Padding mask is passed through to the inner attention layer without crashing.""" + block = GatedCrossAttention(d_model=16, num_heads=4) + x = torch.randn(2, 3, 16) + kv = torch.randn(2, 4, 16) + mask = torch.tensor([[False, False, True, True], [False, True, True, True]]) + out = block(x, kv, key_padding_mask=mask) + assert out.shape == x.shape + + @pytest.mark.parametrize("tanh_gate", [True, False]) + def test_tanh_gate_toggle(self, tanh_gate): + block = GatedCrossAttention(d_model=16, num_heads=4, tanh_gate=tanh_gate) + assert block.tanh_gate is tanh_gate + + def test_backward_pass(self): + block = GatedCrossAttention(d_model=32, num_heads=4) + x = torch.randn(2, 7, 32, requires_grad=True) + kv = torch.randn(2, 5, 32) + out = block(x, kv) + out.sum().backward() + assert x.grad is not None + assert block.attn_gate.grad is not None + assert block.ffn_gate.grad is not None + + +class TestLLMIsMultimodalSchema: + """The VLM flag on LLMModelConfig should parse, default to False, and have no ill effects.""" + + def _base(self) -> dict: + return { + "model_type": "llm", + "base_model": "hf-internal-testing/tiny-random-GPTJForCausalLM", + "input_features": [{"name": "prompt", "type": "text"}], + "output_features": [{"name": "output", "type": "text"}], + } + + def test_default_false(self): + from ludwig.schema.model_types.base import ModelConfig + + cfg = ModelConfig.from_dict(self._base()) + assert cfg.is_multimodal is False + + def test_explicit_true(self): + from ludwig.schema.model_types.base import ModelConfig + + cfg = ModelConfig.from_dict({**self._base(), "is_multimodal": True}) + assert cfg.is_multimodal is True + + +class TestMultimodalCollator: + def test_collator_uses_processor_path(self): + """The collator should call the processor with text + images and return its batch.""" + from ludwig.data.multimodal_collator import MultimodalCollator + + class _FakeProcessor: + def __init__(self): + self.tokenizer = None + self.last_call = None + + def __call__(self, *, text, images, return_tensors, padding, **kwargs): + self.last_call = {"text": text, "images": images, "padding": padding, **kwargs} + return {"input_ids": torch.zeros(len(text), 4, dtype=torch.long)} + + proc = _FakeProcessor() + collator = MultimodalCollator(proc) + examples = [ + {"image": "img0", "text": "hi"}, + {"image": "img1", "text": "hello"}, + ] + batch = collator(examples) + assert "input_ids" in batch + assert proc.last_call["text"] == ["hi", "hello"] + assert proc.last_call["images"] == ["img0", "img1"] + + def test_collator_labels_path_masks_padding(self): + from ludwig.data.multimodal_collator import MultimodalCollator + + class _FakeTokenizer: + pad_token_id = 0 + + def __call__(self, texts, return_tensors, padding, truncation, max_length=None): + # Toy tokenizer: right-pad with 0s, all ids are 1. + max_len = max(len(t) for t in texts) + ids = torch.zeros(len(texts), max_len, dtype=torch.long) + for i, t in enumerate(texts): + ids[i, : len(t)] = 1 + return {"input_ids": ids} + + class _FakeProcessor: + def __init__(self): + self.tokenizer = _FakeTokenizer() + + def __call__(self, *, text, images, return_tensors, padding, **kwargs): + return {"input_ids": torch.zeros(len(text), 3, dtype=torch.long)} + + collator = MultimodalCollator(_FakeProcessor(), max_length=8) + examples = [ + {"image": "img0", "text": "hi", "labels": "y"}, + {"image": "img1", "text": "hey", "labels": "longer"}, + ] + batch = collator(examples) + assert "labels" in batch + # Padding positions replaced with -100; valid token positions kept as 1. + assert (batch["labels"] == -100).any() + assert (batch["labels"] == 1).any() diff --git a/tests/ludwig/modules/test_initializer_modules.py b/tests/ludwig/modules/test_initializer_modules.py new file mode 100644 index 0000000..cb2ee83 --- /dev/null +++ b/tests/ludwig/modules/test_initializer_modules.py @@ -0,0 +1,22 @@ +import torch +import torch.nn as nn + +from ludwig.modules.initializer_modules import get_initializer +from ludwig.utils.torch_utils import get_torch_device + +DEVICE = "cuda:0" if get_torch_device() == "cuda" else "cpu" + + +def test_get_initializer(): + """Currently only checks for when the parameters are default case.""" + tensor_size = (2, 3) + + # Test for when the parameters are default + torch.random.manual_seed(0) + initialized_tensor = get_initializer("xavier_uniform")(*tensor_size, device=DEVICE) + + # Check that the tensor using the expected initialization and the same seed is identical + default_initializer = nn.init.xavier_uniform_ + torch.random.manual_seed(0) + default_tensor = default_initializer(torch.empty(*tensor_size, device=DEVICE)) + assert torch.equal(initialized_tensor, default_tensor) diff --git a/tests/ludwig/modules/test_loss_balancing.py b/tests/ludwig/modules/test_loss_balancing.py new file mode 100644 index 0000000..5e4efc9 --- /dev/null +++ b/tests/ludwig/modules/test_loss_balancing.py @@ -0,0 +1,259 @@ +"""Tests for multi-task loss balancing strategies.""" + +import pytest +import torch + +from ludwig.modules.loss_balancing import ( + create_loss_balancer, + FAMOLossBalancer, + GradNormLossBalancer, + LogTransformLossBalancer, + NashMTLLossBalancer, + NoneLossBalancer, + ParetoMTLLossBalancer, + UncertaintyLossBalancer, +) + +FEATURE_NAMES = ["output_1", "output_2", "output_3"] + + +def _make_losses(values: list[float]) -> dict[str, torch.Tensor]: + return {name: torch.tensor(v, requires_grad=True) for name, v in zip(FEATURE_NAMES, values)} + + +def _make_weights() -> dict[str, float]: + return dict.fromkeys(FEATURE_NAMES, 1.0) + + +class TestNoneLossBalancer: + def test_matches_static_weighted_sum(self): + balancer = NoneLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + weights = {name: w for name, w in zip(FEATURE_NAMES, [0.5, 1.0, 2.0])} + total = balancer(losses, weights) + expected = 0.5 * 1.0 + 1.0 * 2.0 + 2.0 * 3.0 + assert torch.isclose(total, torch.tensor(expected)) + + def test_gradient_flow(self): + balancer = NoneLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + total.backward() + for loss in losses.values(): + assert loss.grad is not None + + +class TestLogTransformLossBalancer: + def test_compresses_large_losses(self): + balancer = LogTransformLossBalancer(FEATURE_NAMES) + weights = _make_weights() + small_losses = _make_losses([0.1, 0.2, 0.3]) + large_losses = _make_losses([100.0, 200.0, 300.0]) + small_total = balancer(small_losses, weights) + large_total = balancer(large_losses, weights) + # Log compression should make ratio much smaller than 1000x + ratio = large_total.item() / small_total.item() + assert ratio < 100 # Much less than 1000x + + def test_gradient_flow(self): + balancer = LogTransformLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + total.backward() + for loss in losses.values(): + assert loss.grad is not None + + +class TestUncertaintyLossBalancer: + def test_has_learnable_parameters(self): + balancer = UncertaintyLossBalancer(FEATURE_NAMES) + params = list(balancer.parameters()) + assert len(params) == 3 # one log_var per task + + def test_gradient_flow(self): + balancer = UncertaintyLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + total.backward() + for name in FEATURE_NAMES: + assert balancer.log_vars[name].grad is not None + + def test_output_is_finite(self): + balancer = UncertaintyLossBalancer(FEATURE_NAMES) + losses = _make_losses([0.001, 100.0, 5.0]) + total = balancer(losses, _make_weights()) + assert torch.isfinite(total) + + +class TestFAMOLossBalancer: + def test_has_learnable_parameters(self): + balancer = FAMOLossBalancer(FEATURE_NAMES) + params = list(balancer.parameters()) + assert len(params) == 3 # one log_weight per task + + def test_post_step_updates_prev_losses(self): + balancer = FAMOLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + balancer(losses, _make_weights()) + balancer.post_step(losses) + assert len(balancer._prev_losses) == 3 + + def test_gradient_flow(self): + balancer = FAMOLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + total.backward() + for name in FEATURE_NAMES: + assert balancer.log_weights[name].grad is not None + + +class TestGradNormLossBalancer: + def test_has_learnable_parameters(self): + balancer = GradNormLossBalancer(FEATURE_NAMES) + params = list(balancer.parameters()) + assert len(params) == 3 # one task_weight per task + + def test_post_step_records_initial_losses(self): + balancer = GradNormLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + balancer.post_step(losses) + assert len(balancer._initial_losses) == 3 + + def test_post_step_adjusts_weights(self): + balancer = GradNormLossBalancer(FEATURE_NAMES) + # First step: record initial losses + losses1 = _make_losses([1.0, 1.0, 1.0]) + balancer.post_step(losses1) + initial_weights = {name: balancer.task_weights[name].item() for name in FEATURE_NAMES} + # Second step: one task improved a lot, others didn't + losses2 = _make_losses([0.1, 1.0, 1.0]) + balancer.post_step(losses2) + # The fast-improving task should get higher weight + assert balancer.task_weights["output_1"].item() != initial_weights["output_1"] + + +class TestCreateLossBalancer: + def test_create_none(self): + b = create_loss_balancer("none", FEATURE_NAMES) + assert isinstance(b, NoneLossBalancer) + + def test_create_log_transform(self): + b = create_loss_balancer("log_transform", FEATURE_NAMES) + assert isinstance(b, LogTransformLossBalancer) + + def test_create_uncertainty(self): + b = create_loss_balancer("uncertainty", FEATURE_NAMES) + assert isinstance(b, UncertaintyLossBalancer) + + def test_create_famo(self): + b = create_loss_balancer("famo", FEATURE_NAMES, lr=0.05) + assert isinstance(b, FAMOLossBalancer) + + def test_create_gradnorm(self): + b = create_loss_balancer("gradnorm", FEATURE_NAMES, alpha=2.0) + assert isinstance(b, GradNormLossBalancer) + + def test_create_nash_mtl(self): + b = create_loss_balancer("nash_mtl", FEATURE_NAMES) + assert isinstance(b, NashMTLLossBalancer) + + def test_create_pareto_mtl(self): + b = create_loss_balancer("pareto_mtl", FEATURE_NAMES, preference_vector=[0.5, 0.3, 0.2]) + assert isinstance(b, ParetoMTLLossBalancer) + + def test_invalid_strategy_raises_value_error(self): + with pytest.raises(ValueError, match="Unknown loss balancing strategy"): + create_loss_balancer("nonexistent_strategy", FEATURE_NAMES) + + def test_invalid_strategy_lists_valid_options(self): + with pytest.raises(ValueError, match="none"): + create_loss_balancer("bad", FEATURE_NAMES) + + +class TestNashMTLLossBalancer: + def test_has_learnable_parameters(self): + balancer = NashMTLLossBalancer(FEATURE_NAMES) + params = list(balancer.parameters()) + assert len(params) == 3 + + def test_uniform_init(self): + n = len(FEATURE_NAMES) + balancer = NashMTLLossBalancer(FEATURE_NAMES) + for name in FEATURE_NAMES: + assert torch.isclose(balancer.task_weights[name], torch.tensor(1.0 / n)) + + def test_gradient_flow(self): + balancer = NashMTLLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + total.backward() + for name in FEATURE_NAMES: + assert balancer.task_weights[name].grad is not None + + def test_post_step_updates_weights(self): + balancer = NashMTLLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 10.0, 5.0]) + initial = {name: balancer.task_weights[name].item() for name in FEATURE_NAMES} + balancer.post_step(losses) + # Weights should change after post_step + changed = any(balancer.task_weights[name].item() != initial[name] for name in FEATURE_NAMES) + assert changed + + def test_low_loss_task_gets_higher_weight(self): + # Nash solution: weights ∝ 1/loss → low-loss task gets higher weight. + balancer = NashMTLLossBalancer(FEATURE_NAMES) + losses = _make_losses([1.0, 100.0, 10.0]) + balancer.post_step(losses) + w1 = balancer.task_weights["output_1"].item() + w2 = balancer.task_weights["output_2"].item() + assert w1 > w2 + + +class TestParetoMTLLossBalancerExtended: + def test_uniform_preference_by_default(self): + balancer = ParetoMTLLossBalancer(FEATURE_NAMES) + pv = balancer.preference_vector + expected = torch.ones(3) / 3 + assert torch.allclose(pv, expected, atol=1e-6) + + def test_skewed_preference_favors_task(self): + # preference heavily toward output_1 + balancer = ParetoMTLLossBalancer(FEATURE_NAMES, preference_vector=[0.9, 0.05, 0.05]) + losses = _make_losses([1.0, 1.0, 1.0]) + weights = _make_weights() + # output_1 dominates the linear term; total should differ from uniform + total = balancer(losses, weights) + assert torch.isfinite(total) + + def test_invalid_pref_vector_length(self): + with pytest.raises(ValueError, match="preference_vector"): + ParetoMTLLossBalancer(FEATURE_NAMES, preference_vector=[0.5, 0.5]) + + def test_negative_pref_vector_raises(self): + with pytest.raises(ValueError, match="non-negative"): + ParetoMTLLossBalancer(FEATURE_NAMES, preference_vector=[-0.1, 0.5, 0.6]) + + def test_invalid_tchebycheff_weight_raises(self): + with pytest.raises(ValueError, match="tchebycheff_weight"): + ParetoMTLLossBalancer(FEATURE_NAMES, tchebycheff_weight=1.5) + + +class TestUncertaintyNumericalStability: + def test_extreme_log_vars_remain_finite(self): + """log_vars pushed to extreme values must not produce inf/nan.""" + balancer = UncertaintyLossBalancer(FEATURE_NAMES) + with torch.no_grad(): + for name in FEATURE_NAMES: + balancer.log_vars[name].fill_(-100.0) # would overflow without clamp + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + assert torch.isfinite(total), f"Expected finite total, got {total}" + + def test_positive_extreme_log_vars_remain_finite(self): + balancer = UncertaintyLossBalancer(FEATURE_NAMES) + with torch.no_grad(): + for name in FEATURE_NAMES: + balancer.log_vars[name].fill_(100.0) # large positive log_var + losses = _make_losses([1.0, 2.0, 3.0]) + total = balancer(losses, _make_weights()) + assert torch.isfinite(total), f"Expected finite total, got {total}" diff --git a/tests/ludwig/modules/test_loss_modules.py b/tests/ludwig/modules/test_loss_modules.py new file mode 100644 index 0000000..39c9ddd --- /dev/null +++ b/tests/ludwig/modules/test_loss_modules.py @@ -0,0 +1,439 @@ +import contextlib + +import pytest +import torch +import torch.nn.functional as F +from pydantic import ValidationError + +from ludwig.features.category_feature import CategoryOutputFeature +from ludwig.features.set_feature import SetOutputFeature +from ludwig.features.text_feature import TextOutputFeature +from ludwig.modules import loss_modules +from ludwig.schema.features.loss.loss import ( + BWCEWLossConfig, + CORNLossConfig, + EntropicOpenSetLossConfig, + HuberLossConfig, + MAELossConfig, + MAPELossConfig, + MSELossConfig, + ObjectosphereLossConfig, + RMSELossConfig, + RMSPELossConfig, + SigmoidCrossEntropyLossConfig, + SoftmaxCrossEntropyLossConfig, +) +from ludwig.schema.model_config import ModelConfig +from tests.integration_tests.utils import category_feature, set_feature, text_feature + + +def from_float(v: float) -> torch.Tensor: + return torch.tensor(v).float() + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(36).float()]) +def test_mse_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.MSELoss(MSELossConfig()) + assert loss(preds, target) == output + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(6).float()]) +def test_mae_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.MAELoss(MAELossConfig()) + assert loss(preds, target) == output + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(0.7365440726280212)]) +def test_mape_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.MAPELoss(MAPELossConfig()) + assert loss(preds, target) == output + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(6).float()]) +def test_rmse_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.RMSELoss(RMSELossConfig()) + assert loss(preds, target) == output + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(0.7527).float()]) +def test_rmspe_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.RMSPELoss(RMSPELossConfig()) + assert torch.isclose(loss(preds, target), output, rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.tensor([[0.1, 0.2]]).float()]) +@pytest.mark.parametrize("target", [torch.tensor([[0.0, 0.2]]).float()]) +@pytest.mark.parametrize("output", [torch.tensor(707.1068).float()]) +def test_rmspe_loss_zero_targets(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.RMSPELoss(RMSPELossConfig()) + assert torch.isclose(loss(preds, target), output, rtol=0.0001) + + +@pytest.mark.parametrize( + "confidence_penalty,positive_class_weight,robust_lambda,output", + [ + (0.0, None, 0, from_float(-21.4655)), + (2.0, None, 0, from_float(-21.1263)), + (0.0, 2.0, 0, from_float(-20.1222)), + (0.0, None, 2, from_float(22.4655)), + (2, 2, 2, from_float(21.4614)), + ], +) +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +def test_bwcew_loss( + preds: torch.Tensor, + target: torch.Tensor, + confidence_penalty: float, + positive_class_weight: float | None, + robust_lambda: int, + output: torch.Tensor, +): + loss = loss_modules.BWCEWLoss( + BWCEWLossConfig( + positive_class_weight=positive_class_weight, + robust_lambda=robust_lambda, + confidence_penalty=confidence_penalty, + ) + ) + assert torch.isclose(loss(preds, target), output) + + +@pytest.mark.parametrize("preds", [torch.tensor([[0.5, 0.5], [0.2, 0.8], [0.6, 0.4]])]) +@pytest.mark.parametrize("target", [torch.tensor([1, 1, 0])]) +@pytest.mark.parametrize("output", [torch.tensor(0.5763)]) +def test_softmax_cross_entropy_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.SoftmaxCrossEntropyLoss(SoftmaxCrossEntropyLossConfig()) + assert torch.isclose(loss(preds, target), output, rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(-21.4655).float()]) +def test_sigmoid_cross_entropy_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.SigmoidCrossEntropyLoss(SigmoidCrossEntropyLossConfig()) + assert torch.isclose(loss(preds, target), output) + + +@pytest.mark.parametrize( + "delta,output", + [ + (1.0, from_float(5.5000)), + (0.5, from_float(2.8750)), + (2.0, from_float(10.0)), + (0.0, ValidationError), + ], +) +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +def test_huber_loss(preds: torch.Tensor, target: torch.Tensor, delta: float, output: torch.Tensor | type[Exception]): + with pytest.raises(output) if not isinstance(output, torch.Tensor) else contextlib.nullcontext(): + loss = loss_modules.HuberLoss(HuberLossConfig.from_dict({"delta": delta})) + value = loss(preds, target) + assert value == output + + +@pytest.mark.parametrize("preds", [torch.tensor([[0.25, 0.2, 0.55], [0.2, 0.35, 0.45], [0.8, 0.1, 0.1]])]) +@pytest.mark.parametrize("target", [torch.tensor([2, 1, 0])]) +@pytest.mark.parametrize("output", [torch.tensor(0.7653)]) +def test_corn_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + loss = loss_modules.CORNLoss(CORNLossConfig()) + assert torch.isclose(loss(preds, target), output, rtol=0.0001) + + +def test_dict_class_weights_category(): + input_features = [text_feature()] + output_features = [category_feature(decoder={"vocab_size": 3})] + config = { + "input_features": input_features, + "output_features": output_features, + } + + # Set class weights as dictionary on config + class_weights_dict = {"token_1": 0.1, "token_2": 0.2, "token_3": 0.3} + config["output_features"][0]["loss"] = {"type": "softmax_cross_entropy", "class_weights": class_weights_dict} + + # Mock feature metadata + feature_metadata = { + "idx2str": ["token_1", "token_2", "token_3"], + "str2idx": {"token_1": 0, "token_2": 1, "token_3": 2}, + "str2freq": {"token_1": 300, "token_2": 200, "token_3": 100}, + "vocab_size": 3, + "preprocessing": { + "missing_value_strategy": "drop_row", + "fill_value": "", + "computed_fill_value": "", + "lowercase": False, + "most_common": 10000, + "cache_encoder_embeddings": False, + }, + } + + model_config = ModelConfig.from_dict(config) + + CategoryOutputFeature.update_config_with_metadata( + feature_config=model_config.output_features[0], + feature_metadata=feature_metadata, + ) + + assert model_config.output_features[0].loss.class_weights == [0.1, 0.2, 0.3] + + +def test_dict_class_weights_text(): + input_features = [text_feature()] + output_features = [text_feature(decoder={"vocab_size": 3, "max_sequence_length": 10})] + config = { + "input_features": input_features, + "output_features": output_features, + } + + # Set class weights as dictionary on config + class_weights_dict = { + "": 0, + "": 0, + "": 0, + "": 0, + "token_1": 0.5, + "token_2": 0.4, + "token_3": 0.1, + } + config["output_features"][0]["loss"] = { + "type": "sequence_softmax_cross_entropy", + "class_weights": class_weights_dict, + } + + # Mock feature metadata + feature_metadata = { + "idx2str": ["", "", "", "", "token_1", "token_2", "token_3"], + "str2idx": {"": 0, "": 1, "": 2, "": 3, "token_1": 4, "token_2": 5, "token_3": 6}, + "str2freq": {"": 0, "": 0, "": 0, "": 0, "token_1": 300, "token_2": 200, "token_3": 100}, + "str2idf": None, + "vocab_size": 7, + "max_sequence_length": 9, + "max_sequence_length_99ptile": 9.0, + "pad_idx": 2, + "padding_symbol": "", + "unknown_symbol": "", + "index_name": None, + "preprocessing": { + "prompt": { + "retrieval": {"type": None, "index_name": None, "model_name": None, "k": 0}, + "task": None, + "template": None, + }, + "pretrained_model_name_or_path": None, + "tokenizer": "space_punct", + "vocab_file": None, + "sequence_length": None, + "max_sequence_length": 256, + "most_common": 20000, + "padding_symbol": "", + "unknown_symbol": "", + "padding": "right", + "lowercase": True, + "missing_value_strategy": "drop_row", + "fill_value": "", + "computed_fill_value": "", + "ngram_size": 2, + "cache_encoder_embeddings": False, + "compute_idf": False, + }, + } + + model_config = ModelConfig.from_dict(config) + + TextOutputFeature.update_config_with_metadata( + feature_config=model_config.output_features[0], + feature_metadata=feature_metadata, + ) + + assert model_config.output_features[0].loss.class_weights == [0, 0, 0, 0, 0.5, 0.4, 0.1] + + +def test_dict_class_weights_set(): + input_features = [category_feature()] + output_features = [set_feature()] + config = { + "input_features": input_features, + "output_features": output_features, + } + + # Set class weights as dictionary on config + class_weights_dict = {"token_1": 0.1, "token_2": 0.2, "token_3": 0.3, "": 0} + config["output_features"][0]["loss"] = {"type": "sigmoid_cross_entropy", "class_weights": class_weights_dict} + + # Mock feature metadata + feature_metadata = { + "idx2str": ["token_1", "token_2", "token_3", ""], + "str2idx": {"token_1": 0, "token_2": 1, "token_3": 2, "": 3}, + "str2freq": {"token_1": 300, "token_2": 200, "token_3": 100, "": 0}, + "vocab_size": 4, + "max_set_size": 3, + "preprocessing": { + "tokenizer": "space", + "missing_value_strategy": "drop_row", + "fill_value": "", + "computed_fill_value": "", + "lowercase": False, + "most_common": 10000, + }, + } + + model_config = ModelConfig.from_dict(config) + + SetOutputFeature.update_config_with_metadata( + feature_config=model_config.output_features[0], + feature_metadata=feature_metadata, + ) + + assert model_config.output_features[0].loss.class_weights == [0.1, 0.2, 0.3, 0] + + +# --------------------------------------------------------------------------- +# Entropic Open-Set Loss (Dhamija et al., NeurIPS 2018) +# --------------------------------------------------------------------------- + +EPSILON = 1e-10 + + +def test_entropic_open_set_no_background_equals_ce(): + """Without background_class, EntropicOpenSetLoss is identical to cross-entropy.""" + logits = torch.tensor([[2.0, 1.0, 0.0], [0.0, 1.0, 2.0], [1.0, 2.0, 0.0]]) + target = torch.tensor([0, 2, 1]) + + loss = loss_modules.EntropicOpenSetLoss(EntropicOpenSetLossConfig()) + expected = F.cross_entropy(logits, target) + assert torch.isclose(loss(logits, target), expected) + + +def test_entropic_open_set_decomposes_into_known_and_unknown(): + """With background_class, loss = CE(known) + neg_entropy(unknown).""" + bg = 2 + logits = torch.tensor( + [ + [2.0, 1.0, 0.0], # known: class 0 + [0.0, 2.0, 1.0], # known: class 1 + [0.5, 0.5, 0.5], # unknown: background class + ] + ) + target = torch.tensor([0, 1, bg]) + + loss_fn = loss_modules.EntropicOpenSetLoss(EntropicOpenSetLossConfig(background_class=bg)) + actual = loss_fn(logits, target) + + known_ce = F.cross_entropy(logits[:2], target[:2]) + probs = torch.softmax(logits[2:], dim=-1) + neg_entropy = (probs * torch.log(probs + EPSILON)).sum(dim=-1).mean() + expected = known_ce + neg_entropy + + assert torch.isclose(actual, expected, rtol=1e-4) + + +def test_entropic_loss_reduces_max_prob_on_background(): + """Optimising the entropic loss on background samples should drive max probability toward 1/C.""" + torch.manual_seed(0) + bg = 4 + logits = torch.nn.Parameter(torch.randn(20, 5)) # 20 background samples, 5 classes + target = torch.full((20,), bg) + + loss_fn = loss_modules.EntropicOpenSetLoss(EntropicOpenSetLossConfig(background_class=bg)) + optimizer = torch.optim.Adam([logits], lr=0.5) + + for _ in range(150): + optimizer.zero_grad() + loss_fn(logits, target).backward() + optimizer.step() + + max_probs = torch.softmax(logits.detach(), dim=-1).max(dim=-1).values + # After entropy maximisation, mean max-prob should be close to uniform (1/5 = 0.2) + assert max_probs.mean().item() < 0.25 + + +# --------------------------------------------------------------------------- +# Objectosphere Loss (Dhamija et al., NeurIPS 2018) +# --------------------------------------------------------------------------- + + +def test_objectosphere_no_background_equals_ce_plus_hinge(): + """Without background_class, loss = CE + mean(clamp(xi - ||z||, 0)^2).""" + xi = 3.0 + logits = torch.tensor([[1.0, 0.5, -0.5], [0.2, 1.0, 0.8]]) + target = torch.tensor([0, 1]) + + loss_fn = loss_modules.ObjectosphereLoss(ObjectosphereLossConfig(xi=xi)) + actual = loss_fn(logits, target) + + ce = F.cross_entropy(logits, target) + norms = logits.norm(dim=-1) + hinge = torch.clamp(xi - norms, min=0.0).pow(2).mean() + expected = ce + hinge + + assert torch.isclose(actual, expected, rtol=1e-4) + + +def test_objectosphere_decomposes_into_known_and_unknown(): + """With background_class, loss splits into CE+hinge (known) and neg_entropy+mag (unknown).""" + bg = 3 + xi = 2.0 + zeta = 0.5 + logits = torch.tensor( + [ + [2.0, 1.0, 0.5, 0.0], # known: class 0 + [0.0, 2.0, 0.5, 0.0], # known: class 1 + [0.3, 0.3, 0.2, 0.2], # background + ] + ) + target = torch.tensor([0, 1, bg]) + + loss_fn = loss_modules.ObjectosphereLoss(ObjectosphereLossConfig(background_class=bg, xi=xi, zeta=zeta)) + actual = loss_fn(logits, target) + + # Known part + kl = logits[:2] + ce = F.cross_entropy(kl, target[:2]) + hinge = torch.clamp(xi - kl.norm(dim=-1), min=0.0).pow(2).mean() + + # Unknown part + ul = logits[2:] + probs = torch.softmax(ul, dim=-1) + neg_entropy = (probs * torch.log(probs + EPSILON)).sum(dim=-1).mean() + mag = ul.norm(dim=-1).pow(2).mean() + expected = ce + hinge + neg_entropy + zeta * mag + + assert torch.isclose(actual, expected, rtol=1e-4) + + +def test_objectosphere_creates_norm_separation(): + """Training with objectosphere loss pushes known logit norms above xi and unknown toward zero.""" + torch.manual_seed(0) + bg = 3 + xi = 5.0 + # 3 known samples (classes 0,1,2) + 3 background samples + logits = torch.nn.Parameter(torch.zeros(6, 4)) + target = torch.tensor([0, 1, 2, bg, bg, bg]) + + loss_fn = loss_modules.ObjectosphereLoss(ObjectosphereLossConfig(background_class=bg, xi=xi, zeta=1.0)) + optimizer = torch.optim.Adam([logits], lr=0.3) + + for _ in range(400): + optimizer.zero_grad() + loss_fn(logits, target).backward() + optimizer.step() + + known_norm = logits[:3].detach().norm(dim=-1).mean().item() + unknown_norm = logits[3:].detach().norm(dim=-1).mean().item() + + # Known norms should be substantially larger than unknown norms + assert known_norm > unknown_norm * 2, f"Known norm {known_norm:.3f} should be > 2x unknown norm {unknown_norm:.3f}" + # Unknown norms should be pulled toward zero (well below xi) + assert unknown_norm < xi / 2, f"Unknown norm {unknown_norm:.3f} should be well below xi={xi}" diff --git a/tests/ludwig/modules/test_lr_scheduler.py b/tests/ludwig/modules/test_lr_scheduler.py new file mode 100644 index 0000000..7ac6c40 --- /dev/null +++ b/tests/ludwig/modules/test_lr_scheduler.py @@ -0,0 +1,276 @@ +import math +import sys + +import numpy as np +from torch.optim import SGD + +from ludwig.features.number_feature import NumberInputFeature, NumberOutputFeature +from ludwig.modules.lr_scheduler import LRScheduler +from ludwig.schema.encoders.base import DenseEncoderConfig +from ludwig.schema.features.number_feature import ECDNumberOutputFeatureConfig, NumberInputFeatureConfig +from ludwig.schema.lr_scheduler import LRSchedulerConfig +from ludwig.utils.metric_utils import TrainerMetric +from ludwig.utils.trainer_utils import get_new_progress_tracker + + +def test_lr_scheduler_warmup_decay(): + total_steps = 10000 + steps_per_checkpoint = 1000 + base_lr = 1.0 + warmup_fraction = 0.1 + + module = NumberInputFeature(NumberInputFeatureConfig(name="num1", encoder=DenseEncoderConfig())) + + const_optimizer = SGD(module.parameters(), lr=base_lr) + const_config = LRSchedulerConfig(warmup_evaluations=0) + const_scheduler = LRScheduler( + config=const_config, + optimizer=const_optimizer, + steps_per_checkpoint=steps_per_checkpoint, + total_steps=total_steps, + ) + + linear_optimizer = SGD(module.parameters(), lr=base_lr) + linear_config = LRSchedulerConfig(warmup_fraction=warmup_fraction, decay="linear") + linear_scheduler = LRScheduler( + config=linear_config, + optimizer=linear_optimizer, + steps_per_checkpoint=steps_per_checkpoint, + total_steps=total_steps, + ) + + exp_optimizer = SGD(module.parameters(), lr=base_lr) + exp_config = LRSchedulerConfig(warmup_fraction=warmup_fraction, decay="exponential") + exp_scheduler = LRScheduler( + config=exp_config, optimizer=exp_optimizer, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps + ) + + cosine_optimizer = SGD(module.parameters(), lr=base_lr) + cosine_config = LRSchedulerConfig(warmup_fraction=warmup_fraction, decay="cosine", t_0=steps_per_checkpoint) + cosine_scheduler = LRScheduler( + config=cosine_config, + optimizer=cosine_optimizer, + steps_per_checkpoint=steps_per_checkpoint, + total_steps=total_steps, + ) + + warmup_steps = total_steps * warmup_fraction + for i in range(total_steps): + # Offset by 1 + step = i + 1 + + const_scheduler.step() + const_lr = const_optimizer.param_groups[0]["lr"] + assert const_lr == base_lr, f"step: {step}" + + linear_scheduler.step() + linear_lr = linear_optimizer.param_groups[0]["lr"] + + exp_scheduler.step() + exp_lr = exp_optimizer.param_groups[0]["lr"] + + cosine_scheduler.step() + cosine_lr = cosine_optimizer.param_groups[0]["lr"] + + if step < warmup_steps: + assert linear_lr == exp_lr, f"step: {step}" + assert linear_lr == cosine_lr, f"step: {step}" + assert linear_lr < base_lr, f"step: {step}" + elif step == warmup_steps: + assert linear_lr == base_lr, f"step: {step}" + assert cosine_lr == base_lr, f"step: {step}" + assert exp_lr < base_lr, f"step: {step}" + else: + assert linear_lr < base_lr, f"step: {step}" + assert exp_lr < base_lr, f"step: {step}" + assert cosine_lr <= base_lr, f"step: {step}" + + assert linear_lr < exp_lr + assert exp_lr < cosine_lr + assert cosine_lr == base_lr + + +def test_lr_scheduler_reduce_on_plateau(): + total_eval_steps = 100 + base_lr = 1.0 + reduce_limit = 3 + + module = NumberInputFeature(NumberInputFeatureConfig(name="num1", encoder=DenseEncoderConfig())) + output1 = NumberOutputFeature(ECDNumberOutputFeatureConfig(name="output1", input_size=10), output_features={}) + + optimizer = SGD(module.parameters(), lr=base_lr) + config = LRSchedulerConfig( + warmup_evaluations=0, + decay=None, + reduce_on_plateau=reduce_limit, + reduce_on_plateau_patience=10, + reduce_on_plateau_rate=0.1, + ) + scheduler = LRScheduler(config=config, optimizer=optimizer, steps_per_checkpoint=0, total_steps=0) + + progress_tracker = get_new_progress_tracker( + batch_size=64, + best_eval_metric_value=sys.float_info.max, + best_increase_batch_size_eval_metric=sys.float_info.max, + learning_rate=base_lr, + output_features={"output1": output1}, + ) + + num_reductions = 0 + + last_lr = optimizer.param_groups[0]["lr"] + steps_to_plateau = 5 + loss = 10.0 + for epoch in range(total_eval_steps): + for i in range(100): + # Simulate batch-wise steps. If we make a mistake, then this will reset + # the learning rate. + scheduler.step() + + steps_to_plateau -= 1 + if steps_to_plateau > 0: + loss -= 0.1 + + progress_tracker.train_metrics["output1"]["loss"].append( + TrainerMetric(epoch=epoch, step=epoch * 100, value=loss) + ) + scheduler.eval_step(progress_tracker, "output1") + lr = optimizer.param_groups[0]["lr"] + if lr < last_lr: + # Reset steps to plateau + steps_to_plateau = 5 + num_reductions += 1 + last_lr = lr + + assert num_reductions == reduce_limit + + # 3 reductions that multiply by 0.1 each time + assert np.isclose(lr, 0.001) + + +def test_lr_scheduler_cosine_decay_fixed_period(): + total_steps = 10000 + steps_per_checkpoint = 1000 + base_lr = 1.0 + + module = NumberInputFeature(NumberInputFeatureConfig(name="num1", encoder=DenseEncoderConfig())) + + optimizer = SGD(module.parameters(), lr=base_lr) + config = LRSchedulerConfig(decay="cosine", t_0=steps_per_checkpoint, decay_rate=0, reduce_on_plateau=0) + scheduler = LRScheduler(config=config, optimizer=optimizer, steps_per_checkpoint=0, total_steps=0) + + curr_lr = base_lr + prev_lr = base_lr + num_restarts = 0 + for step in range(total_steps + 1): + # Cosine annealing formula + expected_lr = base_lr * 0.5 * (1 + math.cos(math.pi * (step % steps_per_checkpoint) / steps_per_checkpoint)) + assert np.isclose(curr_lr, expected_lr), f"step: {step}" + + if prev_lr < curr_lr: + # Since Cosine decay is periodic, we should see the learning rate + # decrease and then increase again. + num_restarts += 1 + + prev_lr = curr_lr + scheduler.step() + + curr_lr = optimizer.param_groups[0]["lr"] + + assert num_restarts == 10, f"num_restarts: {num_restarts}" + + +def test_lr_scheduler_cosine_decay_increasing_period(): + total_steps = 20000 + steps_per_checkpoint = 1000 + base_lr = 1.0 + + module = NumberInputFeature(NumberInputFeatureConfig(name="num1", encoder=DenseEncoderConfig())) + + optimizer = SGD(module.parameters(), lr=base_lr) + config = LRSchedulerConfig( + decay="cosine", + t_0=steps_per_checkpoint, + t_mult=2, + decay_rate=0, + reduce_on_plateau=0, + ) + scheduler = LRScheduler( + config=config, optimizer=optimizer, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps + ) + + curr_lr = base_lr + prev_lr = base_lr + num_restarts = 0 + for _ in range(total_steps + 1): + if prev_lr < curr_lr: + # Since Cosine decay is periodic, we should see the learning rate + # decrease and then increase again. + num_restarts += 1 + + prev_lr = curr_lr + scheduler.step() + + curr_lr = optimizer.param_groups[0]["lr"] + + # 1000, 3000, 6000, 12000, 24000 (but we stop at 20000) + assert num_restarts == 4, f"num_restarts: {num_restarts}" + + +def test_lr_scheduler_save_load(): + steps_per_checkpoint = 10 + total_steps = 100 + base_lr = 1.0 + reduce_limit = 3 + + module = NumberInputFeature(NumberInputFeatureConfig(name="num1", encoder=DenseEncoderConfig())) + output1 = NumberOutputFeature(ECDNumberOutputFeatureConfig(name="output1", input_size=10), output_features={}) + + optimizer = SGD(module.parameters(), lr=base_lr) + config = LRSchedulerConfig(warmup_fraction=0.2, reduce_on_plateau=reduce_limit) + scheduler = LRScheduler( + config=config, optimizer=optimizer, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps + ) + + progress_tracker = get_new_progress_tracker( + batch_size=64, + best_eval_metric_value=sys.float_info.max, + best_increase_batch_size_eval_metric=sys.float_info.max, + learning_rate=base_lr, + output_features={"output1": output1}, + ) + + for _ in range(10): + scheduler.step() + + progress_tracker.train_metrics["output1"]["loss"].append(TrainerMetric(epoch=0, step=10, value=1.0)) + scheduler.eval_step(progress_tracker, "output1") + + optimizer_state = optimizer.state_dict() + scheduler_state = scheduler.state_dict() + + optimizer2 = SGD(module.parameters(), lr=base_lr) + scheduler2 = LRScheduler( + config=config, optimizer=optimizer2, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps + ) + + # Important: state needs to be loaded after init of optimizer and scheduler, otherwise + # it can override loaded state + optimizer2.load_state_dict(optimizer_state) + scheduler2.load_state_dict(scheduler_state) + + lr = optimizer.param_groups[0]["lr"] + assert lr == optimizer2.param_groups[0]["lr"] + assert scheduler.state_dict() == scheduler2.state_dict() + + for _ in range(10): + scheduler.step() + scheduler2.step() + + progress_tracker.train_metrics["output1"]["loss"].append(TrainerMetric(epoch=1, step=20, value=0.8)) + scheduler.eval_step(progress_tracker, "output1") + scheduler2.eval_step(progress_tracker, "output1") + + assert lr != optimizer.param_groups[0]["lr"] + assert optimizer.param_groups[0]["lr"] == optimizer2.param_groups[0]["lr"] + assert scheduler.state_dict() == scheduler2.state_dict() diff --git a/tests/ludwig/modules/test_metric_modules.py b/tests/ludwig/modules/test_metric_modules.py new file mode 100644 index 0000000..eca953d --- /dev/null +++ b/tests/ludwig/modules/test_metric_modules.py @@ -0,0 +1,561 @@ +"""Tests for Ludwig metric modules. + +Distributed sync architecture +------------------------------ +Ludwig's ``LudwigMetric.sync_context()`` overrides the torchmetrics base-class +implementation. Understanding the call flow is critical to avoiding subtle bugs: + +1. torchmetrics wraps every ``Metric.compute()`` call in ``sync_context()`` internally + (see ``torchmetrics/metric.py::wrapped_func``). This means sync happens automatically + whenever ``compute()`` is called. + +2. Ludwig's override asks the registered distributed strategy for a gather function: + - ``AccelerateStrategy`` → returns ``torchmetrics.utilities.distributed.gather_all_tensors`` + - ``LocalStrategy`` → returns ``None`` (no-op when torch.distributed is absent) + +3. **Ray TorchTrainer special case** (``eval_fn`` in ``ludwig/backend/ray.py``): + TorchTrainer always initialises ``torch.distributed`` before entering the worker + function. Even though ``LocalStrategy`` is registered there, ``torch.distributed`` is + available. The override detects this and falls back to ``gather_all_tensors`` so that + accumulator state is all-gathered across workers before ``compute()`` runs, giving + metrics computed over the **full** dataset rather than one worker's shard. + +Common pitfall +-------------- +Do NOT wrap ``metric_fn.compute()`` in an explicit ``sync_context()`` from the outside +(e.g. from ``get_metrics()``). torchmetrics calls ``sync_context()`` internally inside +``compute()``. A manual outer call sets ``_is_synced = True``; when ``compute()`` then +calls ``sync_context()`` again it raises:: + + TorchMetricsUserError: The Metric has already been synced. + +See ``test_double_sync_raises`` below for a regression test that pins this behaviour. +""" + +from unittest.mock import patch + +import pytest +import torch +import torch.distributed +from torchmetrics.utilities.exceptions import TorchMetricsUserError + +from ludwig.distributed import init_dist_strategy +from ludwig.modules import metric_modules +from ludwig.modules.metric_modules import RMSEMetric +from ludwig.schema.features.loss.loss import ( + BWCEWLossConfig, + SigmoidCrossEntropyLossConfig, + SoftmaxCrossEntropyLossConfig, +) + +# Required for local testing. +init_dist_strategy("local") + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(6).float()]) +def test_rmse_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.RMSEMetric() + with metric.sync_context(): + metric.update(preds, target) + assert output == metric.compute() + + +@pytest.mark.parametrize("preds", [torch.tensor([0.2, 0.3, 0.8, 0.1])]) +@pytest.mark.parametrize("target", [torch.tensor([0, 0, 1, 1])]) +@pytest.mark.parametrize("output", [torch.tensor(0.5)]) +def test_roc_auc_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.BinaryAUROCMetric(task="binary") + with metric.sync_context(): + metric.update(preds, target) + assert output == metric.compute() + + +@pytest.mark.parametrize("preds", [torch.tensor([0.2, 0.3, 0.8, 0.1, 0.8])]) +@pytest.mark.parametrize("target", [torch.tensor([0, 0, 1, 1, 0])]) +@pytest.mark.parametrize("output", [torch.tensor(0.6667).float()]) +def test_specificity_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.SpecificityMetric() + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(0.7527).float()]) +def test_rmspe_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.RMSPEMetric() + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize( + "preds,target,num_outputs,output", + [ + (torch.arange(3), torch.arange(3, 6), 1, torch.tensor(-12.5)), + (torch.arange(6).reshape(3, 2), torch.arange(6, 12).reshape(3, 2), 2, torch.tensor(-12.5)), + ], +) +def test_r2_score(preds: torch.Tensor, target: torch.Tensor, num_outputs: int, output: torch.Tensor): + metric = metric_modules.R2Score(num_outputs=num_outputs) + with metric.sync_context(): + metric.update(preds, target) + assert metric.compute() == output + + +def test_r2_score_single_sample(): + metric = metric_modules.R2Score(num_outputs=1) + with metric.sync_context(): + metric.update(preds=torch.tensor([0.8]), target=torch.arange(1)) + assert torch.isnan(metric.compute()) + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(-21.4655).float()]) +def test_bwcewl_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.BWCEWLMetric(BWCEWLossConfig()) + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.tensor([[0.5, 0.5], [0.2, 0.8], [0.6, 0.4]])]) +@pytest.mark.parametrize("target", [torch.tensor([1, 1, 0])]) +@pytest.mark.parametrize("output", [torch.tensor(0.5763)]) +def test_softmax_cross_entropy_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.SoftmaxCrossEntropyMetric(SoftmaxCrossEntropyLossConfig()) + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(-21.4655).float()]) +def test_sigmoid_cross_entropy_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.SigmoidCrossEntropyMetric(SigmoidCrossEntropyLossConfig()) + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize( + "preds,target,output", + [ + ( + torch.tensor([[0, 1], [3, 2], [4, 5]]), + torch.tensor([[0, 1], [1, 2], [4, 5]]), + torch.tensor(0.8), + ), + ( + torch.tensor([[0, 1, 2], [1, 3, 4], [3, 4, 5]]), + torch.tensor([[0, 1, 2], [1, 1, 4], [3, 4, 5]]), + torch.tensor(0.8750), + ), + ( + torch.tensor([[1, 5, 1, 5, 1, 5, 12, 12, 12], [10, 1, 5, 1, 5, 12, 12, 12, 12]]), + torch.tensor([[1, 9, 5, 7, 5, 9, 13, 6, 0], [1, 9, 7, 13, 4, 7, 7, 7, 0]]), + torch.tensor(0.05555555), + ), + ], +) +def test_token_accuracy_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.TokenAccuracyMetric() + with metric.sync_context(): + metric.update(preds, target) + assert torch.allclose(metric.compute(), output) + + +def test_sequence_accuracy_metric(): + target = torch.tensor( + [ + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + ] + ) + preds = torch.tensor( + [ + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 6, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + [1, 4, 5, 4, 0], + ] + ) + metric = metric_modules.SequenceAccuracyMetric() + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(metric.compute(), torch.tensor(0.8438), rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.arange(6)]) +@pytest.mark.parametrize("target", [torch.tensor([0, 1, 2, 1, 4, 5]).float()]) +@pytest.mark.parametrize("output", [torch.tensor(0.7500).float()]) +@pytest.mark.parametrize("one_hot", [False, True]) +def test_category_accuracy(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor, one_hot: bool): + if one_hot: + target = torch.nn.functional.one_hot(target.long(), num_classes=6).float() + metric = metric_modules.CategoryAccuracy(num_classes=6) + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.arange(6)]) +@pytest.mark.parametrize("target", [torch.tensor([0, 1, 2, 1, 4, 5]).float()]) +@pytest.mark.parametrize("output", [torch.tensor(0.8333).float()]) +@pytest.mark.parametrize("one_hot", [False, True]) +def test_category_accuracy_micro(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor, one_hot: bool): + if one_hot: + target = torch.nn.functional.one_hot(target.long(), num_classes=6).float() + metric = metric_modules.CategoryAccuracyMicro(num_classes=6) + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize( + "preds,target,output,k", + [ + ( + torch.tensor([[0.1, 0.9, 0], [0.3, 0.1, 0.6], [0.2, 0.5, 0.3]]), + torch.tensor([0, 1, 2]), + torch.tensor(0.6667).float(), + 2, + ) + ], +) +def test_hits_at_k_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor, k: int): + metric = metric_modules.HitsAtKMetric(num_classes=3, top_k=k) + with metric.sync_context(): + metric.update(preds, target) + assert torch.isclose(output, metric.compute(), rtol=0.0001) + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(6).float()]) +def test_mae_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.MAEMetric() + with metric.sync_context(): + metric.update(preds, target) + assert output == metric.compute() + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(36).float()]) +def test_mse_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.MSEMetric() + with metric.sync_context(): + metric.update(preds, target) + assert output == metric.compute() + + +@pytest.mark.parametrize("preds", [torch.arange(6).reshape(3, 2).float()]) +@pytest.mark.parametrize("target", [torch.arange(6, 12).reshape(3, 2).float()]) +@pytest.mark.parametrize("output", [torch.tensor(0.7365440726280212)]) +def test_mape_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.MAPEMetric() + with metric.sync_context(): + metric.update(preds, target) + assert output.item() == metric.compute().item() + + +@pytest.mark.parametrize("preds", [torch.tensor([[0, 1], [1, 1]])]) +@pytest.mark.parametrize("target", [torch.tensor([[1, 0], [1, 1]])]) +@pytest.mark.parametrize("output", [torch.tensor(0.5)]) +def test_jaccard_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor): + metric = metric_modules.JaccardMetric() + with metric.sync_context(): + metric.update(preds, target) + assert output == metric.compute() + + +def test_char_error_rate(): + metric = metric_modules.CharErrorRateMetric() + with metric.sync_context(): + metric.update( + ["this is the prediction", "there is an other sample"], ["this is the reference", "there is another one"] + ) + assert torch.isclose(torch.tensor(0.3415), metric.compute(), rtol=0.5) + + +# --------------------------------------------------------------------------- +# Distributed sync_context behaviour +# --------------------------------------------------------------------------- + + +class TestSyncContextDispatch: + """Unit tests for the gather-function selection logic in LudwigMetric.sync_context(). + + These tests validate the three dispatch paths described in the module docstring + without requiring a real multi-process distributed environment. The mock helpers + simulate the two dimensions that control dispatch: + + * Whether the registered Ludwig strategy provides a gather function. + * Whether torch.distributed is initialised. + """ + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _make_metric() -> RMSEMetric: + """Fresh RMSEMetric with LocalStrategy registered (mirrors eval_fn setup).""" + init_dist_strategy("local") + return RMSEMetric() + + @staticmethod + def _passthrough_gather(tensor, **kwargs): + """Simulate gather_all_tensors on a single process: return a one-element list.""" + return [tensor] + + # ------------------------------------------------------------------ + # Path 1: LocalStrategy + no torch.distributed → sync is a no-op + # ------------------------------------------------------------------ + + def test_local_strategy_no_distributed_is_noop(self): + """With LocalStrategy and no torch.distributed, compute() works without error.""" + metric = self._make_metric() + preds = torch.arange(6).reshape(3, 2).float() + target = torch.arange(6, 12).reshape(3, 2).float() + metric.update(preds, target) + # No distributed → sync_context is a no-op → compute() must succeed + result = metric.compute() + assert result == torch.tensor(6).float() + + def test_local_strategy_no_distributed_gather_fn_is_none(self): + """With LocalStrategy and no torch.distributed, sync() receives dist_sync_fn=None.""" + metric = self._make_metric() + with patch.object(metric, "sync") as mock_sync, patch.object(metric, "unsync"): + with metric.sync_context(): + pass + mock_sync.assert_called_once() + kwargs = mock_sync.call_args[1] + assert kwargs["dist_sync_fn"] is None, ( + "Expected no gather function when LocalStrategy is active and torch.distributed is not initialised." + ) + + # ------------------------------------------------------------------ + # Path 2: LocalStrategy + torch.distributed initialised → fallback + # ------------------------------------------------------------------ + + def test_distributed_fallback_selected_when_torch_dist_initialized(self): + """When LocalStrategy is active but torch.distributed is initialised, sync_context should fall back to + torchmetrics' gather_all_tensors (not None). + + This is the Ray TorchTrainer / eval_fn scenario: TorchTrainer always calls + torch.distributed.init_process_group() before eval_fn runs, so torch.distributed.is_initialized() is True even + though Ludwig registered LocalStrategy for the eval pass. + """ + from torchmetrics.utilities.distributed import gather_all_tensors + + metric = self._make_metric() + with ( + patch.object(torch.distributed, "is_available", return_value=True), + patch.object(torch.distributed, "is_initialized", return_value=True), + patch.object(metric, "sync") as mock_sync, + patch.object(metric, "unsync"), + ): + with metric.sync_context(): + pass + + mock_sync.assert_called_once() + kwargs = mock_sync.call_args[1] + assert kwargs["dist_sync_fn"] is gather_all_tensors, ( + "Expected gather_all_tensors as the fallback gather function when " + "LocalStrategy is active but torch.distributed is initialised." + ) + + def test_distributed_fallback_compute_produces_correct_result(self): + """End-to-end: with the fallback active, compute() returns the correct value. + + We mock gather_all_tensors to be a single-process passthrough so we can exercise + the full sync → compute path without a real distributed environment. + """ + metric = self._make_metric() + preds = torch.arange(6).reshape(3, 2).float() + target = torch.arange(6, 12).reshape(3, 2).float() + metric.update(preds, target) + + with ( + patch.object(torch.distributed, "is_available", return_value=True), + patch.object(torch.distributed, "is_initialized", return_value=True), + patch( + "torchmetrics.utilities.distributed.gather_all_tensors", + side_effect=self._passthrough_gather, + ), + ): + result = metric.compute() + + assert result == torch.tensor(6).float() + + # ------------------------------------------------------------------ + # Path 3: AccelerateStrategy → strategy's gather fn takes precedence + # ------------------------------------------------------------------ + + def test_accelerate_strategy_gather_fn_used_not_fallback(self): + """When AccelerateStrategy is active its gather function is used directly, even if torch.distributed is + also initialised.""" + from torchmetrics.utilities.distributed import gather_all_tensors + + init_dist_strategy("accelerate") + metric = RMSEMetric() + + # Mock AccelerateStrategy.gather_all_tensors_fn to return a sentinel so we can + # distinguish it from the torch.distributed fallback path. + sentinel_gather = object() + with ( + patch( + "ludwig.distributed.accelerate.AccelerateStrategy.gather_all_tensors_fn", + return_value=sentinel_gather, + ), + patch.object(torch.distributed, "is_available", return_value=True), + patch.object(torch.distributed, "is_initialized", return_value=True), + patch.object(metric, "sync") as mock_sync, + patch.object(metric, "unsync"), + ): + with metric.sync_context(): + pass + + kwargs = mock_sync.call_args[1] + assert kwargs["dist_sync_fn"] is sentinel_gather, ( + "Expected the strategy's gather function, not the torch.distributed fallback." + ) + assert kwargs["dist_sync_fn"] is not gather_all_tensors + + # Restore LocalStrategy for subsequent tests. + init_dist_strategy("local") + + +# --------------------------------------------------------------------------- +# Regression: double-sync raises TorchMetricsUserError +# --------------------------------------------------------------------------- + + +class TestDoubleSyncRegression: + """Regression tests for the double-sync bug. + + torchmetrics calls sync_context() internally inside compute(). If an outer call to + sync_context() is added (e.g. from get_metrics()), the metric ends up synced twice + when torch.distributed is active, raising TorchMetricsUserError. + + This is a canary: if these tests start *passing* without the error, something has + changed in torchmetrics' internals that removes the auto-sync inside compute() and we + need to revisit the sync strategy. + """ + + @staticmethod + def _passthrough_gather(tensor, **kwargs): + return [tensor] + + def test_double_sync_raises_when_distributed_active(self): + """Wrapping compute() in a manual sync_context() must raise when distributed is active. + + This documents WHY get_metrics() must NOT call sync_context() explicitly. + """ + init_dist_strategy("local") + metric = RMSEMetric() + preds = torch.arange(6).reshape(3, 2).float() + target = torch.arange(6, 12).reshape(3, 2).float() + metric.update(preds, target) + + with ( + patch.object(torch.distributed, "is_available", return_value=True), + patch.object(torch.distributed, "is_initialized", return_value=True), + patch( + "torchmetrics.utilities.distributed.gather_all_tensors", + side_effect=self._passthrough_gather, + ), + ): + with pytest.raises(TorchMetricsUserError, match="already been synced"): + # This is the BAD pattern: do not do this in production code. + with metric.sync_context(): # outer sync → sets _is_synced = True + metric.compute() # torchmetrics calls sync_context() again → ERROR + + def test_no_double_sync_without_explicit_outer_call(self): + """Calling compute() directly (no outer sync_context) must NOT raise, even when distributed is active. + + This is the correct usage pattern. + """ + init_dist_strategy("local") + metric = RMSEMetric() + preds = torch.arange(6).reshape(3, 2).float() + target = torch.arange(6, 12).reshape(3, 2).float() + metric.update(preds, target) + + with ( + patch.object(torch.distributed, "is_available", return_value=True), + patch.object(torch.distributed, "is_initialized", return_value=True), + patch( + "torchmetrics.utilities.distributed.gather_all_tensors", + side_effect=self._passthrough_gather, + ), + ): + # Correct pattern: just call compute() — sync happens automatically inside. + result = metric.compute() + + assert result == torch.tensor(6).float() diff --git a/tests/ludwig/modules/test_mlp_mixer_modules.py b/tests/ludwig/modules/test_mlp_mixer_modules.py new file mode 100644 index 0000000..ac91e46 --- /dev/null +++ b/tests/ludwig/modules/test_mlp_mixer_modules.py @@ -0,0 +1,27 @@ +import pytest + +from ludwig.modules.mlp_mixer_modules import MixerBlock, MLP, MLPMixer + +from .test_utils import assert_output_shapes + + +@pytest.mark.parametrize("in_features,hidden_size,out_features", [(3, 8, 8), (8, 64, 32)]) +def test_mlp(in_features: int, hidden_size: int, out_features: int): + assert_output_shapes(module=MLP(in_features, hidden_size, out_features), input_shape=(in_features,)) + + +@pytest.mark.parametrize("embed_size,n_patches,token_dim,channel_dim", [(512, 49, 2048, 256)]) +def test_mixer_block( + embed_size: int, + n_patches: int, + token_dim: int, + channel_dim: int, +): + assert_output_shapes( + module=MixerBlock(embed_size, n_patches, token_dim, channel_dim), input_shape=(n_patches, embed_size) + ) + + +@pytest.mark.parametrize("img_height,img_width,in_channels", [(224, 224, 3)]) +def test_mlp_mixer(img_height: int, img_width: int, in_channels: int): + assert_output_shapes(module=MLPMixer(img_height, img_width, in_channels), input_shape=(3, img_height, img_width)) diff --git a/tests/ludwig/modules/test_modality_dropout.py b/tests/ludwig/modules/test_modality_dropout.py new file mode 100644 index 0000000..585c9b3 --- /dev/null +++ b/tests/ludwig/modules/test_modality_dropout.py @@ -0,0 +1,86 @@ +"""Tests for modality dropout.""" + +import torch + +from ludwig.constants import ENCODER_OUTPUT +from ludwig.modules.modality_dropout import ModalityDropout + + +def _make_encoder_outputs(batch_size=8, hidden_size=16, num_features=3): + outputs = {} + for i in range(num_features): + name = f"feature_{i}" + outputs[name] = {ENCODER_OUTPUT: torch.randn(batch_size, hidden_size)} + return outputs + + +def _make_feature_shapes(hidden_size=16, num_features=3): + return {f"feature_{i}": torch.Size([hidden_size]) for i in range(num_features)} + + +class TestModalityDropout: + def test_eval_mode_passthrough(self): + """In eval mode, outputs should be unchanged.""" + md = ModalityDropout(_make_feature_shapes(), dropout_prob=1.0) + md.eval() + inputs = _make_encoder_outputs() + outputs = md(inputs) + for name in inputs: + assert torch.equal(inputs[name][ENCODER_OUTPUT], outputs[name][ENCODER_OUTPUT]) + + def test_train_mode_zero_prob_passthrough(self): + """With dropout_prob=0, outputs should be unchanged even in train mode.""" + md = ModalityDropout(_make_feature_shapes(), dropout_prob=0.0) + md.train() + inputs = _make_encoder_outputs() + outputs = md(inputs) + for name in inputs: + assert torch.equal(inputs[name][ENCODER_OUTPUT], outputs[name][ENCODER_OUTPUT]) + + def test_train_mode_full_dropout(self): + """With dropout_prob=1.0 in train mode, all features should be replaced.""" + md = ModalityDropout(_make_feature_shapes(), dropout_prob=1.0) + md.train() + inputs = _make_encoder_outputs() + outputs = md(inputs) + for name in inputs: + # Output should be the learned missing embedding, not the original + assert not torch.equal(inputs[name][ENCODER_OUTPUT], outputs[name][ENCODER_OUTPUT]) + + def test_missing_embeddings_are_learnable(self): + """Missing embeddings should be in named_parameters and receive gradients.""" + md = ModalityDropout(_make_feature_shapes(), dropout_prob=1.0) + md.train() + + param_names = [n for n, _ in md.named_parameters()] + assert len(param_names) == 3 + assert all("missing_embeddings" in n for n in param_names) + + inputs = _make_encoder_outputs() + outputs = md(inputs) + loss = sum(o[ENCODER_OUTPUT].sum() for o in outputs.values()) + loss.backward() + for name, param in md.named_parameters(): + assert param.grad is not None, f"No gradient for {name}" + + def test_output_shapes_preserved(self): + """Output shapes should match input shapes.""" + md = ModalityDropout(_make_feature_shapes(hidden_size=32), dropout_prob=0.5) + md.train() + inputs = _make_encoder_outputs(batch_size=4, hidden_size=32) + outputs = md(inputs) + for name in inputs: + assert outputs[name][ENCODER_OUTPUT].shape == inputs[name][ENCODER_OUTPUT].shape + + def test_partial_dropout(self): + """With intermediate dropout_prob, some features should be dropped and others kept.""" + torch.manual_seed(42) + md = ModalityDropout(_make_feature_shapes(num_features=10), dropout_prob=0.5) + md.train() + inputs = _make_encoder_outputs(num_features=10) + outputs = md(inputs) + changed = sum( + 1 for name in inputs if not torch.equal(inputs[name][ENCODER_OUTPUT], outputs[name][ENCODER_OUTPUT]) + ) + # With 10 features and p=0.5, expect roughly 5 changed (but stochastic) + assert 0 < changed < 10 diff --git a/tests/ludwig/modules/test_normalization_modules.py b/tests/ludwig/modules/test_normalization_modules.py new file mode 100644 index 0000000..c9b3a25 --- /dev/null +++ b/tests/ludwig/modules/test_normalization_modules.py @@ -0,0 +1,55 @@ +import pytest +import torch + +from ludwig.modules.normalization_modules import GhostBatchNormalization + +BATCH_SIZE = 16 +OUTPUT_SIZE = 8 + + +@pytest.mark.parametrize("virtual_batch_size", [None, BATCH_SIZE // 2, BATCH_SIZE - 14, BATCH_SIZE - 10]) +@pytest.mark.parametrize("mode", [True, False]) # training (True) or eval(False) +def test_ghostbatchnormalization(mode: bool, virtual_batch_size: int | None) -> None: + # setup up GhostBatchNormalization layer + ghost_batch_norm = GhostBatchNormalization(OUTPUT_SIZE, virtual_batch_size=virtual_batch_size) + + # set training or eval mode + ghost_batch_norm.train(mode=mode) + + # setup inputs to test + inputs = torch.randn([BATCH_SIZE, OUTPUT_SIZE], dtype=torch.float32) + + # run tensor through + norm_tensor = ghost_batch_norm(inputs) + + # check for correctness of output + assert isinstance(norm_tensor, torch.Tensor) + assert norm_tensor.shape == (BATCH_SIZE, OUTPUT_SIZE) + + # check for required properties + assert ghost_batch_norm.input_shape == inputs.shape[1:] + assert ghost_batch_norm.output_shape == inputs.shape[1:] + assert ghost_batch_norm.input_dtype == torch.float32 + + assert isinstance(ghost_batch_norm.moving_mean, torch.Tensor) + assert ghost_batch_norm.moving_mean.shape == (OUTPUT_SIZE,) + + assert isinstance(ghost_batch_norm.moving_variance, torch.Tensor) + assert ghost_batch_norm.moving_variance.shape == (OUTPUT_SIZE,) + + +def test_ghostbatchnormalization_chunk_size_2() -> None: + """Test GhostBatchNormalization with virtual_batch_size=2 and batch_size=7 This creates chunks of size 2, 2, 2, + 1 which should be handled correctly since we should skip applying batch norm to the last chunk since it is size + 1.""" + # setup up GhostBatchNormalization layer + ghost_batch_norm = GhostBatchNormalization(6, virtual_batch_size=2) + + # setup inputs to test + inputs = torch.randn([7, 6], dtype=torch.float32) + + # Set to training mode + ghost_batch_norm.train(mode=True) + + # run tensor through + ghost_batch_norm(inputs) diff --git a/tests/ludwig/modules/test_pareto_mtl.py b/tests/ludwig/modules/test_pareto_mtl.py new file mode 100644 index 0000000..fbeebb3 --- /dev/null +++ b/tests/ludwig/modules/test_pareto_mtl.py @@ -0,0 +1,125 @@ +"""Phase 6.7.2 — Pareto-optimal multi-task loss balancer unit tests.""" + +from __future__ import annotations + +import pytest +import torch + +from ludwig.modules.loss_balancing import create_loss_balancer, LOSS_BALANCER_REGISTRY, ParetoMTLLossBalancer + + +class TestParetoMTLLossBalancer: + def test_registered(self): + assert "pareto_mtl" in LOSS_BALANCER_REGISTRY + assert LOSS_BALANCER_REGISTRY["pareto_mtl"] is ParetoMTLLossBalancer + + def test_uniform_preference_default(self): + balancer = ParetoMTLLossBalancer(["a", "b", "c"]) + torch.testing.assert_close(balancer.preference_vector, torch.tensor([1 / 3, 1 / 3, 1 / 3])) + + def test_explicit_preference_is_normalised(self): + balancer = ParetoMTLLossBalancer(["a", "b"], preference_vector=[2.0, 3.0]) + torch.testing.assert_close(balancer.preference_vector, torch.tensor([0.4, 0.6])) + + def test_rejects_wrong_length(self): + with pytest.raises(ValueError, match="one per output feature"): + ParetoMTLLossBalancer(["a", "b", "c"], preference_vector=[1.0, 1.0]) + + def test_rejects_negative_entries(self): + with pytest.raises(ValueError, match="non-negative"): + ParetoMTLLossBalancer(["a", "b"], preference_vector=[1.0, -0.5]) + + def test_rejects_zero_sum(self): + with pytest.raises(ValueError, match="positive"): + ParetoMTLLossBalancer(["a", "b"], preference_vector=[0.0, 0.0]) + + def test_tchebycheff_weight_range(self): + with pytest.raises(ValueError, match=r"\[0, 1\]"): + ParetoMTLLossBalancer(["a", "b"], tchebycheff_weight=1.5) + + def test_linear_scalarisation_at_tcheb_zero(self): + """With tchebycheff_weight=0, the balancer is exactly sum(lam_i * L_i). + + Equal unit weights_i + losses (1.0, 2.0) with uniform preference (0.5, 0.5) gives + 0.5*1.0 + 0.5*2.0 = 1.5. + """ + balancer = ParetoMTLLossBalancer(["a", "b"], preference_vector=[1.0, 1.0], tchebycheff_weight=0.0) + per_task = {"a": torch.tensor(1.0), "b": torch.tensor(2.0)} + weights = {"a": 1.0, "b": 1.0} + out = balancer(per_task, weights) + torch.testing.assert_close(out, torch.tensor(1.5)) + + def test_tchebycheff_scalarisation_at_tcheb_one(self): + """With tchebycheff_weight=1, the balancer is max(lam_i * L_i).""" + balancer = ParetoMTLLossBalancer(["a", "b"], preference_vector=[1.0, 1.0], tchebycheff_weight=1.0) + per_task = {"a": torch.tensor(1.0), "b": torch.tensor(2.0)} + weights = {"a": 1.0, "b": 1.0} + out = balancer(per_task, weights) + # max(0.5*1.0, 0.5*2.0) = 1.0 + torch.testing.assert_close(out, torch.tensor(1.0)) + + def test_mixed_scalarisation(self): + balancer = ParetoMTLLossBalancer(["a", "b"], preference_vector=[1.0, 3.0], tchebycheff_weight=0.5) + per_task = {"a": torch.tensor(4.0), "b": torch.tensor(2.0)} + weights = {"a": 1.0, "b": 1.0} + # Normalised preference = [0.25, 0.75]. + # linear term = 0.25*4 + 0.75*2 = 1.0 + 1.5 = 2.5 + # tcheb term = max(0.25*4, 0.75*2) = max(1.0, 1.5) = 1.5 + # blended = 0.5*2.5 + 0.5*1.5 = 2.0 + out = balancer(per_task, weights) + torch.testing.assert_close(out, torch.tensor(2.0)) + + def test_backward_flows(self): + balancer = ParetoMTLLossBalancer(["a", "b"], preference_vector=[1.0, 2.0]) + a = torch.tensor(1.0, requires_grad=True) + b = torch.tensor(2.0, requires_grad=True) + out = balancer({"a": a, "b": b}, {"a": 1.0, "b": 1.0}) + out.backward() + assert a.grad is not None + assert b.grad is not None + + def test_create_loss_balancer_passthrough(self): + balancer = create_loss_balancer( + "pareto_mtl", + ["a", "b"], + preference_vector=[1.0, 4.0], + tchebycheff_weight=0.25, + ) + assert isinstance(balancer, ParetoMTLLossBalancer) + torch.testing.assert_close(balancer.preference_vector, torch.tensor([0.2, 0.8])) + assert balancer.tchebycheff_weight == 0.25 + + +class TestParetoMTLSchema: + def _base(self) -> dict: + return { + "input_features": [{"name": "a", "type": "number"}, {"name": "b", "type": "number"}], + "output_features": [ + {"name": "y1", "type": "binary"}, + {"name": "y2", "type": "number"}, + ], + } + + def test_pareto_mtl_config_accepted(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + **self._base(), + "trainer": { + "loss_balancing": "pareto_mtl", + "loss_balancing_preference_vector": [1.0, 2.0], + "loss_balancing_tchebycheff_weight": 0.3, + }, + } + ) + assert cfg.trainer.loss_balancing == "pareto_mtl" + assert cfg.trainer.loss_balancing_preference_vector == [1.0, 2.0] + assert cfg.trainer.loss_balancing_tchebycheff_weight == 0.3 + + def test_preference_vector_defaults_none(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict(self._base()) + assert cfg.trainer.loss_balancing_preference_vector is None + assert cfg.trainer.loss_balancing_tchebycheff_weight == 0.5 diff --git a/tests/ludwig/modules/test_preference_losses.py b/tests/ludwig/modules/test_preference_losses.py new file mode 100644 index 0000000..a84d11a --- /dev/null +++ b/tests/ludwig/modules/test_preference_losses.py @@ -0,0 +1,86 @@ +"""Tests for KTO, ORPO, and GRPO loss functions.""" + +import torch + +from ludwig.modules.dpo_loss import compute_token_log_probs +from ludwig.modules.preference_losses import grpo_loss, kto_loss, orpo_loss + +BATCH = 4 +SEQ_LEN = 20 +VOCAB = 100 + + +def _random_inputs(): + chosen_logits = torch.randn(BATCH, SEQ_LEN, VOCAB) + rejected_logits = torch.randn(BATCH, SEQ_LEN, VOCAB) + chosen_labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + rejected_labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + return chosen_logits, rejected_logits, chosen_labels, rejected_labels + + +class TestKTOLoss: + def test_basic(self): + loss, cr, rr = kto_loss(*_random_inputs()) + assert torch.isfinite(loss) + assert loss.shape == () + + def test_gradient_flow(self): + chosen_logits = torch.randn(BATCH, SEQ_LEN, VOCAB, requires_grad=True) + rejected_logits = torch.randn(BATCH, SEQ_LEN, VOCAB, requires_grad=True) + labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + loss, _, _ = kto_loss(chosen_logits, rejected_logits, labels, labels) + loss.backward() + assert chosen_logits.grad is not None + assert rejected_logits.grad is not None + + +class TestORPOLoss: + def test_basic(self): + loss, cr, rr = orpo_loss(*_random_inputs()) + assert torch.isfinite(loss) + assert loss.shape == () + + def test_gradient_flow(self): + chosen_logits = torch.randn(BATCH, SEQ_LEN, VOCAB, requires_grad=True) + rejected_logits = torch.randn(BATCH, SEQ_LEN, VOCAB, requires_grad=True) + labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + loss, _, _ = orpo_loss(chosen_logits, rejected_logits, labels, labels) + loss.backward() + assert chosen_logits.grad is not None + + +class TestGRPOLoss: + def test_basic(self): + logits = torch.randn(BATCH, SEQ_LEN, VOCAB) + labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + rewards = torch.randn(BATCH) + loss, adv = grpo_loss(logits, labels, rewards) + assert torch.isfinite(loss) + assert loss.shape == () + + def test_with_old_log_probs(self): + logits = torch.randn(BATCH, SEQ_LEN, VOCAB) + labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + rewards = torch.randn(BATCH) + old_lp = torch.randn(BATCH) + loss, _ = grpo_loss(logits, labels, rewards, old_log_probs=old_lp) + assert torch.isfinite(loss) + + def test_with_reference(self): + logits = torch.randn(BATCH, SEQ_LEN, VOCAB) + labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + rewards = torch.randn(BATCH) + ref_lp = torch.randn(BATCH) + loss, _ = grpo_loss(logits, labels, rewards, reference_log_probs=ref_lp) + assert torch.isfinite(loss) + + def test_gradient_flow(self): + logits = torch.randn(BATCH, SEQ_LEN, VOCAB, requires_grad=True) + labels = torch.randint(0, VOCAB, (BATCH, SEQ_LEN)) + # Rewards must be detached (they come from a reward model, not the policy) + # but old_log_probs connect to the policy graph + rewards = torch.randn(BATCH) + old_lp = compute_token_log_probs(logits.detach(), labels) + loss, _ = grpo_loss(logits, labels, rewards, old_log_probs=old_lp.detach()) + loss.backward() + assert logits.grad is not None diff --git a/tests/ludwig/modules/test_recurrent_modules.py b/tests/ludwig/modules/test_recurrent_modules.py new file mode 100644 index 0000000..1813a99 --- /dev/null +++ b/tests/ludwig/modules/test_recurrent_modules.py @@ -0,0 +1,23 @@ +import logging + +import pytest +import torch + +from ludwig.modules import recurrent_modules + +logger = logging.getLogger(__name__) + + +@pytest.mark.parametrize("max_sequence_length,expected_output_shape", [(19, [19, 256]), (None, [256])]) +def test_recurrent_stack(max_sequence_length, expected_output_shape): + recurrent_stack = recurrent_modules.RecurrentStack( + input_size=10, max_sequence_length=max_sequence_length, hidden_size=256 + ) + assert recurrent_stack.output_shape == torch.Size(expected_output_shape) + + # Batch (N), Length (L), Input (H) + inputs = torch.rand(2, 19, 10) + hidden, final_state = recurrent_stack(inputs) + + assert hidden.shape == torch.Size([2, 19, 256]) + assert final_state.shape == torch.Size([2, 256]) diff --git a/tests/ludwig/modules/test_reduction_modules.py b/tests/ludwig/modules/test_reduction_modules.py new file mode 100644 index 0000000..998a39f --- /dev/null +++ b/tests/ludwig/modules/test_reduction_modules.py @@ -0,0 +1,23 @@ +import pytest +import torch + +from ludwig.modules import reduction_modules +from ludwig.utils.torch_utils import get_torch_device + +DEVICE = get_torch_device() + + +@pytest.mark.parametrize("reduce_mode", ["last", "sum", "mean", "avg", "max", "concat", "attention", None]) +@pytest.mark.parametrize("test_input_shape", [(16, 1, 4), (4, 10, 16)]) +def test_sequence_reducer(reduce_mode: str, test_input_shape: tuple[int, ...]): + batch_size, max_sequence_length, encoding_size = test_input_shape + sequence_reducer = reduction_modules.SequenceReducer( + reduce_mode=reduce_mode, max_sequence_length=max_sequence_length, encoding_size=encoding_size + ).to(DEVICE) + inputs = torch.zeros(test_input_shape) + # Generates random sequence of random length for each instance in batch. + for batch_index in range(batch_size): + sequence_length = torch.randint(max_sequence_length, (1,)) + inputs[batch_index, :sequence_length] = torch.rand((sequence_length, encoding_size)) + outputs = sequence_reducer(inputs.to(DEVICE)) + assert outputs.shape[1:] == sequence_reducer.output_shape diff --git a/tests/ludwig/modules/test_regex_freezing.py b/tests/ludwig/modules/test_regex_freezing.py new file mode 100644 index 0000000..ab89fad --- /dev/null +++ b/tests/ludwig/modules/test_regex_freezing.py @@ -0,0 +1,124 @@ +import logging +import os +import re +from contextlib import nullcontext as no_error_raised + +import pytest + +from ludwig.api import LudwigModel +from ludwig.constants import ( + BASE_MODEL, + BATCH_SIZE, + EPOCHS, + GENERATION, + INPUT_FEATURES, + MODEL_LLM, + MODEL_TYPE, + OUTPUT_FEATURES, + TRAINER, + TYPE, +) +from ludwig.encoders.image.torchvision import TVEfficientNetEncoder +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.utils.misc_utils import set_random_seed +from ludwig.utils.trainer_utils import freeze_layers_regex +from tests.integration_tests.utils import category_feature, generate_data, image_feature, text_feature + +RANDOM_SEED = 130 + + +@pytest.mark.parametrize( + "regex", + [ + r"(features\.1.*|features\.2.*|features\.3.*|model\.features\.4\.1\.block\.3\.0\.weight)", + r"(features\.1.*|features\.2\.*|features\.3.*)", + r"(features\.4\.0\.block|features\.4\.\d+\.block)", + r"(features\.5\.*|features\.6\.*|features\.7\.*)", + r"(features\.8\.\d+\.weight|features\.8\.\d+\.bias)", + ], +) +def test_tv_efficientnet_freezing(regex): + set_random_seed(RANDOM_SEED) + + pretrained_model = TVEfficientNetEncoder( + model_variant="b0", use_pretrained=False, saved_weights_in_checkpoint=True, trainable=True + ) + + config = ECDTrainerConfig(layers_to_freeze_regex=regex) + freeze_layers_regex(config, pretrained_model) + for name, param in pretrained_model.named_parameters(): + if re.search(re.compile(regex), name): + assert not param.requires_grad + else: + assert param.requires_grad + + +def test_llm_freezing(tmpdir, csv_filename): + # Force CPU to avoid CUBLAS errors with tiny random LLM models on GPU. + old_val = os.environ.get("CUDA_VISIBLE_DEVICES") + os.environ["CUDA_VISIBLE_DEVICES"] = "" + try: + _run_llm_freezing(tmpdir, csv_filename) + finally: + if old_val is None: + os.environ.pop("CUDA_VISIBLE_DEVICES", None) + else: + os.environ["CUDA_VISIBLE_DEVICES"] = old_val + + +def _run_llm_freezing(tmpdir, csv_filename): + input_features = [text_feature(name="input", encoder={"type": "passthrough"})] + output_features = [text_feature(name="output")] + + train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25) + + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "hf-internal-testing/tiny-random-GPTJForCausalLM", + INPUT_FEATURES: [text_feature(name="input", encoder={"type": "passthrough"})], + OUTPUT_FEATURES: [text_feature(name="output")], + TRAINER: {TYPE: "finetune", BATCH_SIZE: 8, EPOCHS: 1, "layers_to_freeze_regex": r"(h\.0\.attn\.*)"}, + GENERATION: {"pad_token_id": 0}, + } + + model = LudwigModel(config, logging_level=logging.INFO) + + output_directory: str = str(tmpdir) + model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False) + + for name, p in model.model.named_parameters(): + if "h.0.attn" in name: + assert not p.requires_grad + else: + assert p.requires_grad + + +def test_frozen_tv_training(tmpdir, csv_filename): + input_features = [ + image_feature(tmpdir, encoder={"type": "efficientnet", "use_pretrained": False, "model_variant": "b0"}) + ] + output_features = [category_feature()] + + config = { + "input_features": input_features, + "output_features": output_features, + TRAINER: { + "layers_to_freeze_regex": r"(features\.1.*|features\.2\.*|features\.3.*)", + "epochs": 1, + "train_steps": 1, + }, + } + + training_data_csv_path = generate_data(config["input_features"], config["output_features"], csv_filename) + model = LudwigModel(config) + + with no_error_raised(): + model.experiment( + dataset=training_data_csv_path, + skip_save_training_description=True, + skip_save_training_statistics=True, + skip_save_model=True, + skip_save_progress=True, + skip_save_log=True, + skip_save_processed_input=True, + ) diff --git a/tests/ludwig/modules/test_tabnet_modules.py b/tests/ludwig/modules/test_tabnet_modules.py new file mode 100644 index 0000000..1d4504b --- /dev/null +++ b/tests/ludwig/modules/test_tabnet_modules.py @@ -0,0 +1,190 @@ +import pytest +import torch + +from ludwig.modules.tabnet_modules import AttentiveTransformer, FeatureBlock, FeatureTransformer, TabNet +from ludwig.utils.entmax import sparsemax +from tests.integration_tests.parameter_update_utils import check_module_parameters_updated + +RANDOM_SEED = 67 + + +@pytest.mark.parametrize( + "input_tensor", + [ + torch.tensor([[-1.0, 0.0, 1.0], [5.01, 4.0, -2.0]], dtype=torch.float32), + torch.tensor( + [[1.36762051e8, -1.36762051e8, 1.59594639e20], [1.59594639e37, 1.36762051e7, 1.26e6]], dtype=torch.float32 + ), + ], +) +def test_sparsemax(input_tensor: torch.Tensor) -> None: + output_tensor = sparsemax(input_tensor) + + assert isinstance(output_tensor, torch.Tensor) + assert output_tensor.equal(torch.tensor([[0, 0, 1], [1, 0, 0]], dtype=torch.float32)) + + +@pytest.mark.parametrize("bn_virtual_bs", [None, 7]) +@pytest.mark.parametrize("external_shared_fc_layer", [True, False]) +@pytest.mark.parametrize("apply_glu", [True, False]) +@pytest.mark.parametrize("size", [4, 12]) +@pytest.mark.parametrize("input_size", [2, 6]) +@pytest.mark.parametrize("batch_size", [1, 16]) +def test_feature_block( + input_size, + size: int, + apply_glu: bool, + external_shared_fc_layer: bool, + bn_virtual_bs: int | None, + batch_size: int, +) -> None: + # setup synthetic tensor + torch.manual_seed(RANDOM_SEED) + input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32) + + if external_shared_fc_layer: + shared_fc_layer = torch.nn.Linear(input_size, size * 2 if apply_glu else size, bias=False) + else: + shared_fc_layer = None + + feature_block = FeatureBlock( + input_size, size, apply_glu=apply_glu, shared_fc_layer=shared_fc_layer, bn_virtual_bs=bn_virtual_bs + ) + + output_tensor = feature_block(input_tensor) + + # check for expected structure and properties + assert isinstance(output_tensor, torch.Tensor) + assert output_tensor.shape == (batch_size, size) + + assert feature_block.input_shape[-1] == input_size + assert feature_block.output_shape[-1] == size + assert feature_block.input_dtype == torch.float32 + + +@pytest.mark.parametrize("num_total_blocks, num_shared_blocks", [(4, 2), (6, 4), (3, 1)]) +@pytest.mark.parametrize("virtual_batch_size", [None, 7]) +@pytest.mark.parametrize("size", [4, 12]) +@pytest.mark.parametrize("input_size", [2, 6]) +@pytest.mark.parametrize("batch_size", [1, 16]) +def test_feature_transformer( + input_size: int, + size: int, + virtual_batch_size: int | None, + num_total_blocks: int, + num_shared_blocks: int, + batch_size: int, +) -> None: + # setup synthetic tensor + torch.manual_seed(RANDOM_SEED) + input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32) + + feature_transformer = FeatureTransformer( + input_size, + size, + bn_virtual_bs=virtual_batch_size, + num_total_blocks=num_total_blocks, + num_shared_blocks=num_shared_blocks, + ) + + output_tensor = feature_transformer(input_tensor) + + # check for expected structure and properties + assert isinstance(output_tensor, torch.Tensor) + assert output_tensor.shape == (batch_size, size) + + assert feature_transformer.input_shape[-1] == input_size + assert feature_transformer.output_shape[-1] == size + assert feature_transformer.input_dtype == torch.float32 + + +@pytest.mark.parametrize("virtual_batch_size", [None, 7]) +@pytest.mark.parametrize("output_size", [10, 12]) +@pytest.mark.parametrize("size", [4, 8]) +@pytest.mark.parametrize("input_size", [2, 6]) +@pytest.mark.parametrize("entmax_mode", [None, "entmax15", "adaptive", "constant"]) +@pytest.mark.parametrize("batch_size", [1, 16]) +def test_attentive_transformer( + entmax_mode: str | None, + input_size: int, + size: int, + output_size: int, + virtual_batch_size: int | None, + batch_size: int, +) -> None: + # setup synthetic tensors + torch.manual_seed(RANDOM_SEED) + input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32) + prior_scales = torch.ones([batch_size, input_size]) + + # setup required transformers for test + feature_transformer = FeatureTransformer(input_size, size + output_size, bn_virtual_bs=virtual_batch_size) + attentive_transformer = AttentiveTransformer( + size, input_size, bn_virtual_bs=virtual_batch_size, entmax_mode=entmax_mode + ) + + # process synthetic tensor through transformers + x = feature_transformer(input_tensor) + output_tensor = attentive_transformer(x[:, output_size:], prior_scales) + + # check for expected shape and properties + assert isinstance(output_tensor, torch.Tensor) + assert output_tensor.shape == (batch_size, input_size) + + assert attentive_transformer.input_shape[-1] == size + assert attentive_transformer.output_shape[-1] == input_size + assert attentive_transformer.input_dtype == torch.float32 + if entmax_mode == "adaptive": + assert isinstance(attentive_transformer.trainable_alpha, torch.Tensor) + + # TODO: Need variant of assert_model_parameters_updated() to account for the two step calling sequence + # of AttentiveTransformer + + +@pytest.mark.parametrize("virtual_batch_size", [None, 7]) +@pytest.mark.parametrize("size", [2, 4, 8]) +@pytest.mark.parametrize("output_size", [2, 4, 12]) +@pytest.mark.parametrize("input_size", [2]) +@pytest.mark.parametrize("entmax_mode", [None, "entmax15", "adaptive", "constant"]) +@pytest.mark.parametrize("batch_size", [1, 16]) +def test_tabnet( + entmax_mode: str | None, + input_size: int, + output_size: int, + size: int, + virtual_batch_size: int | None, + batch_size: int, +) -> None: + # setup synthetic tensor + torch.manual_seed(RANDOM_SEED) + input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32) + + tabnet = TabNet( + input_size, size, output_size, num_steps=3, num_total_blocks=4, num_shared_blocks=2, entmax_mode=entmax_mode + ) + + output = tabnet(input_tensor) + + # check for expected shape and properties + assert isinstance(output, tuple) + assert output[0].shape == (batch_size, output_size) + + assert tabnet.input_shape[-1] == input_size + assert tabnet.output_shape[-1] == output_size + assert tabnet.input_dtype == torch.float32 + + # check for parameter updates + target = torch.randn([batch_size, 1]) + fpc, tpc, upc, not_updated = check_module_parameters_updated(tabnet, (input_tensor,), target) + + if batch_size == 1: + # for single record batches, batchnorm layer is bypassed, only a subset of parameters are updated + assert upc == 17, ( + f"Updated parameter count not expected value. Parameters not updated: {not_updated}" + f"\nModule structure:\n{tabnet}" + ) + else: + # update count should equal trainable number of parameters + assert tpc == upc, ( + f"All parameter not updated. Parameters not updated: {not_updated}\nModule structure:\n{tabnet}" + ) diff --git a/tests/ludwig/modules/test_utils.py b/tests/ludwig/modules/test_utils.py new file mode 100644 index 0000000..a369fd8 --- /dev/null +++ b/tests/ludwig/modules/test_utils.py @@ -0,0 +1,15 @@ +import torch + +from ludwig.utils.torch_utils import LudwigModule + + +def assert_output_shapes(module: LudwigModule, input_shape: tuple[int]): + """Runs a unit test to confirm that the out shape matches expected output. + + module: Module to be tested. + input_shape: List of integers of the expected input shape (w/o batch dim). + """ + + inputs = torch.rand(2, *input_shape, dtype=module.input_dtype) + output_tensor = module(inputs) + assert output_tensor.shape[1:] == module.output_shape diff --git a/tests/ludwig/schema/hyperopt/test_scheduler.py b/tests/ludwig/schema/hyperopt/test_scheduler.py new file mode 100644 index 0000000..a329cf1 --- /dev/null +++ b/tests/ludwig/schema/hyperopt/test_scheduler.py @@ -0,0 +1,39 @@ +import pytest + +from ludwig.schema.hyperopt.scheduler import BaseSchedulerConfig +from ludwig.schema.hyperopt.utils import register_scheduler_config, scheduler_config_registry +from ludwig.schema.utils import ProtectedString + + +@pytest.fixture( + params=[ # Tuples of SA name, dependency list, whether it should raise an exception + ("no_deps", None, False), + ("installed", [("ludwig", "ludwig")], False), + ("multiple_installed", [("ludwig", "ludwig"), ("marshmallow", "marshmallow")], False), + ("not_installed", [("fake_dependency", "fake_dependency")], True), + ("mixed_installed", [("fake_dependency", "fake_dependency"), ("ludwig", "ludwig")], True), + ] +) +def dependency_check_config(request): + key, deps, raises_exception = request.param + + @register_scheduler_config(key, dependencies=deps) + class DependencyCheckConfig(BaseSchedulerConfig): + type: str = ProtectedString(key) + + yield DependencyCheckConfig(), raises_exception + del scheduler_config_registry[key] + + +def test_dependency_check(dependency_check_config): + """Test that the hyperopt scheduler dependency check properly identifies missing dependencies. + + Some schedulers supported by Ray Tune have additional dependencies that may not be installed. The schema records + these dependencies and can be used to verify they are installed at run time. + """ + config, raises_exception = dependency_check_config + if raises_exception: + with pytest.raises(ImportError): + config.dependencies_installed() + else: + assert config.dependencies_installed() diff --git a/tests/ludwig/schema/hyperopt/test_search_algorithm.py b/tests/ludwig/schema/hyperopt/test_search_algorithm.py new file mode 100644 index 0000000..f70b406 --- /dev/null +++ b/tests/ludwig/schema/hyperopt/test_search_algorithm.py @@ -0,0 +1,39 @@ +import pytest + +from ludwig.schema.hyperopt.search_algorithm import BaseSearchAlgorithmConfig +from ludwig.schema.hyperopt.utils import register_search_algorithm_config, search_algorithm_config_registry +from ludwig.schema.utils import ProtectedString + + +@pytest.fixture( + params=[ # Tuples of SA name, dependency list, whether it should raise an exception + ("no_deps", None, False), + ("installed", [("ludwig", "ludwig")], False), + ("multiple_installed", [("ludwig", "ludwig"), ("marshmallow", "marshmallow")], False), + ("not_installed", [("fake_dependency", "fake_dependency")], True), + ("mixed_installed", [("fake_dependency", "fake_dependency"), ("ludwig", "ludwig")], True), + ] +) +def dependency_check_config(request): + key, deps, raises_exception = request.param + + @register_search_algorithm_config(key, dependencies=deps) + class DependencyCheckConfig(BaseSearchAlgorithmConfig): + type: str = ProtectedString(key) + + yield DependencyCheckConfig(), raises_exception + del search_algorithm_config_registry[key] + + +def test_dependency_check(dependency_check_config): + """Test that the hyperopt search alg dependency check properly identifies missing dependencies. + + Most search algorithms supported by Ray Tune have additional dependencies that may not be installed. The schema + records these dependencies and can be used to verify they are installed at run time. + """ + config, raises_exception = dependency_check_config + if raises_exception: + with pytest.raises(ImportError): + config.dependencies_installed() + else: + assert config.dependencies_installed() diff --git a/tests/ludwig/schema/test_model_config.py b/tests/ludwig/schema/test_model_config.py new file mode 100644 index 0000000..413530e --- /dev/null +++ b/tests/ludwig/schema/test_model_config.py @@ -0,0 +1,1066 @@ +import os +from tempfile import TemporaryDirectory +from typing import Any + +import pytest +import yaml + +from ludwig.constants import ( + ACTIVE, + BASE_MODEL, + CLIP, + COLUMN, + COMBINER, + DECODER, + DEFAULT_VALIDATION_METRIC, + DEFAULTS, + DEPENDENCIES, + ENCODER, + HYPEROPT, + INPUT_FEATURES, + INPUT_SIZE, + LOSS, + MODEL_ECD, + MODEL_LLM, + MODEL_TYPE, + NAME, + NUM_CLASSES, + OPTIMIZER, + OUTPUT_FEATURES, + PREPROCESSING, + PROC_COLUMN, + REDUCE_DEPENDENCIES, + REDUCE_INPUT, + TIED, + TRAINER, + TYPE, +) +from ludwig.error import ConfigValidationError +from ludwig.schema.decoders.base import ClassifierConfig +from ludwig.schema.encoders.text_encoders import BERTConfig +from ludwig.schema.features.augmentation.image import RandomBlurConfig, RandomRotateConfig +from ludwig.schema.features.image_feature import AUGMENTATION_DEFAULT_OPERATIONS +from ludwig.schema.features.number_feature import NumberOutputFeatureConfig +from ludwig.schema.features.text_feature import TextOutputFeatureConfig +from ludwig.schema.llms.quantization import QuantizationConfig +from ludwig.schema.model_config import ModelConfig +from ludwig.schema.utils import convert_submodules, LudwigBaseConfig + +config_sections = {INPUT_FEATURES, OUTPUT_FEATURES, PREPROCESSING, TRAINER, COMBINER, DEFAULTS, HYPEROPT} + + +def test_config_object(): + config = { + "input_features": [ + { + "name": "text_feature", + "type": "text", + "preprocessing": { + "missing_value_strategy": "drop_row", + }, + "encoder": { + "type": "rnn", + "bidirectional": True, + "representation": "dense", + "num_layers": 2, + }, + }, + { + "name": "image_feature_1", + "type": "image", + "preprocessing": { + "height": 32, + "width": 32, + "num_channels": 4, + }, + "encoder": { + "type": "stacked_cnn", + "num_channels": 4, + "dropout": 0.1, + }, + }, + ], + "output_features": [ + { + "name": "category_feature", + "type": "category", + "top_k": 3, + "preprocessing": { + "missing_value_strategy": "bfill", + }, + "decoder": { + "type": "classifier", + "num_classes": 10, + "use_bias": False, + }, + }, + ], + "combiner": { + "type": "concat", + "output_size": 512, + "weights_initializer": "xavier_uniform", + "dropout": 0.2, + }, + "trainer": { + "epochs": 50, + "batch_size": "auto", + "optimizer": { + "type": "adam", + "betas": [0.8, 0.999], + "eps": 5e-09, + }, + }, + } + + config_object = ModelConfig.from_dict(config) + assert config_object.input_features.text_feature.encoder.type == "rnn" + assert config_object.input_features.text_feature.encoder.num_layers == 2 + assert config_object.input_features.text_feature.preprocessing.missing_value_strategy == "drop_row" + + assert config_object.defaults.text.encoder.type != "rnn" + assert config_object.defaults.text.preprocessing.missing_value_strategy != "drop_row" + + assert config_object.output_features.category_feature.decoder.num_classes == 10 + assert config_object.output_features.category_feature.top_k == 3 + + assert config_object.combiner.output_size == 512 + assert config_object.combiner.weights_initializer == "xavier_uniform" + assert config_object.combiner.fc_layers is None + + assert config_object.trainer.epochs == 50 + assert config_object.trainer.batch_size == "auto" + + assert config_object.trainer.optimizer.type == "adam" + assert config_object.trainer.optimizer.betas[0] == 0.8 + assert config_object.trainer.optimizer.betas[1] == 0.999 + assert config_object.trainer.optimizer.eps == 5e-09 + + +def test_config_object_defaults(): + config = { + "input_features": [ + {"name": "number_feature", "type": "number"}, + { + "name": "text_feature_1", + "type": "text", + "encoder": { + "type": "rnn", + "activation": "sigmoid", + }, + }, + { + "name": "text_feature_2", + "type": "text", + }, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + "defaults": { + "number": {"preprocessing": {"missing_value_strategy": "drop_row"}, "encoder": {"type": "dense"}}, + "text": { + "preprocessing": { + "missing_value_strategy": "drop_row", + }, + "encoder": { + "type": "stacked_parallel_cnn", + "activation": "tanh", + }, + }, + }, + } + + config_object = ModelConfig.from_dict(config) + assert config_object.input_features.number_feature.preprocessing.missing_value_strategy == "drop_row" + assert config_object.input_features.number_feature.encoder.type == "dense" + + assert config_object.input_features.text_feature_1.encoder.type == "rnn" + assert config_object.input_features.text_feature_1.encoder.activation == "sigmoid" + assert config_object.input_features.text_feature_1.preprocessing.missing_value_strategy == "drop_row" + + assert config_object.input_features.text_feature_2.encoder.type == "stacked_parallel_cnn" + assert config_object.input_features.text_feature_2.encoder.activation == "tanh" + assert config_object.input_features.text_feature_2.preprocessing.missing_value_strategy == "drop_row" + + +def test_config_object_to_config_dict(): + config = { + "input_features": [ + {"name": "number_feature", "type": "number"}, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + config_object = ModelConfig.from_dict(config) + config_dict = config_object.to_dict() + + for section in config_sections: + assert section in config_dict + assert len(config_dict[DEFAULTS]) == 13 + assert set(config_dict[INPUT_FEATURES][0].keys()) == { + NAME, + ACTIVE, + TYPE, + COLUMN, + PROC_COLUMN, + TIED, + PREPROCESSING, + ENCODER, + } + assert set(config_dict[OUTPUT_FEATURES][0].keys()) == { + NAME, + ACTIVE, + TYPE, + COLUMN, + PROC_COLUMN, + PREPROCESSING, + DECODER, + LOSS, + REDUCE_INPUT, + DEPENDENCIES, + INPUT_SIZE, + CLIP, + REDUCE_DEPENDENCIES, + NUM_CLASSES, + DEFAULT_VALIDATION_METRIC, + } + + +def test_update_config_object(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + config_object = ModelConfig.from_dict(config) + + assert config_object.input_features.text_feature.encoder.type == "parallel_cnn" + assert config_object.input_features.text_feature.encoder.max_sequence_length is None + + temp_config = { + "input_features": [ + {"name": "text_feature", "type": "text", "encoder": {"type": "parallel_cnn", "max_sequence_length": 10}}, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + config_object = ModelConfig.from_dict(temp_config) + + assert config_object.input_features.text_feature.encoder.max_sequence_length == 10 + + +@pytest.mark.parametrize("model_type", [MODEL_ECD]) +def test_config_object_validation_parameters_defaults(model_type: str): + config = { + "input_features": [ + {"name": "category_feature", "type": "category"}, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + "model_type": model_type, + } + + config_object = ModelConfig.from_dict(config) + + assert config_object.trainer.validation_field == "number_output_feature" + assert config_object.trainer.validation_metric == NumberOutputFeatureConfig.default_validation_metric + + +def test_config_object_validation_parameters_multiple_output_features(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [ + { + "name": "text_output_feature", + "type": "text", + }, + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + config_object = ModelConfig.from_dict(config) + + assert config_object.trainer.validation_field == "text_output_feature" + assert config_object.trainer.validation_metric == TextOutputFeatureConfig.default_validation_metric + + # swap features + tmp = config["output_features"][0] + config["output_features"][0] = config["output_features"][1] + config["output_features"][1] = tmp + + config_object = ModelConfig.from_dict(config) + + assert config_object.trainer.validation_field == "number_output_feature" + assert config_object.trainer.validation_metric == NumberOutputFeatureConfig.default_validation_metric + + +def test_config_object_validation_parameters_explicitly_set_validation_field(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [ + { + "name": "text_output_feature", + "type": "text", + }, + { + "name": "number_output_feature", + "type": "number", + }, + ], + "trainer": { + "validation_field": "combined", + }, + } + + config_object = ModelConfig.from_dict(config) + + assert config_object.trainer.validation_field == "combined" + assert config_object.trainer.validation_metric == "loss" + + +def test_config_object_validation_parameters_explicitly_set_validation_metric(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [ + { + "name": "text_output_feature", + "type": "text", + }, + { + "name": "number_output_feature", + "type": "number", + }, + ], + "trainer": { + "validation_metric": NumberOutputFeatureConfig.default_validation_metric, + }, + } + + config_object = ModelConfig.from_dict(config) + + # We find the output feature that the validation_metric corresponds to. + assert config_object.trainer.validation_field == "number_output_feature" + assert config_object.trainer.validation_metric == NumberOutputFeatureConfig.default_validation_metric + + +def test_config_object_validation_parameters_invalid_metric(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [ + { + "name": "text_output_feature", + "type": "text", + }, + ], + "trainer": { + "validation_metric": NumberOutputFeatureConfig.default_validation_metric, + }, + } + + with pytest.raises(Exception): + ModelConfig.from_dict(config) + + +def test_config_object_validation_parameters_metric_conflict(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [ + { + "name": "number_output_feature1", + "type": "number", + }, + { + "name": "number_output_feature2", + "type": "number", + }, + ], + "trainer": { + "validation_metric": NumberOutputFeatureConfig.default_validation_metric, + }, + } + + with pytest.raises(Exception): + ModelConfig.from_dict(config) + + +def test_constructors_yaml(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text", "encoder": {"type": "parallel_cnn", "max_sequence_length": 10}}, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + with TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, "test.yaml") + with open(file_path, "w") as file: + yaml.dump(config, file) + + config_obj = ModelConfig.from_yaml(file_path) + + for section in config_sections: + assert hasattr(config_obj, section) + + +def test_constructors_dict(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text", "encoder": {"type": "parallel_cnn", "max_sequence_length": 10}}, + ], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + config_obj = ModelConfig.from_dict(config) + + for section in config_sections: + assert hasattr(config_obj, section) + + +def test_feature_enabling_disabling(): + config = { + "input_features": [{"name": "text_feature", "type": "text"}, {"name": "category_feature", "type": "number"}], + "output_features": [ + { + "name": "number_output_feature", + "type": "number", + }, + ], + } + + config_obj = ModelConfig.from_dict(config) + + assert config_obj.input_features.text_feature.active + assert config_obj.input_features.category_feature.active + + config_obj.input_features.text_feature.disable() + + assert not config_obj.input_features.text_feature.active + + +def test_sequence_combiner(): + config = { + "input_features": [{"name": "text_feature", "type": "text"}], + "output_features": [{"name": "number_output_feature", "type": "number"}], + "combiner": {"type": "sequence", "encoder": {"type": "rnn"}}, + } + + config_obj = ModelConfig.from_dict(config) + + assert config_obj.combiner.type == "sequence" + assert config_obj.combiner.encoder.type == "rnn" + assert config_obj.combiner.encoder.cell_type == "rnn" + + +@pytest.mark.parametrize( + "session", + [ + {"sess_id": 0, "encoder": "parallel_cnn", "loss": {"type": "mean_squared_error"}}, + {"sess_id": 1, "encoder": "cnnrnn", "loss": {"type": "mean_absolute_error"}}, + {"sess_id": 2, "encoder": "parallel_cnn", "loss": {"type": "mean_absolute_error"}}, + ], +) +def test_shared_state(session): + config = { + "input_features": [ + {"name": "text_feature", "type": "text", "encoder": {"type": session["encoder"]}}, + {"name": "text_feature_2", "type": "text"}, + ], + "output_features": [ + {"name": "number_output_feature", "type": "number"}, + {"name": "category_feature", "type": "category", "preprocessing": {"missing_value_strategy": "bfill"}}, + ], + "defaults": {"text": {"encoder": {"type": session["encoder"]}}}, + } + + if session["sess_id"] == 1: + del config[OUTPUT_FEATURES][1]["preprocessing"] + + if session["sess_id"] == 2: + del config[INPUT_FEATURES][0]["encoder"] + del config[DEFAULTS] + + config_obj = ModelConfig.from_dict(config) + + if session["sess_id"] == 0: + config_obj.input_features.text_feature.encoder.max_sequence_length = 10 + config_obj.input_features.text_feature.tied = "text_feature_2" + + assert config_obj.defaults.text.encoder.max_sequence_length is None # Test no link w/ defaults config + assert config_obj.input_features.text_feature.tied == "text_feature_2" # Test tied set as expected + + if session["sess_id"] == 1: + config_obj.output_features.number_output_feature.loss.weight = 2.0 + + # Test previous edits to config don't carry over + assert config_obj.output_features.category_feature.preprocessing.missing_value_strategy == "drop_row" + assert config_obj.defaults.text.encoder.max_sequence_length is None # Test no link w/ previous encoder config + assert config_obj.input_features.text_feature.tied is None # Test no link w/ previous text feature config + assert config_obj.output_features.number_output_feature.loss.weight == 2.0 # Test loss weight set as expected + + if session["sess_id"] == 2: + assert config_obj.input_features.text_feature.encoder.type == "parallel_cnn" + assert config_obj.output_features.number_output_feature.loss.weight == 1.0 # Test no link previous loss config + assert config_obj.defaults.text.encoder.max_sequence_length is None # Test no link w/ first encoder config + assert config_obj.input_features.text_feature.tied is None # Test no link w/ first tied setting + + +def test_convert_submodules(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [{"name": "number_output_feature", "type": "number"}], + } + + config_obj = ModelConfig.from_dict(config) + trainer = convert_submodules(config_obj.trainer.__dict__) + input_features = config_obj.input_features.to_list() + + assert not isinstance(trainer[OPTIMIZER], LudwigBaseConfig) + assert not isinstance(input_features[0][PREPROCESSING], LudwigBaseConfig) + + +def test_defaults_mixins(): + config = { + "input_features": [ + {"name": "text_feature", "type": "text"}, + ], + "output_features": [{"name": "number_output_feature", "type": "number"}], + } + + config_obj = ModelConfig.from_dict(config) + + assert config_obj.defaults.audio.to_dict().keys() == {ENCODER, PREPROCESSING} + assert config_obj.defaults.category.to_dict().keys() == {ENCODER, PREPROCESSING, DECODER, LOSS} + + +def test_initializer_recursion(): + config = { + "input_features": [ + { + "name": "category_B9834", + "type": "category", + "encoder": { + "type": "dense", + "vocab_size": 2, + "embedding_size": 5, + }, + "reduce_input": "sum", + "column": "category_B9834", + "proc_column": "category_B9834_mZFLky", + }, + { + "name": "number_0F633", + "type": "number", + "encoder": { + "type": "dense", + "norm": "batch", + "norm_params": {"momentum": 0.2}, + }, + }, + ], + "output_features": [ + { + "name": "binary_52912", + "type": "binary", + "column": "binary_52912", + "proc_column": "binary_52912_mZFLky", + } + ], + "combiner": {"type": "concat", "weights_initializer": {"type": "normal", "stddev": 0}}, + } + + config_obj = ModelConfig.from_dict(config) + + assert isinstance(config_obj.combiner.weights_initializer, dict) + + +def test_number_feature_zscore_preprocessing_default(): + """Tests that the default value for the number feature preprocessing is 'zscore'.""" + config = { + "input_features": [ + { + "name": "number_input_feature1", + "type": "number", + }, + ], + "output_features": [ + { + "name": "number_output_feature1", + "type": "number", + }, + ], + } + + config_obj = ModelConfig.from_dict(config) + + assert config_obj.input_features.number_input_feature1.preprocessing.normalization == "zscore" + + +@pytest.mark.parametrize( + "augmentation,expected", + [ + (None, []), + (False, []), + (True, AUGMENTATION_DEFAULT_OPERATIONS), + ( + [{"type": "random_blur"}, {"type": "random_rotate", "degree": 30}], + [RandomBlurConfig(), RandomRotateConfig(degree=30)], + ), + ], +) +def test_augmentation_pipeline(augmentation, expected): + """Tests that augmentation pipeline is correctly deserialized and serialized between config.""" + config = { + "input_features": [ + { + "name": "input1", + "type": "image", + "augmentation": augmentation, + }, + ], + "output_features": [ + { + "name": "output1", + "type": "number", + }, + ], + } + + if augmentation is None: + del config["input_features"][0]["augmentation"] + + config_obj = ModelConfig.from_dict(config) + assert config_obj.input_features[0].augmentation == expected + + # Test serialized dict form is fully rendered + config_dict = config_obj.to_dict() + assert len(config_dict["input_features"][0]["augmentation"]) == len(expected) + for aug in config_dict["input_features"][0]["augmentation"]: + assert isinstance(aug, dict) + + # Test the serializing and reloading yields the same results + config_obj2 = ModelConfig.from_dict(config_dict) + assert config_obj2.input_features[0].augmentation == config_obj.input_features[0].augmentation + + +@pytest.mark.parametrize( + "sequence_length, max_sequence_length, max_sequence_length_expected", + [ + (None, 100, 100), + (50, 100, 100), + (100, 50, 100), + ], +) +def test_preprocessing_max_sequence_length(sequence_length, max_sequence_length, max_sequence_length_expected): + config = { + "input_features": [ + { + "name": "text1", + "type": "text", + "preprocessing": { + "sequence_length": sequence_length, + "max_sequence_length": max_sequence_length, + }, + }, + { + "name": "sequence1", + "type": "sequence", + "preprocessing": { + "sequence_length": sequence_length, + "max_sequence_length": max_sequence_length, + }, + }, + ], + "output_features": [ + { + "name": "number1", + "type": "number", + }, + ], + } + config_obj = ModelConfig.from_dict(config) + assert config_obj.input_features[0].preprocessing.max_sequence_length == max_sequence_length_expected + assert config_obj.input_features[1].preprocessing.max_sequence_length == max_sequence_length_expected + + +def test_encoder_decoder_values_as_str(): + """Tests that encoder / decoder params provided as strings are properly converted to the correct type.""" + config = { + "input_features": [ + {"name": "text_input", "type": "text", "encoder": "bert"}, + ], + "output_features": [{"name": "cat_output", "type": "category", "decoder": "classifier"}], + } + + config_obj = ModelConfig.from_dict(config) + + assert isinstance(config_obj.input_features[0].encoder, BERTConfig) + assert isinstance(config_obj.output_features[0].decoder, ClassifierConfig) + + +@pytest.mark.parametrize( + "base_model_config,model_name", + [ + ("bloomz-3b", "bigscience/bloomz-3b"), + ("vicuna-7b", "lmsys/vicuna-7b-v1.3"), + ("huggyllama/llama-7b", "huggyllama/llama-7b"), + ], +) +def test_llm_base_model_config(base_model_config, model_name): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: base_model_config, + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + } + + config_obj = ModelConfig.from_dict(config) + + assert config_obj.base_model == model_name + + +@pytest.mark.parametrize( + "base_model_config", + [ + None, + "invalid/model/name", + ], +) +def test_llm_base_model_config_error(base_model_config): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: base_model_config, + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +@pytest.mark.parametrize( + "bits,expected_qconfig", + [ + (None, None), + (4, QuantizationConfig(bits=4)), + (8, QuantizationConfig(bits=8)), + ], +) +def test_llm_quantization_config(bits: int | None, expected_qconfig: QuantizationConfig | None): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "bigscience/bloomz-3b", + "quantization": {"bits": bits}, + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + } + + if bits is None: + del config["quantization"] + + config_obj = ModelConfig.from_dict(config) + + assert config_obj.quantization == expected_qconfig + + +@pytest.mark.parametrize( + "rope_scaling_config", + [ + ({"type": "linear"}), + ({"factor": 2.0}), + ({"type": "linear", "factor": 1.0}), + ], +) +def test_llm_rope_scaling_failure_modes( + rope_scaling_config: None | dict[str, Any], +): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + "model_parameters": { + "rope_scaling": rope_scaling_config, + }, + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + +def test_llm_model_parameters_no_rope_scaling(): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + "model_parameters": {}, + } + + config_obj = ModelConfig.from_dict(config) + assert config_obj.model_parameters.rope_scaling is None + assert config_obj.model_parameters.to_dict() == {} + + +def test_llm_finetuning_output_feature_config(): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "category_output", TYPE: "category"}], + "trainer": { + "type": "finetune", + }, + } + + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + config[OUTPUT_FEATURES] = [{NAME: "text_output", TYPE: "text"}] + ModelConfig.from_dict(config) + + +class TestTorchaoQuantization: + """Schema tests for the torchao quantization backend (PTQ and QAT).""" + + def _llm_base(self) -> dict: + # Use default (non-finetune) trainer so the adapter-required-with-quantization check + # doesn't fire — we're testing the quantization schema surface, not the finetune path. + return { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + "adapter": {"type": "lora", "r": 4}, + "trainer": {"type": "finetune"}, + } + + def test_bnb_backend_backcompat(self): + cfg = ModelConfig.from_dict({**self._llm_base(), "quantization": {"bits": 4}}) + assert cfg.quantization.backend == "bitsandbytes" + assert cfg.quantization.bits == 4 + assert cfg.quantization.mode is None + assert cfg.quantization.qat is False + + def test_torchao_ptq(self): + cfg = ModelConfig.from_dict( + {**self._llm_base(), "quantization": {"backend": "torchao", "mode": "int4_weight_only"}} + ) + assert cfg.quantization.backend == "torchao" + assert cfg.quantization.mode == "int4_weight_only" + assert cfg.quantization.qat is False + + def test_torchao_qat(self): + cfg = ModelConfig.from_dict( + { + **self._llm_base(), + "quantization": {"backend": "torchao", "mode": "int8_weight_only", "qat": True}, + } + ) + assert cfg.quantization.qat is True + assert cfg.quantization.mode == "int8_weight_only" + + def test_torchao_requires_mode(self): + with pytest.raises(ConfigValidationError, match="`quantization.mode` is required"): + ModelConfig.from_dict({**self._llm_base(), "quantization": {"backend": "torchao"}}) + + def test_bnb_rejects_mode(self): + with pytest.raises(ConfigValidationError, match="only supported for `backend: torchao`"): + ModelConfig.from_dict( + {**self._llm_base(), "quantization": {"backend": "bitsandbytes", "mode": "int4_weight_only"}} + ) + + def test_bnb_rejects_qat(self): + with pytest.raises(ConfigValidationError, match="`quantization.qat: true`"): + ModelConfig.from_dict({**self._llm_base(), "quantization": {"backend": "bitsandbytes", "qat": True}}) + + @pytest.mark.parametrize("mode", ["int4_weight_only", "int8_weight_only", "int8_dynamic", "float8"]) + def test_all_torchao_modes_parse(self, mode): + cfg = ModelConfig.from_dict({**self._llm_base(), "quantization": {"backend": "torchao", "mode": mode}}) + assert cfg.quantization.mode == mode + + +class TestTorchaoQuantizationUtil: + """Unit tests for ludwig.utils.quantization helpers (no torchao install required).""" + + def test_validate_mode_accepts_known(self): + from ludwig.utils.quantization import _VALID_MODES, _validate_mode + + for mode in _VALID_MODES: + _validate_mode(mode) # should not raise + + def test_validate_mode_rejects_unknown(self): + from ludwig.utils.quantization import _validate_mode + + with pytest.raises(ValueError, match="Unknown quantization mode"): + _validate_mode("bogus") + + def test_qat_bit_width_int4(self): + from ludwig.utils.quantization import _qat_bit_width + + assert _qat_bit_width("int4_weight_only") == 4 + + def test_qat_bit_width_int8(self): + from ludwig.utils.quantization import _qat_bit_width + + assert _qat_bit_width("int8_weight_only") == 8 + assert _qat_bit_width("int8_dynamic") == 8 + + def test_qat_rejects_float8(self): + from ludwig.utils.quantization import _qat_bit_width + + with pytest.raises(ValueError, match="QAT is not supported"): + _qat_bit_width("float8") + + def test_quantize_model_rejects_unknown_mode(self): + # Don't need torchao installed — _validate_mode fires before the import. + from ludwig.utils.quantization import quantize_model + + with pytest.raises(ValueError, match="Unknown quantization mode"): + quantize_model(object(), "bogus") + + +@pytest.mark.skip( + reason="TODO(geoffrey, arnav): re-enable this when we have reconciled the config with the backend kwarg in api.py" +) +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_llm_quantization_backend_compatibility(): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + "quantization": {"bits": 4}, + } + + # Backend not set - defaults to local backend + ModelConfig.from_dict(config) + + # Backend explicitly set to local backend + config["backend"] = {"type": "local"} + ModelConfig.from_dict(config) + + # Backend explicitly set to Ray backend + config["backend"] = {"type": "ray"} + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + # Start local ray process + import ray + + ray.init() + + # Backend not set, but local Ray process is running + config.pop("backend") + with pytest.raises(ConfigValidationError): + ModelConfig.from_dict(config) + + ray.shutdown() + + +class TestMaxNewTokensOverride: + def test_max_new_tokens_override_no_changes_to_max_new_tokens(self): + """Tests that the default value for max_new_tokens is respected when explicitly set in the config.""" + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + # Default value for generation.max_sequence_length is 32 + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + "generation": {"max_new_tokens": 64}, + } + + config_obj = ModelConfig.from_dict(config) + assert config_obj.generation.max_new_tokens == 64 + + def test_max_new_tokens_override_large_max_sequence_length(self): + """Tests that the default value for max_new_tokens is overridden when max_sequence_length is set to a large + value than the default max_new_tokens.""" + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + # Default value for generation.max_sequence_length is 32 + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text", "preprocessing": {"max_sequence_length": 100}}], + } + + config_obj = ModelConfig.from_dict(config) + assert config_obj.generation.max_new_tokens == 100 + + def test_max_new_tokens_override_large_global_max_sequence_length(self): + """Tests that the default value for max_new_tokens is overridden when global_max_sequence_length is set to + a larger value than the default max_new_tokens.""" + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + # Default value for generation.max_sequence_length is 32 + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + PREPROCESSING: {"global_max_sequence_length": 100}, + } + + config_obj = ModelConfig.from_dict(config) + assert config_obj.generation.max_new_tokens == 100 + + def test_max_new_tokens_override_fallback_to_model_window_size(self): + config = { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{NAME: "text_input", TYPE: "text"}], + # Default value for generation.max_sequence_length is 32 + OUTPUT_FEATURES: [{NAME: "text_output", TYPE: "text"}], + } + + config_obj = ModelConfig.from_dict(config) + # Base model context length is 2048 tokens by default + # Since we fallback to setting max_new_tokens to the model context length / 2, we expect it to be 1024 tokens + assert config_obj.generation.max_new_tokens == 1024 diff --git a/tests/ludwig/schema/test_peft_adapters.py b/tests/ludwig/schema/test_peft_adapters.py new file mode 100644 index 0000000..e1792cc --- /dev/null +++ b/tests/ludwig/schema/test_peft_adapters.py @@ -0,0 +1,534 @@ +"""Tests for expanded PEFT adapter support.""" + +import pytest + +from ludwig.schema.llms.peft import adapter_registry + +_ALL_ADAPTERS = [ + "lora", + "adalora", + "ia3", + "vera", + "loha", + "lokr", + "fourierft", + "boft", + "tinylora", + "c3a", + "oft", + "hra", + "waveft", + "ln_tuning", + "vblora", +] +_ADAPTERS_WITH_TARGET_MODULES = [ + "vera", + "loha", + "lokr", + "fourierft", + "boft", + "tinylora", + "c3a", + "oft", + "hra", + "waveft", + "ln_tuning", + "vblora", +] + + +class TestAdapterRegistry: + def test_all_adapters_registered(self): + expected = set(_ALL_ADAPTERS) + assert expected.issubset(set(adapter_registry.keys())) + + @pytest.mark.parametrize("adapter_type", _ALL_ADAPTERS) + def test_adapter_creates_valid_peft_config(self, adapter_type): + cls = adapter_registry[adapter_type] + inst = cls.model_validate({"type": adapter_type}) + peft_config = inst.to_config() + assert peft_config is not None + + @pytest.mark.parametrize("adapter_type", _ADAPTERS_WITH_TARGET_MODULES) + def test_new_adapter_has_target_modules(self, adapter_type): + cls = adapter_registry[adapter_type] + inst = cls.model_validate({"type": adapter_type}) + assert hasattr(inst, "target_modules") + + +class TestLoraPlus: + def test_loraplus_lr_ratio_default_none(self): + cls = adapter_registry["lora"] + inst = cls.model_validate({"type": "lora"}) + assert inst.loraplus_lr_ratio is None + + def test_loraplus_lr_ratio_set(self): + cls = adapter_registry["lora"] + inst = cls.model_validate({"type": "lora", "loraplus_lr_ratio": 8.0}) + assert inst.loraplus_lr_ratio == 8.0 + + def test_loraplus_param_groups(self): + import torch.nn as nn + + from ludwig.modules.optimization_modules import _create_loraplus_param_groups + + class MockModel(nn.Module): + def __init__(self): + super().__init__() + self.base = nn.Linear(10, 10) + self.lora_A = nn.Linear(10, 4, bias=False) + self.lora_B = nn.Linear(4, 10, bias=False) + + model = MockModel() + groups = _create_loraplus_param_groups(model, {"lr": 0.001}, loraplus_lr_ratio=8.0) + + assert len(groups) == 3 + lrs = sorted(g["lr"] for g in groups) + assert lrs == [0.001, 0.001, 0.008] + + +class TestLoraInitializers: + """Tests for PiSSA, EVA, CorDA, LoftQ, and other init_lora_weights options.""" + + @pytest.mark.parametrize("init", ["default", "gaussian", "pissa", "olora", "orthogonal"]) + def test_init_lora_weights_string_options(self, init): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate({"type": "lora", "init_lora_weights": init}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + expected = True if init == "default" else init + assert peft_cfg.init_lora_weights == expected + + def test_pissa_init(self): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate({"type": "lora", "r": 4, "init_lora_weights": "pissa"}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.init_lora_weights == "pissa" + assert peft_cfg.r == 4 + + def test_eva_init_requires_eva_config(self): + from pydantic import ValidationError + + from ludwig.schema.llms.peft import LoraConfig + + with pytest.raises(ValidationError, match="eva_config"): + LoraConfig.model_validate({"type": "lora", "init_lora_weights": "eva"}) + + def test_eva_init_with_config(self): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate( + { + "type": "lora", + "init_lora_weights": "eva", + "eva_config": {"rho": 3.0, "tau": 0.95}, + } + ) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.init_lora_weights == "eva" + assert peft_cfg.eva_config is not None + assert peft_cfg.eva_config.rho == 3.0 + + def test_loftq_init_requires_loftq_config(self): + from pydantic import ValidationError + + from ludwig.schema.llms.peft import LoraConfig + + with pytest.raises(ValidationError, match="loftq_config"): + LoraConfig.model_validate({"type": "lora", "init_lora_weights": "loftq"}) + + def test_loftq_init_with_config(self): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate( + { + "type": "lora", + "init_lora_weights": "loftq", + "loftq_config": {"loftq_bits": 4, "loftq_iter": 2}, + } + ) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.init_lora_weights == "loftq" + assert peft_cfg.loftq_config["loftq_bits"] == 4 + assert peft_cfg.loftq_config["loftq_iter"] == 2 + + def test_corda_init(self): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate({"type": "lora", "init_lora_weights": "corda"}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.init_lora_weights == "corda" + + def test_rank_pattern(self): + from ludwig.schema.llms.peft import LoraConfig + + pattern = {"model.layers.0.self_attn.q_proj": 4, "model.layers.0.self_attn.v_proj": 2} + cfg = LoraConfig.model_validate({"type": "lora", "r": 8, "rank_pattern": pattern}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.rank_pattern == pattern + + def test_alpha_pattern(self): + from ludwig.schema.llms.peft import LoraConfig + + pattern = {"model.layers.0.self_attn.q_proj": 16.0} + cfg = LoraConfig.model_validate({"type": "lora", "alpha_pattern": pattern}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.alpha_pattern == pattern + + def test_layer_replication(self): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate({"type": "lora", "layer_replication": [[0, 4], [2, 5]]}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.layer_replication == [(0, 4), (2, 5)] + + def test_default_rank_pattern_is_empty(self): + from ludwig.schema.llms.peft import LoraConfig + + cfg = LoraConfig.model_validate({"type": "lora"}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.rank_pattern == {} + assert peft_cfg.alpha_pattern == {} + + +class TestTinyLoraAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import TinyLoraAdapterConfig + + cfg = TinyLoraAdapterConfig.model_validate({"type": "tinylora"}) + assert cfg.r == 2 + assert cfg.u == 64 + assert cfg.weight_tying == 0.0 + + def test_custom_params(self): + from ludwig.schema.llms.peft import TinyLoraAdapterConfig + + cfg = TinyLoraAdapterConfig.model_validate({"type": "tinylora", "r": 4, "u": 16, "weight_tying": 0.5}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.r == 4 + assert peft_cfg.u == 16 + assert peft_cfg.weight_tying == 0.5 + + def test_name_and_description(self): + from ludwig.schema.llms.peft import TinyLoraAdapterConfig + + assert TinyLoraAdapterConfig.name() == "TinyLoRA" + assert "SVD" in TinyLoraAdapterConfig.description() + + +class TestC3AAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import C3AAdapterConfig + + cfg = C3AAdapterConfig.model_validate({"type": "c3a"}) + assert cfg.block_size == 256 + + def test_custom_block_size(self): + from ludwig.schema.llms.peft import C3AAdapterConfig + + cfg = C3AAdapterConfig.model_validate({"type": "c3a", "block_size": 128}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.block_size == 128 + + def test_name(self): + from ludwig.schema.llms.peft import C3AAdapterConfig + + assert C3AAdapterConfig.name() == "C3A" + + +class TestOFTAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import OFTAdapterConfig + + cfg = OFTAdapterConfig.model_validate({"type": "oft"}) + assert cfg.oft_block_size == 32 + assert not cfg.coft + + def test_coft_enabled(self): + from ludwig.schema.llms.peft import OFTAdapterConfig + + cfg = OFTAdapterConfig.model_validate({"type": "oft", "coft": True, "eps": 1e-4}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.coft is True + assert peft_cfg.eps == 1e-4 + + def test_name(self): + from ludwig.schema.llms.peft import OFTAdapterConfig + + assert OFTAdapterConfig.name() == "OFT" + + +class TestHRAAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import HRAAdapterConfig + + cfg = HRAAdapterConfig.model_validate({"type": "hra"}) + assert cfg.r == 8 + assert not cfg.apply_GS + + def test_gram_schmidt(self): + from ludwig.schema.llms.peft import HRAAdapterConfig + + cfg = HRAAdapterConfig.model_validate({"type": "hra", "r": 16, "apply_GS": True}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.r == 16 + assert peft_cfg.apply_GS is True + + def test_name(self): + from ludwig.schema.llms.peft import HRAAdapterConfig + + assert HRAAdapterConfig.name() == "HRA" + + +class TestWaveFTAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import WaveFTAdapterConfig + + cfg = WaveFTAdapterConfig.model_validate({"type": "waveft"}) + assert cfg.wavelet_family == "db1" + assert cfg.n_frequency == 2592 + + def test_custom_wavelet(self): + from ludwig.schema.llms.peft import WaveFTAdapterConfig + + cfg = WaveFTAdapterConfig.model_validate({"type": "waveft", "wavelet_family": "db2", "n_frequency": 512}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.wavelet_family == "db2" + assert peft_cfg.n_frequency == 512 + + def test_name(self): + from ludwig.schema.llms.peft import WaveFTAdapterConfig + + assert WaveFTAdapterConfig.name() == "WaveFT" + + +class TestLNTuningAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import LNTuningAdapterConfig + + cfg = LNTuningAdapterConfig.model_validate({"type": "ln_tuning"}) + assert cfg.target_modules is None + + def test_custom_target_modules(self): + from ludwig.schema.llms.peft import LNTuningAdapterConfig + + cfg = LNTuningAdapterConfig.model_validate({"type": "ln_tuning", "target_modules": ["norm1", "norm2"]}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.target_modules == ["norm1", "norm2"] + + def test_name(self): + from ludwig.schema.llms.peft import LNTuningAdapterConfig + + assert LNTuningAdapterConfig.name() == "LN-Tuning" + + +class TestVBLoRAAdapter: + def test_defaults(self): + from ludwig.schema.llms.peft import VBLoRAAdapterConfig + + cfg = VBLoRAAdapterConfig.model_validate({"type": "vblora"}) + assert cfg.r == 4 + assert cfg.num_vectors == 256 + assert cfg.topk == 2 + + def test_custom_params(self): + from ludwig.schema.llms.peft import VBLoRAAdapterConfig + + cfg = VBLoRAAdapterConfig.model_validate({"type": "vblora", "r": 8, "num_vectors": 128, "topk": 4}) + peft_cfg = cfg.to_config(task_type="CAUSAL_LM") + assert peft_cfg.r == 8 + assert peft_cfg.num_vectors == 128 + assert peft_cfg.topk == 4 + + def test_name(self): + from ludwig.schema.llms.peft import VBLoRAAdapterConfig + + assert VBLoRAAdapterConfig.name() == "VBLoRA" + + +class TestECDEncoderAdapter: + def test_adapter_field_exists(self): + from ludwig.schema.encoders.base import DenseEncoderConfig + + inst = DenseEncoderConfig.model_validate({"type": "dense"}) + assert inst.adapter is None + + def test_adapter_field_accepts_dict(self): + from ludwig.schema.encoders.base import DenseEncoderConfig + + inst = DenseEncoderConfig.model_validate({"type": "dense", "adapter": {"type": "lora", "r": 8}}) + assert inst.adapter == {"type": "lora", "r": 8} + + +class TestNamedAdapters: + """Unit tests for the multi-adapter schema (`adapters:` plural, mutually exclusive with `adapter:`).""" + + def _llm_base(self): + return { + "model_type": "llm", + "base_model": "sshleifer/tiny-gpt2", + "input_features": [{"name": "p", "type": "text"}], + "output_features": [{"name": "r", "type": "text"}], + "trainer": {"type": "finetune"}, + } + + def test_named_adapters_config_validates(self): + from ludwig.schema.llms.peft import NamedAdaptersConfig + + cfg = NamedAdaptersConfig.model_validate( + { + "adapters": {"a": {"type": "lora", "r": 8}, "b": {"type": "lora", "r": 16}}, + "active": "a", + } + ) + assert cfg.active == "a" + assert list(cfg.adapters.keys()) == ["a", "b"] + assert cfg.merge is None + + def test_merge_config_validates(self): + from ludwig.schema.llms.peft import NamedAdaptersConfig + + cfg = NamedAdaptersConfig.model_validate( + { + "adapters": {"a": {"type": "lora"}, "b": {"type": "lora"}}, + "merge": { + "name": "m", + "sources": ["a", "b"], + "weights": [0.7, 0.3], + "combination_type": "ties", + "density": 0.3, + }, + } + ) + assert cfg.merge.name == "m" + assert cfg.merge.combination_type == "ties" + assert cfg.merge.density == 0.3 + + def test_llm_config_accepts_plural(self): + from ludwig.schema.model_types.base import ModelConfig + + cfg = {**self._llm_base(), "adapters": {"adapters": {"a": {"type": "lora"}}}} + model_cfg = ModelConfig.from_dict(cfg) + assert model_cfg.adapter is None + assert model_cfg.adapters is not None + assert "a" in model_cfg.adapters.adapters + + def test_llm_config_accepts_singular(self): + from ludwig.schema.model_types.base import ModelConfig + + cfg = {**self._llm_base(), "adapter": {"type": "lora", "r": 8}} + model_cfg = ModelConfig.from_dict(cfg) + assert model_cfg.adapter is not None + assert model_cfg.adapters is None + + def test_both_adapter_and_adapters_rejected(self): + from ludwig.error import ConfigValidationError + from ludwig.schema.model_types.base import ModelConfig + + cfg = { + **self._llm_base(), + "adapter": {"type": "lora", "r": 8}, + "adapters": {"adapters": {"a": {"type": "lora"}}}, + } + with pytest.raises(ConfigValidationError, match="both `adapter:` and `adapters:`"): + ModelConfig.from_dict(cfg) + + def test_empty_adapters_rejected(self): + from ludwig.error import ConfigValidationError + from ludwig.schema.model_types.base import ModelConfig + + cfg = {**self._llm_base(), "adapters": {"adapters": {}}} + with pytest.raises(ConfigValidationError, match="at least one entry"): + ModelConfig.from_dict(cfg) + + def test_active_must_reference_known_adapter(self): + from ludwig.error import ConfigValidationError + from ludwig.schema.model_types.base import ModelConfig + + cfg = { + **self._llm_base(), + "adapters": {"adapters": {"a": {"type": "lora"}}, "active": "b"}, + } + with pytest.raises(ConfigValidationError, match="does not match any"): + ModelConfig.from_dict(cfg) + + def test_active_may_point_at_merged_adapter(self): + from ludwig.schema.model_types.base import ModelConfig + + cfg = { + **self._llm_base(), + "adapters": { + "adapters": {"a": {"type": "lora"}, "b": {"type": "lora"}}, + "active": "m", + "merge": {"name": "m", "sources": ["a", "b"], "combination_type": "linear"}, + }, + } + model_cfg = ModelConfig.from_dict(cfg) + assert model_cfg.adapters.active == "m" + + def test_merge_sources_must_exist(self): + from ludwig.error import ConfigValidationError + from ludwig.schema.model_types.base import ModelConfig + + cfg = { + **self._llm_base(), + "adapters": { + "adapters": {"a": {"type": "lora"}}, + "merge": {"name": "m", "sources": ["a", "ghost"]}, + }, + } + with pytest.raises(ConfigValidationError, match="unknown adapter names"): + ModelConfig.from_dict(cfg) + + def test_merge_weights_length_must_match_sources(self): + from ludwig.error import ConfigValidationError + from ludwig.schema.model_types.base import ModelConfig + + cfg = { + **self._llm_base(), + "adapters": { + "adapters": {"a": {"type": "lora"}, "b": {"type": "lora"}}, + "merge": {"name": "m", "sources": ["a", "b"], "weights": [0.5]}, + }, + } + with pytest.raises(ConfigValidationError, match="Lengths must match"): + ModelConfig.from_dict(cfg) + + def test_merge_name_cannot_collide_with_source(self): + from ludwig.error import ConfigValidationError + from ludwig.schema.model_types.base import ModelConfig + + cfg = { + **self._llm_base(), + "adapters": { + "adapters": {"a": {"type": "lora"}, "b": {"type": "lora"}}, + "merge": {"name": "a", "sources": ["a", "b"]}, + }, + } + with pytest.raises(ConfigValidationError, match="collides with an existing source"): + ModelConfig.from_dict(cfg) + + +class TestInitializeAdapterMulti: + """Unit tests for `_initialize_multi_adapters` (no base model download).""" + + def test_materialize_adapter_config_from_dict(self): + from ludwig.utils.llm_utils import _materialize_adapter_config + + cfg = _materialize_adapter_config({"type": "lora", "r": 8}) + assert hasattr(cfg, "to_config") + assert cfg.type == "lora" + assert cfg.r == 8 + + def test_materialize_adapter_config_unknown_type_raises(self): + from ludwig.utils.llm_utils import _materialize_adapter_config + + with pytest.raises(ValueError, match="Unknown adapter type"): + _materialize_adapter_config({"type": "definitely-not-a-real-adapter"}) + + def test_materialize_adapter_config_missing_type_raises(self): + from ludwig.utils.llm_utils import _materialize_adapter_config + + with pytest.raises(ValueError, match="missing required `type`"): + _materialize_adapter_config({"r": 8}) diff --git a/tests/ludwig/schema/test_phase_6_5_tabular.py b/tests/ludwig/schema/test_phase_6_5_tabular.py new file mode 100644 index 0000000..daf367a --- /dev/null +++ b/tests/ludwig/schema/test_phase_6_5_tabular.py @@ -0,0 +1,144 @@ +"""Phase 6.5 advanced tabular — schema + preset unit tests.""" + +from __future__ import annotations + +import pytest + +from ludwig.error import ConfigValidationError +from ludwig.presets import apply_preset, QUALITY_PRESETS + + +class TestRealMLPPreset: + def test_preset_registered(self): + assert "tabular_realmlp" in QUALITY_PRESETS + + def test_preset_sets_number_robust_scaling(self): + preset = QUALITY_PRESETS["tabular_realmlp"] + # 'iq' is Ludwig's interquartile-range normalizer, the closest match to the + # RobustScaler used by the RealMLP paper. + assert preset["defaults"]["number"]["preprocessing"]["normalization"] == "iq" + + def test_preset_uses_adamw_cosine(self): + trainer = QUALITY_PRESETS["tabular_realmlp"]["trainer"] + assert trainer["optimizer"]["type"] == "adamw" + assert trainer["learning_rate_scheduler"]["decay"] == "cosine" + + def test_user_config_overrides_preset(self): + """User config wins on any collision.""" + user_cfg = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + "trainer": {"epochs": 5}, + } + merged = apply_preset(user_cfg, "tabular_realmlp") + # User's 5 wins over preset's 300. + assert merged["trainer"]["epochs"] == 5 + # But the number-feature normalization is still set from the preset. + assert merged["defaults"]["number"]["preprocessing"]["normalization"] == "iq" + + +class TestRealMLPSchemaEnum: + """ECDModelConfig schema must advertise the new preset name.""" + + def test_preset_accepted_on_ecd(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + "model_type": "ecd", + "preset": "tabular_realmlp", + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + } + ) + assert cfg.preset == "tabular_realmlp" + + def test_unknown_preset_rejected(self): + from ludwig.schema.model_config import ModelConfig + + # Preset application runs before schema validation, so an unknown name surfaces as a + # plain ValueError from ludwig.presets.apply_preset; the schema enum would otherwise + # catch it as ConfigValidationError. Accept either — both are hard rejections. + with pytest.raises((ConfigValidationError, ValueError)): + ModelConfig.from_dict( + { + "model_type": "ecd", + "preset": "i_made_this_up", + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + } + ) + + +class TestTabPFNV2CombinerSchema: + """Schema fields for the TabPFN v2 combiner — no `tabpfn` package required here.""" + + def test_schema_registers_and_parses(self): + from ludwig.schema.combiners.tabpfn_v2 import TabPFNV2CombinerConfig + from ludwig.schema.combiners.utils import combiner_config_registry + + assert "tabpfn_v2" in combiner_config_registry + cfg = TabPFNV2CombinerConfig.model_validate( + { + "type": "tabpfn_v2", + "output_size": 256, + "n_estimators": 8, + "device": "cuda", + } + ) + assert cfg.type == "tabpfn_v2" + assert cfg.output_size == 256 + assert cfg.n_estimators == 8 + assert cfg.device == "cuda" + + def test_defaults(self): + from ludwig.schema.combiners.tabpfn_v2 import TabPFNV2CombinerConfig + + cfg = TabPFNV2CombinerConfig.model_validate({"type": "tabpfn_v2"}) + assert cfg.output_size == 128 + assert cfg.tabpfn_hidden_size == 512 + assert cfg.n_estimators == 4 + assert cfg.device == "auto" + + def test_device_enum_rejects_unknown(self): + from ludwig.schema.combiners.tabpfn_v2 import TabPFNV2CombinerConfig + + with pytest.raises(Exception): # pydantic ValidationError + TabPFNV2CombinerConfig.model_validate({"type": "tabpfn_v2", "device": "tpu"}) + + def test_full_ecd_config_with_tabpfn_v2(self): + from ludwig.schema.model_config import ModelConfig + + cfg = ModelConfig.from_dict( + { + "model_type": "ecd", + "input_features": [ + {"name": "a", "type": "number"}, + {"name": "b", "type": "number"}, + ], + "output_features": [{"name": "y", "type": "binary"}], + "combiner": {"type": "tabpfn_v2", "output_size": 64, "n_estimators": 2}, + } + ) + assert cfg.combiner.type == "tabpfn_v2" + assert cfg.combiner.output_size == 64 + assert cfg.combiner.n_estimators == 2 + + +class TestTabPFNV2CombinerInit: + """The combiner class raises a clear error when the optional ``tabpfn`` package is missing. + + When the dep is installed the import succeeds and instantiation wires the projection head. + """ + + def test_import_error_message_cites_pip_install(self): + try: + import tabpfn # noqa: F401 + except ImportError: + from ludwig.combiners.tabpfn_v2_combiner import TabPFNV2Combiner + from ludwig.schema.combiners.tabpfn_v2 import TabPFNV2CombinerConfig + + with pytest.raises(ImportError, match="pip install tabpfn"): + TabPFNV2Combiner(config=TabPFNV2CombinerConfig()) + else: + pytest.skip("tabpfn is installed; skipping missing-package error-message test") diff --git a/tests/ludwig/schema/test_schema_utils.py b/tests/ludwig/schema/test_schema_utils.py new file mode 100644 index 0000000..1e605fc --- /dev/null +++ b/tests/ludwig/schema/test_schema_utils.py @@ -0,0 +1,9 @@ +from ludwig.constants import TYPE +from ludwig.schema import utils as schema_utils + + +def test_remove_duplicate_fields(): + props = {TYPE: "random", "probabilities": [0.7, 0.1, 0.2]} + schema_utils.remove_duplicate_fields(props, [TYPE]) + assert TYPE not in props + assert "probabilities" in props diff --git a/tests/ludwig/schema/test_timeseries_encoders.py b/tests/ludwig/schema/test_timeseries_encoders.py new file mode 100644 index 0000000..0d18028 --- /dev/null +++ b/tests/ludwig/schema/test_timeseries_encoders.py @@ -0,0 +1,118 @@ +"""Tests for timeseries-specific encoders (PatchTST, N-BEATS) and metrics (MASE, sMAPE).""" + +import torch + + +class TestPatchTSTEncoder: + def test_forward_1d(self): + from ludwig.encoders.timeseries_encoders import PatchTSTEncoder + + enc = PatchTSTEncoder( + max_sequence_length=64, + patch_size=8, + patch_stride=4, + d_model=32, + num_heads=4, + num_layers=1, + ffn_dim=64, + output_size=16, + ) + x = torch.randn(4, 64) + out = enc(x) + assert out["encoder_output"].shape == (4, 16) + + def test_forward_multichannel(self): + from ludwig.encoders.timeseries_encoders import PatchTSTEncoder + + enc = PatchTSTEncoder( + max_sequence_length=64, + patch_size=8, + patch_stride=4, + d_model=32, + num_heads=4, + num_layers=1, + ffn_dim=64, + output_size=16, + ) + x = torch.randn(4, 64, 3) + out = enc(x) + assert out["encoder_output"].shape == (4, 16) + + def test_schema_roundtrip(self): + from ludwig.schema.encoders.timeseries_encoders import PatchTSTConfig + + cfg = PatchTSTConfig.model_validate({"type": "patchtst", "patch_size": 32, "num_layers": 2}) + assert cfg.patch_size == 32 + assert cfg.num_layers == 2 + + +class TestNBEATSEncoder: + def test_forward_1d(self): + from ludwig.encoders.timeseries_encoders import NBEATSEncoder + + enc = NBEATSEncoder( + max_sequence_length=64, + num_stacks=2, + num_blocks=2, + num_layers=2, + layer_size=64, + output_size=32, + ) + x = torch.randn(4, 64) + out = enc(x) + assert out["encoder_output"].shape == (4, 32) + + def test_forward_multichannel(self): + from ludwig.encoders.timeseries_encoders import NBEATSEncoder + + enc = NBEATSEncoder( + max_sequence_length=64, + num_stacks=2, + num_blocks=2, + num_layers=2, + layer_size=64, + output_size=32, + ) + x = torch.randn(4, 64, 3) + out = enc(x) + assert out["encoder_output"].shape == (4, 32) + + def test_schema_roundtrip(self): + from ludwig.schema.encoders.timeseries_encoders import NBEATSConfig + + cfg = NBEATSConfig.model_validate({"type": "nbeats", "num_stacks": 3}) + assert cfg.num_stacks == 3 + + +class TestTimeseriesMetrics: + def test_mase_metric(self): + from ludwig.constants import MEAN_ABSOLUTE_SCALED_ERROR + from ludwig.modules.metric_registry import get_metric_registry + + registry = get_metric_registry() + assert MEAN_ABSOLUTE_SCALED_ERROR in registry + + def test_smape_metric(self): + from ludwig.constants import SYMMETRIC_MEAN_ABSOLUTE_PERCENTAGE_ERROR + from ludwig.modules.metric_registry import get_metric_registry + + registry = get_metric_registry() + assert SYMMETRIC_MEAN_ABSOLUTE_PERCENTAGE_ERROR in registry + + def test_mase_forward(self): + from ludwig.modules.metric_modules import MASEMetric + + metric = MASEMetric() + preds = torch.tensor([[1.0, 2.0, 3.0, 4.0]]) + target = torch.tensor([[1.1, 2.1, 3.1, 4.1]]) + val = metric.get_current_value(preds, target) + assert val.item() > 0 + + def test_smape_forward(self): + from ludwig.modules.metric_modules import SMAPEMetric + + metric = SMAPEMetric() + preds = torch.tensor([[1.0, 2.0, 3.0]]) + target = torch.tensor([[1.0, 2.0, 3.0]]) + val = metric.get_current_value(preds, target) + assert val.item() < 1e-6 # perfect forecast -> 0 diff --git a/tests/ludwig/schema_fields/test_fields_misc.py b/tests/ludwig/schema_fields/test_fields_misc.py new file mode 100644 index 0000000..222f772 --- /dev/null +++ b/tests/ludwig/schema_fields/test_fields_misc.py @@ -0,0 +1,243 @@ +import pytest +from pydantic import ValidationError as PydanticValidationError + +from ludwig.config_validation.validation import get_validator, validate +from ludwig.schema import utils as schema_utils + + +def get_marshmallow_field_from_metadata(dfield): + """Helper method for extracting the marshmallow field from pydantic field metadata.""" + metadata = dfield.metadata + if isinstance(metadata, dict): + return metadata.get("marshmallow_field") + if isinstance(metadata, (list, tuple)): + for item in metadata: + if hasattr(item, "_deserialize"): + return item + return None + + +def test_metaclass_field_override_with_protectedstring(): + """Regression test: _LudwigModelMeta must preserve field annotations when a subclass overrides a base + class field with ProtectedString (or any FieldInfo default). + + On Python 3.14 the class namespace carries annotations in __annotate_func__ rather than + __annotations__, so naively reading namespace['__annotations__'] returns an empty dict. + The metaclass must evaluate __annotate_func__ when present so that the annotations are not + silently discarded, which would make pydantic raise + 'Field X requires a type annotation' at class definition time. + See https://github.com/ludwig-ai/ludwig/issues/4142 (Python 3.14 regression). + """ + + class Base(schema_utils.LudwigBaseConfig): + type: str + + class Child(Base): + type: str = schema_utils.ProtectedString("child_type") + + instance = Child() + assert instance.type == "child_type" + + with pytest.raises(PydanticValidationError): + Child(type="wrong_type") + + +def test_metaclass_annotations_not_lost_on_py314(): + """Regression test: LudwigBaseConfig subclass with multiple annotated fields must retain all + annotations even on Python 3.14 (where __annotate_func__ replaces __annotations__ in the + class namespace during metaclass __new__). + """ + + class MyConfig(schema_utils.LudwigBaseConfig): + name: str = schema_utils.StringOptions(["a", "b"], default="a", allow_none=False) + value: int | None = None + flag: bool = False + + instance = MyConfig() + assert instance.name == "a" + assert instance.value is None + assert instance.flag is False + + # All three fields must appear in the pydantic model fields + fields = MyConfig.model_fields + assert "name" in fields + assert "value" in fields + assert "flag" in fields + + +# Simple marshmallow fields: + + +def test_StringOptions(): + # Test case of default conflicting with allowed options: + test_options = ["one"] + with pytest.raises(ValueError): + schema_utils.StringOptions(test_options, default=None, allow_none=False) + + # Test creating a schema with simple option, null not allowed: + test_options = ["one"] + + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: str = schema_utils.StringOptions(test_options, "one", allow_none=False) + + with pytest.raises(PydanticValidationError): + CustomTestSchema.model_validate({"foo": None}) + + +# Complex, custom marshmallow fields: + + +def test_Embed(): + # Test simple schema creation: + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: str | int | None = schema_utils.Embed() + + # Test null/empty loading cases: + assert CustomTestSchema.model_validate({}).foo is None + assert CustomTestSchema.model_validate({"foo": None}).foo is None + + # Test valid strings/numbers: + assert CustomTestSchema.model_validate({"foo": "add"}).foo == "add" + assert CustomTestSchema.model_validate({"foo": 1}).foo == 1 + + +def test_InitializerOrDict(): + # Test default value validation: + with pytest.raises(Exception): + schema_utils.InitializerOrDict("test") + + # Test simple schema creation: + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: str | dict | None = schema_utils.InitializerOrDict() + + # Test valid loads: + assert CustomTestSchema.model_validate({}).foo == "xavier_uniform" + assert CustomTestSchema.model_validate({"foo": "zeros"}).foo == "zeros" + + # Test valid dict loads: + assert CustomTestSchema.model_validate({"foo": {"type": "zeros"}}).foo == {"type": "zeros"} + + +def test_FloatRangeTupleDataclassField(): + # Test dimensional mismatch: + with pytest.raises(Exception): + schema_utils.FloatRangeTupleDataclassField(n=3, default=(1, 1)) + + # Test default schema creation: + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: tuple[float, float] | None = schema_utils.FloatRangeTupleDataclassField(allow_none=True) + + # Test empty load: + assert CustomTestSchema.model_validate({}).foo == (0.9, 0.999) + assert CustomTestSchema.model_validate({"foo": None}).foo is None + + # Test valid loads: + assert CustomTestSchema.model_validate({"foo": [0.5, 0.6]}).foo == (0.5, 0.6) + + # Test non-default schema (N=3, other custom metadata): + class CustomTestSchema2(schema_utils.LudwigBaseConfig): + foo: tuple[float, float, float] | None = schema_utils.FloatRangeTupleDataclassField( + n=3, default=(1, 1, 1), min=-10, max=10 + ) + + assert CustomTestSchema2.model_validate({}).foo == (1, 1, 1) + assert CustomTestSchema2.model_validate({"foo": [2, 2, 2]}).foo == (2, 2, 2) + + +def test_OneOfOptionsField(): + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: float | str = schema_utils.OneOfOptionsField( + default=0.1, + description="", + allow_none=False, + field_options=[ + schema_utils.FloatRange(default=0.001, min=0, max=1, allow_none=False), + schema_utils.StringOptions(options=["placeholder"], default="placeholder", allow_none=False), + ], + ) + + # Test valid loads: + assert CustomTestSchema.model_validate({}).foo == 0.1 + assert CustomTestSchema().foo == 0.1 + + # Reverse the order and allow none (via StringOptions): + class CustomTestSchema2(schema_utils.LudwigBaseConfig): + foo: float | str | None = schema_utils.OneOfOptionsField( + default="placeholder", + description="", + field_options=[ + schema_utils.FloatRange(default=0.001, min=0, max=1, allow_none=False), + schema_utils.StringOptions(options=["placeholder"], default="placeholder", allow_none=False), + ], + allow_none=True, + ) + + # Test valid loads: + assert CustomTestSchema2.model_validate({}).foo == "placeholder" + assert CustomTestSchema2.model_validate({"foo": 0.1}).foo == 0.1 + assert CustomTestSchema2().foo == "placeholder" + CustomTestSchema2.model_validate({"foo": None}) + + # Test JSON schema generation: + json = schema_utils.unload_jsonschema_from_config_class(CustomTestSchema2) + assert "foo" in json["properties"] + + +def test_OneOfOptionsField_allows_none(): + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: float | str | None = schema_utils.OneOfOptionsField( + default=None, + allow_none=True, + description="", + field_options=[ + schema_utils.PositiveInteger(description="", default=1, allow_none=False), + schema_utils.List(list_type=int, allow_none=False), + ], + ) + + json = schema_utils.unload_jsonschema_from_config_class(CustomTestSchema) + schema = { + "type": "object", + "properties": { + "hello": json, + }, + "definitions": {}, + } + validate(instance={"hello": {"foo": None}}, schema=schema, cls=get_validator()) + + +def test_OneOfOptionsField_multiple_fields_allow_none(): + # With pydantic, multiple fields allowing none is handled by union validation. + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: float | str | None = schema_utils.OneOfOptionsField( + default=None, + description="", + field_options=[ + schema_utils.PositiveInteger(description="", default=1, allow_none=True), + schema_utils.List(list_type=int, allow_none=True), + ], + ) + + assert CustomTestSchema().foo is None + + +def test_OneOfOptionsField_allows_none_one_field_allows_none(): + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: float | str | None = schema_utils.OneOfOptionsField( + default=None, + description="", + field_options=[ + schema_utils.PositiveInteger(description="", default=1, allow_none=False), + schema_utils.List(list_type=int, allow_none=True), + ], + ) + + json = schema_utils.unload_jsonschema_from_config_class(CustomTestSchema) + schema = { + "type": "object", + "properties": { + "hello": json, + }, + "definitions": {}, + } + validate(instance={"hello": {"foo": None}}, schema=schema, cls=get_validator()) diff --git a/tests/ludwig/schema_fields/test_fields_optimization.py b/tests/ludwig/schema_fields/test_fields_optimization.py new file mode 100644 index 0000000..d99a6c7 --- /dev/null +++ b/tests/ludwig/schema_fields/test_fields_optimization.py @@ -0,0 +1,97 @@ +#! /usr/bin/env python + +import pytest +from pydantic import ValidationError as PydanticValidationError + +import ludwig.schema.optimizers as lso +from ludwig.schema import utils as schema_utils + + +def test_torch_description_pull(): + example_empty_desc_prop = schema_utils.unload_jsonschema_from_config_class(lso.AdamOptimizerConfig)["properties"][ + "eps" + ] + assert ( + isinstance(example_empty_desc_prop, dict) + and "description" in example_empty_desc_prop + and isinstance(example_empty_desc_prop["description"], str) + and len(example_empty_desc_prop["description"]) > 3 + ) + + +def test_OptimizerDataclassField(): + # Test default case: + default_optimizer_field = lso.OptimizerDataclassField() + assert default_optimizer_field.default_factory is not None + assert default_optimizer_field.default_factory() == lso.AdamOptimizerConfig() + + # Test normal cases: + optimizer_field = lso.OptimizerDataclassField("adamax") + assert optimizer_field.default_factory is not None + assert optimizer_field.default_factory() == lso.AdamaxOptimizerConfig() + + # Test invalid default case: + with pytest.raises(AttributeError): + lso.OptimizerDataclassField({}) + with pytest.raises(KeyError): + lso.OptimizerDataclassField("test") + with pytest.raises(AttributeError): + lso.OptimizerDataclassField(1) + + # Test creating a schema with default options: + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: lso.BaseOptimizerConfig | None = lso.OptimizerDataclassField() + + with pytest.raises((PydanticValidationError, Exception)): + CustomTestSchema.model_validate({"foo": "test"}) + + assert CustomTestSchema.model_validate({}).foo == lso.AdamOptimizerConfig() + + # Test creating a schema with set default: + class CustomTestSchema2(schema_utils.LudwigBaseConfig): + foo: lso.BaseOptimizerConfig | None = lso.OptimizerDataclassField("adamax") + + with pytest.raises((PydanticValidationError, Exception)): + CustomTestSchema2.model_validate({"foo": "test"}) + + assert CustomTestSchema2.model_validate( + {"foo": {"type": "adamax", "betas": (0.2, 0.2)}} + ).foo == lso.AdamaxOptimizerConfig(betas=(0.2, 0.2)) + + +def test_ClipperDataclassField(): + # Test default case: + default_clipper_field = lso.GradientClippingDataclassField(description="", default={}) + assert default_clipper_field.default_factory is not None + assert default_clipper_field.default_factory() == lso.GradientClippingConfig() + + # Test normal cases: + clipper_field = lso.GradientClippingDataclassField(description="", default={"clipglobalnorm": 0.1}) + assert clipper_field.default_factory is not None + assert clipper_field.default_factory() == lso.GradientClippingConfig(clipglobalnorm=0.1) + + clipper_field = lso.GradientClippingDataclassField(description="", default={"clipglobalnorm": None}) + assert clipper_field.default_factory is not None + assert clipper_field.default_factory() == lso.GradientClippingConfig(clipglobalnorm=None) + + # Test invalid default case: + with pytest.raises(Exception): + lso.GradientClippingDataclassField(description="", default="test") + with pytest.raises(Exception): + lso.GradientClippingDataclassField(description="", default=None) + with pytest.raises(Exception): + lso.GradientClippingDataclassField(description="", default=1) + + # Test creating a schema with set default: + class CustomTestSchema(schema_utils.LudwigBaseConfig): + foo: lso.GradientClippingConfig | None = lso.GradientClippingDataclassField( + description="", default={"clipglobalnorm": 0.1} + ) + + with pytest.raises((PydanticValidationError, Exception)): + CustomTestSchema.model_validate({"foo": "test"}) + + assert CustomTestSchema.model_validate({}).foo == lso.GradientClippingConfig(clipglobalnorm=0.1) + assert CustomTestSchema.model_validate({"foo": {"clipglobalnorm": 1}}).foo == lso.GradientClippingConfig( + clipglobalnorm=1 + ) diff --git a/tests/ludwig/schema_fields/test_fields_preprocessing.py b/tests/ludwig/schema_fields/test_fields_preprocessing.py new file mode 100644 index 0000000..a340e3a --- /dev/null +++ b/tests/ludwig/schema_fields/test_fields_preprocessing.py @@ -0,0 +1,23 @@ +#! /usr/bin/env python + + +from ludwig.schema.features.preprocessing.binary import BinaryPreprocessingConfig +from ludwig.schema.features.preprocessing.category import CategoryPreprocessingConfig +from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField + + +def get_marshmallow_from_dataclass_field(dfield): + """Helper method for checking marshmallow metadata succinctly.""" + return dfield.metadata["marshmallow_field"] + + +def test_preprocessing_dataclass_field(): + binary_preproc_dataclass = PreprocessingDataclassField("binary") + assert binary_preproc_dataclass.default_factory is not None + assert get_marshmallow_from_dataclass_field(binary_preproc_dataclass).allow_none is False + assert binary_preproc_dataclass.default_factory() == BinaryPreprocessingConfig() + + category_preproc_dataclass = PreprocessingDataclassField("category") + assert category_preproc_dataclass.default_factory is not None + assert get_marshmallow_from_dataclass_field(category_preproc_dataclass).allow_none is False + assert category_preproc_dataclass.default_factory() == CategoryPreprocessingConfig() diff --git a/tests/ludwig/schema_fields/test_marshmallow_misc.py b/tests/ludwig/schema_fields/test_marshmallow_misc.py new file mode 100644 index 0000000..ba216cb --- /dev/null +++ b/tests/ludwig/schema_fields/test_marshmallow_misc.py @@ -0,0 +1,40 @@ +import pytest + +import ludwig.combiners.combiners as lcc +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.schema.utils import ( + assert_is_a_config_class, + load_config_with_kwargs, + LudwigBaseConfig, +) + + +class CustomTestSchema(LudwigBaseConfig): + """Sample docstring.""" + + foo: int = 5 + "foo (default: 5)" + + +def test_assert_is_a_marshmallow_clas(): + assert_is_a_config_class(ECDTrainerConfig) + with pytest.raises(TypeError, match=r"Expected.*config class"): + assert_is_a_config_class(lcc.ConcatCombiner) + + +def test_load_config_with_kwargs(): + test_kwargs = { + "foo": 6, + "bar": 6, + } + initialized_class, leftover = load_config_with_kwargs(CustomTestSchema, test_kwargs) + + assert initialized_class.foo == 6 + assert leftover == {"bar": 6} + + # TransformerCombiner has no required/non-default arguments: + initialized_class, leftover = load_config_with_kwargs(lcc.TransformerCombinerConfig, test_kwargs) + assert initialized_class.bias_initializer == "zeros" + assert leftover == test_kwargs + initialized_class, leftover = load_config_with_kwargs(lcc.TransformerCombinerConfig, {}) + assert leftover == {} diff --git a/tests/ludwig/test_api_unit.py b/tests/ludwig/test_api_unit.py new file mode 100644 index 0000000..622cc65 --- /dev/null +++ b/tests/ludwig/test_api_unit.py @@ -0,0 +1,346 @@ +"""Unit tests for LudwigModel API edge cases.""" + +import logging +from unittest.mock import MagicMock, patch + +import numpy as np +import pandas as pd +import pytest + +from ludwig.api import LudwigModel +from ludwig.callbacks import Callback + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class RecordingCallback(Callback): + """Records which hooks were called and in what order.""" + + def __init__(self): + self.calls: list[str] = [] + + def on_train_start(self, *args, **kwargs): + self.calls.append("on_train_start") + + def on_train_end(self, *args, **kwargs): + self.calls.append("on_train_end") + + def on_evaluation_start(self, **kwargs): + self.calls.append("on_evaluation_start") + + def on_evaluation_end(self, **kwargs): + self.calls.append("on_evaluation_end") + + def on_preprocess_start(self, *args, **kwargs): + self.calls.append("on_preprocess_start") + + def on_preprocess_end(self, *args, **kwargs): + self.calls.append("on_preprocess_end") + + +# --------------------------------------------------------------------------- +# 1a. Model card / training report exception handling +# --------------------------------------------------------------------------- + + +def test_model_card_failure_does_not_abort_training(tmpdir, caplog): + """Training should complete even if model card generation fails. + + The failure should be logged at WARNING with a DEBUG traceback. + """ + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + "trainer": {"train_steps": 1, "batch_size": 8}, + } + + import pandas as pd + + df = pd.DataFrame({"x": range(20), "y": [0, 1] * 10}) + + with patch("ludwig.utils.model_card.save_model_card", side_effect=RuntimeError("card boom")): + with caplog.at_level(logging.WARNING, logger="ludwig"): + # logging_level=WARNING so LudwigModel.__init__ doesn't override caplog to ERROR + model = LudwigModel(config, logging_level=logging.WARNING) + result = model.train(dataset=df, output_directory=str(tmpdir)) + + assert result is not None, "training should complete despite model card failure" + assert any("Failed to generate model card" in m for m in caplog.messages) + + +def test_training_report_failure_does_not_abort_training(tmpdir, caplog): + """Training should complete even if training report generation fails.""" + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + "trainer": {"train_steps": 1, "batch_size": 8}, + } + + import pandas as pd + + df = pd.DataFrame({"x": range(20), "y": [0, 1] * 10}) + + with patch("ludwig.utils.training_report.save_training_report", side_effect=RuntimeError("report boom")): + with caplog.at_level(logging.WARNING, logger="ludwig"): + model = LudwigModel(config, logging_level=logging.WARNING) + result = model.train(dataset=df, output_directory=str(tmpdir)) + + assert result is not None + assert any("Failed to generate training report" in m for m in caplog.messages) + + +# --------------------------------------------------------------------------- +# 1b. evaluate() batch size fallback +# --------------------------------------------------------------------------- + + +def _make_evaluate_mock(trainer_dict: dict): + """Return a MagicMock LudwigModel wired for evaluate() batch size tests.""" + # Don't pass spec= here — config_obj is an instance attribute set in __init__, + # not a class-level attribute, so spec=LudwigModel would block access to it. + model = MagicMock() + model.callbacks = [] + model.config_obj.trainer.to_dict.return_value = trainer_dict + + dataset_mock = MagicMock() + metadata_mock = MagicMock() + model._preprocess_for_prediction.return_value = (dataset_mock, metadata_mock) + + predictor = MagicMock() + predictor.batch_evaluation.return_value = ({}, MagicMock()) + + ctx = MagicMock() + ctx.__enter__ = MagicMock(return_value=predictor) + ctx.__exit__ = MagicMock(return_value=False) + model.backend.create_predictor.return_value = ctx + + model.backend.df_engine.df_lib = pd + model.model.output_features = {} + return model, predictor + + +def _run_evaluate(model): + """Call LudwigModel.evaluate() with all saving and collection disabled.""" + return LudwigModel.evaluate( + model, + dataset=MagicMock(), + collect_predictions=False, + collect_overall_stats=False, + skip_save_unprocessed_output=True, + skip_save_predictions=True, + skip_save_eval_stats=True, + ) + + +def test_evaluate_uses_eval_batch_size_from_config(): + """Evaluate() should use eval_batch_size from trainer config when batch_size arg is None.""" + model, predictor = _make_evaluate_mock({"eval_batch_size": 64, "batch_size": 32}) + model.backend.is_coordinator.return_value = False + predictor.batch_evaluation.return_value = ({}, {}) + + _run_evaluate(model) + + model.backend.create_predictor.assert_called_once_with(model.model, batch_size=64) + + +def test_evaluate_falls_back_to_batch_size_when_eval_batch_size_absent(): + """Evaluate() should fall back to batch_size when eval_batch_size is not in trainer config.""" + model, predictor = _make_evaluate_mock({"batch_size": 16}) + model.backend.is_coordinator.return_value = False + predictor.batch_evaluation.return_value = ({}, {}) + + _run_evaluate(model) + + model.backend.create_predictor.assert_called_once_with(model.model, batch_size=16) + + +def test_evaluate_raises_when_no_batch_size_in_config(): + """Evaluate() must raise ValueError when neither batch_size nor eval_batch_size are set.""" + model, _ = _make_evaluate_mock({}) # empty trainer dict — no batch sizes + + with pytest.raises(ValueError, match="batch_size not specified"): + _run_evaluate(model) + + +# --------------------------------------------------------------------------- +# 1c. forecast() boundary conditions +# --------------------------------------------------------------------------- + + +def _make_forecast_mock(output_features, input_features=None): + """Return a MagicMock LudwigModel wired for forecast() boundary tests.""" + model = MagicMock(spec=LudwigModel) + model.callbacks = [] + model.config_obj.output_features = output_features + model.config_obj.input_features = input_features or [] + + dataset_mock = MagicMock() + model.backend.df_engine.df_lib = pd + return model, dataset_mock + + +def test_forecast_raises_when_no_timeseries_input_feature(): + """Forecast() should raise ValueError when no timeseries input feature is present.""" + from unittest.mock import patch as _patch + + # No timeseries input features + input_features = [MagicMock(type="number", preprocessing=MagicMock(window_size=5))] + input_features[0].type = "number" # not TIMESERIES + + model = MagicMock() + model.callbacks = [] + model.config_obj.input_features = input_features + + df = pd.DataFrame({"x": range(5)}) + + with _patch("ludwig.api.load_dataset_uris", return_value=(df, None, None, None)): + with _patch("ludwig.api.load_dataset", return_value=df): + with pytest.raises(ValueError, match="timeseries"): + LudwigModel.forecast(model, dataset=df, horizon=3) + + +def test_forecast_returns_dataframe_for_valid_config(): + """Forecast() should return a DataFrame with the output feature column for a valid config.""" + from ludwig.constants import TIMESERIES + + window_size = 3 + df = pd.DataFrame({"x": range(window_size + 2), "y": [float(i) for i in range(window_size + 2)]}) + + in_feat = MagicMock() + in_feat.type = TIMESERIES + in_feat.name = "x" + in_feat.preprocessing.window_size = window_size + in_feat.preprocessing.padding_value = 0.0 + + out_feat = MagicMock() + out_feat.type = TIMESERIES + out_feat.column = "y" + out_feat.name = "y" + + model = MagicMock() + model.callbacks = [] + model.config_obj.input_features = [in_feat] + model.config_obj.output_features = [out_feat] + model.backend.is_coordinator.return_value = False + + # Set up _check_initialization to be a no-op + model._check_initialization = MagicMock() + + # Set up preprocessed dataset mock — one row with window_size embedding + proc_dataset = MagicMock() + proc_dataset.dataset = {"x__proc": np.array([[1.0, 2.0, 3.0]])} + model._preprocess_for_prediction.return_value = (proc_dataset, {}) + + # Set up model.input_features for the embedding extraction loop + i_feat_mock = MagicMock() + i_feat_mock.proc_column = "x__proc" + model.model.input_features.values.return_value = [i_feat_mock] + model.model.output_features = {out_feat.name: MagicMock()} + + # Set up predictor as context manager returning predict_single results + raw_pred = pd.DataFrame({"y_predictions": [np.array([42.0])]}) + predictor = MagicMock() + predictor.predict_single.return_value = raw_pred + ctx = MagicMock() + ctx.__enter__ = MagicMock(return_value=predictor) + ctx.__exit__ = MagicMock(return_value=False) + model.backend.create_predictor.return_value = ctx + + postproc_pred = pd.DataFrame({"y_predictions": [np.array([42.0])]}) + + horizon = 3 + with patch("ludwig.api.load_dataset_uris", return_value=(df, None, None, None)): + with patch("ludwig.api.load_dataset", return_value=df): + with patch("ludwig.api.postprocess", return_value=postproc_pred): + result = LudwigModel.forecast(model, dataset=df, horizon=horizon) + + assert isinstance(result, pd.DataFrame) + assert "y" in result.columns + assert len(result) <= horizon + + +# --------------------------------------------------------------------------- +# _check_initialization +# --------------------------------------------------------------------------- + + +def test_check_initialization_missing_model(): + """_check_initialization should name missing components.""" + model = MagicMock(spec=LudwigModel) + model.model = None + model._user_config = {"input_features": [], "output_features": []} + model.training_set_metadata = {"x": {}} + + # Call the real method on the mock instance + with pytest.raises(ValueError, match="model"): + LudwigModel._check_initialization(model) + + +def test_check_initialization_missing_metadata(): + model = MagicMock(spec=LudwigModel) + model.model = MagicMock() + model._user_config = {"input_features": [], "output_features": []} + model.training_set_metadata = None + + with pytest.raises(ValueError, match="training_set_metadata"): + LudwigModel._check_initialization(model) + + +def test_check_initialization_all_present(): + model = MagicMock(spec=LudwigModel) + model.model = MagicMock() + model._user_config = {"input_features": [], "output_features": []} + model.training_set_metadata = {"x": {}} + + # Should not raise + LudwigModel._check_initialization(model) + + +# --------------------------------------------------------------------------- +# 1d. Callback lifecycle ordering +# --------------------------------------------------------------------------- + + +def test_preprocess_callbacks_fire_in_order(tmpdir): + """on_preprocess_start fires before on_preprocess_end.""" + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + "trainer": {"train_steps": 1, "batch_size": 8}, + } + + import pandas as pd + + df = pd.DataFrame({"x": range(20), "y": [0, 1] * 10}) + cb = RecordingCallback() + model = LudwigModel(config, callbacks=[cb]) + model.preprocess(dataset=df, output_directory=str(tmpdir)) + + assert "on_preprocess_start" in cb.calls + assert "on_preprocess_end" in cb.calls + assert cb.calls.index("on_preprocess_start") < cb.calls.index("on_preprocess_end") + + +def test_evaluate_callbacks_fire_in_order(tmpdir): + """on_evaluation_start fires before on_evaluation_end.""" + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "binary"}], + "trainer": {"train_steps": 1, "batch_size": 8}, + } + + import pandas as pd + + df = pd.DataFrame({"x": range(20), "y": [0, 1] * 10}) + cb = RecordingCallback() + model = LudwigModel(config, callbacks=[cb]) + model.train(dataset=df, output_directory=str(tmpdir)) + + cb.calls.clear() + model.evaluate(dataset=df) + + assert "on_evaluation_start" in cb.calls + assert "on_evaluation_end" in cb.calls + assert cb.calls.index("on_evaluation_start") < cb.calls.index("on_evaluation_end") diff --git a/tests/ludwig/test_collect.py b/tests/ludwig/test_collect.py new file mode 100644 index 0000000..5f013c9 --- /dev/null +++ b/tests/ludwig/test_collect.py @@ -0,0 +1,82 @@ +"""Unit tests for ludwig.collect — save_tensors and related helpers. + +Tests here do not require a trained model or GPU; they verify the pure-logic +functions that handle tensor serialization and filename generation. +""" + +import os + +import numpy as np +import pytest +import torch + +from ludwig.collect import save_tensors +from ludwig.utils.strings_utils import make_safe_filename + + +class TestSaveTensors: + def test_saves_1d_tensor(self, tmp_path): + t = torch.tensor([1.0, 2.0, 3.0]) + files = save_tensors([("my_layer", t)], str(tmp_path)) + assert len(files) == 1 + loaded = np.load(files[0]) + np.testing.assert_array_equal(loaded, t.numpy()) + + def test_saves_2d_tensor(self, tmp_path): + t = torch.randn(4, 8) + files = save_tensors([("encoder.output", t)], str(tmp_path)) + assert len(files) == 1 + loaded = np.load(files[0]) + assert loaded.shape == (4, 8) + + def test_filename_uses_safe_name(self, tmp_path): + t = torch.tensor([0.0]) + files = save_tensors([("a/b/c", t)], str(tmp_path)) + expected_stem = make_safe_filename("a/b/c") + assert os.path.basename(files[0]) == expected_stem + ".npy" + + def test_multiple_tensors_saved(self, tmp_path): + tensors = [ + ("layer1", torch.ones(3)), + ("layer2", torch.zeros(5)), + ] + files = save_tensors(tensors, str(tmp_path)) + assert len(files) == 2 + + def test_non_tensor_entries_skipped(self, tmp_path): + """Non-tensor values (e.g. used_tokens int) must be silently skipped.""" + collected = [ + ("encoder.output", torch.ones(4)), + ("used_tokens", 42), # not a tensor + ] + files = save_tensors(collected, str(tmp_path)) + # Only the tensor entry should produce a file + assert len(files) == 1 + + def test_creates_output_directory(self, tmp_path): + nested = tmp_path / "subdir" / "deep" + t = torch.tensor([1.0]) + # save_tensors does NOT create the directory — caller must + nested.mkdir(parents=True) + files = save_tensors([("w", t)], str(nested)) + assert len(files) == 1 + assert os.path.exists(files[0]) + + def test_empty_list_returns_empty(self, tmp_path): + files = save_tensors([], str(tmp_path)) + assert files == [] + + def test_tensor_values_are_preserved(self, tmp_path): + expected = torch.tensor([[1.5, -2.0], [0.0, 3.14]]) + files = save_tensors([("weights", expected)], str(tmp_path)) + loaded = np.load(files[0]) + np.testing.assert_allclose(loaded, expected.numpy(), rtol=1e-6) + + def test_gpu_tensor_saved_to_cpu(self, tmp_path): + if not torch.cuda.is_available(): + pytest.skip("CUDA not available") + t = torch.ones(4).cuda() + files = save_tensors([("gpu_layer", t)], str(tmp_path)) + assert len(files) == 1 + loaded = np.load(files[0]) + np.testing.assert_array_equal(loaded, np.ones(4)) diff --git a/tests/ludwig/test_config_generation.py b/tests/ludwig/test_config_generation.py new file mode 100644 index 0000000..31b75ee --- /dev/null +++ b/tests/ludwig/test_config_generation.py @@ -0,0 +1,109 @@ +"""Tests for LLM-driven config generation (mocked LLM calls).""" + +import json +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from ludwig.config_generation import generate_config, get_ludwig_schema_context + + +class TestGetLudwigSchemaContext: + def test_returns_valid_json(self): + context = get_ludwig_schema_context() + parsed = json.loads(context) + assert "input_feature_types" in parsed + assert "combiner_types" in parsed + assert "example_config" in parsed + + def test_contains_feature_types(self): + context = get_ludwig_schema_context() + parsed = json.loads(context) + types = parsed["input_feature_types"] + assert "number" in types + assert "category" in types + assert "text" in types + assert "binary" in types + + +class TestGenerateConfig: + VALID_CONFIG_JSON = json.dumps( + { + "input_features": [ + {"name": "age", "type": "number"}, + {"name": "income", "type": "number"}, + ], + "output_features": [{"name": "churn", "type": "binary"}], + "combiner": {"type": "concat"}, + "trainer": {"epochs": 50, "batch_size": 128, "learning_rate": 0.001}, + } + ) + + def _mock_anthropic(self, response_text): + """Create a mock anthropic module with the given response text.""" + mock_response = MagicMock() + mock_response.content = [MagicMock(text=response_text)] + + mock_client = MagicMock() + mock_client.messages.create.return_value = mock_response + + mock_module = MagicMock() + mock_module.Anthropic.return_value = mock_client + return mock_module, mock_client + + def test_generate_config_anthropic(self): + mock_module, mock_client = self._mock_anthropic(self.VALID_CONFIG_JSON) + + with patch.dict(sys.modules, {"anthropic": mock_module}): + config = generate_config( + "Predict customer churn from age and income", + validate=False, + ) + + assert "input_features" in config + assert config["output_features"][0]["name"] == "churn" + mock_client.messages.create.assert_called_once() + + def test_generate_config_strips_markdown_code_blocks(self): + wrapped = f"```json\n{self.VALID_CONFIG_JSON}\n```" + mock_module, _ = self._mock_anthropic(wrapped) + + with patch.dict(sys.modules, {"anthropic": mock_module}): + config = generate_config("some task", validate=False) + assert "input_features" in config + + def test_generate_config_invalid_json_raises_value_error(self): + mock_module, _ = self._mock_anthropic("not json at all") + + with patch.dict(sys.modules, {"anthropic": mock_module}): + with pytest.raises(ValueError, match="Raw response"): + generate_config("some task", validate=False) + + def test_generate_config_openai_fallback(self): + """When anthropic is not installed, should fall back to openai.""" + mock_oai_response = MagicMock() + mock_oai_response.choices = [MagicMock()] + mock_oai_response.choices[0].message.content = self.VALID_CONFIG_JSON + + mock_oai_client = MagicMock() + mock_oai_client.chat.completions.create.return_value = mock_oai_response + + mock_oai = MagicMock() + mock_oai.OpenAI.return_value = mock_oai_client + + # Simulate anthropic not installed by raising ImportError, openai available + with patch.dict(sys.modules, {"anthropic": None, "openai": mock_oai}): + config = generate_config("Predict churn", validate=False) + + assert "input_features" in config + + def test_schema_context_import_error_logs_warning(self, caplog): + """ImportError in get_ludwig_schema_context should log a warning, not silently fail.""" + with patch("ludwig.config_generation.get_ludwig_schema_context") as mock_ctx: + mock_ctx.return_value = "{}" + mock_module, _ = self._mock_anthropic(self.VALID_CONFIG_JSON) + with patch.dict(sys.modules, {"anthropic": mock_module}): + # Just verify the function completes even with empty schema context + config = generate_config("some task", validate=False) + assert "input_features" in config diff --git a/tests/ludwig/test_model_inspector.py b/tests/ludwig/test_model_inspector.py new file mode 100644 index 0000000..4d039b1 --- /dev/null +++ b/tests/ludwig/test_model_inspector.py @@ -0,0 +1,49 @@ +"""Tests for ModelInspector.""" + +import torch.nn as nn + +from ludwig.model_inspector import ModelInspector + + +class SimpleModel(nn.Module): + def __init__(self): + super().__init__() + self.linear1 = nn.Linear(10, 20) + self.relu = nn.ReLU() + self.linear2 = nn.Linear(20, 5) + + def forward(self, x): + return self.linear2(self.relu(self.linear1(x))) + + +class TestModelInspector: + def test_collect_weights(self): + model = SimpleModel() + inspector = ModelInspector(model, {}, {}) + weights = inspector.collect_weights() + assert len(weights) == 4 # 2 weight + 2 bias + assert all("name" in w for w in weights) + assert all("shape" in w for w in weights) + + def test_collect_specific_weights(self): + model = SimpleModel() + inspector = ModelInspector(model, {}, {}) + weights = inspector.collect_weights(tensor_names=["linear1.weight"]) + assert len(weights) == 1 + assert weights[0]["name"] == "linear1.weight" + + def test_model_summary(self): + model = SimpleModel() + config = { + "model_type": "ecd", + "combiner": {"type": "concat"}, + "input_features": [{"name": "x"}], + "output_features": [{"name": "y"}], + } + inspector = ModelInspector(model, config, {}) + summary = inspector.model_summary() + assert summary["total_parameters"] > 0 + assert summary["trainable_parameters"] == summary["total_parameters"] + assert summary["model_size_mb"] >= 0 + assert summary["model_type"] == "ecd" + assert "Linear" in summary["layer_counts"] diff --git a/tests/ludwig/test_presets.py b/tests/ludwig/test_presets.py new file mode 100644 index 0000000..b65e1c6 --- /dev/null +++ b/tests/ludwig/test_presets.py @@ -0,0 +1,86 @@ +"""Tests for quality presets.""" + +import pytest + +from ludwig.presets import _deep_merge, apply_preset, QUALITY_PRESETS + + +class TestDeepMerge: + def test_flat_override(self): + base = {"a": 1, "b": 2} + override = {"b": 3, "c": 4} + result = _deep_merge(base, override) + assert result == {"a": 1, "b": 3, "c": 4} + + def test_nested_merge(self): + base = {"x": {"a": 1, "b": 2}, "y": 3} + override = {"x": {"b": 99, "c": 100}} + result = _deep_merge(base, override) + assert result == {"x": {"a": 1, "b": 99, "c": 100}, "y": 3} + + def test_override_wins_for_non_dict(self): + base = {"x": {"a": 1}} + override = {"x": "replaced"} + result = _deep_merge(base, override) + assert result == {"x": "replaced"} + + def test_does_not_mutate_inputs(self): + base = {"x": {"a": 1}} + override = {"x": {"b": 2}} + _deep_merge(base, override) + assert base == {"x": {"a": 1}} + assert override == {"x": {"b": 2}} + + +class TestApplyPreset: + def test_medium_quality_applies(self): + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number"}], + } + result = apply_preset(config, "medium_quality") + assert result["combiner"]["type"] == "concat" + assert result["trainer"]["epochs"] == 50 + # User features preserved + assert result["input_features"] == config["input_features"] + + def test_high_quality_applies(self): + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number"}], + } + result = apply_preset(config, "high_quality") + assert result["combiner"]["type"] == "transformer" + assert result["trainer"]["loss_balancing"] == "uncertainty" + + def test_best_quality_applies(self): + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number"}], + } + result = apply_preset(config, "best_quality") + assert result["combiner"]["type"] == "ft_transformer" + assert result["trainer"]["model_soup"] == "uniform" + + def test_user_override_wins(self): + config = { + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number"}], + "combiner": {"type": "tabnet"}, # User override + "trainer": {"epochs": 10}, # User override + } + result = apply_preset(config, "best_quality") + # User overrides should win + assert result["combiner"]["type"] == "tabnet" + assert result["trainer"]["epochs"] == 10 + # But preset values not overridden should still apply + assert result["trainer"]["model_soup"] == "uniform" + + def test_unknown_preset_raises(self): + with pytest.raises(ValueError, match="Unknown preset"): + apply_preset({}, "nonexistent_preset") + + def test_all_presets_exist(self): + assert "medium_quality" in QUALITY_PRESETS + assert "high_quality" in QUALITY_PRESETS + assert "best_quality" in QUALITY_PRESETS diff --git a/tests/ludwig/test_serve_deployment_shims.py b/tests/ludwig/test_serve_deployment_shims.py new file mode 100644 index 0000000..ae46b10 --- /dev/null +++ b/tests/ludwig/test_serve_deployment_shims.py @@ -0,0 +1,89 @@ +"""Phase 6.8 production deployment shims — unit tests. + +These tests avoid actually starting Ray Serve or KServe (both are heavy optional dependencies). They verify the import- +error contract when the optional package is missing and exercise the pure-Python helpers (v2 input transpose, CLI +argparse). +""" + +from __future__ import annotations + +import pytest + +from ludwig.serve_kserve import _v2_inputs_to_records + + +class TestV2InputsTransposer: + def test_basic_transpose(self): + inputs = [ + {"name": "text", "shape": [2], "datatype": "BYTES", "data": ["hi", "bye"]}, + {"name": "num", "shape": [2], "datatype": "INT64", "data": [1, 2]}, + ] + rows = _v2_inputs_to_records(inputs) + assert rows == [{"text": "hi", "num": 1}, {"text": "bye", "num": 2}] + + def test_single_input(self): + inputs = [{"name": "x", "shape": [3], "datatype": "FP32", "data": [0.1, 0.2, 0.3]}] + rows = _v2_inputs_to_records(inputs) + assert rows == [{"x": 0.1}, {"x": 0.2}, {"x": 0.3}] + + def test_empty_inputs(self): + assert _v2_inputs_to_records([]) == [] + + def test_inconsistent_lengths_rejected(self): + inputs = [ + {"name": "a", "shape": [2], "data": [1, 2]}, + {"name": "b", "shape": [3], "data": [1, 2, 3]}, + ] + with pytest.raises(ValueError, match="inconsistent lengths"): + _v2_inputs_to_records(inputs) + + +class TestRaySrveImportMissing: + """If ray.serve isn't installed, the helpers raise a clear ImportError.""" + + def test_deploy_helper_raises_when_ray_serve_missing(self): + try: + import ray.serve # noqa: F401 + except ImportError: + from ludwig.serve_ray_serve import deploy_ludwig_model + + with pytest.raises(ImportError, match="ludwig\\[distributed\\]"): + deploy_ludwig_model(model_path="/does/not/matter", name="test") + else: + pytest.skip("ray.serve is installed; skipping missing-package test") + + +class TestKServeImportMissing: + def test_serve_helper_raises_when_kserve_missing(self): + try: + import kserve # noqa: F401 + except ImportError: + from ludwig.serve_kserve import serve_ludwig_model + + with pytest.raises(ImportError, match="pip install kserve"): + serve_ludwig_model("name", "/does/not/matter") + else: + pytest.skip("kserve is installed; skipping missing-package test") + + +class TestKServeCLIArgparse: + def test_cli_requires_model_name_and_path(self): + from ludwig.serve_kserve import cli + + with pytest.raises(SystemExit): + cli([]) # argparse exits with code 2 on missing required args + + def test_cli_passes_parsed_args_to_server(self, monkeypatch): + """Verify argparse extracts the right fields and delegates to serve_ludwig_model.""" + from ludwig import serve_kserve as module_under_test + + calls = {} + + def _fake_serve(model_name, model_path, http_port=8080): + calls["model_name"] = model_name + calls["model_path"] = model_path + calls["http_port"] = http_port + + monkeypatch.setattr(module_under_test, "serve_ludwig_model", _fake_serve) + module_under_test.cli(["--model_name", "m", "--model_path", "/p", "--http_port", "9000"]) + assert calls == {"model_name": "m", "model_path": "/p", "http_port": 9000} diff --git a/tests/ludwig/test_serve_v2.py b/tests/ludwig/test_serve_v2.py new file mode 100644 index 0000000..b547b02 --- /dev/null +++ b/tests/ludwig/test_serve_v2.py @@ -0,0 +1,72 @@ +"""Tests for modernized serving.""" + +from ludwig.serve_v2 import build_request_schema, build_response_schema, ModelManager +from ludwig.utils.data_utils import numpy_to_python + + +class TestNumpySafe: + def test_int(self): + import numpy as np + + assert numpy_to_python(np.int64(42)) == 42 + assert isinstance(numpy_to_python(np.int64(42)), int) + + def test_float(self): + import numpy as np + + assert isinstance(numpy_to_python(np.float32(3.14)), float) + + def test_array(self): + import numpy as np + + result = numpy_to_python(np.array([1, 2, 3])) + assert result == [1, 2, 3] + + def test_nested_dict(self): + import numpy as np + + data = {"a": np.int64(1), "b": {"c": np.array([1.0, 2.0])}} + result = numpy_to_python(data) + assert result == {"a": 1, "b": {"c": [1.0, 2.0]}} + + def test_passthrough(self): + assert numpy_to_python("hello") == "hello" + assert numpy_to_python(42) == 42 + + +class TestSchemaGeneration: + def test_build_request_schema(self): + config = { + "input_features": [ + {"name": "age", "type": "number"}, + {"name": "name", "type": "text"}, + ] + } + schema = build_request_schema(config) + assert "age" in schema.model_fields + assert "name" in schema.model_fields + + def test_build_response_schema(self): + config = { + "output_features": [ + {"name": "price", "type": "number"}, + {"name": "category", "type": "category"}, + ] + } + schema = build_response_schema(config) + assert "price_predictions" in schema.model_fields + assert "category_predictions" in schema.model_fields + + +class TestModelManager: + def test_initial_state(self): + mm = ModelManager() + assert mm.model is None + assert mm.config is None + + def test_get_model_raises_when_not_loaded(self): + import pytest + + mm = ModelManager() + with pytest.raises(RuntimeError, match="Model not loaded"): + mm.get_model() diff --git a/tests/ludwig/trainers/__init__.py b/tests/ludwig/trainers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ludwig/trainers/test_dpo_trainers.py b/tests/ludwig/trainers/test_dpo_trainers.py new file mode 100644 index 0000000..164457f --- /dev/null +++ b/tests/ludwig/trainers/test_dpo_trainers.py @@ -0,0 +1,127 @@ +"""Unit tests for DPO-family trainers (DPOTrainer, KTOTrainer, ORPOTrainer, GRPOTrainer). + +These tests verify trainer construction and attribute initialization without a GPU +or a real LLM model. We bypass Trainer.__init__ via patch and inject a mock config. +""" + +from unittest.mock import MagicMock, patch + +from ludwig.trainers.registry import get_llm_trainers_registry +from ludwig.trainers.trainer import Trainer +from ludwig.trainers.trainer_dpo import DPOTrainer, GRPOTrainer, KTOTrainer, ORPOTrainer + + +def _noop_trainer_init(self, *args, **kwargs): + """Replaces Trainer.__init__ so subclass __init__ can run without full model/config setup.""" + + +def _make_dpo(cls, extra_config_attrs: dict | None = None): + """Create a DPO-family trainer instance, bypassing Trainer.__init__.""" + trainer = cls.__new__(cls) + trainer.config = MagicMock(spec=[]) + # DPOTrainer reads these attributes from config + trainer.config.dpo_beta = 0.1 + trainer.config.dpo_loss_type = "sigmoid" + trainer.config.dpo_label_smoothing = 0.0 + for attr, val in (extra_config_attrs or {}).items(): + setattr(trainer.config, attr, val) + with patch.object(Trainer, "__init__", _noop_trainer_init): + cls.__init__(trainer) + return trainer + + +class TestDPOTrainerDefaults: + def test_beta_default(self): + trainer = _make_dpo(DPOTrainer) + assert trainer.beta == 0.1 + + def test_loss_type_default(self): + trainer = _make_dpo(DPOTrainer) + assert trainer.loss_type == "sigmoid" + + def test_label_smoothing_default(self): + trainer = _make_dpo(DPOTrainer) + assert trainer.label_smoothing == 0.0 + + def test_reference_log_probs_initially_none(self): + trainer = _make_dpo(DPOTrainer) + assert trainer._reference_chosen_log_probs is None + assert trainer._reference_rejected_log_probs is None + + +class TestKTOTrainerInit: + def test_loss_type_is_kto(self): + trainer = _make_dpo(KTOTrainer, {"kto_beta": 0.1}) + assert trainer.loss_type == "kto" + + def test_beta_default(self): + trainer = _make_dpo(KTOTrainer, {"kto_beta": 0.1}) + assert trainer.beta == 0.1 + + def test_beta_override_from_config(self): + trainer = _make_dpo(KTOTrainer, {"kto_beta": 0.5}) + assert trainer.beta == 0.5 + + +class TestORPOTrainerInit: + def test_loss_type_is_orpo(self): + trainer = _make_dpo(ORPOTrainer, {"orpo_beta": 0.1}) + assert trainer.loss_type == "orpo" + + def test_beta_default(self): + trainer = _make_dpo(ORPOTrainer, {"orpo_beta": 0.1}) + assert trainer.beta == 0.1 + + def test_beta_override_from_config(self): + trainer = _make_dpo(ORPOTrainer, {"orpo_beta": 0.25}) + assert trainer.beta == 0.25 + + +class TestGRPOTrainerInit: + def test_loss_type_is_grpo(self): + trainer = _make_dpo(GRPOTrainer, {"grpo_beta": 0.04, "grpo_epsilon": 0.2, "grpo_num_generations": 4}) + assert trainer.loss_type == "grpo" + + def test_beta_default(self): + trainer = _make_dpo(GRPOTrainer, {"grpo_beta": 0.04, "grpo_epsilon": 0.2, "grpo_num_generations": 4}) + assert trainer.beta == 0.04 + + def test_epsilon_default(self): + trainer = _make_dpo(GRPOTrainer, {"grpo_beta": 0.04, "grpo_epsilon": 0.2, "grpo_num_generations": 4}) + assert trainer.epsilon == 0.2 + + def test_num_generations_default(self): + trainer = _make_dpo(GRPOTrainer, {"grpo_beta": 0.04, "grpo_epsilon": 0.2, "grpo_num_generations": 4}) + assert trainer.num_generations == 4 + + def test_override_all_grpo_params(self): + trainer = _make_dpo(GRPOTrainer, {"grpo_beta": 0.1, "grpo_epsilon": 0.4, "grpo_num_generations": 8}) + assert trainer.beta == 0.1 + assert trainer.epsilon == 0.4 + assert trainer.num_generations == 8 + + +class TestTrainerRegistration: + def test_dpo_registered(self): + assert "dpo" in get_llm_trainers_registry() + + def test_kto_registered(self): + assert "kto" in get_llm_trainers_registry() + + def test_orpo_registered(self): + assert "orpo" in get_llm_trainers_registry() + + def test_grpo_registered(self): + assert "grpo" in get_llm_trainers_registry() + + def test_dpo_maps_to_dpo_trainer(self): + assert get_llm_trainers_registry()["dpo"] is DPOTrainer + + def test_kto_maps_to_kto_trainer(self): + assert get_llm_trainers_registry()["kto"] is KTOTrainer + + def test_orpo_maps_to_orpo_trainer(self): + assert get_llm_trainers_registry()["orpo"] is ORPOTrainer + + def test_grpo_maps_to_grpo_trainer(self): + assert get_llm_trainers_registry()["grpo"] is GRPOTrainer diff --git a/tests/ludwig/trainers/test_mixins.py b/tests/ludwig/trainers/test_mixins.py new file mode 100644 index 0000000..21bea18 --- /dev/null +++ b/tests/ludwig/trainers/test_mixins.py @@ -0,0 +1,109 @@ +"""Tests for trainer mixins.""" + +import time + +from ludwig.trainers.mixins import ( + BatchSizeTuningMixin, + CheckpointMixin, + EarlyStoppingMixin, + MetricsMixin, + ProfilingMixin, +) + + +class TestCheckpointMixin: + def setup_method(self): + self.mixin = CheckpointMixin.__new__(CheckpointMixin) + + def test_checkpoint_at_epoch_end(self): + assert self.mixin.should_checkpoint(steps=5, steps_per_checkpoint=100, epoch_end=True) + + def test_checkpoint_at_step_interval(self): + assert self.mixin.should_checkpoint(steps=100, steps_per_checkpoint=100) + + def test_no_checkpoint_mid_interval(self): + assert not self.mixin.should_checkpoint(steps=50, steps_per_checkpoint=100) + + def test_no_checkpoint_when_disabled(self): + assert not self.mixin.should_checkpoint(steps=100, steps_per_checkpoint=0) + + +class TestEarlyStoppingMixin: + def setup_method(self): + self.mixin = EarlyStoppingMixin.__new__(EarlyStoppingMixin) + + def test_stop_when_no_improvement(self): + assert self.mixin.should_early_stop(steps_since_improvement=10, early_stop_rounds=10) + + def test_no_stop_when_improving(self): + assert not self.mixin.should_early_stop(steps_since_improvement=3, early_stop_rounds=10) + + def test_no_stop_when_disabled_zero(self): + assert not self.mixin.should_early_stop(steps_since_improvement=100, early_stop_rounds=0) + + def test_no_stop_when_disabled_negative(self): + assert not self.mixin.should_early_stop(steps_since_improvement=100, early_stop_rounds=-1) + + +class TestMetricsMixin: + def setup_method(self): + self.mixin = MetricsMixin.__new__(MetricsMixin) + + def test_format_simple_metrics(self): + metrics = {"label": {"accuracy": 0.9512, "loss": 0.1234}} + result = self.mixin.format_metrics(metrics) + assert "label.accuracy=0.9512" in result + assert "label.loss=0.1234" in result + + def test_format_with_prefix(self): + metrics = {"label": {"accuracy": 0.85}} + result = self.mixin.format_metrics(metrics, prefix="val") + assert "val.label.accuracy=0.8500" in result + + def test_format_empty_metrics(self): + result = self.mixin.format_metrics({}) + assert result == "" + + def test_format_skips_non_float(self): + metrics = {"label": {"accuracy": 0.9, "predictions": [1, 2, 3]}} + result = self.mixin.format_metrics(metrics) + assert "accuracy" in result + assert "predictions" not in result + + +class TestBatchSizeTuningMixin: + def test_returns_max_when_no_oom(self): + mixin = BatchSizeTuningMixin.__new__(BatchSizeTuningMixin) + # Without actual model/dataset, find_max_batch_size just returns max + result = mixin.find_max_batch_size(model=None, dataset=None, max_batch_size=512) + assert result == 512 + + +class TestProfilingMixin: + def setup_method(self): + self.mixin = ProfilingMixin.__new__(ProfilingMixin) + self.mixin.__init_profiling__() + + def test_start_stop_timer(self): + self.mixin.start_timer("train_step") + time.sleep(0.01) + elapsed = self.mixin.stop_timer("train_step") + assert elapsed > 0 + + def test_timing_accumulates(self): + self.mixin.start_timer("eval") + time.sleep(0.01) + self.mixin.stop_timer("eval") + self.mixin.start_timer("eval") + time.sleep(0.01) + self.mixin.stop_timer("eval") + summary = self.mixin.get_timing_summary() + assert "eval" in summary + assert summary["eval"] > 0.01 + + def test_stop_nonexistent_timer(self): + elapsed = self.mixin.stop_timer("nonexistent") + assert elapsed == 0.0 + + def test_timing_summary_empty(self): + assert self.mixin.get_timing_summary() == {} diff --git a/tests/ludwig/trainers/test_trainer_helpers.py b/tests/ludwig/trainers/test_trainer_helpers.py new file mode 100644 index 0000000..b26e5e4 --- /dev/null +++ b/tests/ludwig/trainers/test_trainer_helpers.py @@ -0,0 +1,91 @@ +"""Unit tests for Trainer helper methods extracted in PR-6. + +Tests use lightweight mocks so they run without a GPU, a trained model, or a +full dataset — each extracted method is tested in isolation. +""" + +from unittest.mock import MagicMock + +import numpy as np +import torch + + +class TestBatchToTensors: + """Tests for Trainer._batch_to_tensors.""" + + def _make_trainer(self, device="cpu"): + """Build a minimal Trainer-like object with just the state _batch_to_tensors needs.""" + from ludwig.trainers.trainer import Trainer + + trainer = Trainer.__new__(Trainer) + trainer.device = device + + # Create mock input and output features + in_feat = MagicMock() + in_feat.feature_name = "text" + in_feat.proc_column = "text_proc" + + out_feat = MagicMock() + out_feat.feature_name = "label" + out_feat.proc_column = "label_proc" + + trainer.model = MagicMock() + trainer.model.input_features = {"text": in_feat} + trainer.model.output_features = {"label": out_feat} + return trainer + + def test_returns_inputs_and_targets(self): + trainer = self._make_trainer() + batch = { + "text_proc": np.array([1.0, 2.0, 3.0]), + "label_proc": np.array([0.0, 1.0, 0.0]), + } + inputs, targets = trainer._batch_to_tensors(batch) + assert "text" in inputs + assert "label" in targets + + def test_inputs_are_tensors(self): + trainer = self._make_trainer() + batch = { + "text_proc": np.array([1.0, 2.0]), + "label_proc": np.array([0.0, 1.0]), + } + inputs, targets = trainer._batch_to_tensors(batch) + assert isinstance(inputs["text"], torch.Tensor) + assert isinstance(targets["label"], torch.Tensor) + + def test_values_match_numpy_input(self): + trainer = self._make_trainer() + arr = np.array([3.0, 1.0, 4.0, 1.0, 5.0]) + batch = {"text_proc": arr, "label_proc": np.zeros(5)} + inputs, _ = trainer._batch_to_tensors(batch) + np.testing.assert_array_equal(inputs["text"].numpy(), arr) + + def test_tensors_on_correct_device(self): + trainer = self._make_trainer(device="cpu") + batch = {"text_proc": np.array([1.0]), "label_proc": np.array([0.0])} + inputs, targets = trainer._batch_to_tensors(batch) + assert inputs["text"].device.type == "cpu" + assert targets["label"].device.type == "cpu" + + def test_multiple_input_features(self): + from ludwig.trainers.trainer import Trainer + + trainer = Trainer.__new__(Trainer) + trainer.device = "cpu" + + feats = {} + for name in ("a", "b", "c"): + f = MagicMock() + f.feature_name = name + f.proc_column = f"{name}_proc" + feats[name] = f + + trainer.model = MagicMock() + trainer.model.input_features = feats + trainer.model.output_features = {} + + batch = {f"{n}_proc": np.array([float(i)]) for i, n in enumerate("abc")} + inputs, targets = trainer._batch_to_tensors(batch) + assert set(inputs.keys()) == {"a", "b", "c"} + assert targets == {} diff --git a/tests/ludwig/utils/__init__.py b/tests/ludwig/utils/__init__.py new file mode 100644 index 0000000..42715bb --- /dev/null +++ b/tests/ludwig/utils/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== diff --git a/tests/ludwig/utils/automl/test_type_inference.py b/tests/ludwig/utils/automl/test_type_inference.py new file mode 100644 index 0000000..1554956 --- /dev/null +++ b/tests/ludwig/utils/automl/test_type_inference.py @@ -0,0 +1,127 @@ +import random + +import pytest + +from ludwig.constants import AUDIO, BINARY, CATEGORY, DATE, IMAGE, NUMBER, TEXT +from ludwig.data.dataset_synthesizer import generate_string +from ludwig.utils.automl.field_info import FieldInfo +from ludwig.utils.automl.type_inference import infer_type, should_exclude + +ROW_COUNT = 100 +TARGET_NAME = "target" + + +@pytest.mark.parametrize( + "num_distinct_values,distinct_values,img_values,audio_values,avg_words,missing_vals,expected", + [ + # Random numbers. + (ROW_COUNT, [str(random.random()) for _ in range(ROW_COUNT)], 0, 0, None, 0.0, NUMBER), + # Random numbers with NaNs. + (ROW_COUNT, [str(random.random()) for _ in range(ROW_COUNT - 1)] + ["NaN"], 0, 0, None, 0.0, NUMBER), + # Finite list of numbers. + (10, ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 0, 0, None, 0.0, CATEGORY), + (2, ["1.5", "3.7"], 0, 0, None, 0.1, NUMBER), + (2, ["1.5", "3.7", "nan"], 0, 0, None, 0.1, NUMBER), + # Bool-like values. + (2, ["0", "1"], 0, 0, None, 0.0, BINARY), + # Mostly bool-like values. + (3, ["0", "1", "True"], 0, 0, None, 0.0, CATEGORY), + # Non-conventional booleans are treated as categories since we cannot infer true/false labels. + pytest.param(2, ["<=50K", ">50K"], 0, 0, None, 0.0, CATEGORY, id="non-conventional-bools"), + # Finite list of strings. + (2, ["human", "bot"], 0, 0, None, 0.0, CATEGORY), + (10, [generate_string(5) for _ in range(10)], 0, 0, None, 0.0, CATEGORY), + (40, [generate_string(5) for _ in range(40)], 0, 0, None, 0.0, CATEGORY), + # Mostly random strings. + (90, [generate_string(5) for _ in range(90)], 0, 0, None, 0.0, TEXT), + # Mostly random strings with capped distinct values. + (90, [generate_string(5) for _ in range(10)], 0, 0, None, 0.0, TEXT), + # All random strings. + (ROW_COUNT, [generate_string(5) for _ in range(ROW_COUNT)], 0, 0, None, 0.0, TEXT), + # Images. + (ROW_COUNT, [], ROW_COUNT, 0, None, 0.0, IMAGE), + # Audio. + (ROW_COUNT, [], 0, ROW_COUNT, None, 0.0, AUDIO), + # Text with low distinct value percent / high missing value percent + (ROW_COUNT // 4, [generate_string(5) for _ in range(ROW_COUNT)], 0, 0, 5, 0.75, TEXT), + (ROW_COUNT // 4, [generate_string(5) for _ in range(ROW_COUNT)], 0, 0, 5, 0.25, CATEGORY), + ], +) +def test_infer_type(num_distinct_values, distinct_values, img_values, audio_values, avg_words, missing_vals, expected): + field = FieldInfo( + name="foo", + dtype="object", + num_distinct_values=num_distinct_values, + distinct_values=distinct_values, + image_values=img_values, + audio_values=audio_values, + avg_words=avg_words, + ) + assert infer_type(field, missing_vals, ROW_COUNT) == expected + + +def test_infer_type_explicit_date(): + field = FieldInfo( + name="foo", + distinct_values=["1", "2"], + num_distinct_values=2, + dtype=DATE, + ) + assert infer_type(field, 0, ROW_COUNT) == DATE + + +@pytest.mark.parametrize( + "idx,num_distinct_values,dtype,name,expected", + [ + (3, ROW_COUNT, NUMBER, "id", True), + (0, ROW_COUNT, NUMBER, "index", True), + (1, ROW_COUNT, NUMBER, "index", False), + (0, ROW_COUNT, NUMBER, "foo", False), + (3, ROW_COUNT, TEXT, "uuid", True), + (0, ROW_COUNT, TEXT, "name", False), + (0, ROW_COUNT, NUMBER, TARGET_NAME, False), + (0, ROW_COUNT - 1, NUMBER, "id", False), + (0, 0, CATEGORY, "empty_col", True), + ], +) +def test_should_exclude(idx, num_distinct_values, dtype, name, expected): + column_count = 10 + field = FieldInfo(name=name, dtype=dtype, num_distinct_values=num_distinct_values, avg_words=10) + assert should_exclude(idx, field, dtype, column_count, ROW_COUNT, {TARGET_NAME}) == expected + + +def test_auto_type_inference_single_value_binary_feature(): + field = FieldInfo( + name="foo", dtype="object", num_distinct_values=1, distinct_values=["1" for i in range(ROW_COUNT)] + ) + assert infer_type(field=field, missing_value_percent=0, row_count=ROW_COUNT) == CATEGORY + assert should_exclude( + idx=3, field=field, dtype="object", column_count=10, row_count=ROW_COUNT, targets={TARGET_NAME} + ) + + +@pytest.mark.parametrize( + "column_count,avg_words,expected", + [ + (1, 10, False), + (1, 2, False), + (5, 2, True), + (5, 10, False), + ], +) +def test_should_exclude_text(column_count, avg_words, expected): + field = FieldInfo(name="sentence", dtype=TEXT, avg_words=avg_words, num_distinct_values=ROW_COUNT) + assert should_exclude(0, field, TEXT, column_count, ROW_COUNT, {TARGET_NAME}) == expected + + +@pytest.mark.parametrize("negative_class", ("-1", "-1.0"), ids=["-1", "-1.0"]) +def test_type_inference_with_negative_positive_binary_values(negative_class): + """This test ensures that we infer binary type for a feature with negative and positive values, specifically -1 + and 1.""" + field = FieldInfo( + name="foo", + dtype="object", + num_distinct_values=2, + distinct_values=["1", negative_class], + ) + assert infer_type(field=field, missing_value_percent=0, row_count=ROW_COUNT) == BINARY diff --git a/tests/ludwig/utils/automl/test_utils.py b/tests/ludwig/utils/automl/test_utils.py new file mode 100644 index 0000000..d8909a1 --- /dev/null +++ b/tests/ludwig/utils/automl/test_utils.py @@ -0,0 +1,18 @@ +import pandas as pd +import pytest + +from ludwig.utils.automl.utils import avg_num_tokens + + +@pytest.mark.parametrize( + "field,expected", + [ + (pd.Series([None]), 0), + (pd.Series(["string1", "string2", "string3"]), 1), + (pd.Series([b"string1", b"string2", b"string3"]), 1), + (pd.Series([b"string1 string1", b"string2 string2", b"string3 string3"]), 2), + (pd.Series([1, 2, 3]), 1), + ], +) +def test_avg_num_tokens(field, expected): + assert avg_num_tokens(field) == expected diff --git a/tests/ludwig/utils/entmax/test_losses.py b/tests/ludwig/utils/entmax/test_losses.py new file mode 100644 index 0000000..6c56a7e --- /dev/null +++ b/tests/ludwig/utils/entmax/test_losses.py @@ -0,0 +1,53 @@ +from functools import partial + +import pytest +import torch +from torch.autograd import gradcheck + +from ludwig.constants import IGNORE_INDEX_TOKEN_ID +from ludwig.utils.entmax.losses import Entmax15Loss, EntmaxBisectLoss, SparsemaxBisectLoss, SparsemaxLoss + +# make data +Xs = [torch.randn(4, 10, dtype=torch.float64, requires_grad=True) for _ in range(5)] + +ys = [torch.max(torch.randn_like(X), dim=1)[1] for X in Xs] + + +losses = [ + SparsemaxLoss, + partial(SparsemaxLoss, k=5), + Entmax15Loss, + partial(Entmax15Loss, k=5), + SparsemaxBisectLoss, + EntmaxBisectLoss, +] + + +@pytest.mark.parametrize("Loss", losses) +def test_non_neg(Loss): + for X, y in zip(Xs, ys): + ls = Loss(reduction="none") + lval = ls(X, y) + assert torch.all(lval >= 0) + + +@pytest.mark.parametrize("Loss", losses) +@pytest.mark.parametrize("ignore_index", (False, True)) +@pytest.mark.parametrize("reduction", ("sum", "elementwise_mean")) +def test_loss(Loss, ignore_index, reduction): + for X, y in zip(Xs, ys): + iix = y[0] if ignore_index else -100 + ls = Loss(ignore_index=iix, reduction=reduction) + gradcheck(ls, (X, y), eps=1e-5) + + +@pytest.mark.parametrize("Loss", losses) +def test_index_ignored(Loss): + x = torch.randn(20, 6, dtype=torch.float64, requires_grad=True) + _, y = torch.max(torch.randn_like(x), dim=1) + + loss_ignore = Loss(reduction="sum", ignore_index=y[0]) + loss_noignore = Loss(reduction="sum", ignore_index=IGNORE_INDEX_TOKEN_ID) + + # Note: since these are sparse losses, it is possible that an element makes no contribution to the loss. + assert loss_ignore(x, y) <= loss_noignore(x, y) diff --git a/tests/ludwig/utils/entmax/test_mask.py b/tests/ludwig/utils/entmax/test_mask.py new file mode 100644 index 0000000..588a20a --- /dev/null +++ b/tests/ludwig/utils/entmax/test_mask.py @@ -0,0 +1,45 @@ +import pytest +import torch + +from ludwig.utils.entmax.activations import Entmax15, Sparsemax +from ludwig.utils.entmax.root_finding import entmax_bisect, sparsemax_bisect + +funcs = [ + Sparsemax(dim=1), + Entmax15(dim=1), + Sparsemax(dim=1, k=512), + Entmax15(dim=1, k=512), + sparsemax_bisect, + entmax_bisect, +] + + +@pytest.mark.parametrize("func", funcs) +@pytest.mark.parametrize("dtype", (torch.float32, torch.float64)) +def test_mask(func, dtype): + torch.manual_seed(42) + x = torch.randn(2, 6, dtype=dtype) + x[:, 3:] = -float("inf") + x0 = x[:, :3] + + y = func(x) + y0 = func(x0) + + y[:, :3] -= y0 + + assert torch.allclose(y, torch.zeros_like(y)) + + +@pytest.mark.parametrize("alpha", (1.25, 1.5, 1.75, 2.25)) +def test_mask_alphas(alpha): + torch.manual_seed(42) + x = torch.randn(2, 6) + x[:, 3:] = -float("inf") + x0 = x[:, :3] + + y = entmax_bisect(x, alpha) + y0 = entmax_bisect(x0, alpha) + + y[:, :3] -= y0 + + assert torch.allclose(y, torch.zeros_like(y)) diff --git a/tests/ludwig/utils/entmax/test_root_finding.py b/tests/ludwig/utils/entmax/test_root_finding.py new file mode 100644 index 0000000..b55ddde --- /dev/null +++ b/tests/ludwig/utils/entmax/test_root_finding.py @@ -0,0 +1,99 @@ +from functools import partial +from itertools import product + +import pytest +import torch +from torch.autograd import gradcheck + +from ludwig.utils.entmax.activations import entmax15, sparsemax +from ludwig.utils.entmax.root_finding import entmax_bisect, sparsemax_bisect + +# @pytest.mark.parametrize("dim", (0, 1, 2)) +# def test_dim(dim, Map): +# for _ in range(10): +# x = torch.randn(5, 6, 7, requires_grad=True, dtype=torch.float64) +# # gradcheck(f, (x,)) + + +@pytest.mark.parametrize("training", [True, False]) +@pytest.mark.parametrize("bisect_training", [True, False]) +def test_sparsemax(training, bisect_training): + x = 0.5 * torch.randn(4, 6, dtype=torch.float32) + p1 = sparsemax(x, 1, training=training) + p2 = sparsemax_bisect(x, training=bisect_training) + assert torch.sum((p1 - p2) ** 2) < 1e-7 + + +@pytest.mark.parametrize("training", [True, False]) +@pytest.mark.parametrize("bisect_training", [True, False]) +def test_entmax15(training, bisect_training): + x = 0.5 * torch.randn(4, 6, dtype=torch.float32) + p1 = entmax15(x, 1, training=training) + p2 = entmax_bisect(x, alpha=1.5, training=bisect_training) + assert torch.sum((p1 - p2) ** 2) < 1e-7 + + +def test_sparsemax_grad(): + x = torch.randn(4, 6, dtype=torch.float64, requires_grad=True) + gradcheck(sparsemax_bisect, (x,), eps=1e-5) + + +@pytest.mark.parametrize("alpha", (0.2, 0.5, 0.75, 1.2, 1.5, 1.75, 2.25)) +def test_entmax_grad(alpha): + alpha = torch.tensor(alpha, dtype=torch.float64, requires_grad=True) + x = torch.randn(4, 6, dtype=torch.float64, requires_grad=True) + gradcheck(entmax_bisect, (x, alpha), eps=1e-5) + + +def test_entmax_correct_multiple_alphas(): + n = 4 + x = torch.randn(n, 6, dtype=torch.float64, requires_grad=True) + alpha = 0.05 + 2.5 * torch.rand((n, 1), dtype=torch.float64, requires_grad=True) + + p1 = entmax_bisect(x, alpha) + p2_ = [entmax_bisect(x[i].unsqueeze(0), alpha[i].item()).squeeze() for i in range(n)] + p2 = torch.stack(p2_) + + assert torch.allclose(p1, p2) + + +def test_entmax_grad_multiple_alphas(): + n = 4 + x = torch.randn(n, 6, dtype=torch.float64, requires_grad=True) + alpha = 0.05 + 2.5 * torch.rand((n, 1), dtype=torch.float64, requires_grad=True) + gradcheck(entmax_bisect, (x, alpha), eps=1e-5) + + +@pytest.mark.parametrize("dim", (0, 1, 2, 3)) +def test_arbitrary_dimension(dim): + shape = [3, 4, 2, 5] + X = torch.randn(*shape, dtype=torch.float64) + + alpha_shape = shape + alpha_shape[dim] = 1 + + alphas = 0.05 + 2.5 * torch.rand(alpha_shape, dtype=torch.float64) + + P = entmax_bisect(X, alpha=alphas, dim=dim) + + ranges = [list(range(k)) if i != dim else [slice(None)] for i, k in enumerate(shape)] + + for ix in product(*ranges): + x = X[ix].unsqueeze(0) + alpha = alphas[ix].item() + p_true = entmax_bisect(x, alpha=alpha, dim=-1) + assert torch.allclose(P[ix], p_true) + + +@pytest.mark.parametrize("dim", (0, 1, 2, 3)) +def test_arbitrary_dimension_grad(dim): + shape = [3, 4, 2, 5] + + alpha_shape = shape + alpha_shape[dim] = 1 + + f = partial(entmax_bisect, dim=dim) + + X = torch.randn(*shape, dtype=torch.float64, requires_grad=True) + alphas = 0.05 + 2.5 * torch.rand(alpha_shape, dtype=torch.float64, requires_grad=True) + gradcheck(f, (X, alphas), eps=1e-5) diff --git a/tests/ludwig/utils/entmax/test_topk.py b/tests/ludwig/utils/entmax/test_topk.py new file mode 100644 index 0000000..d4f7063 --- /dev/null +++ b/tests/ludwig/utils/entmax/test_topk.py @@ -0,0 +1,41 @@ +import pytest +import torch +from torch.autograd import gradcheck + +from ludwig.utils.entmax.activations import ( + _entmax_threshold_and_support, + _sparsemax_threshold_and_support, + Entmax15, + Sparsemax, +) + + +@pytest.mark.parametrize("dim", (0, 1, 2)) +@pytest.mark.parametrize("Map", (Sparsemax, Entmax15)) +def test_mapping(dim, Map): + f = Map(dim=dim, k=3) + x = torch.randn(3, 4, 5, requires_grad=True, dtype=torch.float64) + gradcheck(f, (x,)) + + +@pytest.mark.parametrize("dim", (0, 1, 2)) +@pytest.mark.parametrize("coef", (0.00001, 0.5, 10000)) +def test_entmax_topk(dim, coef): + x = coef * torch.randn(3, 4, 5) + tau1, supp1 = _entmax_threshold_and_support(x, dim=dim, k=None) + tau2, supp2 = _entmax_threshold_and_support(x, dim=dim, k=5) + + assert torch.all(tau1 == tau2) + assert torch.all(supp1 == supp2) + + +@pytest.mark.parametrize("dim", (0, 1, 2)) +@pytest.mark.parametrize("coef", (0.00001, 0.5, 10000)) +@pytest.mark.parametrize("k", (5, 30)) +def test_sparsemax_topk(dim, coef, k): + x = coef * torch.randn(3, 4, 5) + tau1, supp1 = _sparsemax_threshold_and_support(x, dim=dim, k=None) + tau2, supp2 = _sparsemax_threshold_and_support(x, dim=dim, k=k) + + assert torch.all(tau1 == tau2) + assert torch.all(supp1 == supp2) diff --git a/tests/ludwig/utils/test_algorithm_utils.py b/tests/ludwig/utils/test_algorithm_utils.py new file mode 100644 index 0000000..30153ca --- /dev/null +++ b/tests/ludwig/utils/test_algorithm_utils.py @@ -0,0 +1,20 @@ +import pytest + +from ludwig.utils.algorithms_utils import topological_sort + + +@pytest.mark.parametrize( + "unsorted,sorted", + [ + ( + [(2, []), (5, [11]), (11, [2, 9, 10]), (7, [11, 8]), (9, []), (10, []), (8, [9]), (3, [10, 8])], + [(2, []), (9, []), (10, []), (8, [9]), (3, [10, 8]), (11, [2, 9, 10]), (7, [11, 8]), (5, [11])], + ), + ( + [("macro", ["action", "contact_type"]), ("contact_type", None), ("action", ["contact_type"])], + [("contact_type", []), ("action", ["contact_type"]), ("macro", ["action", "contact_type"])], + ), + ], +) +def test_topological_sort(unsorted: list, sorted: list) -> None: + assert topological_sort(unsorted) == sorted diff --git a/tests/ludwig/utils/test_audio_utils.py b/tests/ludwig/utils/test_audio_utils.py new file mode 100644 index 0000000..1602d00 --- /dev/null +++ b/tests/ludwig/utils/test_audio_utils.py @@ -0,0 +1,23 @@ +import pytest + +from ludwig.utils.audio_utils import is_audio_score + + +@pytest.mark.parametrize( + "path, score", + [ + ("data.wav", 1), + ("/home/peter/file.amb", 1), + ("my.mp3", 1), + ("data.ogg", 1), + ("data.vorbis", 1), + ("data.flac", 1), + ("data.opus", 1), + ("data.sphere", 1), + ("video.mp4", 0), + ("image.png", 0), + (".wav/image.png", 0), + ], +) +def test_is_audio_score(path: str, score: int): + assert is_audio_score(path) == score diff --git a/tests/ludwig/utils/test_backward_compatibility.py b/tests/ludwig/utils/test_backward_compatibility.py new file mode 100644 index 0000000..a46fd9d --- /dev/null +++ b/tests/ludwig/utils/test_backward_compatibility.py @@ -0,0 +1,864 @@ +import copy +import math +from typing import Any + +import pytest + +from ludwig.constants import ( + BATCH_SIZE, + BFILL, + CLASS_WEIGHTS, + DEFAULTS, + EVAL_BATCH_SIZE, + EXECUTOR, + HYPEROPT, + INPUT_FEATURES, + LEARNING_RATE_SCHEDULER, + LOSS, + NUMBER, + OUTPUT_FEATURES, + PREPROCESSING, + SCHEDULER, + SPLIT, + TRAINER, + TYPE, +) +from ludwig.schema.model_config import ModelConfig +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.utils.backward_compatibility import ( + _update_backend_cache_credentials, + _upgrade_encoder_decoder_params, + _upgrade_feature, + _upgrade_preprocessing_split, + upgrade_config_dict_to_latest_version, + upgrade_missing_value_strategy, + upgrade_model_progress, +) +from ludwig.utils.trainer_utils import TrainerMetric + + +def test_preprocessing_backward_compatibility(): + # From v0.5.3. + preprocessing_config = { + "force_split": False, + "split_probabilities": [0.7, 0.1, 0.2], + "stratify": None, + } + + _upgrade_preprocessing_split(preprocessing_config) + + assert preprocessing_config == { + "split": {"probabilities": [0.7, 0.1, 0.2], "type": "random"}, + } + + +def test_audio_feature_backward_compatibility(): + # From v0.5.3. + + audio_feature_preprocessing_config = { + "name": "audio_feature", + "type": "audio", + "preprocessing": { + "audio_file_length_limit_in_s": 7.5, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": None, + "audio_feature": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_fft_points": None, + "window_type": "hamming", + "num_filter_bands": 80, + }, + }, + } + + global_preprocessing_config = { + "audio": { + "audio_file_length_limit_in_s": 7.5, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": None, + "audio_feature": { + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_fft_points": None, + "window_type": "hamming", + "num_filter_bands": 80, + }, + }, + } + + _upgrade_feature(audio_feature_preprocessing_config) + _upgrade_preprocessing_split(global_preprocessing_config) + + assert global_preprocessing_config == { + "audio": { + "audio_file_length_limit_in_s": 7.5, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": None, + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_fft_points": None, + "window_type": "hamming", + "num_filter_bands": 80, + } + } + + assert audio_feature_preprocessing_config == { + "name": "audio_feature", + "type": "audio", + "preprocessing": { + "audio_file_length_limit_in_s": 7.5, + "missing_value_strategy": BFILL, + "in_memory": True, + "padding_value": 0, + "norm": None, + "type": "fbank", + "window_length_in_s": 0.04, + "window_shift_in_s": 0.02, + "num_fft_points": None, + "window_type": "hamming", + "num_filter_bands": 80, + }, + } + + +def test_encoder_decoder_backwards_compatibility(): + old_config = { + "input_features": [ + { + "name": "text_feature", + "type": "text", + "preprocessing": { + "missing_value_strategy": "drop_row", + }, + "encoder": "rnn", + "bidirectional": True, + "representation": "dense", + "num_layers": 2, + }, + { + "name": "image_feature_1", + "type": "image", + "preprocessing": { + "height": 7.5, + "width": 7.5, + "num_channels": 4, + }, + "encoder": "resnet", + "num_channels": 4, + "dropout": 0.1, + "resnet_size": 100, + }, + { + "name": "image_feature_2", + "type": "image", + "tied": "image_feature_1", + "preprocessing": { + "height": 7.5, + "width": 7.5, + "num_channels": 4, + }, + "encoder": "resnet", + }, + ], + "output_features": [ + { + "name": "category_feature", + "type": "category", + "top_k": 3, + "preprocessing": { + "missing_value_strategy": BFILL, + }, + "decoder": "classifier", + "num_classes": 10, + "use_bias": False, + }, + { + "name": "binary_feature", + "type": "binary", + "dependencies": ["category_feature"], + "loss": { + "type": "cross_entropy", + }, + "reduce_dependencies": "mean", + "decoder": "regressor", + "use_bias": True, + "bias_initializer": "constant", + }, + { + "name": "vector_feature", + "type": "vector", + "decoder": "projector", + "num_fc_layers": 5, + "output_size": 128, + "activation": "tanh", + "dropout": 0.1, + }, + ], + } + + for feature in old_config[INPUT_FEATURES]: + _upgrade_encoder_decoder_params(feature, True) + + for feature in old_config[OUTPUT_FEATURES]: + _upgrade_encoder_decoder_params(feature, False) + + assert old_config == { + "input_features": [ + { + "name": "text_feature", + "type": "text", + "preprocessing": { + "missing_value_strategy": "drop_row", + }, + "encoder": { + "type": "rnn", + "bidirectional": True, + "representation": "dense", + "num_layers": 2, + }, + }, + { + "name": "image_feature_1", + "type": "image", + "preprocessing": { + "height": 7.5, + "width": 7.5, + "num_channels": 4, + }, + "encoder": { + "type": "resnet", + "num_channels": 4, + "dropout": 0.1, + "resnet_size": 100, + }, + }, + { + "name": "image_feature_2", + "type": "image", + "tied": "image_feature_1", + "preprocessing": { + "height": 7.5, + "width": 7.5, + "num_channels": 4, + }, + "encoder": {"type": "resnet"}, + }, + ], + "output_features": [ + { + "name": "category_feature", + "type": "category", + "num_classes": 10, + "top_k": 3, + "preprocessing": { + "missing_value_strategy": BFILL, + }, + "decoder": { + "type": "classifier", + "fc_use_bias": False, + "use_bias": False, + }, + }, + { + "name": "binary_feature", + "type": "binary", + "dependencies": ["category_feature"], + "loss": { + "type": "cross_entropy", + }, + "reduce_dependencies": "mean", + "decoder": { + "type": "regressor", + "fc_use_bias": True, + "fc_bias_initializer": "constant", + "bias_initializer": "constant", + "use_bias": True, + }, + }, + { + "name": "vector_feature", + "type": "vector", + "decoder": { + "type": "projector", + "num_fc_layers": 5, + "fc_output_size": 128, + "fc_activation": "tanh", + "fc_dropout": 0.1, + "output_size": 128, + "activation": "tanh", + "dropout": 0.1, + }, + }, + ], + } + + +def test_deprecated_field_aliases(): + config = { + "ludwig_version": "0.4", + INPUT_FEATURES: [{"name": "num_in", "type": "numerical"}], + OUTPUT_FEATURES: [{"name": "num_out", "type": "numerical"}], + HYPEROPT: { + "parameters": { + "training.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + }, + "goal": "minimize", + "sampler": {"type": "grid", "num_samples": 2, "scheduler": {"type": "fifo"}}, + "executor": { + "type": "grid", + "search_alg": "bohb", + }, + }, + PREPROCESSING: { + "numerical": { + "fill_value": 2, + "missing_value_strategy": "fill_with_const", + }, + }, + "training": { + "epochs": 2, + "eval_batch_size": 0, + "reduce_learning_rate_on_plateau": 2, + "reduce_learning_rate_on_plateau_patience": 5, + "decay": True, + "learning_rate_warmup_epochs": 2, + }, + } + + updated_config = upgrade_config_dict_to_latest_version(config) + + assert updated_config["input_features"][0][TYPE] == NUMBER + assert updated_config["output_features"][0][TYPE] == NUMBER + + # "numerical" preprocssing directive should be translated to "number" and moved into the defaults section. + assert PREPROCESSING not in updated_config + assert updated_config[DEFAULTS][NUMBER][PREPROCESSING]["fill_value"] == 2 + + assert "training" not in updated_config + assert updated_config[TRAINER]["epochs"] == 2 + assert updated_config[TRAINER][EVAL_BATCH_SIZE] is None + + assert LEARNING_RATE_SCHEDULER in updated_config[TRAINER] + assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER]["reduce_on_plateau"] == 2 + assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER]["reduce_on_plateau_patience"] == 5 + assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER]["decay"] == "exponential" + assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER]["warmup_evaluations"] == 2 + + hparams = updated_config[HYPEROPT]["parameters"] + assert "training.learning_rate" not in hparams + assert "trainer.learning_rate" in hparams + + assert "sampler" not in updated_config[HYPEROPT] + + assert updated_config[HYPEROPT]["executor"]["type"] == "ray" + assert "num_samples" in updated_config[HYPEROPT]["executor"] + assert "scheduler" in updated_config[HYPEROPT]["executor"] + + ModelConfig.from_dict(updated_config) + + +@pytest.mark.parametrize("force_split", [None, False, True]) +@pytest.mark.parametrize("stratify", [None, "cat_in"]) +def test_deprecated_split_aliases(stratify, force_split): + split_probabilities = [0.6, 0.2, 0.2] + config = { + "ludwig_version": "0.4", + INPUT_FEATURES: [{"name": "num_in", "type": "number"}, {"name": "cat_in", "type": "category"}], + OUTPUT_FEATURES: [{"name": "num_out", "type": "number"}], + PREPROCESSING: { + "force_split": force_split, + "split_probabilities": split_probabilities, + "stratify": stratify, + }, + } + + updated_config = upgrade_config_dict_to_latest_version(config) + + assert "force_split" not in updated_config[PREPROCESSING] + assert "split_probabilities" not in updated_config[PREPROCESSING] + assert "stratify" not in updated_config[PREPROCESSING] + + assert SPLIT in updated_config[PREPROCESSING] + split = updated_config[PREPROCESSING][SPLIT] + + assert split["probabilities"] == split_probabilities + if stratify is None: + if force_split: + assert split.get(TYPE) == "random" + else: + assert split.get(TYPE) == "stratify" + assert split.get("column") == stratify + + +@pytest.mark.parametrize("use_scheduler", [True, False]) +def test_deprecated_hyperopt_sampler_early_stopping(use_scheduler): + sampler = { + "type": "ray", + "num_samples": 2, + } + + if use_scheduler: + sampler[SCHEDULER] = { + "type": "async_hyperband", + "max_t": 200, + "time_attr": "time_total_s", + "grace_period": 72, + "reduction_factor": 5, + } + + config = { + INPUT_FEATURES: [ + { + "type": "category", + "name": "cat_input_feature", + }, + ], + OUTPUT_FEATURES: [ + { + "type": "number", + "name": "num_output_feature", + }, + ], + "hyperopt": { + "search_alg": { + "type": "variant_generator", + }, + "executor": { + "type": "ray", + "time_budget_s": 200, + "cpu_resources_per_trial": 1, + }, + "sampler": sampler, + "parameters": { + "trainer.batch_size": { + "space": "choice", + "categories": [64, 128, 256], + }, + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + }, + }, + } + + updated_config = upgrade_config_dict_to_latest_version(config) + if use_scheduler: + assert SCHEDULER in updated_config[HYPEROPT][EXECUTOR] + + merged_config = ModelConfig.from_dict(updated_config).to_dict() + + # When a scheulder is provided, early stopping in the rendered config needs to be disabled to allow the + # hyperopt scheduler to manage trial lifecycle. + expected_early_stop = -1 if use_scheduler else ECDTrainerConfig().early_stop + assert merged_config[TRAINER]["early_stop"] == expected_early_stop + + +def test_validate_old_model_config(): + old_valid_config = { + "input_features": [ + {"name": "feature_1", "type": "category"}, + {"name": "Sex", "type": "category", "encoder": "dense"}, + ], + "output_features": [ + {"name": "Survived", "type": "category"}, + ], + } + + old_invalid_config = { + "input_features": [ + {"name": "feature_1", "type": "category"}, + {"name": "Sex", "type": "category", "encoder": "fake_encoder"}, + ], + "output_features": [ + {"name": "Survived", "type": "category"}, + ], + } + + ModelConfig.from_dict(old_valid_config) + + with pytest.raises(Exception): + ModelConfig.from_dict(old_invalid_config) + + +@pytest.mark.parametrize("missing_value_strategy", ["backfill", "pad"]) +def test_update_missing_value_strategy(missing_value_strategy: str): + old_valid_config = { + "input_features": [ + { + "name": "input_feature_1", + "type": "category", + "preprocessing": {"missing_value_strategy": missing_value_strategy}, + } + ], + "output_features": [ + {"name": "output_feature_1", "type": "category"}, + ], + } + + updated_config = upgrade_missing_value_strategy(old_valid_config) + + expected_config = copy.deepcopy(old_valid_config) + if missing_value_strategy == "backfill": + expected_config["input_features"][0]["preprocessing"]["missing_value_strategy"] = "bfill" + else: + expected_config["input_features"][0]["preprocessing"]["missing_value_strategy"] = "ffill" + + assert updated_config == expected_config + + +def test_update_increase_batch_size_on_plateau_max(): + old_valid_config = { + "input_features": [{"name": "input_feature_1", "type": "category"}], + "output_features": [{"name": "output_feature_1", "type": "category"}], + "trainer": { + "increase_batch_size_on_plateau_max": 256, + }, + } + + updated_config = upgrade_config_dict_to_latest_version(old_valid_config) + del updated_config["ludwig_version"] + + expected_config = copy.deepcopy(old_valid_config) + del expected_config["trainer"]["increase_batch_size_on_plateau_max"] + expected_config["trainer"]["max_batch_size"] = 256 + + assert updated_config == expected_config + + +def test_old_class_weights_default(): + old_config = { + "input_features": [ + { + "name": "input_feature_1", + "type": "category", + } + ], + "output_features": [ + {"name": "output_feature_1", "type": "category", "loss": {"class_weights": 1}}, + ], + } + + new_config = { + "input_features": [ + { + "name": "input_feature_1", + "type": "category", + } + ], + "output_features": [ + {"name": "output_feature_1", "type": "category", "loss": {"class_weights": None}}, + ], + } + + upgraded_config = upgrade_config_dict_to_latest_version(old_config) + del upgraded_config["ludwig_version"] + assert new_config == upgraded_config + + old_config[OUTPUT_FEATURES][0][LOSS][CLASS_WEIGHTS] = [0.5, 0.8, 1] + new_config[OUTPUT_FEATURES][0][LOSS][CLASS_WEIGHTS] = [0.5, 0.8, 1] + + upgraded_config = upgrade_config_dict_to_latest_version(old_config) + del upgraded_config["ludwig_version"] + assert new_config == upgraded_config + + +def test_upgrade_model_progress(): + old_model_progress = { + "batch_size": 64, + "best_eval_metric": 0.5, + "best_increase_batch_size_eval_metric": math.inf, + "best_reduce_learning_rate_eval_metric": math.inf, + "epoch": 2, + "last_improvement": 1, + "last_improvement_epoch": 1, + "best_eval_metric_epoch": 1, + "last_increase_batch_size": 0, + "last_increase_batch_size_epoch": 0, + "last_increase_batch_size_eval_metric_improvement": 0, + "last_learning_rate_reduction": 0, + "last_learning_rate_reduction_epoch": 0, + "last_reduce_learning_rate_eval_metric_improvement": 0, + "learning_rate": 0.001, + "num_increases_batch_size": 0, + "num_reductions_learning_rate": 0, + "steps": 224, + "test_metrics": { + "combined": {"loss": [0.59, 0.56]}, + "delinquent": { + "accuracy": [0.77, 0.78], + }, + }, + "train_metrics": {"combined": {"loss": [0.58, 0.55]}, "delinquent": {"roc_auc": [0.53, 0.54]}}, + "vali_metrics": {"combined": {"loss": [0.59, 0.60]}, "delinquent": {"roc_auc": [0.53, 0.44]}}, + } + + new_model_progress = upgrade_model_progress(old_model_progress) + + assert new_model_progress == { + "batch_size": 64, + "best_eval_metric_value": 0.5, + "best_increase_batch_size_eval_metric": float("inf"), + "epoch": 2, + "last_improvement_steps": 64, + "best_eval_metric_steps": 0, + "best_eval_metric_epoch": 1, + "last_increase_batch_size": 0, + "last_increase_batch_size_eval_metric_improvement": 0, + "last_learning_rate_reduction": 0, + "learning_rate": 0.001, + "num_increases_batch_size": 0, + "num_reductions_learning_rate": 0, + "steps": 224, + "test_metrics": { + "combined": { + "loss": [TrainerMetric(epoch=1, step=64, value=0.59), TrainerMetric(epoch=2, step=128, value=0.56)] + }, + "delinquent": { + "accuracy": [TrainerMetric(epoch=1, step=64, value=0.77), TrainerMetric(epoch=2, step=128, value=0.78)] + }, + }, + "train_metrics": { + "combined": { + "loss": [TrainerMetric(epoch=1, step=64, value=0.58), TrainerMetric(epoch=2, step=128, value=0.55)] + }, + "delinquent": { + "roc_auc": [TrainerMetric(epoch=1, step=64, value=0.53), TrainerMetric(epoch=2, step=128, value=0.54)] + }, + }, + "last_learning_rate_reduction_steps": 0, + "last_increase_batch_size_steps": 0, + "validation_metrics": { + "combined": { + "loss": [TrainerMetric(epoch=1, step=64, value=0.59), TrainerMetric(epoch=2, step=128, value=0.6)] + }, + "delinquent": { + "roc_auc": [TrainerMetric(epoch=1, step=64, value=0.53), TrainerMetric(epoch=2, step=128, value=0.44)] + }, + }, + "tune_checkpoint_num": 0, + "checkpoint_number": 0, + "best_eval_metric_checkpoint_number": 0, + "best_eval_train_metrics": {}, + "best_eval_validation_metrics": {}, + "best_eval_test_metrics": {}, + } + + # Verify that we don't make changes to already-valid model progress dicts. + # To do so, we modify the batch size value and re-run the upgrade on the otherwise-valid `new_model_progress` dict. + new_model_progress["batch_size"] = 1 + unchanged_model_progress = upgrade_model_progress(new_model_progress) + assert unchanged_model_progress == new_model_progress + + +def test_upgrade_model_progress_already_valid(): + # Verify that we don't make changes to already-valid model progress dicts. + valid_model_progress = { + BATCH_SIZE: 128, + "best_eval_metric_checkpoint_number": 7, + "best_eval_metric_epoch": 6, + "best_eval_metric_steps": 35, + "best_eval_metric_value": 0.719, + "best_eval_test_metrics": { + "Survived": {"accuracy": 0.634, "loss": 3.820, "roc_auc": 0.598}, + "combined": {"loss": 3.820}, + }, + "best_eval_train_metrics": { + "Survived": {"accuracy": 0.682, "loss": 4.006, "roc_auc": 0.634}, + "combined": {"loss": 4.006}, + }, + "best_eval_validation_metrics": { + "Survived": {"accuracy": 0.719, "loss": 4.396, "roc_auc": 0.667}, + "combined": {"loss": 4.396}, + }, + "best_increase_batch_size_eval_metric": float("inf"), + "checkpoint_number": 12, + "epoch": 12, + "last_increase_batch_size": 0, + "last_increase_batch_size_eval_metric_improvement": 0, + "last_increase_batch_size_steps": 0, + "last_learning_rate_reduction": 0, + "last_learning_rate_reduction_steps": 0, + "learning_rate": 0.001, + "num_increases_batch_size": 0, + "num_reductions_learning_rate": 0, + "steps": 60, + "test_metrics": { + "Survived": { + "accuracy": [ + [0, 5, 0.651], + [1, 10, 0.651], + ], + "loss": [ + [0, 5, 4.130], + [1, 10, 4.074], + ], + "roc_auc": [ + [0, 5, 0.574], + [1, 10, 0.595], + ], + }, + "combined": { + "loss": [ + [0, 5, 4.130], + [1, 10, 4.074], + ] + }, + }, + "train_metrics": { + "Survived": { + "accuracy": [ + [0, 5, 0.6875], + [1, 10, 0.6875], + ], + "loss": [ + [0, 5, 4.417], + [1, 10, 4.344], + ], + "roc_auc": [ + [0, 5, 0.628], + [1, 10, 0.629], + ], + }, + "combined": { + "loss": [ + [0, 5, 4.417], + [1, 10, 4.344], + ] + }, + }, + "tune_checkpoint_num": 0, + "validation_metrics": { + "Survived": { + "accuracy": [ + [0, 5, 0.696], + [1, 10, 0.696], + ], + "loss": [ + [0, 5, 4.494], + [1, 10, 4.473], + ], + "roc_auc": [ + [0, 5, 0.675], + [1, 10, 0.671], + ], + }, + "combined": { + "loss": [ + [0, 5, 4.494], + [1, 10, 4.473], + ] + }, + }, + } + + unchanged_model_progress = upgrade_model_progress(valid_model_progress) + assert unchanged_model_progress == valid_model_progress + + +def test_cache_credentials_backward_compatibility(): + # From v0.6.3. + creds = {"s3": {"client_kwargs": {}}} + backend = {"type": "local", "cache_dir": "/foo/bar", "cache_credentials": creds} + + _update_backend_cache_credentials(backend) + + assert backend == {"type": "local", "cache_dir": "/foo/bar", "credentials": {"cache": creds}} + + +@pytest.mark.parametrize( + "encoder,upgraded_type", + [ + ({"type": "resnet"}, "resnet"), + ({"type": "vit"}, "vit"), + ({"type": "_resnet_legacy"}, "resnet"), + ({"type": "_vit_legacy"}, "vit"), + ], + ids=["resnet", "vit", "resnet_legacy_internal", "vit_legacy_internal"], +) +def test_legacy_image_encoders(encoder: dict[str, Any], upgraded_type: str): + config = { + "input_features": [{"name": "image1", "type": "image", "encoder": encoder}], + "output_features": [{"name": "binary1", "type": "binary"}], + } + + updated_config = upgrade_config_dict_to_latest_version(config) + + expected_encoder = { + **encoder, + **{"type": upgraded_type}, + } + assert updated_config["input_features"][0]["encoder"] == expected_encoder + + +def test_load_config_missing_hyperopt(): + old_valid_config = { + "input_features": [ + {"name": "feature_1", "type": "category"}, + {"name": "Sex", "type": "category", "encoder": "dense"}, + ], + "output_features": [ + {"name": "Survived", "type": "category"}, + ], + "combiner": {"type": "concat"}, + "trainer": {}, + "hyperopt": {}, + } + + config_obj = ModelConfig.from_dict(old_valid_config) + assert config_obj.hyperopt is None + assert config_obj.to_dict()[HYPEROPT] is None + + +def test_type_removed_from_defaults_config(): + config = { + "input_features": [ + {"name": "feature_1", "type": "category"}, + {"name": "Sex", "type": "category"}, + ], + "output_features": [ + {"name": "Survived", "type": "category"}, + ], + "defaults": { + "binary": { + "encoder": { + "type": "passthrough", + }, + "preprocessing": { + "missing_value_strategy": "fill_with_false", + }, + "type": "binary", + }, + "category": { + "encoder": { + "type": "onehot", + }, + "preprocessing": { + "missing_value_strategy": "fill_with_const", + "most_common": 10000, + }, + "type": "category", + }, + }, + "model_type": "ecd", + } + + config_obj = ModelConfig.from_dict(config).to_dict() + + for feature_type in config_obj.get("defaults"): + assert "type" not in config_obj["defaults"][feature_type] diff --git a/tests/ludwig/utils/test_calibration.py b/tests/ludwig/utils/test_calibration.py new file mode 100644 index 0000000..59f292f --- /dev/null +++ b/tests/ludwig/utils/test_calibration.py @@ -0,0 +1,67 @@ +import numpy as np +import pytest + +from ludwig.utils import calibration + + +@pytest.fixture +def uncalibrated_logits_and_labels(): + """Returns a pair of logits (10x3) and labels (10).""" + return ( + np.array( + [ + [-3.596756, 6.728981, 6.3807454], + [-16.818138, -3.5217745, -1.7786252], + [-16.060827, 4.7207646, 3.5336719], + [-4.784969, 5.062503, 3.515455], + [-4.669478, 7.171067, 6.5137157], + [-32.596764, -3.5582566, -5.2003713], + [-4.4035864, 6.3911495, 4.7273974], + [-4.2035627, 7.846533, 6.0476217], + [-20.748848, -3.1521742, -4.873552], + [-4.8901286, 4.726167, 3.208372], + ] + ), + np.array([2, 0, 2, 1, 1, 2, 0, 1, 0, 1]), + ) + + +EPSILON = 0.1 # maximum relative precision error allowed. + + +def test_temperature_scaling_binary(uncalibrated_logits_and_labels): + logits, labels = uncalibrated_logits_and_labels + # Selects one category of the 3-class test fixture to treat as a binary classifier. + binary_logits = logits[:, 1] + binary_labels = labels == 1 + temperature_scaling = calibration.TemperatureScaling(binary=True) + calibration_result = temperature_scaling.train_calibration(binary_logits, binary_labels) + # Checks that we got close to optimal temperature. + # The exact temperature depends on optimizer internals (PyTorch version). + assert temperature_scaling.temperature.item() > 5.0 + # Checks that negative log-likelhood and expected calibration error are the same or lower post-calibration. + assert calibration_result.after_calibration_nll <= calibration_result.before_calibration_nll + assert calibration_result.after_calibration_ece <= calibration_result.before_calibration_ece + + +def test_temperature_scaling_category(uncalibrated_logits_and_labels): + logits, labels = uncalibrated_logits_and_labels + temperature_scaling = calibration.TemperatureScaling(num_classes=logits.shape[-1]) + calibration_result = temperature_scaling.train_calibration(logits, labels) + # Checks that we got close to optimal temperature + # The exact temperature depends on optimizer internals (PyTorch version). + # Check it's in a reasonable range and that calibration improved metrics. + assert temperature_scaling.temperature.item() > 5.0 + assert calibration_result.after_calibration_nll <= calibration_result.before_calibration_nll + assert calibration_result.after_calibration_ece <= calibration_result.before_calibration_ece + + +def test_matrix_scaling_category(uncalibrated_logits_and_labels): + logits, labels = uncalibrated_logits_and_labels + matrix_scaling = calibration.MatrixScaling(num_classes=logits.shape[-1]) + calibration_result = matrix_scaling.train_calibration(logits, labels) + # Matrix scaling may not have a single optimum, so multiple runs could give different results. + # In this case we don't check any specific values + # Checks that negative log-likelhood and expected calibration error are the same or lower post-calibration. + assert calibration_result.after_calibration_nll <= calibration_result.before_calibration_nll + assert calibration_result.after_calibration_ece <= calibration_result.before_calibration_ece diff --git a/tests/ludwig/utils/test_class_balancing.py b/tests/ludwig/utils/test_class_balancing.py new file mode 100644 index 0000000..6e44e80 --- /dev/null +++ b/tests/ludwig/utils/test_class_balancing.py @@ -0,0 +1,51 @@ +import numpy as np +import pandas as pd +import pytest + +from ludwig.backend.base import LocalBackend +from ludwig.constants import BALANCE_PERCENTAGE_TOLERANCE, NAME +from ludwig.data.preprocessing import balance_data + + +@pytest.mark.parametrize( + "method, balance", + [ + ("oversample_minority", 0.25), + ("oversample_minority", 0.5), + ("oversample_minority", 0.75), + ("undersample_majority", 0.25), + ("undersample_majority", 0.5), + ("undersample_majority", 0.75), + ("undersample_majority", 0.9), + ], +) +def test_balance(method, balance): + config = { + "input_features": [ + {"name": "Index", "proc_column": "Index", "type": "number"}, + {"name": "random_1", "proc_column": "random_1", "type": "number"}, + {"name": "random_2", "proc_column": "random_2", "type": "number"}, + ], + "output_features": [{"name": "Label", "proc_column": "Label", "type": "binary"}], + "preprocessing": {"oversample_minority": None, "undersample_majority": None}, + } + input_df = pd.DataFrame( + { + "Index": np.arange(0, 200, 1), + "random_1": np.random.randint(0, 50, 200), + "random_2": np.random.choice(["Type A", "Type B", "Type C", "Type D"], 200), + "Label": np.concatenate((np.zeros(180), np.ones(20))), + "split": np.zeros(200), + } + ) + + config["preprocessing"][method] = balance + backend = LocalBackend() + + test_df = balance_data(input_df, config["output_features"], config["preprocessing"], backend, 42) + target = config["output_features"][0][NAME] + majority_class = test_df[target].value_counts()[test_df[target].value_counts().idxmax()] + minority_class = test_df[target].value_counts()[test_df[target].value_counts().idxmin()] + new_class_balance = round(minority_class / majority_class, 2) + + assert abs(balance - new_class_balance) < BALANCE_PERCENTAGE_TOLERANCE diff --git a/tests/ludwig/utils/test_config_utils.py b/tests/ludwig/utils/test_config_utils.py new file mode 100644 index 0000000..91b1ca7 --- /dev/null +++ b/tests/ludwig/utils/test_config_utils.py @@ -0,0 +1,334 @@ +import copy +from typing import Any + +import pytest + +from ludwig.constants import ( + BASE_MODEL, + BINARY, + ENCODER, + INPUT_FEATURES, + MODEL_ECD, + MODEL_LLM, + MODEL_TYPE, + NAME, + OUTPUT_FEATURES, + TEXT, + TYPE, +) +from ludwig.schema.encoders.text_encoders import BERTConfig +from ludwig.schema.encoders.utils import get_encoder_cls +from ludwig.schema.features.preprocessing.text import TextPreprocessingConfig +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.config_utils import config_uses_llm, get_quantization + + +@pytest.mark.parametrize( + "pretrained_model_name_or_path", + [None, "bert-large-uncased"], + ids=["default_model", "override_model"], +) +def test_set_fixed_preprocessing_params(pretrained_model_name_or_path: str): + expected_model_name = "bert-base-uncased" + + preprocessing = TextPreprocessingConfig.from_dict( + { + "tokenizer": "space", + "lowercase": True, + } + ) + + encoder_params = {} + if pretrained_model_name_or_path is not None: + encoder_params["pretrained_model_name_or_path"] = pretrained_model_name_or_path + expected_model_name = pretrained_model_name_or_path + + encoder = BERTConfig.from_dict(encoder_params) + encoder.set_fixed_preprocessing_params(MODEL_ECD, preprocessing) + + assert preprocessing.tokenizer == "hf_tokenizer" + assert preprocessing.lowercase + assert preprocessing.pretrained_model_name_or_path == expected_model_name + + +@pytest.mark.parametrize( + "encoder_params,expected", + [ + ({"type": "parallel_cnn"}, False), + ({"type": "bert", "trainable": False}, True), + ({"type": "bert", "trainable": True}, False), + ], + ids=["parallel_cnn", "bert_fixed", "bert_trainable"], +) +def test_set_fixed_preprocessing_params_cache_embeddings(encoder_params: dict[str, Any], expected: bool | None): + preprocessing = TextPreprocessingConfig.from_dict( + { + "tokenizer": "space", + "lowercase": True, + "cache_encoder_embeddings": True, + } + ) + + encoder = get_encoder_cls(MODEL_ECD, TEXT, encoder_params[TYPE]).from_dict(encoder_params) + encoder.set_fixed_preprocessing_params(MODEL_ECD, preprocessing) + assert preprocessing.cache_encoder_embeddings == expected + + +@pytest.fixture(scope="module") +def llm_config_dict() -> dict[str, Any]: + return { + MODEL_TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + INPUT_FEATURES: [{TYPE: TEXT, NAME: "in1"}], + OUTPUT_FEATURES: [{TYPE: TEXT, NAME: "out1"}], + } + + +@pytest.fixture(scope="module") +def ecd_config_dict_llm_encoder() -> dict[str, Any]: + return { + MODEL_TYPE: MODEL_ECD, + INPUT_FEATURES: [ + { + TYPE: TEXT, + NAME: "in1", + ENCODER: {TYPE: MODEL_LLM, BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM"}, + } + ], + OUTPUT_FEATURES: [{TYPE: BINARY, NAME: "out1"}], + } + + +@pytest.fixture(scope="module") +def ecd_config_dict_llm_encoder_multiple_features() -> dict[str, Any]: + return { + MODEL_TYPE: MODEL_ECD, + INPUT_FEATURES: [ + {TYPE: BINARY, NAME: "in1"}, + { + TYPE: TEXT, + NAME: "in2", + ENCODER: {TYPE: MODEL_LLM, BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM"}, + }, + ], + OUTPUT_FEATURES: [{TYPE: BINARY, NAME: "out1"}], + } + + +@pytest.fixture(scope="module") +def ecd_config_dict_no_llm_encoder() -> dict[str, Any]: + return { + MODEL_TYPE: MODEL_ECD, + INPUT_FEATURES: [{TYPE: TEXT, NAME: "in1", ENCODER: {TYPE: "parallel_cnn"}}], + OUTPUT_FEATURES: [{TYPE: BINARY, NAME: "out1"}], + } + + +@pytest.fixture(scope="module") +def ecd_config_dict_no_text_features() -> dict[str, Any]: + return { + MODEL_TYPE: MODEL_ECD, + INPUT_FEATURES: [{TYPE: BINARY, NAME: "in1"}], + OUTPUT_FEATURES: [{TYPE: BINARY, NAME: "out1"}], + } + + +@pytest.mark.parametrize( + "config,expectation", + [ + # LLM configurations + ("llm_config_dict", True), + # LLM encoder configurations + ("ecd_config_dict_llm_encoder", True), + # LLM encoder configurations, multiple features + ("ecd_config_dict_llm_encoder_multiple_features", True), + # ECD configuration with text feature and non-LLM encoder + ("ecd_config_dict_no_llm_encoder", False), + # ECD configuration with no text features + ("ecd_config_dict_no_text_features", False), + ], +) +@pytest.mark.parametrize("config_type", ["dict", "object"]) +def test_is_or_uses_llm(config: dict[str, Any], expectation: bool, config_type, request): + """Test LLM detection on a variety of configs. Configs that use an LLM anywhere should return True, otherwise + False. + + Args: + config: The name of the config fixture to test + expectation: The expected result + request: pytest `request` fixture + """ + config = request.getfixturevalue(config) + if config_type == "object": + config = ModelConfig.from_dict(config) + assert config_uses_llm(config) == expectation + + +@pytest.mark.parametrize("invalid_config", [1, 1.0, "foo", True, False, None, [], {}, {"foo": "bar"}]) +def test_is_or_uses_llm_invalid_input(invalid_config): + """Sanity checks for invalid config handling. + + These should all raise an exception. + + Args: + invalid_config: An invalid argument to `config_uses_llm` + """ + with pytest.raises(ValueError): + config_uses_llm(invalid_config) + + +@pytest.fixture(scope="module") +def quantization_4bit_config() -> dict[str, Any]: + return {"quantization": {"bits": 4}} + + +@pytest.fixture(scope="module") +def quantization_8bit_config() -> dict[str, Any]: + return {"quantization": {"bits": 8}} + + +@pytest.fixture(scope="module") +def llm_config_dict_4bit(llm_config_dict: dict[str, Any], quantization_4bit_config: dict[str, Any]) -> dict[str, Any]: + config = copy.deepcopy(llm_config_dict) + config.update(quantization_4bit_config) + return config + + +@pytest.fixture(scope="module") +def llm_config_dict_8bit(llm_config_dict: dict[str, Any], quantization_8bit_config: dict[str, Any]) -> dict[str, Any]: + config = copy.deepcopy(llm_config_dict) + config.update(quantization_8bit_config) + return config + + +@pytest.fixture(scope="module") +def ecd_config_dict_llm_encoder_4bit( + ecd_config_dict_llm_encoder: dict[str, Any], quantization_4bit_config: dict[str, Any] +) -> dict[str, Any]: + config = copy.deepcopy(ecd_config_dict_llm_encoder) + config[INPUT_FEATURES][0][ENCODER].update(quantization_4bit_config) + return config + + +@pytest.fixture(scope="module") +def ecd_config_dict_llm_encoder_8bit( + ecd_config_dict_llm_encoder: dict[str, Any], quantization_8bit_config: dict[str, Any] +) -> dict[str, Any]: + config = copy.deepcopy(ecd_config_dict_llm_encoder) + config[INPUT_FEATURES][0][ENCODER].update(quantization_8bit_config) + return config + + +@pytest.mark.parametrize( + "config,expectation", + [ + # LLM configurations + ("llm_config_dict", [None]), + ("llm_config_dict_4bit", [4]), + ("llm_config_dict_8bit", [8]), + # LLM encoder configurations with one feature + ("ecd_config_dict_llm_encoder", [None]), + ("ecd_config_dict_llm_encoder_4bit", [4]), + ("ecd_config_dict_llm_encoder_8bit", [8]), + ], +) +@pytest.mark.parametrize("config_type", ["dict", "object"]) +def test_get_quantization( + config: dict[str, Any], expectation: int | list[int] | None | list[None], config_type: str, request +): + """Test get_quantization with LLM and single-feature ECD configs. + + Args: + config: The configuration to test + expectation: The expected quantization + config_type: Whether to test the config as a dict or object + request: pytest builtin fixture + """ + config = request.getfixturevalue(config) + if config_type == "object": + config = ModelConfig.from_dict(config) + assert get_quantization(config) == expectation + + +TEST_FEATURE_CONFIGS = [ + ( + { + TYPE: BINARY, + }, + None, + ), + ( + { + TYPE: TEXT, + }, + None, + ), + ({TYPE: TEXT, ENCODER: {TYPE: MODEL_LLM, BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM"}}, None), + ( + { + TYPE: TEXT, + ENCODER: { + TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + "quantization": {"bits": 4}, + }, + }, + 4, + ), + ( + { + TYPE: TEXT, + ENCODER: { + TYPE: MODEL_LLM, + BASE_MODEL: "HuggingFaceH4/tiny-random-LlamaForCausalLM", + "quantization": {"bits": 8}, + }, + }, + 8, + ), +] + +TEST_FEATURE_CONFIGS_IDS = [BINARY, TEXT, MODEL_LLM, f"{MODEL_LLM}-4bit", f"{MODEL_LLM}-8bit"] + + +@pytest.mark.parametrize("feature1,quantization1", TEST_FEATURE_CONFIGS, ids=TEST_FEATURE_CONFIGS_IDS) +@pytest.mark.parametrize("feature2,quantization2", TEST_FEATURE_CONFIGS, ids=TEST_FEATURE_CONFIGS_IDS) +@pytest.mark.parametrize("config_type", ["dict", "object"]) +def test_get_quantization_multiple_features( + ecd_config_dict_llm_encoder_multiple_features: dict[str, Any], + feature1: dict[str, Any], + quantization1: int, + feature2: dict[str, Any], + quantization2: int, + config_type: str, +): + """Test get_quantization with multiple features. + + Args: + ecd_config_dict_llm_encoder_multiple_features: Baseline config to add features to. + feature1: First input feature config dict + quantization1: First input feature expected quantization + feature2: Second input feature config dict + quantization2: Second input feature expected quantization + config_type: Whether to test the config as a dict or object + """ + config = copy.deepcopy(ecd_config_dict_llm_encoder_multiple_features) + feature1 = dict(name="in1", **feature1) + feature2 = dict(name="in2", **feature2) + config[INPUT_FEATURES] = [feature1, feature2] + + if config_type == "object": + config = ModelConfig.from_dict(config) + + assert get_quantization(config) == [quantization1, quantization2] + + +@pytest.mark.parametrize("invalid_config", [1, 1.0, "foo", True, False, None, [], {}, {"foo": "bar"}]) +def test_get_quantization_invalid_input(invalid_config): + """Test get_quantization with invalid configs. These should always raise a ValueError. + + Args: + invalid_config: The invalid config to test + """ + with pytest.raises(ValueError): + get_quantization(invalid_config) diff --git a/tests/ludwig/utils/test_data_utils.py b/tests/ludwig/utils/test_data_utils.py new file mode 100644 index 0000000..7bfd9eb --- /dev/null +++ b/tests/ludwig/utils/test_data_utils.py @@ -0,0 +1,251 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import functools +import json +import logging + +import numpy as np +import pandas as pd +import pytest +from fsspec.config import conf + +from ludwig.api import LudwigModel +from ludwig.data.cache.types import CacheableDataframe +from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df +from ludwig.utils.data_utils import ( + add_sequence_feature_column, + figure_data_format_dataset, + get_abs_path, + hash_dict, + NumpyEncoder, + PANDAS_DF, + read_csv, + read_html, + read_parquet, + sanitize_column_names, + use_credentials, +) + +try: + import dask.dataframe as dd +except ImportError: + dd = None + + +def test_add_sequence_feature_column(): + df = pd.DataFrame([1, 2, 3, 4, 5], columns=["x"]) + + add_sequence_feature_column(df, "x", 2) + assert df.equals( + pd.DataFrame( + [ + [1, "1 2"], + [2, "1 2"], + [3, "1 2"], + [4, "2 3"], + [5, "3 4"], + ], + columns=["x", "x_feature"], + ) + ) + + add_sequence_feature_column(df, "x", 1) + assert df.equals( + pd.DataFrame( + [ + [1, "1"], + [2, "1"], + [3, "2"], + [4, "3"], + [5, "4"], + ], + columns=["x", "x_feature"], + ) + ) + + df = pd.DataFrame([1, 2, 3, 4, 5], columns=["x"]) + + add_sequence_feature_column(df, "y", 2) + assert df.equals(pd.DataFrame([1, 2, 3, 4, 5], columns=["x"])) + + +def test_get_abs_path(): + assert get_abs_path("a", "b.jpg") == "a/b.jpg" + assert get_abs_path(None, "b.jpg") == "b.jpg" + + +@pytest.mark.parametrize( + "path, expected_format", [("s3://path/to.parquet ", "parquet"), ("/Users/path/to.csv \n", "csv")] +) +def test_figure_data_format_dataset_strip(path, expected_format): + assert figure_data_format_dataset(path) == expected_format + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_figure_data_format_dataset(): + assert figure_data_format_dataset({"a": "b"}) == dict + assert figure_data_format_dataset(pd.DataFrame([1, 2, 3, 4, 5], columns=["x"])) == pd.DataFrame + assert ( + figure_data_format_dataset( + dd.from_pandas(pd.DataFrame([1, 2, 3, 4, 5], columns=["x"]), npartitions=1).reset_index() + ) + == dd.DataFrame + ) + assert ( + figure_data_format_dataset( + CacheableDataframe(df=pd.DataFrame([1, 2, 3, 4, 5], columns=["x"]), name="test", checksum="test123") + ) + == pd.DataFrame + ) + assert ( + figure_data_format_dataset( + CacheableDataframe( + df=dd.from_pandas(pd.DataFrame([1, 2, 3, 4, 5], columns=["x"]), npartitions=1).reset_index(), + name="test", + checksum="test123", + ) + ) + == dd.DataFrame + ) + + +def test_hash_dict_numpy_types(): + d = {"float32": np.float32(1)} + assert hash_dict(d) == b"uqtgWB" + + +def test_use_credentials(): + conf.clear() + with use_credentials(None): + assert len(conf) == 0 + + s3_creds = { + "s3": { + "client_kwargs": { + "endpoint_url": "http://localhost:9000", + "aws_access_key_id": "test", + "aws_secret_access_key": "test", + } + } + } + with use_credentials(s3_creds): + assert len(conf) == 1 + assert conf == s3_creds + + assert len(conf) == 0 + + +def test_numpy_encoder(): + # Test Python builtin data type encoding. + assert json.dumps(None, cls=NumpyEncoder) == "null" + assert json.dumps({}, cls=NumpyEncoder) == "{}" + assert json.dumps(1, cls=NumpyEncoder) == "1" + assert json.dumps(1.0, cls=NumpyEncoder) == "1.0" + assert json.dumps("a", cls=NumpyEncoder) == '"a"' + assert json.dumps([0, 1, 2, 3, 4], cls=NumpyEncoder) == "[0, 1, 2, 3, 4]" + assert json.dumps((0, 1, 2, 3, 4), cls=NumpyEncoder) == "[0, 1, 2, 3, 4]" + assert json.dumps({0, 1, 2, 3, 4}, cls=NumpyEncoder) == "[0, 1, 2, 3, 4]" + assert json.dumps({"a": "b"}, cls=NumpyEncoder) == '{"a": "b"}' + + # Test numpy data type encoding + for dtype in [np.byte, np.ubyte, np.short, np.ushort, np.int32, np.int64, np.uint, np.longlong, np.ulonglong]: + x = np.arange(5, dtype=dtype) + assert json.dumps(x, cls=NumpyEncoder) == "[0, 1, 2, 3, 4]" + for i in x: + assert json.dumps(i, cls=NumpyEncoder) == f"{i}" + + for dtype in [np.half, np.single, np.double, np.longdouble]: + x = np.arange(5, dtype=dtype) + assert json.dumps(x, cls=NumpyEncoder) == "[0.0, 1.0, 2.0, 3.0, 4.0]" + for i in x: + assert json.dumps(i, cls=NumpyEncoder) == f"{i}" + + +def test_dataset_synthesizer_output_feature_decoder(): + config = { + "input_features": [{"name": "sentence", "type": "text"}], + "output_features": [{"name": "product", "type": "category"}], + "trainer": {"epochs": 5}, + "model_type": "ecd", + } + build_synthetic_dataset_df(dataset_size=100, config=config) + LudwigModel(config=config, logging_level=logging.INFO) + + +@pytest.fixture +def synthetic_1k_files(tmp_path): + """Create synthetic 1000-row CSV and Parquet files for chunking tests.""" + df = pd.DataFrame({f"col_{i}": range(1000) for i in range(5)}) + csv_path = str(tmp_path / "synthetic_1k.csv") + parquet_path = str(tmp_path / "synthetic_1k.parquet") + df.to_csv(csv_path, index=False) + df.to_parquet(parquet_path, index=False) + return csv_path, parquet_path + + +@pytest.mark.parametrize("fmt_idx", [0, 1], ids=["csv", "parquet"]) +@pytest.mark.parametrize("nrows", [None, 100]) +def test_chunking(synthetic_1k_files, fmt_idx, nrows): + dataset_path = synthetic_1k_files[fmt_idx] + reader_fn = {"csv": read_csv, "parquet": functools.partial(read_parquet, df_lib=PANDAS_DF)} + + fmt = figure_data_format_dataset(dataset_path) + + assert reader_fn[fmt](dataset_path, nrows=nrows).shape[0] == (nrows if nrows else 1000) + + +@pytest.mark.parametrize( + "df_lib", + [ + pytest.param(pd, id="pandas"), + pytest.param(dd, marks=[pytest.mark.distributed, pytest.mark.distributed_f], id="dask"), + ], +) +@pytest.mark.parametrize("nrows", [None, 10]) +def test_read_html(df_lib, nrows): + HTML_DOCUMENT = """ + + + TITLE + + + + +
Col 1Col 2
12
+ + + """ + + kwargs = {} + if not nrows: + kwargs["nrows"] = nrows + + read_html(HTML_DOCUMENT, df_lib, **kwargs) + + +def test_sanitize_column_names(): + df = pd.DataFrame( + { + "col.one": [1, 2, 3, 4], + "col(two)": [4, 5, 6, 7], + "col[]:three": [7, 8, 9, 10], + "col 'one' (new)": [1, 2, 3, 4], + } + ) + + df = sanitize_column_names(df) + + assert list(df.columns) == ["col_one", "col_two_", "col___three", "col _one_ _new_"] diff --git a/tests/ludwig/utils/test_dataframe_utils.py b/tests/ludwig/utils/test_dataframe_utils.py new file mode 100644 index 0000000..03ab20a --- /dev/null +++ b/tests/ludwig/utils/test_dataframe_utils.py @@ -0,0 +1,85 @@ +import numpy as np +import pandas as pd +import pytest + +from ludwig.backend import create_backend, LOCAL_BACKEND +from ludwig.utils.dataframe_utils import to_numpy_dataset, to_scalar_df + +try: + import dask.dataframe as dd +except ImportError: + pass + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_to_numpy_dataset_with_dask(ray_cluster_2cpu): + dd_df = dd.from_pandas(pd.DataFrame([[1, 2, 3]], columns=["col1", "col2", "col3"]), npartitions=1) + ray_backend = create_backend("ray") + + np_df = to_numpy_dataset(dd_df, backend=ray_backend) + + assert np_df == {"col1": np.array([1]), "col2": np.array([2]), "col3": np.array([3])} + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_to_numpy_dataset_with_dask_backend_mismatch(): + dd_df = dd.from_pandas(pd.DataFrame([[1, 2, 3]], columns=["col1", "col2", "col3"]), npartitions=1) + + with pytest.raises(AttributeError): + to_numpy_dataset(dd_df, backend=LOCAL_BACKEND) + + +def test_to_numpy_dataset_with_pandas(): + pd_df = pd.DataFrame([[1, 2, 3]], columns=["col1", "col2", "col3"]) + + np_df = to_numpy_dataset(pd_df, backend=LOCAL_BACKEND) + + assert np_df == {"col1": np.array([1]), "col2": np.array([2]), "col3": np.array([3])} + + +def test_to_numpy_dataset_empty_with_columns(): + pd_df = pd.DataFrame(columns=["col1", "col2", "col3"]) + + np_df = to_numpy_dataset(pd_df, backend=LOCAL_BACKEND) + + assert np_df == {"col1": [], "col2": [], "col3": []} + + +def test_to_numpy_dataset_empty(): + pd_df = pd.DataFrame() + + np_df = to_numpy_dataset(pd_df, backend=LOCAL_BACKEND) + + assert np_df == {} + + +@pytest.mark.distributed +@pytest.mark.distributed_f +def test_to_numpy_dataset_with_pandas_backend_mismatch(ray_cluster_2cpu): + pd_df = pd.DataFrame([[1, 2, 3]], columns=["col1", "col2", "col3"]) + ray_backend = create_backend("ray") + + with pytest.raises(AttributeError): + to_numpy_dataset(pd_df, backend=ray_backend) + + +def test_to_scalar_df(): + data = [ + [True, np.array([1, 2, 3]), 42], + [False, np.array([4, 5, 6]), 28], + [True, np.array([7, 8, 9]), 99], + ] + df = pd.DataFrame(data, columns=["bin", "cat_encoded", "num"]) + + scalar_data = [ + [True, 1, 2, 3, 42], + [False, 4, 5, 6, 28], + [True, 7, 8, 9, 99], + ] + expected_df = pd.DataFrame(scalar_data, columns=["bin", "cat_encoded_0", "cat_encoded_1", "cat_encoded_2", "num"]) + + scalar_df = to_scalar_df(df) + assert scalar_df.columns.tolist() == expected_df.columns.tolist() + assert scalar_df.equals(expected_df) diff --git a/tests/ludwig/utils/test_dataset_quality.py b/tests/ludwig/utils/test_dataset_quality.py new file mode 100644 index 0000000..e40421b --- /dev/null +++ b/tests/ludwig/utils/test_dataset_quality.py @@ -0,0 +1,190 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Unit tests for ludwig.utils.dataset_quality.""" + +import numpy as np +import pandas as pd + +from ludwig.utils.dataset_quality import ( + check_dataset_quality, + CheckStatus, + drop_quality_issues, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _clean_df(n: int = 500) -> pd.DataFrame: + rng = np.random.default_rng(0) + return pd.DataFrame( + { + "feature_a": rng.standard_normal(n), + "feature_b": rng.integers(0, 5, size=n).astype(float), + "feature_c": rng.standard_normal(n), + "label": rng.choice(["cat", "dog"], size=n), + } + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_clean_dataset_passes(): + df = _clean_df(500) + report = check_dataset_quality(df, target_column="label", dataset_name="test", min_rows=200) + assert report.passed + + +def test_too_few_rows_fails(): + df = _clean_df(50) + report = check_dataset_quality(df, target_column="label", min_rows=200) + failure_names = [c.name for c in report.failures] + assert "minimum_size" in failure_names + + +def test_too_few_features_fails(): + # Only 1 non-target column + df = pd.DataFrame({"feature_a": range(300), "label": [0, 1] * 150}) + report = check_dataset_quality(df, target_column="label", min_features=2) + failure_names = [c.name for c in report.failures] + assert "minimum_features" in failure_names + + +def test_constant_column_warns(): + df = _clean_df(300) + df["constant"] = 42 # all same value + report = check_dataset_quality(df, target_column="label") + check_statuses = {c.name: c.status for c in report.checks} + assert check_statuses["constant_columns"] == CheckStatus.WARN + + +def test_near_duplicate_warns(): + rng = np.random.default_rng(1) + x = rng.standard_normal(300) + df = pd.DataFrame( + { + "a": x, + "b": x, # perfectly correlated with a + "label": rng.choice([0, 1], size=300), + } + ) + report = check_dataset_quality(df, target_column="label") + check_statuses = {c.name: c.status for c in report.checks} + assert check_statuses["near_duplicate_columns"] == CheckStatus.WARN + + +def test_target_leakage_fails(): + rng = np.random.default_rng(2) + target = rng.standard_normal(300) + leaky = target + rng.standard_normal(300) * 0.001 # r > 0.99 + df = pd.DataFrame( + { + "leaky_feature": leaky, + "other": rng.standard_normal(300), + "target": target, + } + ) + report = check_dataset_quality(df, target_column="target") + failure_names = [c.name for c in report.failures] + assert "target_leakage" in failure_names + + +def test_id_column_warns(): + df = pd.DataFrame( + { + "id": range(300), # sequential integer + "feature": np.random.default_rng(3).standard_normal(300), + "label": [0, 1] * 150, + } + ) + report = check_dataset_quality(df, target_column="label") + check_statuses = {c.name: c.status for c in report.checks} + assert check_statuses["id_columns"] == CheckStatus.WARN + + +def test_class_imbalance_warns(): + rng = np.random.default_rng(4) + n = 10_000 + # Minority class is 0.1% of rows (10 samples out of 10000) + labels = np.array([0] * (n - 10) + [1] * 10) + df = pd.DataFrame( + { + "feature": rng.standard_normal(n), + "label": labels, + } + ) + report = check_dataset_quality(df, target_column="label") + check_statuses = {c.name: c.status for c in report.checks} + assert check_statuses["class_imbalance"] == CheckStatus.WARN + + +def test_single_class_fails(): + df = pd.DataFrame( + { + "feature": range(300), + "label": ["cat"] * 300, # only 1 distinct value + } + ) + report = check_dataset_quality(df, target_column="label") + failure_names = [c.name for c in report.failures] + assert "single_class" in failure_names + + +def test_drop_quality_issues_removes_constant(): + df = _clean_df(300) + df["constant_col"] = 7 + report = check_dataset_quality(df, target_column="label") + cleaned = drop_quality_issues(df, report) + assert "constant_col" not in cleaned.columns + + +def test_drop_quality_issues_removes_id(): + df = pd.DataFrame( + { + "id_col": range(300), + "feature": np.random.default_rng(5).standard_normal(300), + "label": [0, 1] * 150, + } + ) + report = check_dataset_quality(df, target_column="label") + cleaned = drop_quality_issues(df, report) + assert "id_col" not in cleaned.columns + + +def test_summary_string(): + df = _clean_df(500) + report = check_dataset_quality(df, target_column="label") + summary = report.summary() + assert isinstance(summary, str) + assert len(summary) > 0 + + +def test_report_passed_property(): + df = _clean_df(500) + report = check_dataset_quality(df, target_column="label", min_rows=200) + # A clean 500-row df should pass + assert report.passed is True + + +def test_report_failures_property(): + df = _clean_df(50) # too few rows + report = check_dataset_quality(df, target_column="label", min_rows=200) + assert len(report.failures) > 0 + for f in report.failures: + assert f.status == CheckStatus.FAIL diff --git a/tests/ludwig/utils/test_dataset_utils.py b/tests/ludwig/utils/test_dataset_utils.py new file mode 100644 index 0000000..e7b25f2 --- /dev/null +++ b/tests/ludwig/utils/test_dataset_utils.py @@ -0,0 +1,225 @@ +import pandas as pd + +from ludwig.utils.dataset_utils import get_repeatable_train_val_test_split + + +def test_get_repeatable_train_val_test_split(): + # Test adding split with stratify + df = pd.DataFrame( + [ + [0, 0], + [1, 0], + [2, 0], + [3, 0], + [4, 0], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [10, 0], + [11, 0], + [12, 0], + [13, 0], + [14, 0], + [15, 1], + [16, 1], + [17, 1], + [18, 1], + [19, 1], + ], + columns=["input", "target"], + ) + split_df = get_repeatable_train_val_test_split(df, "target", random_seed=42) + assert split_df.equals( + pd.DataFrame( + [ + [7, 1, 0], + [16, 1, 0], + [5, 1, 0], + [14, 0, 0], + [19, 1, 0], + [6, 1, 0], + [11, 0, 0], + [18, 1, 0], + [1, 0, 0], + [10, 0, 0], + [2, 0, 0], + [15, 1, 0], + [0, 0, 0], + [17, 1, 1], + [12, 0, 1], + [8, 1, 2], + [4, 0, 2], + [13, 0, 2], + [3, 0, 2], + [9, 1, 2], + ], + columns=["input", "target", "split"], + ) + ) + + # Test adding split without stratify + df = pd.DataFrame( + [ + [0, 0], + [1, 0], + [2, 0], + [3, 0], + [4, 0], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [10, 0], + [11, 0], + [12, 0], + [13, 0], + [14, 0], + [15, 1], + [16, 1], + [17, 1], + [18, 1], + [19, 1], + ], + columns=["input", "target"], + ) + split_df = get_repeatable_train_val_test_split(df, random_seed=42) + assert split_df.equals( + pd.DataFrame( + [ + [3, 0, 0], + [4, 0, 0], + [5, 1, 0], + [7, 1, 0], + [8, 1, 0], + [10, 0, 0], + [11, 0, 0], + [12, 0, 0], + [13, 0, 0], + [14, 0, 0], + [15, 1, 0], + [16, 1, 0], + [18, 1, 0], + [19, 1, 0], + [0, 0, 1], + [17, 1, 1], + [1, 0, 2], + [2, 0, 2], + [9, 1, 2], + [6, 1, 2], + ], + columns=["input", "target", "split"], + ) + ) + + # Test needing no change + df = pd.DataFrame( + [ + [0, 0, 0], + [1, 0, 0], + [2, 0, 0], + [5, 1, 0], + [6, 1, 0], + [7, 1, 0], + [10, 0, 0], + [11, 0, 0], + [14, 0, 0], + [15, 1, 0], + [16, 1, 0], + [18, 1, 0], + [19, 1, 0], + [12, 0, 1], + [17, 1, 1], + [3, 0, 2], + [4, 0, 2], + [8, 1, 2], + [9, 1, 2], + [13, 0, 2], + ], + columns=["input", "target", "split"], + ) + split_df = get_repeatable_train_val_test_split(df, "target", random_seed=42) + assert split_df.equals( + pd.DataFrame( + [ + [0, 0, 0], + [1, 0, 0], + [2, 0, 0], + [5, 1, 0], + [6, 1, 0], + [7, 1, 0], + [10, 0, 0], + [11, 0, 0], + [14, 0, 0], + [15, 1, 0], + [16, 1, 0], + [18, 1, 0], + [19, 1, 0], + [12, 0, 1], + [17, 1, 1], + [3, 0, 2], + [4, 0, 2], + [8, 1, 2], + [9, 1, 2], + [13, 0, 2], + ], + columns=["input", "target", "split"], + ) + ) + + # Test adding only validation split + df = pd.DataFrame( + [ + [0, 0, 0], + [1, 0, 0], + [2, 0, 0], + [5, 1, 0], + [6, 1, 0], + [7, 1, 0], + [10, 0, 0], + [11, 0, 0], + [14, 0, 0], + [15, 1, 0], + [16, 1, 0], + [18, 1, 0], + [19, 1, 0], + [12, 0, 0], + [17, 1, 0], + [3, 0, 2], + [4, 0, 2], + [8, 1, 2], + [9, 1, 2], + [13, 0, 2], + ], + columns=["input", "target", "split"], + ) + split_df = get_repeatable_train_val_test_split(df, "target", random_seed=42) + assert split_df.equals( + pd.DataFrame( + [ + [0, 0, 0], + [1, 0, 0], + [2, 0, 0], + [5, 1, 0], + [6, 1, 0], + [7, 1, 0], + [10, 0, 0], + [11, 0, 0], + [14, 0, 0], + [16, 1, 0], + [19, 1, 0], + [12, 0, 0], + [17, 1, 0], + [15, 1, 1], + [18, 1, 1], + [3, 0, 2], + [4, 0, 2], + [8, 1, 2], + [9, 1, 2], + [13, 0, 2], + ], + columns=["input", "target", "split"], + ) + ) diff --git a/tests/ludwig/utils/test_date_utils.py b/tests/ludwig/utils/test_date_utils.py new file mode 100644 index 0000000..ffd6e10 --- /dev/null +++ b/tests/ludwig/utils/test_date_utils.py @@ -0,0 +1,49 @@ +import datetime +from contextlib import nullcontext as does_not_raise +from typing import Any, ContextManager + +import pytest + +from ludwig.utils.date_utils import convert_number_to_datetime + + +@pytest.fixture(scope="module") +def reference_datetime() -> datetime.datetime: + return datetime.datetime.fromtimestamp(1691600953.443032, tz=datetime.UTC).replace(tzinfo=None) + + +@pytest.mark.parametrize( + "timestamp,raises", + [ + pytest.param(1691600953.443032, does_not_raise(), id="float-s"), + pytest.param(1691600953443.032, does_not_raise(), id="float-ms"), + pytest.param(1691600953, does_not_raise(), id="int-s"), + pytest.param(1691600953443, does_not_raise(), id="int-ms"), + pytest.param("1691600953.443032", does_not_raise(), id="string[float]-s"), + pytest.param("1691600953443.0032", does_not_raise(), id="string[float]-ms"), + pytest.param("1691600953", does_not_raise(), id="string[int]-s"), + pytest.param("1691600953443", does_not_raise(), id="string[int]-ms"), + pytest.param("foo", pytest.raises(ValueError), id="string error"), + pytest.param([1691600953.443032], pytest.raises(ValueError), id="list error"), + pytest.param(datetime.datetime(2023, 8, 9, 13, 9, 13), pytest.raises(ValueError), id="datetime error"), + pytest.param(None, pytest.raises(ValueError), id="NoneType error"), + ], +) +def test_convert_number_to_datetime(reference_datetime: datetime.datetime, timestamp: Any, raises: ContextManager): + """Ensure that numeric timestamps are correctly converted to datetime objects. + + Args: + reference_datetime: A datetime object with the expected date/time + timestamp: The timestamp to convert in s or ms + raises: context manager to check for expected exceptions + """ + with raises: + dt = convert_number_to_datetime(timestamp) + + # Check that the returned datetime is accurate to the scale of seconds. + assert dt.year == reference_datetime.year + assert dt.month == reference_datetime.month + assert dt.day == reference_datetime.day + assert dt.hour == reference_datetime.hour + assert dt.minute == reference_datetime.minute + assert dt.second == reference_datetime.second diff --git a/tests/ludwig/utils/test_defaults.py b/tests/ludwig/utils/test_defaults.py new file mode 100644 index 0000000..2ac3bff --- /dev/null +++ b/tests/ludwig/utils/test_defaults.py @@ -0,0 +1,247 @@ +import copy + +import pytest + +from ludwig.constants import ( + CATEGORY, + COMBINER, + DECODER, + DEFAULTS, + DEPENDENCIES, + DROP_ROW, + EARLY_STOP, + ENCODER, + EXECUTOR, + FILL_WITH_MODE, + HYPEROPT, + INPUT_FEATURES, + LOSS, + MISSING_VALUE_STRATEGY, + MODEL_ECD, + MODEL_TYPE, + OUTPUT_FEATURES, + PREPROCESSING, + REDUCE_DEPENDENCIES, + REDUCE_INPUT, + SCHEDULER, + SUM, + TIED, + TOP_K, + TRAINER, + TYPE, +) +from ludwig.schema.model_config import ModelConfig +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version +from ludwig.utils.misc_utils import merge_dict, set_default_values +from tests.integration_tests.utils import ( + binary_feature, + category_feature, + number_feature, + sequence_feature, + text_feature, + vector_feature, +) + +HYPEROPT_CONFIG = { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + }, + "combiner.num_fc_layers": {"space": "randint", "lower": 2, "upper": 6}, + "utterance.encoder.norm": {"space": "grid_search", "values": ["layer", "batch"]}, + "utterance.encoder.dropout": {"space": "choice", "categories": [0.0001, 0.001, 0.01]}, + "utterance.encoder.fc_layers": { + "space": "choice", + "categories": [ + [{"output_size": 512}, {"output_size": 256}], + [{"output_size": 512}], + [{"output_size": 256}], + ], + }, + }, + "search_alg": {"type": "variant_generator"}, + "executor": {"type": "ray"}, + "goal": "minimize", +} + +SCHEDULER_DICT = {"type": "async_hyperband", "time_attr": "time_total_s"} + + +@pytest.mark.parametrize( + "use_train,use_hyperopt_scheduler", + [ + (True, True), + (False, True), + (True, False), + (False, False), + ], +) +def test_merge_with_defaults_early_stop(use_train, use_hyperopt_scheduler): + all_input_features = [ + binary_feature(), + category_feature(), + number_feature(), + text_feature(name="utterance"), + ] + all_output_features = [ + category_feature(output_feature=True), + sequence_feature(output_feature=True), + vector_feature(), + ] + + # validate config with all features + config = { + INPUT_FEATURES: all_input_features, + OUTPUT_FEATURES: all_output_features, + HYPEROPT: HYPEROPT_CONFIG, + } + config = copy.deepcopy(config) + + if use_train: + config[TRAINER] = {"batch_size": 42} + + if use_hyperopt_scheduler: + # hyperopt scheduler cannot be used with early stopping + config[HYPEROPT][EXECUTOR][SCHEDULER] = SCHEDULER_DICT + + merged_config = ModelConfig.from_dict(config).to_dict() + + # When a scheulder is provided, early stopping in the rendered config needs to be disabled to allow the + # hyperopt scheduler to manage trial lifecycle. + expected = -1 if use_hyperopt_scheduler else ECDTrainerConfig().early_stop + assert merged_config[TRAINER]["early_stop"] == expected + + +def test_missing_outputs_drop_rows(): + config = { + INPUT_FEATURES: [category_feature()], + OUTPUT_FEATURES: [category_feature(output_feature=True)], + DEFAULTS: {CATEGORY: {PREPROCESSING: {MISSING_VALUE_STRATEGY: FILL_WITH_MODE}}}, + } + + merged_config = ModelConfig.from_dict(config).to_dict() + + global_preprocessing = merged_config[DEFAULTS] + input_feature_config = merged_config[INPUT_FEATURES][0] + output_feature_config = merged_config[OUTPUT_FEATURES][0] + + assert output_feature_config[PREPROCESSING][MISSING_VALUE_STRATEGY] == DROP_ROW + + assert global_preprocessing[input_feature_config[TYPE]][PREPROCESSING][MISSING_VALUE_STRATEGY] == FILL_WITH_MODE + feature_preprocessing = merge_dict( + global_preprocessing[output_feature_config[TYPE]][PREPROCESSING], output_feature_config[PREPROCESSING] + ) + assert feature_preprocessing[MISSING_VALUE_STRATEGY] == DROP_ROW + + +def test_default_model_type(): + config = { + INPUT_FEATURES: [category_feature()], + OUTPUT_FEATURES: [category_feature(output_feature=True)], + } + + merged_config = ModelConfig.from_dict(config).to_dict() + + assert merged_config[MODEL_TYPE] == MODEL_ECD + + +def test_set_default_values(): + config = { + INPUT_FEATURES: [number_feature(encoder={"max_sequence_length": 10})], + OUTPUT_FEATURES: [category_feature(decoder={})], + } + + assert TIED not in config[INPUT_FEATURES][0] + assert TOP_K not in config[OUTPUT_FEATURES][0] + assert DEPENDENCIES not in config[OUTPUT_FEATURES][0] + assert REDUCE_INPUT not in config[OUTPUT_FEATURES][0] + assert REDUCE_DEPENDENCIES not in config[OUTPUT_FEATURES][0] + + set_default_values(config[INPUT_FEATURES][0], {ENCODER: {TYPE: "passthrough"}, TIED: None}) + + set_default_values( + config[OUTPUT_FEATURES][0], + { + DECODER: { + TYPE: "classifier", + }, + TOP_K: 3, + DEPENDENCIES: [], + REDUCE_INPUT: SUM, + REDUCE_DEPENDENCIES: SUM, + }, + ) + + assert config[INPUT_FEATURES][0][ENCODER][TYPE] == "passthrough" + assert config[INPUT_FEATURES][0][TIED] is None + assert config[OUTPUT_FEATURES][0][DECODER][TYPE] == "classifier" + assert config[OUTPUT_FEATURES][0][TOP_K] == 3 + assert config[OUTPUT_FEATURES][0][DEPENDENCIES] == [] + assert config[OUTPUT_FEATURES][0][REDUCE_INPUT] == SUM + assert config[OUTPUT_FEATURES][0][REDUCE_DEPENDENCIES] == SUM + + +def test_merge_with_defaults(): + # configuration with legacy parameters + legacy_config_format = { + "ludwig_version": "0.4", + INPUT_FEATURES: [ + {"type": "numerical", "name": "number_input_feature", "encoder": {"type": "dense"}}, + { + "type": "image", + "name": "image_input_feature", + "encoder": "stacked_cnn", + "conv_bias": True, + "conv_layers": [ + {"num_filters": 32, "pool_size": 2, "pool_stride": 2, "bias": False}, + { + "num_filters": 64, + "pool_size": 2, + "pool_stride": 2, + }, + ], + }, + ], + OUTPUT_FEATURES: [ + { + "type": "numerical", + "name": "number_output_feature", + }, + ], + "training": {"eval_batch_size": 0, "optimizer": {"type": "adadelta"}}, + HYPEROPT: { + "parameters": { + "training.learning_rate": {"space": "choice", "categories": [0.0001, 0.001, 0.01]}, + "training.early_stop": {"space": "choice", "categories": [5, 10, 15]}, + "number_input_feature.encoder.num_layers": {"space": "choice", "categories": [2, 3, 4]}, + "number_output_feature.decoder.fc_output_size": {"space": "choice", "categories": [128, 256, 512]}, + "number_output_feature.decoder.fc_dropout": {"space": "uniform", "lower": 0, "upper": 1}, + }, + "executor": { + "type": "serial", + "search_alg": {TYPE: "variant_generator"}, + }, + "sampler": { + "num_samples": 99, + "scheduler": {}, + }, + }, + } + + updated_config = upgrade_config_dict_to_latest_version(legacy_config_format) + merged_config = ModelConfig.from_dict(updated_config).to_dict() + + assert len(merged_config[DEFAULTS]) == 13 + assert ENCODER in merged_config[DEFAULTS][CATEGORY] + assert PREPROCESSING in merged_config[DEFAULTS][CATEGORY] + assert DECODER in merged_config[DEFAULTS][CATEGORY] + assert LOSS in merged_config[DEFAULTS][CATEGORY] + assert COMBINER in merged_config + assert merged_config[TRAINER][EARLY_STOP] == 5 + assert SCHEDULER in merged_config[HYPEROPT][EXECUTOR] + assert merged_config[HYPEROPT][EXECUTOR][SCHEDULER]["type"] == "fifo" + assert TYPE in merged_config[INPUT_FEATURES][1][ENCODER] + assert TYPE in merged_config[OUTPUT_FEATURES][0][DECODER] diff --git a/tests/ludwig/utils/test_error_handling_utils.py b/tests/ludwig/utils/test_error_handling_utils.py new file mode 100644 index 0000000..35d8157 --- /dev/null +++ b/tests/ludwig/utils/test_error_handling_utils.py @@ -0,0 +1,88 @@ +import pytest + +from ludwig.constants import TRIES +from ludwig.utils.error_handling_utils import default_retry + + +def test_default_retry_success(): + ctr = 0 + + @default_retry() + def flaky_function(): + nonlocal ctr + if ctr < TRIES - 1: + ctr += 1 + raise Exception(f"Ctr: {ctr} too low.") + + return + + flaky_function() + + +def test_default_retry_failure(): + ctr = 0 + + @default_retry() + def flaky_function(): + nonlocal ctr + if ctr < TRIES: + ctr += 1 + raise Exception(f"Ctr: {ctr} too low.") + + return + + with pytest.raises(Exception): + flaky_function() + + +def test_default_retry_success_custom_num_tries(): + CUSTOM_TRIES = 3 + ctr = 0 + + @default_retry(tries=CUSTOM_TRIES) + def flaky_function(): + nonlocal ctr + if ctr < CUSTOM_TRIES - 1: + ctr += 1 + raise Exception(f"Ctr: {ctr} too low.") + + return + + flaky_function() + + +def test_default_retry_does_not_retry_non_oserror(): + """Non-OSError exceptions (e.g. AttributeError from a broken import) must propagate immediately. + + Regression test for https://github.com/ludwig-ai/ludwig/issues/4170: torchao raises + AttributeError on PyTorch < 2.7 and the retry wrapper was retrying 8 times, wasting + ~2.5 minutes before surfacing the real error. + """ + call_count = 0 + + @default_retry(tries=8, exceptions=OSError) + def always_attribute_error(): + nonlocal call_count + call_count += 1 + raise AttributeError("module 'torch.utils._pytree' has no attribute 'register_constant'") + + with pytest.raises(AttributeError): + always_attribute_error() + + assert call_count == 1, f"Expected 1 call (no retries), got {call_count}" + + +def test_default_retry_retries_oserror(): + """OSError (transient network/IO) should still be retried.""" + call_count = 0 + CUSTOM_TRIES = 3 + + @default_retry(tries=CUSTOM_TRIES, exceptions=OSError) + def flaky_network(): + nonlocal call_count + call_count += 1 + if call_count < CUSTOM_TRIES: + raise OSError("connection reset by peer") + + flaky_network() + assert call_count == CUSTOM_TRIES diff --git a/tests/ludwig/utils/test_errors.py b/tests/ludwig/utils/test_errors.py new file mode 100644 index 0000000..bd0a050 --- /dev/null +++ b/tests/ludwig/utils/test_errors.py @@ -0,0 +1,25 @@ +import pickle + +from ludwig.error import ConfigValidationError, InputDataError + + +def test_input_data_error_serializeable(): + err = InputDataError( + "location", "category", "At least 2 distinct values are required, column only contains ['here']" + ) + + loaded_err: InputDataError = pickle.loads(pickle.dumps(err)) + + assert loaded_err.column_name == err.column_name + assert loaded_err.feature_type == err.feature_type + assert loaded_err.message == err.message + assert str(err) == str(loaded_err) + + +def test_config_validation_error_serializeable(): + err = ConfigValidationError(message="At least 2 distinct values are required, column only contains ['here']") + + loaded_err: ConfigValidationError = pickle.loads(pickle.dumps(err)) + + assert loaded_err.message == err.message + assert str(err) == str(loaded_err) diff --git a/tests/ludwig/utils/test_fs_utils.py b/tests/ludwig/utils/test_fs_utils.py new file mode 100644 index 0000000..fef3e2b --- /dev/null +++ b/tests/ludwig/utils/test_fs_utils.py @@ -0,0 +1,109 @@ +import logging +import os +import platform +import tempfile +from urllib.parse import quote + +import pytest + +from ludwig.utils.fs_utils import get_fs_and_path, list_file_names_in_directory, safe_move_directory + +logger = logging.getLogger(__name__) + + +def create_file(url): + _, path = get_fs_and_path(url) + logger.info(f"saving url '{url}' to path '{path}'") + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, path) + os.makedirs(os.path.dirname(file_path)) + with open(file_path, "w"): + return path + + +@pytest.mark.filesystem +def test_get_fs_and_path_simple(): + assert create_file("http://a/b.jpg") == os.path.join("a", "b.jpg") + + +@pytest.mark.filesystem +def test_get_fs_and_path_query_string(): + assert create_file("http://a/b.jpg?c=d") == os.path.join("a", "b.jpg") + + +@pytest.mark.filesystem +def test_get_fs_and_path_decode(): + assert create_file("http://a//b%20c.jpg") == os.path.join("a", "b c.jpg") + + +@pytest.mark.filesystem +def test_get_fs_and_path_unicode(): + assert create_file("http://a/æ.jpg") == "a/æ.jpg" + + +@pytest.mark.filesystem +@pytest.mark.skipif(platform.system() == "Windows", reason="Skipping if windows.") +def test_get_fs_and_path_invalid_linux(): + invalid_chars = { + "\x00": ValueError, + "/": FileExistsError, + } + for c, e in invalid_chars.items(): + url = f"http://a/{quote(c)}" + with pytest.raises(e): + create_file(url) + + +@pytest.mark.filesystem +@pytest.mark.skipif(platform.system() != "Windows", reason="Skipping if not windows.") +def test_get_fs_and_path_invalid_windows(): + invalid_chars = { + "\x00": ValueError, + "\\": FileExistsError, + "/": OSError, + ":": OSError, + "*": OSError, + "?": OSError, + '"': OSError, + "<": OSError, + ">": OSError, + "|": OSError, + } + for c, e in invalid_chars.items(): + url = f"http://a/{quote(c)}" + with pytest.raises(e): + create_file(url) + + +@pytest.mark.filesystem +def test_safe_move_directory(tmpdir): + src_dir = os.path.join(tmpdir, "src") + dst_dir = os.path.join(tmpdir, "dst") + + os.mkdir(src_dir) + os.mkdir(dst_dir) + + with open(os.path.join(src_dir, "file.txt"), "w") as f: + f.write("test") + + safe_move_directory(src_dir, dst_dir) + + assert not os.path.exists(src_dir) + assert os.path.exists(os.path.join(dst_dir, "file.txt")) + with open(os.path.join(dst_dir, "file.txt")) as f: + assert f.read() == "test" + + +@pytest.mark.filesystem +def test_list_file_names_in_directory(tmpdir): + my_dir = os.path.join(tmpdir, "my_dir") + + os.mkdir(my_dir) + + with open(os.path.join(my_dir, "my_file.txt"), "w") as f: + f.write("test_0") + + with open(os.path.join(my_dir, "my_other_file.txt"), "w") as f: + f.write("test_1") + + assert set(list_file_names_in_directory(directory_name=my_dir)) == {"my_file.txt", "my_other_file.txt"} diff --git a/tests/ludwig/utils/test_heuristics.py b/tests/ludwig/utils/test_heuristics.py new file mode 100644 index 0000000..897b50f --- /dev/null +++ b/tests/ludwig/utils/test_heuristics.py @@ -0,0 +1,48 @@ +from typing import Any + +import pytest + +from ludwig.constants import DEFAULTS, ENCODER, TEXT, TRAINABLE, TRAINER, TYPE +from ludwig.schema.model_config import ModelConfig +from ludwig.utils.heuristics import get_auto_learning_rate + + +@pytest.mark.parametrize( + "text_encoder,expected_lr", + [ + (None, 0.001), + ({}, 0.00001), + ({"type": "parallel_cnn"}, 0.0001), + ({"type": "bert"}, 0.00002), + ({"type": "bert", "trainable": True}, 0.00001), + ({"type": "bert", "trainable": True, "use_pretrained": False}, 0.0001), + ], + ids=["no_text", "default_electra", "parallel_cnn", "bert_fixed", "bert_trainable", "bert_untrained"], +) +def test_get_auto_learning_rate(text_encoder: dict[str, Any] | None, expected_lr: float): + input_features = [{"name": "bin1", "type": "binary"}] + if text_encoder is not None: + input_features.append({"name": "text1", "type": "text", "encoder": text_encoder}) + + config = { + "input_features": input_features, + "output_features": [{"name": "bin2", "type": "binary"}], + TRAINER: { + "train_steps": 1, + "learning_rate": "auto", + }, + DEFAULTS: { + TEXT: { + ENCODER: { + # Note that encoder defaults are all or nothing: if the encoder type is overridden, trainable + # here is ignored + TYPE: "electra", + TRAINABLE: True, + } + } + }, + } + + config = ModelConfig.from_dict(config) + lr = get_auto_learning_rate(config) + assert lr == expected_lr diff --git a/tests/ludwig/utils/test_hf_utils.py b/tests/ludwig/utils/test_hf_utils.py new file mode 100644 index 0000000..a469f8e --- /dev/null +++ b/tests/ludwig/utils/test_hf_utils.py @@ -0,0 +1,90 @@ +import os +import shutil + +import pytest +from transformers import AlbertModel, BertModel, BertTokenizer + +from ludwig.encoders.text_encoders import ALBERTEncoder, BERTEncoder +from ludwig.utils.hf_utils import ( + load_pretrained_hf_model_from_hub, + load_pretrained_hf_model_with_hub_fallback, + upload_folder_to_hfhub, +) + + +@pytest.mark.parametrize( + ("model", "name"), + [ + (AlbertModel, ALBERTEncoder.DEFAULT_MODEL_NAME), + (BertTokenizer, "bert-base-uncased"), + ], +) +def test_load_pretrained_hf_model_from_hub(model: type, name: str, tmpdir: os.PathLike): + """Ensure that the HF models used in ludwig download correctly.""" + cache_dir = os.path.join(tmpdir, name.replace(os.path.sep, "_") if name else str(model.__name__)) + os.makedirs(cache_dir, exist_ok=True) + loaded_model = load_pretrained_hf_model_from_hub(model, name, cache_dir=cache_dir, force_download=True) + assert isinstance(loaded_model, model) + assert os.listdir(cache_dir) + + +def test_load_pretrained_hf_model_with_hub_fallback(tmpdir): + """Ensure that the HF models used in ludwig download correctly with S3 or hub fallback.""" + # Don't set env var. + _, used_fallback = load_pretrained_hf_model_with_hub_fallback(AlbertModel, ALBERTEncoder.DEFAULT_MODEL_NAME) + assert used_fallback + + # Download the model, load it from tmpdir, and set env var. + load_pretrained_hf_model_from_hub(AlbertModel, "albert-base-v2").save_pretrained( + os.path.join(tmpdir, "albert-base-v2") + ) + os.environ["LUDWIG_PRETRAINED_MODELS_DIR"] = f"file://{tmpdir}" # Needs to be an absolute path. + _, used_fallback = load_pretrained_hf_model_with_hub_fallback(AlbertModel, ALBERTEncoder.DEFAULT_MODEL_NAME) + assert not used_fallback + + # Fallback is used for a model that doesn't exist in models directory. + _, used_fallback = load_pretrained_hf_model_with_hub_fallback(BertModel, BERTEncoder.DEFAULT_MODEL_NAME) + assert used_fallback + + # Clean up. + del os.environ["LUDWIG_PRETRAINED_MODELS_DIR"] + + +@pytest.fixture +def tmp_folder_with_file(tmpdir): + # Create a temporary folder + tmp_folder = str(tmpdir.mkdir("tmp_folder")) + + # Create a file within the temporary folder + file_path = os.path.join(tmp_folder, "test_file.txt") + with open(file_path, "w") as f: + f.write("Test content") + + yield tmp_folder + + # Clean up: Remove the temporary folder and its contents + shutil.rmtree(tmp_folder) + + +def test_upload_folder_to_hfhub_folder_not_exist(): + with pytest.raises(FileNotFoundError, match=r"Folder .* does not exist."): + upload_folder_to_hfhub("test_repo", "/nonexistent_folder") + + +def test_upload_folder_to_hfhub_folder_empty(tmpdir): + empty_folder = str(tmpdir.mkdir("empty_folder")) + with pytest.raises(ValueError, match=r"Folder .* is empty."): + upload_folder_to_hfhub("test_repo", empty_folder) + + +def test_upload_folder_to_hfhub_folder_is_file(tmpdir): + file_path = str(tmpdir.join("test_file.txt")) + with open(file_path, "w") as f: + f.write("Test content") + with pytest.raises(ValueError, match=r"Folder .* is a file. Please provide a folder."): + upload_folder_to_hfhub("test_repo", file_path) + + +def test_upload_folder_to_hfhub_invalid_repo_type(tmp_folder_with_file): + with pytest.raises(ValueError, match=r"Invalid repo_type .*"): + upload_folder_to_hfhub("test_repo", tmp_folder_with_file, repo_type="invalid_type") diff --git a/tests/ludwig/utils/test_hyperopt_ray_utils.py b/tests/ludwig/utils/test_hyperopt_ray_utils.py new file mode 100644 index 0000000..0b4f180 --- /dev/null +++ b/tests/ludwig/utils/test_hyperopt_ray_utils.py @@ -0,0 +1,92 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +import pytest + +try: + from ray import tune + + from ludwig.hyperopt.execution import get_build_hyperopt_executor +except ImportError: + RAY_AVAILABLE = False +else: + RAY_AVAILABLE = True + +# from ludwig.hyperopt.sampling import RayTuneSampler TDOO: remove +from ludwig.constants import RAY, TYPE + +HYPEROPT_PARAMS = { + "test_1": { + "parameters": { + "trainer.learning_rate": {"space": "uniform", "lower": 0.001, "upper": 0.1}, + "combiner.num_fc_layers": {"space": "qrandint", "lower": 3, "upper": 6, "q": 3}, + "utterance.cell_type": {"space": "grid_search", "values": ["rnn", "gru", "lstm"]}, + }, + }, + "test_2": { + "parameters": { + "trainer.learning_rate": { + "space": "loguniform", + "lower": 0.001, + "upper": 0.1, + "base": 10, + }, + "combiner.num_fc_layers": {"space": "randint", "lower": 2, "upper": 6}, + "utterance.cell_type": {"space": "choice", "categories": ["rnn", "gru", "lstm"]}, + }, + }, +} + +if RAY_AVAILABLE: + EXPECTED_SEARCH_SPACE = { + "test_1": { + "trainer.learning_rate": tune.uniform(0.001, 0.1), + "combiner.num_fc_layers": tune.qrandint(3, 6, 3), + "utterance.cell_type": tune.grid_search(["rnn", "gru", "lstm"]), + }, + "test_2": { + "trainer.learning_rate": tune.loguniform(0.001, 0.1), + "combiner.num_fc_layers": tune.randint(2, 6), + "utterance.cell_type": tune.choice(["rnn", "gru", "lstm"]), + }, + } + + +@pytest.mark.skipif(not RAY_AVAILABLE, reason="Ray is not installed for testing") +@pytest.mark.parametrize("key", ["test_1", "test_2"]) +def test_grid_strategy(key): + hyperopt_test_params = HYPEROPT_PARAMS[key] + expected_search_space = EXPECTED_SEARCH_SPACE[key] + + tune_sampler_params = hyperopt_test_params["parameters"] + + hyperopt_executor = get_build_hyperopt_executor(RAY)( + tune_sampler_params, + "output_feature", + "mse", + "minimize", + "validation", + search_alg={TYPE: "variant_generator"}, + **{"type": "ray", "num_samples": 2, "scheduler": {"type": "fifo"}}, + ) + + search_space = hyperopt_executor.search_space + + actual_params_keys = search_space.keys() + expected_params_keys = expected_search_space.keys() + + for param in search_space: + assert isinstance(search_space[param], type(expected_search_space[param])) + + assert actual_params_keys == expected_params_keys diff --git a/tests/ludwig/utils/test_image_utils.py b/tests/ludwig/utils/test_image_utils.py new file mode 100644 index 0000000..741f993 --- /dev/null +++ b/tests/ludwig/utils/test_image_utils.py @@ -0,0 +1,389 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +from collections.abc import Callable + +import pytest +import torch +import torchvision.transforms.functional as F + +from ludwig.utils.image_utils import ( + crop, + crop_or_pad, + get_class_mask_from_image, + get_image_from_class_mask, + get_unique_channels, + grayscale, + is_image_score, + num_channels_in_image, + pad, + read_image_as_tif, + resize_image, + ResizeChannels, +) + + +@pytest.mark.parametrize("pad_fn", [pad, torch.jit.script(pad)]) +@pytest.mark.parametrize( + "img,size,padded_img", + [ + ( + torch.arange(12, dtype=torch.int).reshape(3, 2, 2), + 4, + torch.Tensor( + [ + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 10, + 10, + 11, + 11, + ] + ) + .type(torch.int) + .reshape(3, 4, 4), + ) + ], +) +def test_pad(pad_fn: Callable, img: torch.Tensor, size: int, padded_img: torch.Tensor): + output_img = pad_fn(img, size) + assert torch.equal(output_img, padded_img) + + +@pytest.mark.parametrize("crop_fn", [crop, torch.jit.script(crop)]) +@pytest.mark.parametrize( + "img,size,cropped_img", + [ + ( + torch.arange(27, dtype=torch.int).reshape(3, 3, 3), + 2, + torch.Tensor([0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22]).type(torch.int).reshape(3, 2, 2), + ) + ], +) +def test_crop(crop_fn: Callable, img: torch.Tensor, size: int, cropped_img: torch.Tensor): + output_img = crop_fn(img, size) + assert torch.equal(output_img, cropped_img) + + +@pytest.mark.parametrize("crop_or_pad_fn", [crop_or_pad, torch.jit.script(crop_or_pad)]) +@pytest.mark.parametrize( + "img,new_size,expected_img", + [ + ( + torch.arange(12, dtype=torch.int).reshape(3, 2, 2), + 4, + torch.Tensor( + [ + 0, + 0, + 1, + 1, + 0, + 0, + 1, + 1, + 2, + 2, + 3, + 3, + 2, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 6, + 6, + 7, + 7, + 8, + 8, + 9, + 9, + 8, + 8, + 9, + 9, + 10, + 10, + 11, + 11, + 10, + 10, + 11, + 11, + ] + ) + .type(torch.int) + .reshape(3, 4, 4), + ), + ( + torch.arange(27, dtype=torch.int).reshape(3, 3, 3), + 2, + torch.Tensor([0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22]).type(torch.int).reshape(3, 2, 2), + ), + ], +) +def test_crop_or_pad(crop_or_pad_fn: Callable, img: torch.Tensor, new_size: int, expected_img: torch.Tensor): + output_image = crop_or_pad_fn(img, new_size) + assert torch.equal(output_image, expected_img) + + +@pytest.mark.parametrize("resize_image_fn", [resize_image, torch.jit.script(resize_image)]) +@pytest.mark.parametrize( + "img,new_size,resize_method", + [ + ( + torch.arange(27, dtype=torch.int).reshape(3, 3, 3), + 2, + "crop_or_pad", + ), + ( + torch.arange(27, dtype=torch.int).reshape(3, 3, 3), + 2, + "interpolate", + ), + ], +) +def test_resize_image(resize_image_fn: Callable, img: torch.Tensor, new_size: int, resize_method: str): + # Get the expected output from the underlying function + if resize_method == "crop_or_pad": + expected_img = crop_or_pad(img, new_size) + else: + expected_img = F.resize(img, new_size) + + output_img = resize_image_fn(img, new_size, resize_method) + + # Test that resize_image is equivalent to the underlying function output + assert torch.equal(output_img, expected_img) + + +@pytest.mark.parametrize("grayscale_fn", [grayscale, torch.jit.script(grayscale)]) +@pytest.mark.parametrize( + "input_img,grayscale_img", + [(torch.arange(12).reshape(3, 2, 2).type(torch.int), torch.Tensor([[[3, 4], [5, 6]]]).type(torch.int))], +) +def test_grayscale(grayscale_fn: Callable, input_img: torch.Tensor, grayscale_img: torch.Tensor): + output_img = grayscale_fn(input_img) + assert torch.equal(output_img, grayscale_img) + + +def test_num_channels_in_image(): + image_2d = torch.randint(0, 1, (10, 10)) + image_3d = torch.randint(0, 1, (3, 10, 10)) + assert num_channels_in_image(image_2d) == 1 + assert num_channels_in_image(image_3d) == 3 + + with pytest.raises(ValueError): + num_channels_in_image(torch.rand(5)) + num_channels_in_image(None) + + +@pytest.mark.parametrize("image_shape", [(1, 10, 10), (3, 10, 10), (5, 10, 10)]) +@pytest.mark.parametrize("num_channels_expected", [1, 2, 3, 4]) +def test_ResizeChannels_module(image_shape, num_channels_expected): + image = torch.randint(0, 1, image_shape) + fn = ResizeChannels(num_channels_expected) + assert fn(image).shape == tuple([num_channels_expected] + list(image_shape[1:])) + + +@pytest.mark.parametrize("image_shape", [(2, 1, 10, 10), (2, 3, 10, 10), (2, 5, 10, 10)]) +@pytest.mark.parametrize("num_channels_expected", [1, 2, 3, 4]) +def test_ResizeChannels_module_with_batch_dim(image_shape, num_channels_expected): + image = torch.randint(0, 1, image_shape) + fn = ResizeChannels(num_channels_expected) + assert fn(image).shape == tuple([image_shape[0], num_channels_expected] + list(image_shape[2:])) + + +def test_read_image_as_tif(): + img_bytes = b"II*\x00\x0c\x00\x00\x00\x05 \x8c\xe5\x10\x00\x00\x01\x03\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x01\x03\x00\x01\x00\x00\x00\x02\x00\x00\x00\x02\x01\x03\x00\x01\x00\x00\x00\x08\x00\x00\x00\x03\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x06\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x11\x01\x04\x00\x01\x00\x00\x00\x08\x00\x00\x00\x12\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x15\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x16\x01\x03\x00\x01\x00\x00\x00\x80\x00\x00\x00\x17\x01\x04\x00\x01\x00\x00\x00\x04\x00\x00\x00\x1a\x01\x05\x00\x01\x00\x00\x00\xd2\x00\x00\x00\x1b\x01\x05\x00\x01\x00\x00\x00\xda\x00\x00\x00\x1c\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x1d\x01\x02\x00\x07\x00\x00\x00\xe2\x00\x00\x00(\x01\x03\x00\x01\x00\x00\x00\x02\x00\x00\x00S\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x004.tiff\x00" + tensor = read_image_as_tif(img_bytes) + assert tensor is not None + assert tensor.equal(torch.tensor([[[5, 32], [140, 229]]], dtype=torch.uint8)) + + +@pytest.mark.parametrize( + "extension, score", + [ + ("data.png", 1), + ("/home/peter/data.jpg", 1), + ("./data/file.jpeg", 1), + ("new.tiff", 1), + ("b.tif", 1), + (".bmp", 1), + ("a.gif", 1), + ("b.tif", 1), + ("audio.wav", 0), + (".png/video.mp4", 0), + ], +) +def test_is_image_score(extension: str, score: int): + assert is_image_score(extension) == score + + +@pytest.mark.parametrize( + "img_list,num_channels,num_classes,expected_class_map", + [ + ( + [ + torch.Tensor([0, 0, 8, 8, 120, 120, 180, 180, 230, 230, 255, 255]).type(torch.uint8).reshape(3, 2, 2), + torch.Tensor([1, 2, 3, 4, 131, 132, 133, 134, 241, 242, 243, 244]).type(torch.uint8).reshape(3, 2, 2), + ], + 3, + None, + torch.Tensor( + [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]] + ).type(torch.uint8), + ), + ( + [ + torch.Tensor([0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255]) + .type(torch.uint8) + .reshape(1, 3, 5), + ], + 1, + None, + torch.Tensor([[0], [255]]).type(torch.uint8), + ), + ( + [ + torch.Tensor([0, 31, 17, 185, 192, 173, 55, 76, 24, 128, 255, 238]).type(torch.uint8).reshape(3, 4), + ], + 1, + 2, + torch.Tensor([[0], [255]]).type(torch.uint8), + ), + ], +) +def test_unique_channels( + img_list: list[torch.Tensor], num_channels: int, num_classes: int, expected_class_map: torch.Tensor +): + channel_class_map = get_unique_channels(img_list, num_channels, num_classes) + + channel_class_map, _ = channel_class_map.sort(dim=0) + expected_class_map, _ = expected_class_map.sort(dim=0) + assert torch.equal(channel_class_map, expected_class_map) + + +@pytest.mark.parametrize( + "img,channel_class_map,expected_mask", + [ + ( + torch.Tensor([1, 2, 3, 4, 131, 132, 133, 134, 241, 242, 243, 244]).type(torch.uint8).reshape(3, 2, 2), + torch.Tensor( + [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]] + ).type(torch.uint8), + torch.Tensor([2, 3, 4, 5]).type(torch.uint8).reshape(2, 2), + ), + ( + torch.Tensor([0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255]) + .type(torch.uint8) + .reshape(1, 3, 5), + torch.Tensor([[0], [255]]).type(torch.uint8), + torch.Tensor([0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1]).type(torch.uint8).reshape(3, 5), + ), + ( + torch.Tensor([0, 31, 17, 185, 192, 173, 55, 76, 24, 128, 255, 238]).type(torch.uint8).reshape(3, 4), + torch.Tensor([[0], [255]]).type(torch.uint8), + torch.Tensor([0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1]).type(torch.uint8).reshape(3, 4), + ), + ], +) +def test_class_mask_from_image(img: torch.Tensor, channel_class_map: torch.Tensor, expected_mask: torch.Tensor): + mask = get_class_mask_from_image(channel_class_map, img) + assert torch.equal(mask, expected_mask) + + +@pytest.mark.parametrize( + "mask,channel_class_map,expected_img", + [ + ( + torch.Tensor([0, 0, 1, 1]).type(torch.uint8).reshape(2, 2), + torch.Tensor( + [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]] + ).type(torch.uint8), + torch.Tensor([0, 0, 8, 8, 120, 120, 180, 180, 230, 230, 255, 255]).type(torch.uint8).reshape(3, 2, 2), + ), + ( + torch.Tensor([2, 3, 4, 5]).type(torch.uint8).reshape(2, 2), + torch.Tensor( + [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]] + ).type(torch.uint8), + torch.Tensor([1, 2, 3, 4, 131, 132, 133, 134, 241, 242, 243, 244]).type(torch.uint8).reshape(3, 2, 2), + ), + ( + torch.Tensor([0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1]).type(torch.uint8).reshape(3, 5), + torch.Tensor([[0], [255]]).type(torch.uint8), + torch.Tensor([0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255]) + .type(torch.uint8) + .reshape(1, 3, 5), + ), + ], +) +def test_image_from_class_mask(mask: torch.Tensor, channel_class_map: torch.Tensor, expected_img: torch.Tensor): + img = get_image_from_class_mask(channel_class_map, mask.numpy()) + assert torch.equal(torch.from_numpy(img), expected_img) diff --git a/tests/ludwig/utils/test_import_safety.py b/tests/ludwig/utils/test_import_safety.py new file mode 100644 index 0000000..3c99cfe --- /dev/null +++ b/tests/ludwig/utils/test_import_safety.py @@ -0,0 +1,92 @@ +"""Regression tests for top-level transformers import safety. + +When torchao and PyTorch are version-mismatched, transformers' lazy loader +raises ModuleNotFoundError for classes defined in modeling_utils.py +(notably PreTrainedModel). Ludwig modules that are imported at startup +must not have top-level imports of those classes, or `from ludwig.api +import LudwigModel` will crash before the user can do anything. + +These tests simulate that broken environment and verify the imports survive. +See: https://github.com/ludwig-ai/ludwig/issues/4142 +""" + +import sys + +import pytest + + +def _patch_transformers_pretrained_model_broken(): + """Monkey-patch the transformers module so __getattr__('PreTrainedModel') raises. + + This reproduces the failure mode where torchao calls + torch.utils._pytree.register_constant (added in PyTorch 2.5+) at import + time, causing transformers' lazy loader for modeling_utils.py classes to + raise ModuleNotFoundError. + """ + import transformers + + original_class = transformers.__class__ + + class BrokenTransformers(original_class): + def __getattr__(self, name): + if name == "PreTrainedModel": + raise ModuleNotFoundError( + "Could not import module 'PreTrainedModel'. Are this object's requirements defined correctly?" + ) + return super().__getattr__(name) + + transformers.__class__ = BrokenTransformers + return transformers, original_class + + +def _restore_transformers(transformers_module, original_class): + transformers_module.__class__ = original_class + + +def _evict_ludwig_modules(*substrings): + """Remove cached Ludwig modules containing any of the given substrings.""" + to_del = [k for k in sys.modules if any(s in k for s in substrings)] + for k in to_del: + del sys.modules[k] + + +@pytest.fixture() +def broken_pretrained_model(): + """Fixture that makes transformers.PreTrainedModel unavailable for the test.""" + + transformers_module, original_class = _patch_transformers_pretrained_model_broken() + _evict_ludwig_modules("llm_utils", "text_feature", "hf_utils") + yield + _restore_transformers(transformers_module, original_class) + _evict_ludwig_modules("llm_utils", "text_feature", "hf_utils") + + +def test_llm_utils_imports_without_pretrained_model(broken_pretrained_model): + """ludwig.utils.llm_utils must be importable when PreTrainedModel is broken. + + Regression: PreTrainedModel was imported at module level in llm_utils.py. + Fix: moved to TYPE_CHECKING + from __future__ import annotations. + """ + import ludwig.utils.llm_utils # must not raise # noqa: F401 + + +def test_text_feature_imports_without_pretrained_model(broken_pretrained_model): + """ludwig.features.text_feature must be importable when PreTrainedModel is broken. + + Regression: PreTrainedTokenizer (which shares the same broken lazy-loader + path) was imported at module level in text_feature.py. + Fix: moved to TYPE_CHECKING + from __future__ import annotations. + """ + import ludwig.features.text_feature # must not raise # noqa: F401 + + +def test_ludwig_api_imports_without_pretrained_model(broken_pretrained_model): + """from ludwig.api import LudwigModel must succeed when PreTrainedModel is broken. + + This is the exact import that the end-user runs and that was failing. + It exercises the full startup import chain: + ludwig.api → ludwig.backend → ... → ludwig.encoders.text_encoders + → ludwig.utils.llm_utils → transformers.PreTrainedModel (broken) + """ + _evict_ludwig_modules("ludwig") + import ludwig.utils.llm_utils # noqa: F401 diff --git a/tests/ludwig/utils/test_llm_utils.py b/tests/ludwig/utils/test_llm_utils.py new file mode 100644 index 0000000..a79085e --- /dev/null +++ b/tests/ludwig/utils/test_llm_utils.py @@ -0,0 +1,345 @@ +import pytest +import torch +from transformers import AutoConfig, AutoModelForCausalLM + +from ludwig.constants import LOGITS, PREDICTIONS, PROBABILITIES +from ludwig.modules.training_hooks import NEFTuneHook +from ludwig.utils.llm_utils import ( + add_left_padding, + create_attention_mask, + FALLBACK_CONTEXT_LEN, + find_last_matching_index, + generate_merged_ids, + get_context_len, + get_realigned_target_and_prediction_tensors_for_inference, + has_padding_token, + pad_target_tensor_for_fine_tuning, + remove_left_padding, +) +from ludwig.utils.tokenizers import HFTokenizer + +pytestmark = [pytest.mark.llm] + +# Pad token ID is 1 for OPT even though it uses the GPT2 tokenizer +# BOS token ID is 2 +TEST_MODEL_NAME = "hf-internal-testing/tiny-random-OPTForCausalLM" + + +@pytest.fixture +def tokenizer(): + return HFTokenizer(TEST_MODEL_NAME).tokenizer + + +@pytest.fixture +def input_ids(): + # Provide sample input IDs tensor + return torch.tensor([[3, 4, 5], [6, 7, 8]]) + + +@pytest.fixture +def target_ids(): + # Provide sample target IDs tensor + return torch.tensor([[9, 10, 11], [12, 13, 14]]) + + +class TestSetContextLen: + def test_max_sequence_length(self): + # Test when 'max_sequence_length' is present in the model configuration + config = AutoConfig.from_pretrained("huggyllama/llama-7b") + assert get_context_len(config) == config.max_sequence_length + + def test_max_position_embeddings(self): + # Test when 'max_position_embeddings' is present in the model configuration + config = AutoConfig.from_pretrained("huggyllama/llama-7b") + del config.max_sequence_length + assert get_context_len(config) == config.max_position_embeddings + + def test_n_positions(self): + # Test when 'n_positions' is present in the model configuration + config = AutoConfig.from_pretrained("hf-internal-testing/tiny-random-GPTJForCausalLM") + assert get_context_len(config) == config.n_positions + + def test_default_value(self): + config = AutoConfig.from_pretrained("hf-internal-testing/tiny-random-GPTJForCausalLM") + del config.n_positions + assert get_context_len(config) == FALLBACK_CONTEXT_LEN + + +def test_has_padding_token_with_padding_tokens(tokenizer): + input_sentence = "This is an example sentence." + input_ids = tokenizer([input_sentence]) + input_ids["input_ids"] = torch.tensor(input_ids["input_ids"]) + padded_input_ids = torch.nn.functional.pad(input_ids["input_ids"], (10 - len(input_ids["input_ids"]), 1), value=1) + + assert has_padding_token(padded_input_ids, tokenizer) + + +def test_has_padding_token_without_padding_tokens(tokenizer): + input_sentence = "This is an example sentence." + input_ids = tokenizer([input_sentence]) + input_ids["input_ids"] = torch.tensor(input_ids["input_ids"]) + + assert not has_padding_token(input_ids["input_ids"], tokenizer) + + +@pytest.mark.parametrize( + "input_ids, expected", + [ + # No padding + (torch.tensor([5]), torch.tensor([5])), + (torch.tensor([5, 3]), torch.tensor([5, 3])), + # Padding + (torch.tensor([1, 5, 5, 3]), torch.tensor([5, 5, 3])), + # EOS token + (torch.tensor([2, 5, 5, 3]), torch.tensor([2, 5, 5, 3])), + # Padding + EOS token + (torch.tensor([1, 2, 5, 5, 3]), torch.tensor([2, 5, 5, 3])), + ], +) +def test_remove_left_padding(input_ids, expected, tokenizer): + assert torch.equal(remove_left_padding(input_ids, tokenizer).squeeze(0), expected) + + +@pytest.mark.parametrize( + "input_ids, max_length, pad_value, expected", + [ + (torch.tensor([1, 2, 3]), 3, 0, torch.tensor([1, 2, 3])), + (torch.tensor([1, 2, 3]), 5, 0, torch.tensor([0, 0, 1, 2, 3])), + (torch.tensor([4, 5, 6, 7]), 6, 2, torch.tensor([2, 2, 4, 5, 6, 7])), + (torch.tensor([8, 9]), 3, 1, torch.tensor([1, 8, 9])), + ], +) +def test_add_left_padding(input_ids, max_length, pad_value, expected): + padded = add_left_padding(input_ids, max_length, pad_value).squeeze(0) + + assert torch.equal(padded, expected) + + +def test_create_attention_mask_last_token_padding(tokenizer): + input_ids = torch.tensor([3, 4, tokenizer.pad_token_id]) + attention_mask = create_attention_mask(input_ids, tokenizer) + assert attention_mask[-1] == 1 + + +@pytest.mark.parametrize( + "input_ids, expected_output", + [ + # No padding + (torch.tensor([3, 4, 5]), torch.tensor([1, 1, 1])), + (torch.tensor([1, 1, 4, 6, 8]), torch.tensor([0, 0, 1, 1, 1])), + # All padding + (torch.tensor([1, 1, 1]), torch.tensor([0, 0, 1])), + ], +) +def test_create_attention_mask(input_ids, expected_output, tokenizer): + attention_mask = create_attention_mask(input_ids, tokenizer) + + assert torch.equal(attention_mask, expected_output) + + +@pytest.mark.parametrize( + "tensor_a, tensor_b, expected_index", + [ + # Matching index at the end + (torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]), torch.tensor([6, 7, 8]), 5), + # No matching index + (torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]), torch.tensor([9, 10]), -1), + # Matching index in the middle. Fails because we're only checking the last X elements. + (torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]), torch.tensor([4, 5, 6]), -1), + ], +) +def test_find_last_matching_index(tensor_a, tensor_b, expected_index): + last_matching_index = find_last_matching_index(tensor_a, tensor_b) + assert last_matching_index == expected_index + + +def test_generate_merged_ids_with_target(tokenizer, input_ids, target_ids): + # Test case when target_ids is not None + merged_ids, attention_masks = generate_merged_ids(input_ids, target_ids, tokenizer) + assert torch.equal(merged_ids, torch.tensor([[3, 4, 5, 9, 10, 11, 2], [6, 7, 8, 12, 13, 14, 2]])) + assert merged_ids.shape == (2, 7) # Check the shape of merged_ids + assert attention_masks.shape == (2, 7) # Check the shape of attention_masks + + +def test_generate_merged_ids_with_max_sequence_length(tokenizer, input_ids, target_ids): + # Test case when max_sequence_length is provided + max_sequence_length = 5 + merged_ids, attention_masks = generate_merged_ids(input_ids, target_ids, tokenizer, max_sequence_length) + assert merged_ids.shape == (2, 5) # Check the shape of merged_ids with truncation + assert attention_masks.shape == (2, 5) # Check the shape of attention_masks + + +def test_generate_merged_ids_padding_removal(tokenizer, input_ids, target_ids): + # Test case to check removal of left padding from inputs and targets during merge + padding_tokens = torch.tensor([tokenizer.pad_token_id, tokenizer.pad_token_id]) + + # Adds 2 padding tokens to the left of input_ids and target_ids individually. Typically, if we just merged this + # naively, we would expect to see [1, 1, 3, 4, 5, 1, 1, 9, 10, 11, 1], but we shouldn't see the padding tokens + # except for the padding token at the end. + input_ids_with_padding = torch.cat((padding_tokens.unsqueeze(0).expand(input_ids.size(0), -1), input_ids), dim=1) + target_ids_with_padding = torch.cat((padding_tokens.unsqueeze(0).expand(target_ids.size(0), -1), target_ids), dim=1) + + merged_ids, attention_masks = generate_merged_ids(input_ids_with_padding, target_ids_with_padding, tokenizer) + + assert merged_ids.shape == (2, 7) # Check the shape of merged_ids + assert attention_masks.shape == (2, 7) # Check the shape of attention_masks + + assert torch.equal(merged_ids[0][:3], input_ids[0]) # Check the input_ids part without padding + assert torch.equal(merged_ids[0][3:-1], target_ids[0]) # Check the target_ids part without padding + assert torch.equal(merged_ids[0][-1], torch.tensor(tokenizer.eos_token_id)) # Check the padding tokens + + assert torch.all(attention_masks == 1) + + +def test_generate_merged_ids_returns_tensor(tokenizer, input_ids, target_ids): + # Test that the function returns torch.Tensor objects + merged_ids, attention_masks = generate_merged_ids(input_ids, target_ids, tokenizer) + assert isinstance(merged_ids, torch.Tensor) + assert isinstance(attention_masks, torch.Tensor) + + +def test_pad_target_tensor_for_fine_tuning(): + of_name = "out_1" + prediction = { + of_name: {PREDICTIONS: torch.tensor([[764, 764, 764, 764, 764, 764, 764, 578, 619, 841, 182, 905, 483, 764]])} + } + + # Scenario 1: Entire target tensor was passed into model inputs + model_input = torch.tensor([[0, 0, 24, 52, 654, 529, 221, 78, 79, 504, 76, 397, 84, 0]]) + target = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])} + expected_target = {of_name: torch.tensor([[-100, -100, -100, -100, -100, -100, -100, 78, 79, 504, 76, 397, 84, 0]])} + updated_targets = pad_target_tensor_for_fine_tuning(target, prediction, model_input, of_name) + assert torch.equal(expected_target[of_name], updated_targets[of_name]) + + # Scenario 2: Entire target tensor was not passed into model inputs + model_input = torch.tensor([[13, 24, 395, 13, 46, 57, 52, 41, 45, 37, 51, 14, 380, 435]]) + target = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])} + expected_target = { + of_name: torch.tensor([[-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100]]) + } + updated_targets = pad_target_tensor_for_fine_tuning(target, prediction, model_input, of_name) + assert torch.equal(expected_target[of_name], updated_targets[of_name]) + + # Scenario 3: Partial target tensor was passed into model inputs + model_input = torch.tensor([[0, 0, 24, 52, 654, 529, 221, 78, 79, 504, 76, 78, 79, 504]]) + target = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])} + expected_target = { + of_name: torch.tensor([[-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 78, 79, 504]]) + } + updated_targets = pad_target_tensor_for_fine_tuning(target, prediction, model_input, of_name) + assert torch.equal(expected_target[of_name], updated_targets[of_name]) + + +def test_get_realigned_target_and_prediction_tensors_for_inference(tokenizer): + of_name = "out_1" + vocab_size = 8 + + # Scenario 1: Prediction and target tensors have the same length, so nothing should change + targets = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])} + predictions = { + of_name: { + PREDICTIONS: torch.tensor([[78, 79, 504, 76, 397, 84, 0]], dtype=torch.int64), + PROBABILITIES: torch.randn(1, 7, vocab_size).to(torch.float32), + LOGITS: torch.randn(1, 7, vocab_size).to(torch.float32), + } + } + updated_targets, updated_predictions = get_realigned_target_and_prediction_tensors_for_inference( + targets, predictions, of_name, tokenizer + ) + + assert targets == updated_targets + assert predictions == updated_predictions + assert predictions[of_name][PREDICTIONS].shape[1] == targets[of_name].shape[1] + assert predictions[of_name][PROBABILITIES].shape[1] == targets[of_name].shape[1] + assert predictions[of_name][LOGITS].shape[1] == targets[of_name].shape[1] + + # Scenario 2: Prediction length is longer than the target tensor, so we need to realign the target tensor + targets = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])} + predictions = { + of_name: { + PREDICTIONS: torch.tensor([[98, 47, 78, 79, 504, 76, 397, 84, 0]], dtype=torch.int64), + PROBABILITIES: torch.randn(1, 9, vocab_size).to(torch.float32), + LOGITS: torch.randn(1, 9, vocab_size).to(torch.float32), + } + } + updated_targets, updated_predictions = get_realigned_target_and_prediction_tensors_for_inference( + targets, predictions, of_name, tokenizer + ) + + for key in updated_predictions.keys(): + assert torch.equal(updated_predictions[key][PREDICTIONS], predictions[key][PREDICTIONS]) + assert torch.equal(updated_predictions[key][PROBABILITIES], predictions[key][PROBABILITIES]) + assert torch.equal(updated_predictions[key][LOGITS], predictions[key][LOGITS]) + + assert torch.equal(updated_targets[of_name], torch.tensor([[78, 79, 504, 76, 397, 84, 0, 1, 1]])) + + # Scenario 3: Target length is longer than the prediction tensor, so we need to realign them + targets = {of_name: torch.tensor([[98, 47, 78, 79, 504, 76, 397, 84, 0]])} + predictions = { + of_name: { + PREDICTIONS: torch.tensor([[78, 79, 504, 76, 397, 84, 0]], dtype=torch.int64), + PROBABILITIES: torch.randn(1, 7, vocab_size).to(torch.float32), + LOGITS: torch.randn(1, 7, vocab_size).to(torch.float32), + } + } + updated_targets, updated_predictions = get_realigned_target_and_prediction_tensors_for_inference( + targets, predictions, of_name, tokenizer + ) + + assert torch.equal(updated_targets[of_name], targets[of_name]) + + assert torch.equal(updated_predictions[of_name][PREDICTIONS], torch.tensor([[78, 79, 504, 76, 397, 84, 0, 1, 1]])) + assert updated_predictions[of_name][PROBABILITIES].shape[1] == targets[of_name].shape[1] + assert updated_predictions[of_name][LOGITS].shape[1] == targets[of_name].shape[1] + + assert torch.equal(updated_predictions[of_name][PROBABILITIES][0][-1], torch.zeros(vocab_size)) + assert torch.equal(updated_predictions[of_name][PROBABILITIES][0][-2], torch.zeros(vocab_size)) + assert not torch.equal(updated_predictions[of_name][PROBABILITIES][0][-3], torch.zeros(vocab_size)) + + assert torch.equal(updated_predictions[of_name][LOGITS][0][-1], torch.zeros(vocab_size)) + assert torch.equal(updated_predictions[of_name][LOGITS][0][-2], torch.zeros(vocab_size)) + assert not torch.equal(updated_predictions[of_name][LOGITS][0][-3], torch.zeros(vocab_size)) + + +def _setup_models_for_neftune(): + module_without_hook = AutoModelForCausalLM.from_pretrained(TEST_MODEL_NAME) + module_with_hook = AutoModelForCausalLM.from_pretrained(TEST_MODEL_NAME) + + # Only module_with_hook should have the NEFTuneHook + neftune_hook = NEFTuneHook(neftune_noise_alpha=5) + module_with_hook = neftune_hook.activate_hook(module_with_hook) + + return module_without_hook, module_with_hook + + +def _forward_pass_and_assert_neftune_hook(module_without_hook, module_with_hook, mode): + assert module_with_hook.get_input_embeddings()._forward_hooks + assert not module_without_hook.get_input_embeddings()._forward_hooks + + if mode == "train": + module_without_hook.train() + module_with_hook.train() + elif mode == "eval": + module_without_hook.eval() + module_with_hook.eval() + + input_tensor = torch.tensor([[1, 2, 3]]) + output_tensor_with_noise = module_with_hook.get_input_embeddings()(input_tensor) + output_tensor_without_noise = module_without_hook.get_input_embeddings()(input_tensor) + + if mode == "train": + assert not torch.equal(output_tensor_with_noise, output_tensor_without_noise) + elif mode == "eval": + assert torch.equal(output_tensor_with_noise, output_tensor_without_noise) + + +def test_neftune_hook_with_noise_alpha_train_mode(): + """Test that the NEFTuneHook is only applied when the module is in training mode.""" + module_without_hook, module_with_hook = _setup_models_for_neftune() + _forward_pass_and_assert_neftune_hook(module_without_hook, module_with_hook, mode="train") + + +def test_neftune_hook_with_noise_alpha_eval_mode(): + """Test that the NEFTuneHook is not applied when the module is in eval mode.""" + module_without_hook, module_with_hook = _setup_models_for_neftune() + _forward_pass_and_assert_neftune_hook(module_without_hook, module_with_hook, mode="eval") diff --git a/tests/ludwig/utils/test_metric_utils.py b/tests/ludwig/utils/test_metric_utils.py new file mode 100644 index 0000000..7c764e9 --- /dev/null +++ b/tests/ludwig/utils/test_metric_utils.py @@ -0,0 +1,77 @@ +from collections import OrderedDict + +import torch + +from ludwig.utils import metric_utils +from ludwig.utils.metric_utils import TrainerMetric + + +def test_dynamic_partition(): + data = torch.Tensor([10, 20, 30, 40, 50]) + partitions = torch.Tensor([0, 0, 1, 1, 0]) + + partitioned_data = metric_utils.dynamic_partition(data, partitions, 2) + + assert torch.equal(partitioned_data[0], torch.Tensor([10.0, 20.0, 50.0])) + assert torch.equal(partitioned_data[1], torch.Tensor([30.0, 40.0])) + + +def test_dynamic_partition_2D(): + data = torch.Tensor( + [ + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14, 15, 16, 17, 18], + ] + ) + partitions = torch.Tensor([[1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 0]]) + + partitioned_data = metric_utils.dynamic_partition(data, partitions, 2) + + assert torch.equal(partitioned_data[0], torch.Tensor([9, 18])) + assert torch.equal( + partitioned_data[1], + torch.Tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0]), + ) + + +def test_masked_correct_predictions(): + preds = torch.tensor([[1, 5, 1, 5, 1, 5, 12, 12, 12], [10, 1, 5, 1, 5, 12, 12, 12, 12]]) + targets = torch.tensor([[1, 9, 5, 7, 5, 9, 13, 6, 0], [1, 9, 7, 13, 4, 7, 7, 7, 0]]) + targets_sequence_length = torch.tensor([8, 8]) + + result = metric_utils.masked_correct_predictions(targets, preds, targets_sequence_length) + + assert torch.equal( + result, torch.Tensor([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + ) + + +def test_reduce_trainer_metrics_dict(): + dict_dict_trainer_metrics = { + "feature_name": {"metric_name": [metric_utils.TrainerMetric(epoch=1, step=2, value=10)]} + } + + result = metric_utils.reduce_trainer_metrics_dict(dict_dict_trainer_metrics) + + assert result == {"feature_name": {"metric_name": [10]}} + + +def test_reduce_trainer_metrics_dict_ordered_dict(): + dict_dict_trainer_metrics = OrderedDict( + [ + ( + "category_5B6BF", + OrderedDict( + [ + ("loss", [TrainerMetric(epoch=0, step=1, value=0.0)]), + ("accuracy", [TrainerMetric(epoch=0, step=1, value=1.0)]), + ] + ), + ), + ("combined", {"loss": [TrainerMetric(epoch=0, step=1, value=0.0)]}), + ] + ) + + result = metric_utils.reduce_trainer_metrics_dict(dict_dict_trainer_metrics) + + assert result == {"category_5B6BF": {"accuracy": [1.0], "loss": [0.0]}, "combined": {"loss": [0.0]}} diff --git a/tests/ludwig/utils/test_model_card.py b/tests/ludwig/utils/test_model_card.py new file mode 100644 index 0000000..0afaa2b --- /dev/null +++ b/tests/ludwig/utils/test_model_card.py @@ -0,0 +1,78 @@ +"""Tests for model card generation.""" + +import os +import tempfile + +from ludwig.utils.model_card import generate_model_card, save_model_card + + +class TestGenerateModelCard: + def _make_config(self): + return { + "model_type": "ecd", + "input_features": [ + {"name": "age", "type": "number"}, + {"name": "workclass", "type": "category"}, + ], + "output_features": [{"name": "income", "type": "binary"}], + "combiner": {"type": "ft_transformer", "hidden_size": 192, "num_heads": 8}, + "trainer": {"learning_rate": 0.001, "epochs": 100, "batch_size": 256}, + } + + def _make_metadata(self): + return { + "age": {"mean": 38.5, "std": 13.2, "preprocessing": {"normalization": "zscore"}}, + "workclass": {"idx2str": ["Private", "Self-emp", "Gov"]}, + "income": {"idx2str": ["<=50K", ">50K"]}, + } + + def test_basic_card(self): + card = generate_model_card( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + ) + assert isinstance(card, str) + assert "# Model Card" in card + assert "ECD" in card + + def test_contains_features(self): + card = generate_model_card( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + ) + assert "age" in card + assert "workclass" in card + assert "income" in card + + def test_contains_combiner(self): + card = generate_model_card( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + ) + assert "ft_transformer" in card + + def test_contains_trainer_info(self): + card = generate_model_card( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + ) + assert "0.001" in card or "learning_rate" in card + + +class TestSaveModelCard: + def test_save_creates_file(self): + config = { + "model_type": "ecd", + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number"}], + } + with tempfile.TemporaryDirectory() as tmpdir: + path = save_model_card( + output_directory=tmpdir, + config=config, + training_set_metadata={"x": {}, "y": {}}, + ) + assert os.path.exists(path) + with open(path) as f: + content = f.read() + assert "# Model Card" in content diff --git a/tests/ludwig/utils/test_model_export.py b/tests/ludwig/utils/test_model_export.py new file mode 100644 index 0000000..d1687a6 --- /dev/null +++ b/tests/ludwig/utils/test_model_export.py @@ -0,0 +1,59 @@ +"""Tests for model export utilities.""" + +import os +import tempfile + +import torch +import torch.nn as nn + +from ludwig.utils.model_export import load_exported_model, ModelExporter + + +class SimpleModel(nn.Module): + def __init__(self): + super().__init__() + self.linear = nn.Linear(10, 5) + + def forward(self, x): + return self.linear(x) + + +class TestModelExporter: + def test_export_safetensors(self): + model = SimpleModel() + exporter = ModelExporter(model) + with tempfile.TemporaryDirectory() as tmpdir: + path = exporter.export_safetensors(tmpdir) + assert os.path.exists(path) + assert path.endswith(".safetensors") + + def test_export_torch(self): + model = SimpleModel() + exporter = ModelExporter(model) + sample = torch.randn(2, 10) + with tempfile.TemporaryDirectory() as tmpdir: + path = exporter.export_torch(tmpdir, sample) + assert os.path.exists(path) + + def test_generate_sample_input_fallback(self): + model = SimpleModel() + exporter = ModelExporter(model) + sample = exporter._generate_sample_input() + assert "input" in sample + + +class TestLoadExportedModel: + def test_load_torchscript(self): + model = SimpleModel() + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "model.pt") + traced = torch.jit.trace(model, torch.randn(2, 10)) + traced.save(path) + loaded = load_exported_model(path) + assert loaded is not None + + def test_unknown_format_raises(self): + import pytest + + with pytest.raises(ValueError, match="Unknown model format"): + load_exported_model("model.xyz") diff --git a/tests/ludwig/utils/test_model_soup.py b/tests/ludwig/utils/test_model_soup.py new file mode 100644 index 0000000..cfd3a4b --- /dev/null +++ b/tests/ludwig/utils/test_model_soup.py @@ -0,0 +1,42 @@ +"""Tests for model soup utilities.""" + +import torch + +from ludwig.utils.model_soup import uniform_soup + + +class TestUniformSoup: + def test_two_state_dicts(self): + sd1 = {"w": torch.tensor([1.0, 2.0]), "b": torch.tensor([0.0])} + sd2 = {"w": torch.tensor([3.0, 4.0]), "b": torch.tensor([2.0])} + avg = uniform_soup([sd1, sd2]) + assert torch.allclose(avg["w"], torch.tensor([2.0, 3.0])) + assert torch.allclose(avg["b"], torch.tensor([1.0])) + + def test_three_state_dicts(self): + sds = [ + {"w": torch.tensor([1.0, 1.0])}, + {"w": torch.tensor([2.0, 2.0])}, + {"w": torch.tensor([3.0, 3.0])}, + ] + avg = uniform_soup(sds) + assert torch.allclose(avg["w"], torch.tensor([2.0, 2.0])) + + def test_single_state_dict(self): + sd = {"w": torch.tensor([5.0])} + result = uniform_soup([sd]) + assert torch.equal(result["w"], sd["w"]) + + def test_preserves_dtype(self): + sd1 = {"w": torch.tensor([1.0, 2.0], dtype=torch.float16)} + sd2 = {"w": torch.tensor([3.0, 4.0], dtype=torch.float16)} + avg = uniform_soup([sd1, sd2]) + assert avg["w"].dtype == torch.float16 + + def test_multiple_keys(self): + sd1 = {"a": torch.ones(3), "b": torch.zeros(2), "c": torch.tensor([10.0])} + sd2 = {"a": torch.ones(3) * 3, "b": torch.ones(2) * 2, "c": torch.tensor([20.0])} + avg = uniform_soup([sd1, sd2]) + assert torch.allclose(avg["a"], torch.ones(3) * 2) + assert torch.allclose(avg["b"], torch.ones(2)) + assert torch.allclose(avg["c"], torch.tensor([15.0])) diff --git a/tests/ludwig/utils/test_model_utils.py b/tests/ludwig/utils/test_model_utils.py new file mode 100644 index 0000000..c5acbfc --- /dev/null +++ b/tests/ludwig/utils/test_model_utils.py @@ -0,0 +1,139 @@ +import pytest +import torch +from transformers import AutoModelForCausalLM + +from ludwig.utils.model_utils import ( + contains_nan_or_inf_tensors, + extract_tensors, + find_embedding_layer_with_path, + replace_tensors, +) + + +class SampleModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.conv = torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1) + self.relu = torch.nn.ReLU() + + +def test_extract_tensors(): + # Create a sample model + model = SampleModel() + + # Call extract_tensors function + stripped_model, tensors = extract_tensors(model) + + # Assert that the model and tensors are returned + assert isinstance(stripped_model, torch.nn.Module) + assert isinstance(tensors, list) + + # Assert that the tensors contain the expected keys + for tensor_dict in tensors: + assert "params" in tensor_dict + assert "buffers" in tensor_dict + + # Assert that all model parameters are set to None + for module in stripped_model.modules(): + for name, param in module.named_parameters(recurse=False): + assert param is None + + for name, buf in module.named_buffers(recurse=False): + assert buf is None + + +def test_replace_tensors(): + # Create a sample model + model = SampleModel() + + # Call extract_tensors function to get the tensors + _, tensors = extract_tensors(model) + + # Create a new device for testing + device = torch.device("cpu") + + # Call replace_tensors function + replace_tensors(model, tensors, device) + + # Assert that the tensors are restored + for module, tensor_dict in zip(model.modules(), tensors): + for name, array in tensor_dict["params"].items(): + assert name in module._parameters + assert torch.allclose(module._parameters[name], torch.as_tensor(array, device=device)) + + for name, array in tensor_dict["buffers"].items(): + assert name in module._buffers + assert torch.allclose(module._buffers[name], torch.as_tensor(array, device=device)) + + +class SampleModule(torch.nn.Module): + def __init__(self): + super().__init__() + self.embedding = torch.nn.Embedding(10, 20) + self.rnn = torch.nn.LSTM(20, 30) + + +def test_find_embedding_layer_with_path_simple(): + # Test case 1: Test the function with a simple module structure + module = SampleModule() + embedding_layer, path = find_embedding_layer_with_path(module) + assert embedding_layer is not None + assert isinstance(embedding_layer, torch.nn.Embedding) + assert path == "embedding" + + +def test_find_embedding_layer_with_path_complex(): + # Test case 2: Test the function with a more complex module structure including AutoModelForCausalLM + model = AutoModelForCausalLM.from_pretrained("HuggingFaceM4/tiny-random-LlamaForCausalLM") + + embedding_layer, path = find_embedding_layer_with_path(model) + assert embedding_layer is not None + assert isinstance(embedding_layer, torch.nn.Embedding) + assert path == "model.embed_tokens" + + +def test_no_embedding_layer(): + # Test case 3: Embedding layer is not present + no_embedding_model = torch.nn.Sequential(torch.nn.Linear(10, 10), torch.nn.Linear(10, 10)) + embedding_layer, path = find_embedding_layer_with_path(no_embedding_model) + assert embedding_layer is None + assert path is None + + +class TestHasNanOrInfTensors: + """Test suite for the 'has_nan_or_inf_tensors' function, which checks for NaN or infinity (inf) values in + PyTorch tensors.""" + + class SampleModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.param = torch.nn.Parameter(torch.tensor(1.0, requires_grad=True)) + self.buffer = torch.nn.Parameter(torch.tensor(1.0, requires_grad=True)) + + @pytest.fixture(autouse=True) + def setup(self): + self.model_with_nan_or_inf = self.SampleModel() + self.model_without_nan_or_inf = self.SampleModel() + self.transformer_model = AutoModelForCausalLM.from_pretrained("HuggingFaceM4/tiny-random-LlamaForCausalLM") + + def test_has_nan_or_inf_tensors_without_nan_or_inf(self): + assert contains_nan_or_inf_tensors(self.model_without_nan_or_inf) is False + + def test_has_nan_or_inf_tensors_with_nan(self): + self.model_with_nan_or_inf.param.data = torch.tensor(float("nan")) + assert contains_nan_or_inf_tensors(self.model_with_nan_or_inf) is True + + def test_has_nan_or_inf_tensors_without_nan(self): + self.model_with_nan_or_inf.buffer.data = torch.tensor(float("inf")) + assert contains_nan_or_inf_tensors(self.model_with_nan_or_inf) is True + + def test_has_nan_or_inf_tensors_transformer_model(self): + assert contains_nan_or_inf_tensors(self.transformer_model) is False + + def test_has_nan_or_inf_tensors_transformer_model_with_nan(self): + self.transformer_model.model.embed_tokens.weight.data[0][0] = float("nan") + assert contains_nan_or_inf_tensors(self.transformer_model) is True + + def test_has_nan_or_inf_tensors_transformer_model_with_inf(self): + self.transformer_model.model.embed_tokens.weight.data[0][0] = float("inf") + assert contains_nan_or_inf_tensors(self.transformer_model) is True diff --git a/tests/ludwig/utils/test_normalization.py b/tests/ludwig/utils/test_normalization.py new file mode 100644 index 0000000..10790c6 --- /dev/null +++ b/tests/ludwig/utils/test_normalization.py @@ -0,0 +1,132 @@ +# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import numpy as np +import pandas as pd +import pytest + +from ludwig.backend import initialize_backend +from ludwig.constants import COLUMN, NAME, PROC_COLUMN +from ludwig.features.feature_utils import compute_feature_hash +from ludwig.features.number_feature import NumberFeatureMixin, numeric_transformation_registry +from ludwig.utils.types import DataFrame + + +def number_feature(): + feature = {NAME: "x", COLUMN: "x", "type": "number"} + feature[PROC_COLUMN] = compute_feature_hash(feature) + return feature + + +def get_test_data(backend: str) -> tuple[DataFrame, DataFrame]: + """Returns test data for the given backend.""" + data_df = pd.DataFrame(pd.Series([2, 4, 6, 8, 10]), columns=["x"]) + proc_df = pd.DataFrame(columns=["x"]) + if backend == "ray": + import dask.dataframe as dd + + data_df = dd.from_pandas(data_df, npartitions=1).reset_index() + proc_df = dd.from_pandas(proc_df, npartitions=1).reset_index() + return data_df, proc_df + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=[pytest.mark.distributed, pytest.mark.distributed_f]), + ], +) +def test_norm(backend, ray_cluster_2cpu): + data_df, proc_df = get_test_data(backend) + backend = initialize_backend(backend) + + feature_1_meta = NumberFeatureMixin.get_feature_meta({}, data_df["x"], {"normalization": "zscore"}, backend, True) + feature_2_meta = NumberFeatureMixin.get_feature_meta({}, data_df["x"], {"normalization": "minmax"}, backend, True) + feature_3_meta = NumberFeatureMixin.get_feature_meta({}, data_df["x"], {"normalization": "iq"}, backend, True) + + assert feature_1_meta["mean"] == 6 + assert feature_2_meta["min"] == 2 + assert feature_2_meta["max"] == 10 + assert feature_3_meta["q1"] == 4 + assert feature_3_meta["q2"] == 6 + assert feature_3_meta["q3"] == 8 + + # value checks after normalization + num_feature = number_feature() + + NumberFeatureMixin.add_feature_data( + feature_config=num_feature, + input_df=data_df, + proc_df=proc_df, + metadata={num_feature[NAME]: feature_1_meta}, + preprocessing_parameters={"normalization": "zscore"}, + backend=backend, + skip_save_processed_input=False, + ) + assert np.allclose( + np.array(proc_df[num_feature[PROC_COLUMN]]), np.array([-1.26491106, -0.63245553, 0, 0.63245553, 1.26491106]) + ) + + NumberFeatureMixin.add_feature_data( + feature_config=num_feature, + input_df=data_df, + proc_df=proc_df, + metadata={num_feature[NAME]: feature_2_meta}, + preprocessing_parameters={"normalization": "minmax"}, + backend=backend, + skip_save_processed_input=False, + ) + assert np.allclose(np.array(proc_df[num_feature[PROC_COLUMN]]), np.array([0, 0.25, 0.5, 0.75, 1])) + + NumberFeatureMixin.add_feature_data( + feature_config=num_feature, + input_df=data_df, + proc_df=proc_df, + metadata={num_feature[NAME]: feature_3_meta}, + preprocessing_parameters={"normalization": "iq"}, + backend=backend, + skip_save_processed_input=False, + ) + assert np.allclose(np.array(proc_df[num_feature[PROC_COLUMN]]), np.array([-1, -0.5, 0, 0.5, 1])) + + +@pytest.mark.parametrize("transformation", numeric_transformation_registry.keys()) +@pytest.mark.parametrize( + "backend", + [ + pytest.param("local", id="local"), + pytest.param("ray", id="ray", marks=[pytest.mark.distributed, pytest.mark.distributed_f]), + ], +) +def test_numeric_transformation_registry(transformation, backend, ray_cluster_2cpu): + data_df, proc_df = get_test_data(backend) + backend = initialize_backend(backend) + + feature_meta = NumberFeatureMixin.get_feature_meta( + {}, data_df["x"], {"normalization": transformation}, backend, True + ) + + num_feature = number_feature() + + NumberFeatureMixin.add_feature_data( + feature_config=num_feature, + input_df=data_df, + proc_df=proc_df, + metadata={num_feature[NAME]: feature_meta}, + preprocessing_parameters={"normalization": transformation}, + backend=backend, + skip_save_processed_input=False, + ) diff --git a/tests/ludwig/utils/test_numerical_test_utils.py b/tests/ludwig/utils/test_numerical_test_utils.py new file mode 100644 index 0000000..45e6d0e --- /dev/null +++ b/tests/ludwig/utils/test_numerical_test_utils.py @@ -0,0 +1,40 @@ +import numpy as np +import pytest + +from ludwig.utils.numerical_test_utils import assert_all_finite + + +@pytest.fixture +def finite_valued_dict(): + return { + "scalar": 1, + "metrics": {"val": 0.2, "series": [0.1, 0.2, 0.3], "ndarray": np.ones((8, 4, 2))}, + } + + +def test_assert_all_finite(finite_valued_dict): + assert_all_finite(finite_valued_dict) + + +def test_fail_with_nan(finite_valued_dict): + finite_valued_dict["scalar"] = float("nan") + with pytest.raises(Exception): + assert_all_finite(finite_valued_dict) + + +def test_fail_with_inf(finite_valued_dict): + finite_valued_dict["scalar"] = float("inf") + with pytest.raises(Exception): + assert_all_finite(finite_valued_dict) + + +def test_fail_with_nan_in_list(finite_valued_dict): + finite_valued_dict["scalar"] = float("nan") + with pytest.raises(Exception): + assert_all_finite(finite_valued_dict) + + +def test_fail_with_nan_in_ndarray(finite_valued_dict): + finite_valued_dict["metrics"]["ndarray"][0, 0, 1] = np.nan + with pytest.raises(Exception): + assert_all_finite(finite_valued_dict) diff --git a/tests/ludwig/utils/test_output_feature_utils.py b/tests/ludwig/utils/test_output_feature_utils.py new file mode 100644 index 0000000..3025e30 --- /dev/null +++ b/tests/ludwig/utils/test_output_feature_utils.py @@ -0,0 +1,19 @@ +import pytest +import torch + +from ludwig.utils import output_feature_utils + + +def test_output_feature_utils(): + tensor_dict = {} + output_feature_utils.set_output_feature_tensor(tensor_dict, "feature_1", "1", torch.Tensor([1])) + output_feature_utils.set_output_feature_tensor(tensor_dict, "feature_1", "10", torch.Tensor([10])) + output_feature_utils.set_output_feature_tensor(tensor_dict, "feature_2", "2", torch.Tensor([2])) + output_feature_utils.set_output_feature_tensor(tensor_dict, "feature_2", "20", torch.Tensor([20])) + + assert list(tensor_dict.keys()) == ["feature_1::1", "feature_1::10", "feature_2::2", "feature_2::20"] + assert output_feature_utils.get_output_feature_tensor(tensor_dict, "feature_1", "1") == torch.Tensor([1]) + assert list(output_feature_utils.get_single_output_feature_tensors(tensor_dict, "feature_1").keys()) == ["1", "10"] + assert list(output_feature_utils.get_single_output_feature_tensors(tensor_dict, "feature_3").keys()) == [] + with pytest.raises(Exception): + output_feature_utils.get_output_feature_tensor(tensor_dict, "feature_1", "2") diff --git a/tests/ludwig/utils/test_registry_v2.py b/tests/ludwig/utils/test_registry_v2.py new file mode 100644 index 0000000..30bd498 --- /dev/null +++ b/tests/ludwig/utils/test_registry_v2.py @@ -0,0 +1,91 @@ +"""Tests for modernized registry.""" + +import pytest + +from ludwig.utils.registry import Registry + + +class TestRegistry: + def test_register_and_get(self): + reg = Registry() + reg["foo"] = "bar" + assert reg["foo"] == "bar" + + def test_register_decorator(self): + reg = Registry() + + @reg.register("my_class") + class MyClass: + pass + + assert reg["my_class"] is MyClass + + def test_default_registration(self): + reg = Registry() + + @reg.register("default_cls", default=True) + class DefaultClass: + pass + + assert reg[None] is DefaultClass + assert reg["none"] is DefaultClass + + def test_unregister(self): + reg = Registry() + reg["foo"] = "bar" + reg.unregister("foo") + assert "foo" not in reg + + def test_unregister_missing_raises(self): + reg = Registry() + with pytest.raises(KeyError): + reg.unregister("nonexistent") + + def test_get_default(self): + reg = Registry() + + @reg.register("cls", default=True) + class Cls: + pass + + assert reg.get_default() is Cls + + def test_get_default_none(self): + reg = Registry() + reg["foo"] = "bar" + assert reg.get_default() is None + + def test_list_registered(self): + reg = Registry() + reg["a"] = 1 + reg["b"] = 2 + + @reg.register("c", default=True) + class C: + pass + + names = reg.list_registered() + assert "a" in names + assert "b" in names + assert "c" in names + assert None not in names + + def test_parent_delegation(self): + parent = Registry() + parent["parent_key"] = "parent_value" + child = Registry(parent) + child["child_key"] = "child_value" + assert child["parent_key"] == "parent_value" + assert child["child_key"] == "child_value" + + def test_contains(self): + reg = Registry() + reg["x"] = 1 + assert "x" in reg + assert "y" not in reg + + def test_iteration(self): + reg = Registry() + reg["a"] = 1 + reg["b"] = 2 + assert set(reg.keys()) == {"a", "b"} diff --git a/tests/ludwig/utils/test_sequence_packing.py b/tests/ludwig/utils/test_sequence_packing.py new file mode 100644 index 0000000..491b9c8 --- /dev/null +++ b/tests/ludwig/utils/test_sequence_packing.py @@ -0,0 +1,111 @@ +"""Tests for sequence packing utilities.""" + +import torch + +from ludwig.utils.sequence_packing import create_block_diagonal_mask, pack_sequences + + +class TestPackSequences: + def test_basic_packing(self): + seqs = [torch.tensor([1, 2, 3]), torch.tensor([4, 5]), torch.tensor([6])] + masks = [torch.ones(3), torch.ones(2), torch.ones(1)] + labels = [torch.tensor([10, 20, 30]), torch.tensor([40, 50]), torch.tensor([60])] + + packed_ids, packed_mask, packed_labels, seq_ids = pack_sequences( + seqs, masks, labels, max_length=8, pad_token_id=0 + ) + + # All 3 short sequences should fit in one pack (3+2+1=6 <= 8) + assert packed_ids.shape[0] == 1 + assert packed_ids.shape[1] == 8 + + def test_attention_is_block_diagonal(self): + seqs = [torch.tensor([1, 2, 3]), torch.tensor([4, 5])] + masks = [torch.ones(3), torch.ones(2)] + + packed_ids, packed_mask, _, seq_ids = pack_sequences(seqs, masks, max_length=8, pad_token_id=0) + + # Attention mask should be 2D (block diagonal) + assert packed_mask.dim() == 3 # [num_packs, max_length, max_length] + + # Tokens from sequence 1 should NOT attend to tokens from sequence 2 + # seq1 is at positions 0-2, seq2 is at positions 3-4 + # Position 3 (first token of seq2) should NOT attend to position 2 (last token of seq1) + assert not packed_mask[0, 3, 2].item() + + # But position 1 SHOULD attend to position 0 (same sequence, causal) + assert packed_mask[0, 1, 0].item() + + def test_separate_packs_when_too_long(self): + seqs = [torch.tensor([1, 2, 3, 4, 5]), torch.tensor([6, 7, 8, 9, 10])] + masks = [torch.ones(5), torch.ones(5)] + + packed_ids, _, _, _ = pack_sequences(seqs, masks, max_length=6, pad_token_id=0) + + # Each sequence is 5 tokens, max_length=6, so they can't fit together + assert packed_ids.shape[0] == 2 + + def test_labels_masked_correctly(self): + seqs = [torch.tensor([1, 2, 3])] + masks = [torch.ones(3)] + labels = [torch.tensor([10, 20, 30])] + + _, _, packed_labels, _ = pack_sequences(seqs, masks, labels, max_length=6, pad_token_id=0) + + # Labels for actual tokens should be set, padding should be -100 + assert packed_labels[0, 0] == 10 + assert packed_labels[0, 1] == 20 + assert packed_labels[0, 2] == 30 + assert packed_labels[0, 3] == -100 # padding + + def test_sequence_ids(self): + seqs = [torch.tensor([1, 2]), torch.tensor([3, 4, 5])] + masks = [torch.ones(2), torch.ones(3)] + + _, _, _, seq_ids = pack_sequences(seqs, masks, max_length=8, pad_token_id=0) + + # Should have different sequence IDs for each packed sequence + # and -1 for padding + assert (seq_ids[0, :5] >= 0).all() # 2+3 = 5 tokens have valid seq IDs + assert (seq_ids[0, 5:] == -1).all() # rest is padding + + def test_max_sequences_per_pack(self): + seqs = [torch.tensor([i]) for i in range(10)] + masks = [torch.ones(1) for _ in range(10)] + + _, _, _, _ = pack_sequences(seqs, masks, max_length=100, pad_token_id=0, max_sequences_per_pack=3) + # 10 sequences, max 3 per pack = at least 4 packs (ceil(10/3)) + + +class TestBlockDiagonalMask: + def test_basic(self): + mask = create_block_diagonal_mask([3, 2], max_length=5, causal=True) + assert mask.shape == (5, 5) + + # First block: 3x3 lower triangular + assert mask[0, 0].item() + assert mask[1, 0].item() + assert mask[1, 1].item() + assert not mask[0, 1].item() # causal: can't look ahead + + # Second block: positions 3-4 + assert mask[3, 3].item() + assert mask[4, 3].item() + assert mask[4, 4].item() + + # Cross-block: should be zero + assert not mask[3, 2].item() + assert not mask[4, 0].item() + + def test_non_causal(self): + mask = create_block_diagonal_mask([2, 2], max_length=4, causal=False) + # Non-causal: full attention within blocks + assert mask[0, 1].item() # position 0 can see position 1 + assert mask[1, 0].item() + # Cross-block still zero + assert not mask[2, 1].item() + + def test_exceeds_max_length(self): + mask = create_block_diagonal_mask([3, 3], max_length=4) + # Second sequence gets truncated at max_length + assert mask.shape == (4, 4) diff --git a/tests/ludwig/utils/test_server_utils.py b/tests/ludwig/utils/test_server_utils.py new file mode 100644 index 0000000..5d56acd --- /dev/null +++ b/tests/ludwig/utils/test_server_utils.py @@ -0,0 +1,31 @@ +import numpy as np + +from ludwig.utils.server_utils import NumpyJSONResponse + + +def test_numpy_json_response(): + response = NumpyJSONResponse({"message": "Ludwig server is up"}) + + # Test Python builtin data type encoding. + assert response.render(None) == b"null" + assert response.render({}) == b"{}" + assert response.render(1) == b"1" + assert response.render(1.0) == b"1.0" + assert response.render("a") == b'"a"' + assert response.render([0, 1, 2, 3, 4]) == b"[0,1,2,3,4]" + assert response.render((0, 1, 2, 3, 4)) == b"[0,1,2,3,4]" + assert response.render({0, 1, 2, 3, 4}) == b"[0,1,2,3,4]" + assert response.render({"a": "b"}) == b'{"a":"b"}' + + # Test numpy data type encoding + for dtype in [np.byte, np.ubyte, np.short, np.ushort, np.int32, np.int64, np.uint, np.longlong, np.ulonglong]: + x = np.arange(5, dtype=dtype) + assert response.render(x) == b"[0,1,2,3,4]" + for i in x: + assert response.render(i) == f"{i}".encode() + + for dtype in [np.half, np.single, np.double, np.longdouble]: + x = np.arange(5, dtype=dtype) + assert response.render(x) == b"[0.0,1.0,2.0,3.0,4.0]" + for i in x: + assert response.render(i) == f"{i}".encode() diff --git a/tests/ludwig/utils/test_state_dict_backward_compatibility.py b/tests/ludwig/utils/test_state_dict_backward_compatibility.py new file mode 100644 index 0000000..4873081 --- /dev/null +++ b/tests/ludwig/utils/test_state_dict_backward_compatibility.py @@ -0,0 +1,31 @@ +from ludwig.utils.state_dict_backward_compatibility import update_state_dict + + +def test_update_transformer_module_keys(): + state_dict_with_old_keys = { + "input_features.module_dict.sentence__ludwig.encoder_obj.transformer.embeddings.LayerNorm.bias": 0.0, + "sentence__ludwig.encoder_obj.transformer.encoder.layer.0.attention.output.LayerNorm.weight": 0.0, + "module_dict.sentence__ludwig.encoder_obj.transformer.embeddings.word_embeddings.weight": 0.0, + } + + expected_state_dict = { + "input_features.module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.LayerNorm.bias": 0.0, + "sentence__ludwig.encoder_obj.transformer.module.encoder.layer.0.attention.output.LayerNorm.weight": 0.0, + "module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.word_embeddings.weight": 0.0, + } + + # Ensures that, for models saved before FreezeModule was added, 'module' is added to the key path. + updated_state_dict = update_state_dict(state_dict_with_old_keys) + assert updated_state_dict == expected_state_dict + + +def test_does_not_update_freeze_module(): + state_dict = { + "module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.LayerNorm.bias": 0.0, + "sentence__ludwig.encoder_obj.transformer.module.encoder.layer.0.attention.output.LayerNorm.weight": 0.0, + "module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.word_embeddings.weight": 0.0, + } + + # Ensures that models saved with FreezeModule aren't modified. + updated_state_dict = update_state_dict(state_dict) + assert updated_state_dict == state_dict diff --git a/tests/ludwig/utils/test_strings_utils.py b/tests/ludwig/utils/test_strings_utils.py new file mode 100644 index 0000000..5c775c9 --- /dev/null +++ b/tests/ludwig/utils/test_strings_utils.py @@ -0,0 +1,306 @@ +from collections import defaultdict + +import numpy as np +import pandas as pd +import pytest + +from ludwig.schema.features.preprocessing.text import TextPreprocessingConfig +from ludwig.utils import strings_utils + + +def test_is_number(): + assert strings_utils.is_number("1.1") + assert strings_utils.is_number("1.000001") + assert strings_utils.is_number("1000001") + assert strings_utils.is_number("Nan") + assert strings_utils.is_number("NaN") + assert strings_utils.is_number(1) + assert strings_utils.is_number(1.1) + assert not strings_utils.is_number("NaNaaa") + + +def test_are_sequential_integers(): + assert strings_utils.are_sequential_integers(["1.0", "2", "3"]) + assert strings_utils.are_sequential_integers(["1", "2", "3"]) + assert not strings_utils.are_sequential_integers(["1", "2", "4"]) + assert not strings_utils.are_sequential_integers(["1.1", "2", "3"]) + assert not strings_utils.are_sequential_integers(["a", "2", "3"]) + + +def test_str_to_bool(): + # Global bool mappings are used. + assert strings_utils.str2bool("True") + assert strings_utils.str2bool(True) + assert strings_utils.str2bool("true") + assert not strings_utils.str2bool("0") + + # Error raised if non-mapped value is encountered and no fallback is specified. + with pytest.raises(Exception): + strings_utils.str2bool("bot") + + # Fallback label is used. + assert strings_utils.str2bool("bot", fallback_true_label="bot") + assert not strings_utils.str2bool("human", fallback_true_label="bot") + assert strings_utils.str2bool("human", fallback_true_label="human") + assert not strings_utils.str2bool("human", fallback_true_label="Human") + + # Fallback label is used, strictly as a fallback. + assert strings_utils.str2bool("True", fallback_true_label="False") + + +def test_are_conventional_bools(): + assert strings_utils.are_conventional_bools(["True", "False"]) + assert strings_utils.are_conventional_bools([True, False]) + assert strings_utils.are_conventional_bools(["True", False, True]) + assert strings_utils.are_conventional_bools(["T", "F"]) + assert strings_utils.are_conventional_bools(["t", "f"]) + assert not strings_utils.are_conventional_bools(["True", "Fails"]) + assert strings_utils.are_conventional_bools(["0", "1"]) + assert not strings_utils.are_conventional_bools(["0", "2"]) + assert strings_utils.are_conventional_bools(["1.0", "0.0"]) + assert not strings_utils.are_conventional_bools(["high", "low"]) + assert not strings_utils.are_conventional_bools(["human", "bot"]) + + +def test_create_vocabulary_chars(): + data = pd.DataFrame(["Hello, I'm a single sentence!", "And another sentence", "And the very very last one"]) + column = data[0] + preprocessing_parameters = TextPreprocessingConfig().to_dict() + + vocabulary = strings_utils.create_vocabulary( + column, + tokenizer_type="characters", + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + ) + vocab = vocabulary.vocab + + assert len(vocab) == 27 + assert vocab[strings_utils.SpecialSymbol.START.value] == strings_utils.START_SYMBOL + assert vocab[strings_utils.SpecialSymbol.STOP.value] == strings_utils.STOP_SYMBOL + assert vocab[strings_utils.SpecialSymbol.PADDING.value] == strings_utils.PADDING_SYMBOL + assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL + + +def test_create_vocabulary_word(): + data = pd.DataFrame(["Hello, I'm a single sentence!", "And another sentence", "And the very very last one"]) + column = data[0] + preprocessing_parameters = TextPreprocessingConfig().to_dict() + + vocabulary = strings_utils.create_vocabulary( + column, + tokenizer_type=preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + vocab_file=preprocessing_parameters["vocab_file"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + ) + vocab = vocabulary.vocab + + assert len(vocab) == 19 + assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL + assert vocab[strings_utils.SpecialSymbol.STOP.value] == strings_utils.STOP_SYMBOL + assert vocab[strings_utils.SpecialSymbol.PADDING.value] == strings_utils.PADDING_SYMBOL + assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL + + +def test_create_vocabulary_no_special_symbols(): + data = pd.DataFrame(["Hello, I'm a single sentence!", "And another sentence", "And the very very last one"]) + column = data[0] + preprocessing_parameters = TextPreprocessingConfig().to_dict() + + vocabulary = strings_utils.create_vocabulary( + column, + tokenizer_type=preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + vocab_file=preprocessing_parameters["vocab_file"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + add_special_symbols=False, + ) + vocab = vocabulary.vocab + + assert len(vocab) == 16 + assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL + + +def test_create_vocabulary_from_hf(): + data = pd.DataFrame(["Hello, I'm a single sentence!", "And another sentence", "And the very very last one"]) + column = data[0] + preprocessing_parameters = TextPreprocessingConfig().to_dict() + + vocabulary = strings_utils.create_vocabulary( + column, + tokenizer_type="hf_tokenizer", + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path="albert-base-v2", + ) + vocab = vocabulary.vocab + + assert len(vocab) == 30000 + + +def test_create_vocabulary_single_token(): + data = pd.DataFrame(["dog", "cat", "bird", "dog", "cat", "super cat"]) + column = data[0] + + vocab, str2idx, str2freq = strings_utils.create_vocabulary_single_token( + column, + num_most_frequent=10000, + ) + + assert set(vocab) == {"dog", "cat", "bird", "super cat"} + assert str2freq == {"dog": 2, "cat": 2, "bird": 1, "super cat": 1} + assert strings_utils.UNKNOWN_SYMBOL not in str2idx + + +def test_create_vocabulary_single_token_small_most_frequent(): + data = pd.DataFrame(["dog", "cat", "bird", "dog", "cat", "super cat"]) + column = data[0] + + vocab, str2idx, str2freq = strings_utils.create_vocabulary_single_token(column, num_most_frequent=2) + + assert set(vocab) == {"dog", "cat", strings_utils.UNKNOWN_SYMBOL} + assert str2idx[strings_utils.UNKNOWN_SYMBOL] == 0 + assert str2freq == {"dog": 2, "cat": 2, strings_utils.UNKNOWN_SYMBOL: 0} + + +def test_build_sequence_matrix(): + inverse_vocabulary = { + "": 0, + "": 1, + "": 2, + "": 3, + "a": 4, + "b": 5, + "c": 6, + } + sequences = pd.core.series.Series(["a b c", "c b a"]) + sequence_matrix = strings_utils.build_sequence_matrix( + sequences, inverse_vocabulary, tokenizer_type="space", length_limit=10 + ) + assert not ( + sequence_matrix.tolist() - np.array([[1, 4, 5, 6, 0, 2, 2, 2, 2, 2], [1, 6, 5, 4, 0, 2, 2, 2, 2, 2]]) + ).any() + + +@pytest.mark.parametrize( + "pretrained_model_name_or_path", + [ + "bert-base-uncased", + "gpt2", + "HuggingFaceH4/zephyr-7b-beta", + ], +) +def test_get_vocabulary_hf(pretrained_model_name_or_path): + tokenizer_type = "hf_tokenizer" + vocab_file = None + data = pd.DataFrame(["Hello, I'm a single sentence!", "And another sentence", "And the very very last one"]) + column = data[0] + preprocessing_parameters = ( + TextPreprocessingConfig() + .from_dict( + { + "tokenizer": tokenizer_type, + "vocab_file": vocab_file, + "pretrained_model_name_or_path": pretrained_model_name_or_path, + } + ) + .to_dict() + ) + + vocabulary = strings_utils.create_vocabulary( + column, + tokenizer_type=preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + vocab_file=preprocessing_parameters["vocab_file"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + compute_idf=False, + add_special_symbols=False, + ) + + tokenizer = strings_utils.get_tokenizer( + tokenizer_type=preprocessing_parameters["tokenizer"], + tokenizer_vocab_file=preprocessing_parameters["vocab_file"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + ) + + # check special tokens + assert vocabulary.padding_symbol == tokenizer.get_pad_token() + assert vocabulary.pad_idx == tokenizer.convert_token_to_id(tokenizer.get_pad_token()) + assert vocabulary.unknown_symbol == tokenizer.get_unk_token() + + # check all tokens + for token, idx in tokenizer.get_vocab().items(): + assert vocabulary.str2idx[token] == idx + + +@pytest.mark.parametrize("compute_idf", [False, True]) +def test_create_vocabulary_idf(compute_idf: bool): + data = pd.DataFrame(["Hello, I'm a single sentence!", "And another sentence", "And the very very last one"]) + column = data[0] + preprocessing_parameters = TextPreprocessingConfig().to_dict() + + vocabulary = strings_utils.create_vocabulary( + column, + tokenizer_type=preprocessing_parameters["tokenizer"], + num_most_frequent=preprocessing_parameters["most_common"], + lowercase=preprocessing_parameters["lowercase"], + vocab_file=preprocessing_parameters["vocab_file"], + unknown_symbol=preprocessing_parameters["unknown_symbol"], + padding_symbol=preprocessing_parameters["padding_symbol"], + pretrained_model_name_or_path=preprocessing_parameters["pretrained_model_name_or_path"], + compute_idf=compute_idf, + add_special_symbols=False, + ) + + str2idf = vocabulary.str2idf + + if not compute_idf: + assert str2idf is None + return + + idf2str = defaultdict(set) + for k, v in str2idf.items(): + idf2str[v].add(k) + idf_sorted = sorted(idf2str.items(), key=lambda x: x[0]) + assert len(idf_sorted) == 3 + + # Unknown symbol should have the lowest idf as it never appears in any documents + assert idf_sorted[0][0] == 0 + assert idf_sorted[0][1] == {""} + + # "sentence" and "and" should be next, as they appear in two docs each + assert idf_sorted[1][0] > idf_sorted[0][0] + assert idf_sorted[1][1] == {"sentence", "And"} + + # finally, every token that only appears once + assert idf_sorted[2][0] > idf_sorted[1][0] + assert idf_sorted[2][1] == { + ",", + "I", + "'", + "one", + "very", + "single", + "the", + "m", + "!", + "last", + "Hello", + "a", + "another", + } diff --git a/tests/ludwig/utils/test_tokenizers.py b/tests/ludwig/utils/test_tokenizers.py new file mode 100644 index 0000000..bcaa75b --- /dev/null +++ b/tests/ludwig/utils/test_tokenizers.py @@ -0,0 +1,79 @@ +import pytest + +from ludwig.utils.tokenizers import EnglishLemmatizeFilterTokenizer, NgramTokenizer, StringSplitTokenizer + + +def test_ngram_tokenizer(): + inputs = "Hello, I'm a single sentence!" + tokenizer = NgramTokenizer(n=2) + tokens_expected = [ + "Hello,", + "I'm", + "a", + "single", + "sentence!", + "Hello, I'm", + "I'm a", + "a single", + "single sentence!", + ] + tokens = tokenizer(inputs) + assert tokens == tokens_expected + + +def test_string_split_tokenizer(): + inputs = "Multiple,Elements,Are here!" + tokenizer = StringSplitTokenizer(",") + tokens = tokenizer(inputs) + assert tokens == ["Multiple", "Elements", "Are here!"] + + +def test_english_lemmatize_filter_tokenizer(): + inputs = "Hello, I'm a single sentence!" + tokenizer = EnglishLemmatizeFilterTokenizer() + tokens = tokenizer(inputs) + assert len(tokens) > 0 + + +@pytest.mark.parametrize( + "model_name,expected_cls", + [ + # Standard BERT models must use BERTTokenizer (WordPiece) + ("bert-base-uncased", "BERTTokenizer"), + ("bert-large-cased", "BERTTokenizer"), + # Models with "bert" in their name that use different tokenization + # must NOT use BERTTokenizer + ("roberta-base", "HFTokenizer"), + ("albert-base-v2", "HFTokenizer"), + ("distilbert-base-uncased", "HFTokenizer"), + # ModernBERT uses BPE (no [UNK] token) — must NOT use BERTTokenizer + ("answerdotai/ModernBERT-base", "HFTokenizer"), + ("answerdotai/ModernBERT-large", "HFTokenizer"), + ], +) +def test_get_hf_tokenizer_routing(model_name, expected_cls): + """Regression: get_hf_tokenizer() must route ModernBERT and RoBERTa-family + models to HFTokenizer, not BERTTokenizer. + + ModernBERT uses BPE (no [UNK] token), so loading it via BertTokenizer raises + 'WordPiece error: Missing [UNK] token from the vocabulary'. + """ + from unittest.mock import MagicMock, patch + + from ludwig.utils.tokenizers import get_hf_tokenizer + + mock_tokenizer = MagicMock() + with ( + patch("ludwig.utils.tokenizers.BERTTokenizer") as mock_bert, + patch("ludwig.utils.tokenizers.HFTokenizer") as mock_hf, + ): + mock_bert.return_value = mock_tokenizer + mock_hf.return_value = mock_tokenizer + get_hf_tokenizer(model_name) + + if expected_cls == "BERTTokenizer": + mock_bert.assert_called_once() + mock_hf.assert_not_called() + else: + mock_hf.assert_called_once() + mock_bert.assert_not_called() diff --git a/tests/ludwig/utils/test_torch_utils.py b/tests/ludwig/utils/test_torch_utils.py new file mode 100644 index 0000000..8f9bdbd --- /dev/null +++ b/tests/ludwig/utils/test_torch_utils.py @@ -0,0 +1,108 @@ +import contextlib +import os +from unittest.mock import patch + +import pytest +import torch + +from ludwig.utils.torch_utils import ( + _get_torch_init_params, + _set_torch_init_params, + initialize_pytorch, + sequence_length_2D, + sequence_length_3D, +) + +_CUDA_AVAILABLE = torch.cuda.is_available() and torch.cuda.device_count() > 0 + + +@pytest.mark.parametrize("input_sequence", [[[0, 1, 1], [2, 0, 0], [3, 3, 3]]]) +@pytest.mark.parametrize("expected_output", [[3, 2, 3]]) +def test_sequence_length_2D(input_sequence: list[list[int]], expected_output: list[int]): + output_seq_length = sequence_length_2D(torch.tensor(input_sequence)) + assert torch.equal(torch.tensor(expected_output), output_seq_length) + + +@pytest.mark.parametrize("input_sequence", [[[[-1, 0, 1], [1, -2, 0]], [[0, 0, 0], [3, 0, -2]]]]) +@pytest.mark.parametrize("expected_output", [[2, 1]]) +def test_sequence_length_3D(input_sequence: list[list[list[int]]], expected_output: list[int]): + input_sequence = torch.tensor(input_sequence, dtype=torch.int32) + expected_output = torch.tensor(expected_output, dtype=torch.int32) + output_seq_length = sequence_length_3D(input_sequence) + assert torch.equal(expected_output, output_seq_length) + + +@contextlib.contextmanager +def clean_params(): + prev = _get_torch_init_params() + prev_cuda = os.environ.get("CUDA_VISIBLE_DEVICES") + try: + _set_torch_init_params(None) + if "CUDA_VISIBLE_DEVICES" in os.environ: + del os.environ["CUDA_VISIBLE_DEVICES"] + yield + finally: + _set_torch_init_params(prev) + # Restore CUDA_VISIBLE_DEVICES to prevent contaminating other tests + if prev_cuda is not None: + os.environ["CUDA_VISIBLE_DEVICES"] = prev_cuda + elif "CUDA_VISIBLE_DEVICES" in os.environ: + del os.environ["CUDA_VISIBLE_DEVICES"] + + +def test_initialize_pytorch_only_once(): + """Second call with identical params is a no-op; mismatched params emit a warning.""" + with clean_params(): + initialize_pytorch(allow_parallel_threads=True) + assert _get_torch_init_params() == (None, None, True) + + # Exact same params: silent no-op, stored params unchanged + initialize_pytorch(allow_parallel_threads=True) + assert _get_torch_init_params() == (None, None, True) + + # Different params: warns, still no-op + with pytest.warns(UserWarning, match="already been initialized"): + initialize_pytorch(allow_parallel_threads=False) + assert _get_torch_init_params() == (None, None, True) + + +@pytest.mark.skipif(not _CUDA_AVAILABLE, reason="requires CUDA") +@patch("ludwig.utils.torch_utils.torch") +def test_initialize_pytorch_with_gpu_list(mock_torch): + # For test purposes, these devices can be anything, we just need to be able to uniquely + # identify them. + mock_torch.cuda.is_available.return_value = True + mock_torch.cuda.device_count.return_value = 4 + with clean_params(): + initialize_pytorch(gpus=[1, 2]) + assert os.environ["CUDA_VISIBLE_DEVICES"] == "1,2" + + +@pytest.mark.skipif(not _CUDA_AVAILABLE, reason="requires CUDA") +@patch("ludwig.utils.torch_utils.torch") +def test_initialize_pytorch_with_gpu_string(mock_torch): + mock_torch.cuda.is_available.return_value = True + mock_torch.cuda.device_count.return_value = 4 + with clean_params(): + initialize_pytorch(gpus="1,2") + assert os.environ["CUDA_VISIBLE_DEVICES"] == "1,2" + + +@pytest.mark.skipif(not _CUDA_AVAILABLE, reason="requires CUDA") +@patch("ludwig.utils.torch_utils.torch") +def test_initialize_pytorch_with_gpu_int(mock_torch): + mock_torch.cuda.is_available.return_value = True + mock_torch.cuda.device_count.return_value = 4 + with clean_params(): + initialize_pytorch(gpus=1) + mock_torch.cuda.set_device.assert_called_with(1) + assert "CUDA_VISIBLE_DEVICES" not in os.environ + + +@patch("ludwig.utils.torch_utils.torch") +def test_initialize_pytorch_without_gpu(mock_torch): + mock_torch.cuda.is_available.return_value = True + mock_torch.cuda.device_count.return_value = 4 + with clean_params(): + initialize_pytorch(gpus=-1) + assert os.environ["CUDA_VISIBLE_DEVICES"] == "" diff --git a/tests/ludwig/utils/test_trainer_utils.py b/tests/ludwig/utils/test_trainer_utils.py new file mode 100644 index 0000000..3976d6a --- /dev/null +++ b/tests/ludwig/utils/test_trainer_utils.py @@ -0,0 +1,367 @@ +import sys +from collections import OrderedDict + +import pytest + +from ludwig.constants import AUTO, BATCH_SIZE, COMBINED, LOSS +from ludwig.features.category_feature import CategoryOutputFeature +from ludwig.features.feature_utils import LudwigFeatureDict +from ludwig.schema.features.category_feature import ECDCategoryOutputFeatureConfig +from ludwig.schema.trainer import ECDTrainerConfig +from ludwig.schema.utils import load_config_with_kwargs +from ludwig.utils import trainer_utils +from ludwig.utils.metric_utils import TrainerMetric + + +def test_get_latest_metrics_dict(): + progress_tracker_metrics = OrderedDict( + [ + ( + "category_92E9E", + OrderedDict( + [ + ( + "loss", + [ + TrainerMetric(epoch=0, step=1, value=0.7929425835609436), + TrainerMetric(epoch=1, step=2, value=0.7906522750854492), + ], + ), + ( + "accuracy", + [ + TrainerMetric(epoch=0, step=1, value=0.4117647111415863), + TrainerMetric(epoch=1, step=2, value=0.4117647111415863), + ], + ), + ] + ), + ), + ( + "combined", + { + "loss": [ + TrainerMetric(epoch=0, step=1, value=0.7929425835609436), + TrainerMetric(epoch=1, step=2, value=0.7906522750854492), + ] + }, + ), + ] + ) + + latest_metrics_dict = trainer_utils.get_latest_metrics_dict(progress_tracker_metrics) + + assert latest_metrics_dict == { + "category_92E9E": {"accuracy": 0.4117647111415863, "loss": 0.7906522750854492}, + "combined": {"loss": 0.7906522750854492}, + } + + +def test_get_latest_metrics_dict_empty(): + progress_tracker_metrics = OrderedDict( + [("category_F18D1", OrderedDict([("loss", []), ("accuracy", [])])), ("combined", {"loss": []})] + ) + + latest_metrics_dict = trainer_utils.get_latest_metrics_dict(progress_tracker_metrics) + + assert not latest_metrics_dict + + +def test_progress_tracker_empty(): + output_features = LudwigFeatureDict() + category_feature, _ = load_config_with_kwargs( + ECDCategoryOutputFeatureConfig, + { + "name": "category_feature", + "type": "category", + "decoder": { + "type": "classifier", + }, + "num_classes": 3, + "input_size": 10, + }, + ) + output_features.set("category_feature", CategoryOutputFeature(category_feature, {})) + + progress_tracker = trainer_utils.get_new_progress_tracker( + batch_size=5, + best_eval_metric_value=0, + best_increase_batch_size_eval_metric=0, + learning_rate=0.01, + output_features=output_features, + ) + + assert progress_tracker.log_metrics() == { + "batch_size": 5, + "best_valid_metric": 0, + "epoch": 0, + "best_eval_metric_steps": 0, + "learning_rate": 0.01, + "num_increases_bs": 0, + "num_reductions_lr": 0, + "steps": 0, + "tune_checkpoint_num": 0, + "best_eval_metric_checkpoint_number": 0, + "best_eval_metric_epoch": 0, + "checkpoint_number": 0, + "last_improvement_steps": 0, + "total_tokens_used": 0, + } + + +def test_progress_tracker(): + output_features = LudwigFeatureDict() + category_feature, _ = load_config_with_kwargs( + ECDCategoryOutputFeatureConfig, + { + "name": "category_feature", + "type": "category", + "decoder": { + "type": "classifier", + }, + "num_classes": 3, + "input_size": 10, + }, + ) + output_features.set("category_feature", CategoryOutputFeature(category_feature, {})) + + progress_tracker = trainer_utils.get_new_progress_tracker( + batch_size=5, + best_eval_metric_value=0, + best_increase_batch_size_eval_metric=0, + learning_rate=0.01, + output_features=output_features, + ) + + progress_tracker.validation_metrics[COMBINED][LOSS].append(TrainerMetric(epoch=1, step=10, value=0.1)) + progress_tracker.validation_metrics[COMBINED][LOSS].append(TrainerMetric(epoch=1, step=20, value=0.2)) + + assert progress_tracker.log_metrics() == { + "batch_size": 5, + "best_eval_metric_checkpoint_number": 0, + "best_eval_metric_epoch": 0, + "best_valid_metric": 0, + "checkpoint_number": 0, + "epoch": 0, + "best_eval_metric_steps": 0, + "learning_rate": 0.01, + "num_increases_bs": 0, + "num_reductions_lr": 0, + "steps": 0, + "tune_checkpoint_num": 0, + "validation_metrics.combined.loss": 0.2, + "last_improvement_steps": 0, + "total_tokens_used": 0, + } + + +def test_full_progress_tracker(): + llm_eval_examples = { + "inputs": {"input": [1, 2, 3]}, + "targets": {"output": [1, 2, 3]}, + "outputs": {"output": [1, 2, 3]}, + } + progress_tracker = trainer_utils.ProgressTracker( + **{ + BATCH_SIZE: 128, + "best_eval_metric_checkpoint_number": 7, + "best_eval_metric_epoch": 6, + "best_eval_metric_steps": 35, + "best_eval_metric_value": 0.719, + "last_improvement_steps": 35, + "best_eval_test_metrics": { + "Survived": {"accuracy": 0.634, "loss": 3.820, "roc_auc": 0.598}, + "combined": {"loss": 3.820}, + }, + "best_eval_train_metrics": { + "Survived": {"accuracy": 0.682, "loss": 4.006, "roc_auc": 0.634}, + "combined": {"loss": 4.006}, + }, + "best_eval_validation_metrics": { + "Survived": {"accuracy": 0.719, "loss": 4.396, "roc_auc": 0.667}, + "combined": {"loss": 4.396}, + }, + "best_increase_batch_size_eval_metric": sys.float_info.max, + "checkpoint_number": 12, + "epoch": 12, + "last_increase_batch_size": 0, + "last_increase_batch_size_eval_metric_improvement": 0, + "last_increase_batch_size_steps": 0, + "last_learning_rate_reduction": 0, + "last_learning_rate_reduction_steps": 0, + "learning_rate": 0.001, + "num_increases_batch_size": 0, + "num_reductions_learning_rate": 0, + "steps": 60, + "test_metrics": { + "Survived": { + "accuracy": [ + [0, 5, 0.651], + [1, 10, 0.651], + ], + "loss": [ + [0, 5, 4.130], + [1, 10, 4.074], + ], + "roc_auc": [ + [0, 5, 0.574], + [1, 10, 0.595], + ], + }, + "combined": { + "loss": [ + [0, 5, 4.130], + [1, 10, 4.074], + ] + }, + }, + "train_metrics": { + "Survived": { + "accuracy": [ + [0, 5, 0.6875], + [1, 10, 0.6875], + ], + "loss": [ + [0, 5, 4.417], + [1, 10, 4.344], + ], + "roc_auc": [ + [0, 5, 0.628], + [1, 10, 0.629], + ], + }, + "combined": { + "loss": [ + [0, 5, 4.417], + [1, 10, 4.344], + ] + }, + }, + "tune_checkpoint_num": 0, + "validation_metrics": { + "Survived": { + "accuracy": [ + [0, 5, 0.696], + [1, 10, 0.696], + ], + "loss": [ + [0, 5, 4.494], + [1, 10, 4.473], + ], + "roc_auc": [ + [0, 5, 0.675], + [1, 10, 0.671], + ], + }, + "combined": { + "loss": [ + [0, 5, 4.494], + [1, 10, 4.473], + ] + }, + }, + "llm_eval_examples": llm_eval_examples, + } + ) + + assert progress_tracker.log_metrics() == { + BATCH_SIZE: 128, + "best.train_metrics.Survived.accuracy": 0.682, + "best.train_metrics.Survived.loss": 4.006, + "best.train_metrics.Survived.roc_auc": 0.634, + "best.train_metrics.combined.loss": 4.006, + "best.test_metrics.Survived.accuracy": 0.634, + "best.test_metrics.Survived.loss": 3.82, + "best.test_metrics.Survived.roc_auc": 0.598, + "best.test_metrics.combined.loss": 3.82, + "best.validation_metrics.Survived.accuracy": 0.719, + "best.validation_metrics.Survived.loss": 4.396, + "best.validation_metrics.Survived.roc_auc": 0.667, + "best.validation_metrics.combined.loss": 4.396, + "best_eval_metric_checkpoint_number": 7, + "best_eval_metric_epoch": 6, + "best_eval_metric_steps": 35, + "best_valid_metric": 0.719, + "checkpoint_number": 12, + "epoch": 12, + "last_improvement_steps": 35, + "learning_rate": 0.001, + "num_increases_bs": 0, + "num_reductions_lr": 0, + "steps": 60, + "test_metrics.Survived.accuracy": 0.651, + "test_metrics.Survived.loss": 4.074, + "test_metrics.Survived.roc_auc": 0.595, + "test_metrics.combined.loss": 4.074, + "train_metrics.Survived.accuracy": 0.6875, + "train_metrics.Survived.loss": 4.344, + "train_metrics.Survived.roc_auc": 0.629, + "train_metrics.combined.loss": 4.344, + "tune_checkpoint_num": 0, + "validation_metrics.Survived.accuracy": 0.696, + "validation_metrics.Survived.loss": 4.473, + "validation_metrics.Survived.roc_auc": 0.671, + "validation_metrics.combined.loss": 4.473, + "llm_eval_examples": { + "inputs": {"input": [1, 2, 3]}, + "targets": {"output": [1, 2, 3]}, + "outputs": {"output": [1, 2, 3]}, + }, + "total_tokens_used": 0, + } + + +def test_get_final_steps_per_checkpoint(): + # steps_per_checkpoint and checkpoints_per_epoch cannot both be specified. + with pytest.raises(Exception): + trainer_utils.get_final_steps_per_checkpoint( + steps_per_epoch=1024, + steps_per_checkpoint=1, + checkpoints_per_epoch=1, + ) + + assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, steps_per_checkpoint=100) == 100 + assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, steps_per_checkpoint=2048) == 1024 + assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, checkpoints_per_epoch=2) == 512 + assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, checkpoints_per_epoch=2.5) == 409 + assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, checkpoints_per_epoch=0.5) == 1024 + assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024) == 1024 + assert ( + trainer_utils.get_final_steps_per_checkpoint( + steps_per_epoch=1024, steps_per_checkpoint=0, checkpoints_per_epoch=0 + ) + == 1024 + ) + + +@pytest.mark.parametrize( + "effective_batch_size,batch_size,gradient_accumulation_steps,num_workers,expected_batch_size,expected_grad_accum", + [ + (128, 16, 4, 2, 16, 4), + (AUTO, 16, 4, 2, 16, 4), + (128, 16, AUTO, 2, 16, 4), + (128, AUTO, 4, 2, 16, 4), + (128, AUTO, AUTO, 2, AUTO, AUTO), + (AUTO, AUTO, AUTO, 2, AUTO, AUTO), + (AUTO, 16, AUTO, 2, 16, 1), + (AUTO, AUTO, 4, 2, AUTO, 4), + ], +) +def test_get_rendered_batch_size_grad_accum( + effective_batch_size: str | int, + batch_size: str | int, + gradient_accumulation_steps: str | int, + num_workers: int, + expected_batch_size: int, + expected_grad_accum: int, +): + config = ECDTrainerConfig.from_dict( + { + "effective_batch_size": effective_batch_size, + "batch_size": batch_size, + "gradient_accumulation_steps": gradient_accumulation_steps, + } + ) + rendered_batch_size, rendered_grad_accum = trainer_utils.get_rendered_batch_size_grad_accum(config, num_workers) + assert rendered_batch_size == expected_batch_size + assert rendered_grad_accum == expected_grad_accum diff --git a/tests/ludwig/utils/test_training_report.py b/tests/ludwig/utils/test_training_report.py new file mode 100644 index 0000000..f1d3077 --- /dev/null +++ b/tests/ludwig/utils/test_training_report.py @@ -0,0 +1,105 @@ +"""Tests for training report generation.""" + +import json +import os +import tempfile +from types import SimpleNamespace + +from ludwig.utils.training_report import generate_training_report, save_training_report + + +class TestGenerateTrainingReport: + def _make_config(self): + return { + "model_type": "ecd", + "input_features": [ + {"name": "age", "type": "number"}, + {"name": "workclass", "type": "category"}, + ], + "output_features": [{"name": "income", "type": "binary"}], + "combiner": {"type": "concat"}, + } + + def _make_metadata(self): + return { + "age": {"mean": 38.5, "std": 13.2}, + "workclass": {"idx2str": ["Private", "Self-emp", "Gov"]}, + "income": {"idx2str": ["<=50K", ">50K"]}, + } + + def test_basic_report(self): + report = generate_training_report( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + ) + assert report["report_version"] == "1.0" + assert "generated_at" in report + assert report["model_type"] == "ecd" + assert "environment" in report + assert "python_version" in report["environment"] + + def test_data_schema(self): + report = generate_training_report( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + ) + schema = report["data_schema"] + assert len(schema["input_features"]) == 2 + assert len(schema["output_features"]) == 1 + # Number feature should have mean/std + age_feat = schema["input_features"][0] + assert age_feat["name"] == "age" + assert age_feat["mean"] == 38.5 + # Category feature should have vocab_size + wc_feat = schema["input_features"][1] + assert wc_feat["vocab_size"] == 3 + + def test_with_train_stats(self): + train_stats = SimpleNamespace( + training={"combined": {"loss": [0.5, 0.3, 0.2]}}, + validation={"combined": {"loss": [0.6, 0.4, 0.35]}}, + test=None, + ) + report = generate_training_report( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + train_stats=train_stats, + ) + assert report["epochs_trained"] == 3 + assert "metrics" in report + assert report["metrics"]["training"]["combined"]["loss"]["best"] == 0.2 + + def test_with_timing(self): + report = generate_training_report( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + training_time_seconds=145.678, + ) + assert report["training_time_seconds"] == 145.68 + + def test_with_random_seed(self): + report = generate_training_report( + config=self._make_config(), + training_set_metadata=self._make_metadata(), + random_seed=42, + ) + assert report["random_seed"] == 42 + + +class TestSaveTrainingReport: + def test_save_creates_file(self): + config = { + "model_type": "ecd", + "input_features": [{"name": "x", "type": "number"}], + "output_features": [{"name": "y", "type": "number"}], + } + with tempfile.TemporaryDirectory() as tmpdir: + path = save_training_report( + output_directory=tmpdir, + config=config, + training_set_metadata={"x": {}, "y": {}}, + ) + assert os.path.exists(path) + with open(path) as f: + data = json.load(f) + assert data["model_type"] == "ecd" diff --git a/tests/ludwig/utils/test_upload_utils.py b/tests/ludwig/utils/test_upload_utils.py new file mode 100644 index 0000000..e3c8920 --- /dev/null +++ b/tests/ludwig/utils/test_upload_utils.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import logging +import pathlib +import shutil + +import pytest + +from ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME +from ludwig.utils.upload_utils import HuggingFaceHub + +logger = logging.getLogger(__name__) + + +def _build_fake_model_repo( + destination_directory: str, + experiment_name: str, + file_names: list[str], + *, + model_directory_name: str = MODEL_FILE_NAME, + model_weights_directory_name: str = MODEL_WEIGHTS_FILE_NAME, +) -> None: + """This utility function accepts the "destination_directory" and list of file names on input. + + It then makes directory hierarchy "my_simple_experiment_run" / "model" / "model_weights" under + "destination_directory" and creates empty files for each file name specified in bottom-most (leaf) directory (file + names must be leaf file names, not paths). + """ + # Create a temporary folder designating training output directory. + model_directory: pathlib.Path = pathlib.Path(destination_directory) / experiment_name / model_directory_name + model_weights_directory: pathlib.Path = model_directory / model_weights_directory_name + model_weights_directory.mkdir(parents=True, exist_ok=True) + + # Create files within the "model_weights" subdirectory. + file_name: str + for file_name in file_names: + pathlib.Path(model_weights_directory / file_name).touch() + pathlib.Path(model_directory / MODEL_HYPERPARAMETERS_FILE_NAME).touch() + + +@pytest.fixture +def output_directory_manager(tmpdir) -> str: + """This convenience fixture creates temporary directory "training_results_output" and yields it to user test + functions. + + When the user test functions complete their execution, this fixture resumes and cleans up the temporary directory. + """ + # Create a temporary folder designating training output directory. + output_directory: str = str(tmpdir.mkdir("training_results_output")) + + yield output_directory + + # Clean up: Remove the temporary output directory and its contents. + shutil.rmtree(output_directory) + + +@pytest.mark.parametrize( + "file_names,error_raised", + [ + pytest.param( + [ + "pytorch_model.bin", + ], + None, + id="pretrained_model_weights_bin", + ), + pytest.param( + [ + "adapter_model.bin", + ], + None, + id="adapter_model_weights_bin_unmerged", # backward compatibility for peft versions < 0.7.0 + ), + pytest.param( + [ + "adapter_model.safetensors", + ], + None, + id="adapter_model_weights_safetensors_unmerged", + ), + pytest.param( + [ + "adapter_model.bin", + "adapter_model.safetensors", + ], + None, + id="adapter_model_weights_bin_and_safetensors_unmerged", # backward compatibility for peft versions < 0.7.0 + ), + pytest.param( + [ + "pytorch_model.bin", + "adapter_model.safetensors", + ], + None, + id="pretrained_model_weights_bin_and_adapter_model_weights_safetensors_merged", + ), + pytest.param( + [], + ( + ValueError, + "Can't find model weights at {model_weights_path}. Trained model weights should either be saved as `pytorch_model.bin` for regular model training, or have `adapter_model.bin`or `adapter_model.safetensors` if using parameter efficient fine-tuning methods like LoRA.", + ), + id="model_weights_missing", + ), + pytest.param( + [ + "pytorch_model.safetensors", + ], + ( + ValueError, + "Can't find model weights at {model_weights_path}. Trained model weights should either be saved as `pytorch_model.bin` for regular model training, or have `adapter_model.bin`or `adapter_model.safetensors` if using parameter efficient fine-tuning methods like LoRA.", + ), + id="model_weights_unexpected_name_format_combination", + ), + pytest.param( + [ + "pytorch_model.unkn", + ], + ( + ValueError, + "Can't find model weights at {model_weights_path}. Trained model weights should either be saved as `pytorch_model.bin` for regular model training, or have `adapter_model.bin`or `adapter_model.safetensors` if using parameter efficient fine-tuning methods like LoRA.", + ), + id="model_weights_unrecognized_format", + ), + pytest.param( + [ + "unknown_model.safetensors", + ], + ( + ValueError, + "Can't find model weights at {model_weights_path}. Trained model weights should either be saved as `pytorch_model.bin` for regular model training, or have `adapter_model.bin`or `adapter_model.safetensors` if using parameter efficient fine-tuning methods like LoRA.", + ), + id="model_weights_unrecognized_name", + ), + ], +) +@pytest.mark.unit +def test_upload_to_hf_hub__validate_upload_parameters( + output_directory_manager, file_names: list[str], error_raised: tuple[type, str] | None +): + """Test "HuggingFaceHub._validate_upload_parameters()", which is executed in the path of upload to HuggingFace + Hub; for example: `upload hf_hub -repo_id "hf-account/repo-name" --model_path. + + /content/results/api_experiment_run`. + + Each test case consists of: 1) Populating the temporary output directory ("training_results_output) with zero or + more test model weights file; 2) Executing "HuggingFaceHub._validate_upload_parameters()"; and 3) Asserting on + presence/absence of errors. + """ + output_directory: str = output_directory_manager + _build_fake_model_repo( + destination_directory=output_directory, experiment_name="my_simple_experiment_run", file_names=file_names + ) + + model_path: pathlib.Path = pathlib.Path(output_directory) / "my_simple_experiment_run" + model_weights_path: pathlib.Path = pathlib.Path(model_path / MODEL_FILE_NAME / MODEL_WEIGHTS_FILE_NAME) + + repo_id: str = "test_account/test_repo" + model_path: str = str(model_path) + if error_raised: + error_class: type + error_message: str + error_class, error_message = error_raised + with pytest.raises(error_class) as excinfo: + HuggingFaceHub._validate_upload_parameters( + repo_id=repo_id, + model_path=model_path, + ) + + assert str(excinfo.value) == error_message.format(model_weights_path=model_weights_path) + else: + try: + HuggingFaceHub._validate_upload_parameters( + repo_id=repo_id, + model_path=model_path, + ) + except Exception as exc: + assert False, f'"HuggingFaceHub._validate_upload_parameters()" raised an exception: "{exc}".' diff --git a/tests/ludwig/utils/test_version_transformation.py b/tests/ludwig/utils/test_version_transformation.py new file mode 100644 index 0000000..3cd3540 --- /dev/null +++ b/tests/ludwig/utils/test_version_transformation.py @@ -0,0 +1,46 @@ +from ludwig.utils.version_transformation import VersionTransformation, VersionTransformationRegistry + + +def test_version_transformation_registry(): + def transform_a(config): + config["b"] = config["a"] + del config["a"] + return config + + def transform_b(config): + config["c"] = config["b"] + del config["b"] + return config + + def transform_e(e): + e["g"] = e["f"] + del e["f"] + return e + + transformation_registry = VersionTransformationRegistry() + transformation_registry.register(VersionTransformation(transform=transform_a, version="0.1")) + transformation_registry.register(VersionTransformation(transform=transform_b, version="0.2")) + transformation_registry.register(VersionTransformation(transform=transform_e, version="0.2", prefixes=["e"])) + input_config = {"a": "a value", "e": {"f": "f_value"}} + + transformed_0_1 = transformation_registry.update_config(input_config, from_version="0.0", to_version="0.1") + assert "a" not in transformed_0_1 + assert transformed_0_1["b"] == "a value" + + transformed_0_2 = transformation_registry.update_config(input_config, from_version="0.0", to_version="0.2") + assert "a" not in transformed_0_2 + assert "b" not in transformed_0_2 + assert transformed_0_2["c"] == "a value" + assert "e" in transformed_0_2 + assert "f" not in transformed_0_2["e"] + assert transformed_0_2["e"]["g"] == "f_value" + + +def test_version_transformation_order(): + v1 = VersionTransformation(transform=lambda x: x, version="0.1") + v2 = VersionTransformation(transform=lambda x: x, version="0.2") + v3 = VersionTransformation(transform=lambda x: x, version="0.10") + + assert v1 < v2 + assert v1 < v3 + assert v2 < v3 diff --git a/tests/regression_tests/automl/golden/adult_census_income.types.json b/tests/regression_tests/automl/golden/adult_census_income.types.json new file mode 100644 index 0000000..c140ae2 --- /dev/null +++ b/tests/regression_tests/automl/golden/adult_census_income.types.json @@ -0,0 +1,77 @@ +[ + { + "column": "age", + "name": "age", + "type": "number" + }, + { + "column": "workclass", + "name": "workclass", + "type": "category" + }, + { + "column": "fnlwgt", + "name": "fnlwgt", + "type": "number" + }, + { + "column": "education", + "name": "education", + "type": "category" + }, + { + "column": "education-num", + "name": "education-num", + "type": "number" + }, + { + "column": "marital-status", + "name": "marital-status", + "type": "category" + }, + { + "column": "occupation", + "name": "occupation", + "type": "category" + }, + { + "column": "relationship", + "name": "relationship", + "type": "category" + }, + { + "column": "race", + "name": "race", + "type": "category" + }, + { + "column": "sex", + "name": "sex", + "type": "category" + }, + { + "column": "capital-gain", + "name": "capital-gain", + "type": "number" + }, + { + "column": "capital-loss", + "name": "capital-loss", + "type": "number" + }, + { + "column": "hours-per-week", + "name": "hours-per-week", + "type": "number" + }, + { + "column": "native-country", + "name": "native-country", + "type": "category" + }, + { + "column": "income", + "name": "income", + "type": "category" + } +] diff --git a/tests/regression_tests/automl/golden/mnist.types.json b/tests/regression_tests/automl/golden/mnist.types.json new file mode 100644 index 0000000..a22787b --- /dev/null +++ b/tests/regression_tests/automl/golden/mnist.types.json @@ -0,0 +1,15 @@ +[ + { + "column": "image_path", + "encoder": { + "type": "stacked_cnn" + }, + "name": "image_path", + "type": "image" + }, + { + "column": "label", + "name": "label", + "type": "category" + } +] diff --git a/tests/regression_tests/automl/scripts/update_golden_types.py b/tests/regression_tests/automl/scripts/update_golden_types.py new file mode 100644 index 0000000..22097ae --- /dev/null +++ b/tests/regression_tests/automl/scripts/update_golden_types.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +"""This script updates all golden JSON files containing expected data types.""" + +import json + +from ludwig.automl import create_auto_config +from tests.regression_tests.automl.utils import get_dataset_golden_types_path, get_dataset_object, TEST_DATASET_REGISTRY + + +def write_json_files(): + for dataset_name in TEST_DATASET_REGISTRY: + dataset_obj = get_dataset_object(dataset_name) + dataset = dataset_obj.load(split=False) + + # NOTE: assuming type inference for input and output features is the same + config = create_auto_config( + dataset=dataset, + target=[], + time_limit_s=3600, + ) + + golden_types_path = get_dataset_golden_types_path(dataset_name) + with open(golden_types_path, "w") as f: + json.dump(config["input_features"], f, indent=4, sort_keys=True) + f.write("\n") + + +if __name__ == "__main__": + write_json_files() diff --git a/tests/regression_tests/automl/test_auto_type_inference.py b/tests/regression_tests/automl/test_auto_type_inference.py new file mode 100644 index 0000000..72c2281 --- /dev/null +++ b/tests/regression_tests/automl/test_auto_type_inference.py @@ -0,0 +1,31 @@ +import json + +import pytest + +from tests.regression_tests.automl.utils import get_dataset_golden_types_path, get_dataset_object, TEST_DATASET_REGISTRY + +try: + from ludwig.automl import create_auto_config +except ImportError: + pass + + +@pytest.mark.slow +@pytest.mark.distributed # ludwig.automl has a dependency on ray +@pytest.mark.parametrize("dataset_name", TEST_DATASET_REGISTRY) +def test_auto_type_inference_regression(dataset_name): + golden_types_path = get_dataset_golden_types_path(dataset_name) + with open(golden_types_path) as f: + golden_types = json.load(f) + + dataset_obj = get_dataset_object(dataset_name) + dataset = dataset_obj.load(split=False) + + # NOTE: assuming type inference for input and output features is the same + config = create_auto_config( + dataset=dataset, + target=[], + time_limit_s=3600, + ) + + assert golden_types == config["input_features"] diff --git a/tests/regression_tests/automl/utils.py b/tests/regression_tests/automl/utils.py new file mode 100644 index 0000000..4c87aa4 --- /dev/null +++ b/tests/regression_tests/automl/utils.py @@ -0,0 +1,17 @@ +from pathlib import Path + +import ludwig.datasets +from ludwig.datasets.loaders.dataset_loader import DatasetLoader + +# Subset of Ludwig Dataset Zoo used for AutoML type inference regression tests. +TEST_DATASET_REGISTRY = {"adult_census_income", "mnist"} + + +def get_dataset_golden_types_path(dataset_name: str) -> str: + """Returns the path to the golden types file for the given dataset.""" + return str(Path(__file__).resolve().parent / "golden" / f"{dataset_name}.types.json") + + +def get_dataset_object(dataset_name: str) -> DatasetLoader: + """Returns a Ludwig dataset instance for the given dataset.""" + return ludwig.datasets.get_dataset(dataset_name) diff --git a/tests/regression_tests/benchmark/configs/adult_census_income.ecd.yaml b/tests/regression_tests/benchmark/configs/adult_census_income.ecd.yaml new file mode 100644 index 0000000..3292ac8 --- /dev/null +++ b/tests/regression_tests/benchmark/configs/adult_census_income.ecd.yaml @@ -0,0 +1,44 @@ +combiner: + type: tabnet +defaults: + number: + preprocessing: + missing_value_strategy: fill_with_const + normalization: null +input_features: + - name: age + type: number + - name: workclass + type: category + - name: fnlwgt + type: number + - name: education + type: category + - name: education-num + type: number + - name: marital-status + type: category + - name: occupation + type: category + - name: relationship + type: category + - name: race + type: category + - name: sex + type: category + - name: capital-gain + type: number + - name: capital-loss + type: number + - name: hours-per-week + type: number + - name: native-country + type: category +output_features: + - name: income + type: category +trainer: + batch_size: 1345 + eval_batch_size: 16384 + evaluate_training_set: false + learning_rate: 0.02714507227517137 diff --git a/tests/regression_tests/benchmark/configs/ames_housing.ecd.yaml b/tests/regression_tests/benchmark/configs/ames_housing.ecd.yaml new file mode 100644 index 0000000..635b356 --- /dev/null +++ b/tests/regression_tests/benchmark/configs/ames_housing.ecd.yaml @@ -0,0 +1,174 @@ +combiner: + type: tabnet +defaults: + number: + preprocessing: + missing_value_strategy: fill_with_mean + normalization: null +input_features: + - name: MSSubClass + type: category + - name: MSZoning + type: category + - name: LotFrontage + type: number + - name: LotArea + type: number + - name: Street + type: category + - name: Alley + type: category + - name: LotShape + type: category + - name: LandContour + type: category + - name: Utilities + type: category + - name: LotConfig + type: category + - name: LandSlope + type: category + - name: Neighborhood + type: category + - name: Condition1 + type: category + - name: Condition2 + type: category + - name: BldgType + type: category + - name: HouseStyle + type: category + - name: OverallQual + type: category + - name: OverallCond + type: category + - name: YearBuilt + type: number + - name: YearRemodAdd + type: number + - name: RoofStyle + type: category + - name: RoofMatl + type: category + - name: Exterior1st + type: category + - name: Exterior2nd + type: category + - name: MasVnrType + type: category + - name: MasVnrArea + type: number + - name: ExterQual + type: category + - name: ExterCond + type: category + - name: Foundation + type: category + - name: BsmtQual + type: category + - name: BsmtCond + type: category + - name: BsmtExposure + type: category + - name: BsmtFinType1 + type: category + - name: BsmtFinSF1 + type: number + - name: BsmtFinType2 + type: category + - name: BsmtFinSF2 + type: number + - name: BsmtUnfSF + type: number + - name: TotalBsmtSF + type: number + - name: Heating + type: category + - name: HeatingQC + type: category + - name: CentralAir + type: binary + - name: Electrical + type: category + - name: 1stFlrSF + type: number + - name: 2ndFlrSF + type: number + - name: LowQualFinSF + type: number + - name: GrLivArea + type: number + - name: BsmtFullBath + type: number + - name: BsmtHalfBath + type: number + - name: FullBath + type: number + - name: HalfBath + type: number + - name: BedroomAbvGr + type: number + - name: KitchenAbvGr + type: number + - name: KitchenQual + type: category + - name: TotRmsAbvGrd + type: number + - name: Functional + type: category + - name: Fireplaces + type: number + - name: FireplaceQu + type: category + - name: GarageType + type: category + - name: GarageYrBlt + type: number + - name: GarageFinish + type: category + - name: GarageCars + type: number + - name: GarageArea + type: number + - name: GarageQual + type: category + - name: GarageCond + type: category + - name: PavedDrive + type: category + - name: WoodDeckSF + type: number + - name: OpenPorchSF + type: number + - name: EnclosedPorch + type: number + - name: 3SsnPorch + type: number + - name: ScreenPorch + type: number + - name: PoolArea + type: number + - name: PoolQC + type: category + - name: Fence + type: category + - name: MiscFeature + type: category + - name: MiscVal + type: number + - name: MoSold + type: category + - name: YrSold + type: number + - name: SaleType + type: category + - name: SaleCondition + type: category +output_features: + - name: SalePrice + type: number +trainer: + batch_size: 35 + eval_batch_size: 16384 + evaluate_training_set: false + learning_rate: 0.0858479746528337 diff --git a/tests/regression_tests/benchmark/configs/mercedes_benz_greener.ecd.yaml b/tests/regression_tests/benchmark/configs/mercedes_benz_greener.ecd.yaml new file mode 100644 index 0000000..590d85e --- /dev/null +++ b/tests/regression_tests/benchmark/configs/mercedes_benz_greener.ecd.yaml @@ -0,0 +1,768 @@ +output_features: + - name: y + type: number +input_features: + - name: X0 + type: category + - name: X1 + type: category + - name: X2 + type: category + - name: X3 + type: category + - name: X4 + type: category + - name: X5 + type: category + - name: X6 + type: category + - name: X8 + type: category + - name: X10 + type: binary + - name: X11 + type: binary + - name: X12 + type: binary + - name: X13 + type: binary + - name: X14 + type: binary + - name: X15 + type: binary + - name: X16 + type: binary + - name: X17 + type: binary + - name: X18 + type: binary + - name: X19 + type: binary + - name: X20 + type: binary + - name: X21 + type: binary + - name: X22 + type: binary + - name: X23 + type: binary + - name: X24 + type: binary + - name: X26 + type: binary + - name: X27 + type: binary + - name: X28 + type: binary + - name: X29 + type: binary + - name: X30 + type: binary + - name: X31 + type: binary + - name: X32 + type: binary + - name: X33 + type: binary + - name: X34 + type: binary + - name: X35 + type: binary + - name: X36 + type: binary + - name: X37 + type: binary + - name: X38 + type: binary + - name: X39 + type: binary + - name: X40 + type: binary + - name: X41 + type: binary + - name: X42 + type: binary + - name: X43 + type: binary + - name: X44 + type: binary + - name: X45 + type: binary + - name: X46 + type: binary + - name: X47 + type: binary + - name: X48 + type: binary + - name: X49 + type: binary + - name: X50 + type: binary + - name: X51 + type: binary + - name: X52 + type: binary + - name: X53 + type: binary + - name: X54 + type: binary + - name: X55 + type: binary + - name: X56 + type: binary + - name: X57 + type: binary + - name: X58 + type: binary + - name: X59 + type: binary + - name: X60 + type: binary + - name: X61 + type: binary + - name: X62 + type: binary + - name: X63 + type: binary + - name: X64 + type: binary + - name: X65 + type: binary + - name: X66 + type: binary + - name: X67 + type: binary + - name: X68 + type: binary + - name: X69 + type: binary + - name: X70 + type: binary + - name: X71 + type: binary + - name: X73 + type: binary + - name: X74 + type: binary + - name: X75 + type: binary + - name: X76 + type: binary + - name: X77 + type: binary + - name: X78 + type: binary + - name: X79 + type: binary + - name: X80 + type: binary + - name: X81 + type: binary + - name: X82 + type: binary + - name: X83 + type: binary + - name: X84 + type: binary + - name: X85 + type: binary + - name: X86 + type: binary + - name: X87 + type: binary + - name: X88 + type: binary + - name: X89 + type: binary + - name: X90 + type: binary + - name: X91 + type: binary + - name: X92 + type: binary + - name: X93 + type: binary + - name: X94 + type: binary + - name: X95 + type: binary + - name: X96 + type: binary + - name: X97 + type: binary + - name: X98 + type: binary + - name: X99 + type: binary + - name: X100 + type: binary + - name: X101 + type: binary + - name: X102 + type: binary + - name: X103 + type: binary + - name: X104 + type: binary + - name: X105 + type: binary + - name: X106 + type: binary + - name: X107 + type: binary + - name: X108 + type: binary + - name: X109 + type: binary + - name: X110 + type: binary + - name: X111 + type: binary + - name: X112 + type: binary + - name: X113 + type: binary + - name: X114 + type: binary + - name: X115 + type: binary + - name: X116 + type: binary + - name: X117 + type: binary + - name: X118 + type: binary + - name: X119 + type: binary + - name: X120 + type: binary + - name: X122 + type: binary + - name: X123 + type: binary + - name: X124 + type: binary + - name: X125 + type: binary + - name: X126 + type: binary + - name: X127 + type: binary + - name: X128 + type: binary + - name: X129 + type: binary + - name: X130 + type: binary + - name: X131 + type: binary + - name: X132 + type: binary + - name: X133 + type: binary + - name: X134 + type: binary + - name: X135 + type: binary + - name: X136 + type: binary + - name: X137 + type: binary + - name: X138 + type: binary + - name: X139 + type: binary + - name: X140 + type: binary + - name: X141 + type: binary + - name: X142 + type: binary + - name: X143 + type: binary + - name: X144 + type: binary + - name: X145 + type: binary + - name: X146 + type: binary + - name: X147 + type: binary + - name: X148 + type: binary + - name: X150 + type: binary + - name: X151 + type: binary + - name: X152 + type: binary + - name: X153 + type: binary + - name: X154 + type: binary + - name: X155 + type: binary + - name: X156 + type: binary + - name: X157 + type: binary + - name: X158 + type: binary + - name: X159 + type: binary + - name: X160 + type: binary + - name: X161 + type: binary + - name: X162 + type: binary + - name: X163 + type: binary + - name: X164 + type: binary + - name: X165 + type: binary + - name: X166 + type: binary + - name: X167 + type: binary + - name: X168 + type: binary + - name: X169 + type: binary + - name: X170 + type: binary + - name: X171 + type: binary + - name: X172 + type: binary + - name: X173 + type: binary + - name: X174 + type: binary + - name: X175 + type: binary + - name: X176 + type: binary + - name: X177 + type: binary + - name: X178 + type: binary + - name: X179 + type: binary + - name: X180 + type: binary + - name: X181 + type: binary + - name: X182 + type: binary + - name: X183 + type: binary + - name: X184 + type: binary + - name: X185 + type: binary + - name: X186 + type: binary + - name: X187 + type: binary + - name: X189 + type: binary + - name: X190 + type: binary + - name: X191 + type: binary + - name: X192 + type: binary + - name: X194 + type: binary + - name: X195 + type: binary + - name: X196 + type: binary + - name: X197 + type: binary + - name: X198 + type: binary + - name: X199 + type: binary + - name: X200 + type: binary + - name: X201 + type: binary + - name: X202 + type: binary + - name: X203 + type: binary + - name: X204 + type: binary + - name: X205 + type: binary + - name: X206 + type: binary + - name: X207 + type: binary + - name: X208 + type: binary + - name: X209 + type: binary + - name: X210 + type: binary + - name: X211 + type: binary + - name: X212 + type: binary + - name: X213 + type: binary + - name: X214 + type: binary + - name: X215 + type: binary + - name: X216 + type: binary + - name: X217 + type: binary + - name: X218 + type: binary + - name: X219 + type: binary + - name: X220 + type: binary + - name: X221 + type: binary + - name: X222 + type: binary + - name: X223 + type: binary + - name: X224 + type: binary + - name: X225 + type: binary + - name: X226 + type: binary + - name: X227 + type: binary + - name: X228 + type: binary + - name: X229 + type: binary + - name: X230 + type: binary + - name: X231 + type: binary + - name: X232 + type: binary + - name: X233 + type: binary + - name: X234 + type: binary + - name: X235 + type: binary + - name: X236 + type: binary + - name: X237 + type: binary + - name: X238 + type: binary + - name: X239 + type: binary + - name: X240 + type: binary + - name: X241 + type: binary + - name: X242 + type: binary + - name: X243 + type: binary + - name: X244 + type: binary + - name: X245 + type: binary + - name: X246 + type: binary + - name: X247 + type: binary + - name: X248 + type: binary + - name: X249 + type: binary + - name: X250 + type: binary + - name: X251 + type: binary + - name: X252 + type: binary + - name: X253 + type: binary + - name: X254 + type: binary + - name: X255 + type: binary + - name: X256 + type: binary + - name: X257 + type: binary + - name: X258 + type: binary + - name: X259 + type: binary + - name: X260 + type: binary + - name: X261 + type: binary + - name: X262 + type: binary + - name: X263 + type: binary + - name: X264 + type: binary + - name: X265 + type: binary + - name: X266 + type: binary + - name: X267 + type: binary + - name: X268 + type: binary + - name: X269 + type: binary + - name: X270 + type: binary + - name: X271 + type: binary + - name: X272 + type: binary + - name: X273 + type: binary + - name: X274 + type: binary + - name: X275 + type: binary + - name: X276 + type: binary + - name: X277 + type: binary + - name: X278 + type: binary + - name: X279 + type: binary + - name: X280 + type: binary + - name: X281 + type: binary + - name: X282 + type: binary + - name: X283 + type: binary + - name: X284 + type: binary + - name: X285 + type: binary + - name: X286 + type: binary + - name: X287 + type: binary + - name: X288 + type: binary + - name: X289 + type: binary + - name: X290 + type: binary + - name: X291 + type: binary + - name: X292 + type: binary + - name: X293 + type: binary + - name: X294 + type: binary + - name: X295 + type: binary + - name: X296 + type: binary + - name: X297 + type: binary + - name: X298 + type: binary + - name: X299 + type: binary + - name: X300 + type: binary + - name: X301 + type: binary + - name: X302 + type: binary + - name: X304 + type: binary + - name: X305 + type: binary + - name: X306 + type: binary + - name: X307 + type: binary + - name: X308 + type: binary + - name: X309 + type: binary + - name: X310 + type: binary + - name: X311 + type: binary + - name: X312 + type: binary + - name: X313 + type: binary + - name: X314 + type: binary + - name: X315 + type: binary + - name: X316 + type: binary + - name: X317 + type: binary + - name: X318 + type: binary + - name: X319 + type: binary + - name: X320 + type: binary + - name: X321 + type: binary + - name: X322 + type: binary + - name: X323 + type: binary + - name: X324 + type: binary + - name: X325 + type: binary + - name: X326 + type: binary + - name: X327 + type: binary + - name: X328 + type: binary + - name: X329 + type: binary + - name: X330 + type: binary + - name: X331 + type: binary + - name: X332 + type: binary + - name: X333 + type: binary + - name: X334 + type: binary + - name: X335 + type: binary + - name: X336 + type: binary + - name: X337 + type: binary + - name: X338 + type: binary + - name: X339 + type: binary + - name: X340 + type: binary + - name: X341 + type: binary + - name: X342 + type: binary + - name: X343 + type: binary + - name: X344 + type: binary + - name: X345 + type: binary + - name: X346 + type: binary + - name: X347 + type: binary + - name: X348 + type: binary + - name: X349 + type: binary + - name: X350 + type: binary + - name: X351 + type: binary + - name: X352 + type: binary + - name: X353 + type: binary + - name: X354 + type: binary + - name: X355 + type: binary + - name: X356 + type: binary + - name: X357 + type: binary + - name: X358 + type: binary + - name: X359 + type: binary + - name: X360 + type: binary + - name: X361 + type: binary + - name: X362 + type: binary + - name: X363 + type: binary + - name: X364 + type: binary + - name: X365 + type: binary + - name: X366 + type: binary + - name: X367 + type: binary + - name: X368 + type: binary + - name: X369 + type: binary + - name: X370 + type: binary + - name: X371 + type: binary + - name: X372 + type: binary + - name: X373 + type: binary + - name: X374 + type: binary + - name: X375 + type: binary + - name: X376 + type: binary + - name: X377 + type: binary + - name: X378 + type: binary + - name: X379 + type: binary + - name: X380 + type: binary + - name: X382 + type: binary + - name: X383 + type: binary + - name: X384 + type: binary + - name: X385 + type: binary +combiner: + type: tabnet +trainer: + eval_batch_size: 16384 + evaluate_training_set: false + learning_rate: 0.02465493752015043 + batch_size: 33 +defaults: + number: + preprocessing: + normalization: null + missing_value_strategy: fill_with_const diff --git a/tests/regression_tests/benchmark/configs/sarcos.ecd.yaml b/tests/regression_tests/benchmark/configs/sarcos.ecd.yaml new file mode 100644 index 0000000..0b2df61 --- /dev/null +++ b/tests/regression_tests/benchmark/configs/sarcos.ecd.yaml @@ -0,0 +1,101 @@ +combiner: + type: tabnet +defaults: + number: + preprocessing: + missing_value_strategy: fill_with_const + normalization: null +input_features: + - column: position_1 + name: position_1 + type: number + - column: position_2 + name: position_2 + type: number + - column: position_3 + name: position_3 + type: number + - column: position_4 + name: position_4 + type: number + - column: position_5 + name: position_5 + type: number + - column: position_6 + name: position_6 + type: number + - column: position_7 + name: position_7 + type: number + - column: velocity_1 + name: velocity_1 + type: number + - column: velocity_2 + name: velocity_2 + type: number + - column: velocity_3 + name: velocity_3 + type: number + - column: velocity_4 + name: velocity_4 + type: number + - column: velocity_5 + name: velocity_5 + type: number + - column: velocity_6 + name: velocity_6 + type: number + - column: velocity_7 + name: velocity_7 + type: number + - column: acceleration_1 + name: acceleration_1 + type: number + - column: acceleration_2 + name: acceleration_2 + type: number + - column: acceleration_3 + name: acceleration_3 + type: number + - column: acceleration_4 + name: acceleration_4 + type: number + - column: acceleration_5 + name: acceleration_5 + type: number + - column: acceleration_6 + name: acceleration_6 + type: number + - column: acceleration_7 + name: acceleration_7 + type: number + - column: torque_2 + name: torque_2 + type: number + - column: torque_3 + name: torque_3 + type: number + - column: torque_4 + name: torque_4 + type: number + - column: torque_5 + name: torque_5 + type: number + - column: torque_6 + name: torque_6 + type: number + - column: torque_7 + name: torque_7 + type: number +output_features: + - column: torque_1 + name: torque_1 + type: number +trainer: + batch_size: 118 + learning_rate_scheduler: + decay: exponential + decay_rate: 0.5371397744663506 + eval_batch_size: 16384 + evaluate_training_set: false + learning_rate: 0.001004563044919135 diff --git a/tests/regression_tests/benchmark/expected_metric.py b/tests/regression_tests/benchmark/expected_metric.py new file mode 100644 index 0000000..87023f8 --- /dev/null +++ b/tests/regression_tests/benchmark/expected_metric.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class ExpectedMetric: + # Output feature name. + output_feature_name: str + + # Metric name. + metric_name: str + + # Expected metric value. + expected_value: int | float + + # The percentage change that would trigger a notification/failure. + tolerance_percentage: float diff --git a/tests/regression_tests/benchmark/expected_metrics/adult_census_income.ecd.yaml b/tests/regression_tests/benchmark/expected_metrics/adult_census_income.ecd.yaml new file mode 100644 index 0000000..68b1bd0 --- /dev/null +++ b/tests/regression_tests/benchmark/expected_metrics/adult_census_income.ecd.yaml @@ -0,0 +1,5 @@ +metrics: + - output_feature_name: income + metric_name: accuracy + expected_value: 0.8547970652580261 + tolerance_percentage: 0.15 diff --git a/tests/regression_tests/benchmark/expected_metrics/ames_housing.ecd.yaml b/tests/regression_tests/benchmark/expected_metrics/ames_housing.ecd.yaml new file mode 100644 index 0000000..4c6f262 --- /dev/null +++ b/tests/regression_tests/benchmark/expected_metrics/ames_housing.ecd.yaml @@ -0,0 +1,5 @@ +metrics: + - output_feature_name: SalePrice + metric_name: r2 + expected_value: 0.7343850135803223 + tolerance_percentage: 0.15 diff --git a/tests/regression_tests/benchmark/expected_metrics/mercedes_benz_greener.ecd.yaml b/tests/regression_tests/benchmark/expected_metrics/mercedes_benz_greener.ecd.yaml new file mode 100644 index 0000000..89b1765 --- /dev/null +++ b/tests/regression_tests/benchmark/expected_metrics/mercedes_benz_greener.ecd.yaml @@ -0,0 +1,5 @@ +metrics: + - output_feature_name: y + metric_name: r2 + expected_value: 0.47405338287353516 + tolerance_percentage: 0.15 diff --git a/tests/regression_tests/benchmark/expected_metrics/sarcos.ecd.yaml b/tests/regression_tests/benchmark/expected_metrics/sarcos.ecd.yaml new file mode 100644 index 0000000..330a086 --- /dev/null +++ b/tests/regression_tests/benchmark/expected_metrics/sarcos.ecd.yaml @@ -0,0 +1,5 @@ +metrics: + - output_feature_name: torque_1 + metric_name: r2 + expected_value: 0.9871084690093994 + tolerance_percentage: 0.15 diff --git a/tests/regression_tests/benchmark/test_model_performance.py b/tests/regression_tests/benchmark/test_model_performance.py new file mode 100644 index 0000000..3d7f493 --- /dev/null +++ b/tests/regression_tests/benchmark/test_model_performance.py @@ -0,0 +1,84 @@ +import os + +import pytest +from expected_metric import ExpectedMetric + +from ludwig.benchmarking.benchmark import benchmark +from ludwig.utils.data_utils import load_yaml +from tests.integration_tests.utils import parse_flag_from_env + +SKIPPED_CONFIG_ISSUES = { + "mercedes_benz_greener.ecd.yaml": "https://github.com/ludwig-ai/ludwig/issues/2978", + "sarcos.ecd.yaml": "Takes more than 300s", + "ames_housing.ecd.yaml": "https://github.com/ludwig-ai/ludwig/issues/3344", + "adult_census_income.ecd.yaml": "UCI ML repo (archive.ics.uci.edu) is unavailable, dataset cannot be downloaded", +} +CONFIGS_REQUIRING_DATASET_CREDENTIALS = { + "mercedes_benz_greener.ecd.yaml", + "ames_housing.ecd.yaml", +} +RUN_PRIVATE = parse_flag_from_env("RUN_PRIVATE", default=False) + + +def update_skipped_configs_issues(config_filename): + if not RUN_PRIVATE and config_filename in CONFIGS_REQUIRING_DATASET_CREDENTIALS: + SKIPPED_CONFIG_ISSUES[config_filename] = "Requires credentials. Can't run from a forked repo." + + +def get_test_config_filenames() -> list[str]: + """Return list of the config filenames used for benchmarking.""" + benchmark_directory = "/".join(__file__.split("/")[:-1] + ["configs"]) + return [config_fp for config_fp in os.listdir(benchmark_directory)] + + +def get_dataset_from_config_path(config_path: str) -> str: + """path/to/config/..yaml -> dataset.""" + return os.path.basename(config_path).split(".")[0] + + +@pytest.mark.benchmark +@pytest.mark.parametrize("config_filename", get_test_config_filenames()) +def test_performance(config_filename, tmpdir): + update_skipped_configs_issues(config_filename) + if config_filename in SKIPPED_CONFIG_ISSUES: + pytest.skip(reason=SKIPPED_CONFIG_ISSUES[config_filename]) + return + + benchmark_directory = "/".join(__file__.split("/")[:-1]) + config_path = os.path.join(benchmark_directory, "configs", config_filename) + expected_test_statistics_fp = os.path.join(benchmark_directory, "expected_metrics", config_filename) + dataset_name = get_dataset_from_config_path(config_path) + + if not os.path.exists(expected_test_statistics_fp): + raise FileNotFoundError("""No corresponding expected metrics found for benchmarking config '{config_path}'. + Please add a new metrics YAML file '{expected_test_statistics_fp}'. Suggested content: + + metrics: + - output_feature_name: + metric_name: + expected_value: + tolerance_percent: 0.15""") + expected_metrics_dict = load_yaml(expected_test_statistics_fp) + + benchmarking_config = { + "experiment_name": "regression_test", + "export": {"export_artifacts": True, "export_base_path": tmpdir}, + "experiments": [{"dataset_name": dataset_name, "config_path": config_path}], + } + benchmarking_artifacts = benchmark(benchmarking_config) + experiment_artifact, err = benchmarking_artifacts[dataset_name] + if err is not None: + raise err + + expected_metrics: list[ExpectedMetric] = [ + ExpectedMetric.from_dict(expected_metric) for expected_metric in expected_metrics_dict["metrics"] + ] + for expected_metric in expected_metrics: + tolerance = expected_metric.tolerance_percentage * expected_metric.expected_value + output_feature_name = expected_metric.output_feature_name + metric_name = expected_metric.metric_name + experiment_metric_value = experiment_artifact.test_statistics[output_feature_name][metric_name] + assert abs(expected_metric.expected_value - experiment_metric_value) <= tolerance, ( + f"The obtained {metric_name} value ({experiment_metric_value}) was not within" + f" {100 * expected_metric.tolerance_percentage}% of the expected value ({expected_metric.expected_value})." + ) diff --git a/tests/regression_tests/model/test_old_models.py b/tests/regression_tests/model/test_old_models.py new file mode 100644 index 0000000..f4028ee --- /dev/null +++ b/tests/regression_tests/model/test_old_models.py @@ -0,0 +1,63 @@ +import os +import zipfile + +import pandas as pd +import pytest +import wget + +from ludwig.api import LudwigModel +from ludwig.data.dataset_synthesizer import build_synthetic_dataset_df +from ludwig.globals import MODEL_FILE_NAME + +NUM_EXAMPLES = 25 + + +def test_model_loaded_from_old_config_prediction_works(tmpdir): + # Titanic model based on 0.5.3. + old_model_url = "https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/old_model.zip" + old_model_filename = wget.download(old_model_url, tmpdir) + with zipfile.ZipFile(old_model_filename, "r") as zip_ref: + zip_ref.extractall(tmpdir) + example_data = { + "PassengerId": 892, + "Pclass": 3, + "Name": "Kelly, Mr. James", + "Sex": "male", + "Age": 34.5, + "SibSp": 0, + "Parch": 0, + "Ticket": "330911", + "Fare": 7.8292, + "Cabin": None, + "Embarked": "Q", + } + test_set = pd.DataFrame(example_data, index=[0]) + + ludwig_model = LudwigModel.load(os.path.join(tmpdir, "old_model/model")) + predictions, _ = ludwig_model.predict(dataset=test_set) + + assert predictions.to_dict()["Survived_predictions"] == {0: False} + + +@pytest.mark.parametrize( + "model_url", + [ + "https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/titanic_v07.zip", + "https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/twitter_bots_v05_1.zip", + "https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/respiratory_v05.zip", + ], + ids=["titanic", "twitter_bots", "respiratory"], +) +def test_predict_deprecated_model(model_url, tmpdir): + model_dir = os.path.join(tmpdir, MODEL_FILE_NAME) + os.makedirs(model_dir) + + archive_path = wget.download(model_url, tmpdir) + with zipfile.ZipFile(archive_path, "r") as zip_ref: + zip_ref.extractall(model_dir) + + ludwig_model = LudwigModel.load(model_dir) + df = build_synthetic_dataset_df(NUM_EXAMPLES, ludwig_model.config) + + pred_df, _ = ludwig_model.predict(df) + assert len(pred_df) == 25 diff --git a/tests/training_success/__init__.py b/tests/training_success/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/training_success/configs.py b/tests/training_success/configs.py new file mode 100644 index 0000000..a96893e --- /dev/null +++ b/tests/training_success/configs.py @@ -0,0 +1,158 @@ +from ludwig.config_sampling.explore_schema import ( + combine_configs, + combine_configs_for_comparator_combiner, + combine_configs_for_sequence_combiner, +) + +# A generic tabular to text config used to generate synthetic data and train a model on it. +TABULAR_TO_TEXT = """ +input_features: + - name: category_1 + type: category + - name: number_1 + type: number + - name: binary_1 + type: binary +output_features: + - name: text_output_1 + type: text +""" + +# A generic tabular config used to generate synthetic data and train a model on it. +TABULAR = """ +input_features: + - name: category_1 + type: category + - name: number_1 + type: number + - name: binary_1 + type: binary +output_features: + - name: category_output_1 + type: category +""" + +# A generic config with a single text input feature used to generate synthetic data and train a model on it. +TEXT_INPUT = """ +input_features: + - name: text_1 + type: text + preprocessing: + max_sequence_length: 8 +output_features: + - name: category_output_1 + type: category +""" + +# A generic config with a single number input feature used to generate synthetic data and train a model on it. +NUMBER_INPUT = """ +input_features: + - name: number_1 + type: number +output_features: + - name: category_output_1 + type: category +""" + +# A generic config with a single category input feature used to generate synthetic data and train a model on it. +CATEGORY_INPUT = """ +input_features: + - name: category_1 + type: category +output_features: + - name: category_output_1 + type: category +""" + +# A generic config with a single binary input feature used to generate synthetic data and train a model on it. +BINARY_INPUT = """ +input_features: + - name: binary_1 + type: binary +output_features: + - name: category_output_1 + type: category +""" + +# A generic config with a text output feature used to generate synthetic data and train a model on it. +TEXT_OUTPUT = """ +input_features: + - name: text_1 + type: text + preprocessing: + max_sequence_length: 8 +output_features: + - name: text_output_1 + type: text +""" + +# A generic config with a number output feature used to generate synthetic data and train a model on it. +NUMBER_OUTPUT = """ +input_features: + - name: number_1 + type: number +output_features: + - name: number_output_1 + type: number +""" + +# A generic config with a category output feature used to generate synthetic data and train a model on it. +CATEGORY_OUTPUT = """ +input_features: + - name: category_1 + type: category +output_features: + - name: category_output_1 + type: category +""" + +# A generic config with a binary output feature used to generate synthetic data and train a model on it. +BINARY_OUTPUT = """ +input_features: + - name: binary_1 + type: binary +output_features: + - name: binary_output_1 + type: binary +""" + +# Dictionary that maps from feature type to base config used to test the encoder and preprocessing sections. +FEATURE_TYPE_TO_CONFIG_FOR_ENCODER_PREPROCESSING = { + "number": NUMBER_INPUT, + "category": CATEGORY_INPUT, + "binary": BINARY_INPUT, + "text": TEXT_INPUT, +} + +# Dictionary that maps from feature type to base config used to test the decoder and loss sections. +FEATURE_TYPE_TO_CONFIG_FOR_DECODER_LOSS = { + "number": NUMBER_OUTPUT, + "category": CATEGORY_OUTPUT, + "binary": BINARY_OUTPUT, + "text": TEXT_OUTPUT, +} + +# Dictionary that maps from config section to base config used to test that section. +ECD_CONFIG_SECTION_TO_CONFIG = { + "trainer": TABULAR, + "comparator": TABULAR, + "concat": TABULAR, + "project_aggregate": TABULAR, + "sequence": TEXT_INPUT, + "sequence_concat": TEXT_INPUT, + "tabnet": TABULAR, + "tabtransformer": TABULAR, + "transformer": TABULAR, +} + +# Dictionary that maps from the combiner type to base config used to test that combiner. +COMBINER_TYPE_TO_COMBINE_FN_MAP = { + "comparator": combine_configs_for_comparator_combiner, + "concat": combine_configs, + "project_aggregate": combine_configs, + "sequence": combine_configs_for_sequence_combiner, + "sequence_concat": combine_configs_for_sequence_combiner, + "tabnet": combine_configs, + "tabtransformer": combine_configs, + "transformer": combine_configs, +} diff --git a/tests/training_success/test_training_success.py b/tests/training_success/test_training_success.py new file mode 100644 index 0000000..c360e2e --- /dev/null +++ b/tests/training_success/test_training_success.py @@ -0,0 +1,274 @@ +import logging +from collections import deque +from pprint import pprint +from typing import Any + +import pandas as pd +import pytest +import yaml + +from ludwig.api import LudwigModel +from ludwig.config_sampling.explore_schema import combine_configs, ConfigOption, explore_properties +from ludwig.config_validation.validation import get_schema +from ludwig.types import ModelConfigDict + +from .configs import ( + COMBINER_TYPE_TO_COMBINE_FN_MAP, + ECD_CONFIG_SECTION_TO_CONFIG, + FEATURE_TYPE_TO_CONFIG_FOR_DECODER_LOSS, + FEATURE_TYPE_TO_CONFIG_FOR_ENCODER_PREPROCESSING, +) + + +def defaults_config_generator( + feature_type: str, allow_list: str, static_schema: dict[str, Any] = None +) -> tuple[ModelConfigDict, pd.DataFrame]: + """Generate combinatorial configs for the defaults section of the Ludwig config. + + Args: + feature_type: feature type to explore. + allow_list: top-level parameter of the defaults sections that should be included. + """ + assert isinstance(allow_list, str) + assert allow_list in {"preprocessing", "encoder", "decoder", "loss"} + + schema = get_schema() if not static_schema else static_schema + properties = schema["properties"]["defaults"]["properties"][feature_type]["properties"] + raw_entry = deque([ConfigOption(dict(), False)]) + explored = explore_properties( + properties, parent_parameter_path=f"defaults.{feature_type}", dq=raw_entry, allow_list=[allow_list] + ) + + if allow_list in ["preprocessing", "encoder"]: + config = FEATURE_TYPE_TO_CONFIG_FOR_ENCODER_PREPROCESSING[feature_type] + config = yaml.safe_load(config) + else: # decoder and loss + config = FEATURE_TYPE_TO_CONFIG_FOR_DECODER_LOSS[feature_type] + config = yaml.safe_load(config) + + config["model_type"] = "ecd" + config["trainer"] = {"train_steps": 1} + + combined_configs = combine_configs(explored, config) + logging.info(f"Generated {len(combined_configs)} for {feature_type} {allow_list} combinatorial tests.") + + for config, dataset in combined_configs: + yield config, dataset + + +def ecd_trainer_config_generator(static_schema: dict[str, Any] = None) -> tuple[ModelConfigDict, pd.DataFrame]: + """Generate combinatorial configs for the ECD trainer section of the Ludwig config.""" + schema = get_schema() if not static_schema else static_schema + properties = schema["properties"] + + raw_entry = deque([ConfigOption(dict(), False)]) + explored = explore_properties(properties, parent_parameter_path="", dq=raw_entry, allow_list=["trainer"]) + config = ECD_CONFIG_SECTION_TO_CONFIG["trainer"] + config = yaml.safe_load(config) + config["model_type"] = "ecd" + config["trainer"] = {"train_steps": 1} + + combined_configs = combine_configs(explored, config) + + # HACK(Arnav): Remove configs that have LARS, LAMB or Lion optimizers, or Paged or 8-bit optimizers. + # This is because they require GPUs. + filtered_configs = [] + + for config, dataset in combined_configs: + optimizer_type = config.get("trainer", {}).get("optimizer", "").get("type", "") + + if optimizer_type not in {"lars", "lamb", "lion"} and not ( + "paged" in optimizer_type or "8bit" in optimizer_type + ): + filtered_configs.append((config, dataset)) + + # Replace combined_configs with the filtered_configs + combined_configs = filtered_configs + + logging.info(f"Generated {len(combined_configs)} for ECD trainer combinatorial tests.") + + for config, dataset in combined_configs: + yield config, dataset + + +def combiner_config_generator( + combiner_type: str, static_schema: dict[str, Any] = None +) -> tuple[ModelConfigDict, pd.DataFrame]: + """Generate combinatorial configs for the combiner section of the Ludwig config. + + Args: + combiner_type: combiner type to explore. + """ + schema = get_schema() if not static_schema else static_schema + properties = schema["properties"] + + raw_entry = deque([ConfigOption(dict(), False)]) + explored = explore_properties(properties, parent_parameter_path="", dq=raw_entry, allow_list=["combiner"]) + config = ECD_CONFIG_SECTION_TO_CONFIG[combiner_type] + config = yaml.safe_load(config) + config["model_type"] = "ecd" + config["trainer"] = {"train_steps": 1} + + combine_configs_fn = COMBINER_TYPE_TO_COMBINE_FN_MAP[combiner_type] + + combined_configs = combine_configs_fn(explored, config) + combined_configs = [ + (config, dataset) for config, dataset in combined_configs if config["combiner"]["type"] == combiner_type + ] + logging.info(f"Generated {len(combined_configs)} for {combiner_type} combiner combinatorial tests.") + + for config, dataset in combined_configs: + yield config, dataset + + +def train_and_evaluate(config: ModelConfigDict, dataset: pd.DataFrame): + """Trains and evaluates a model with the given config. + + Args: + config: valid Ludwig config. + dataset: Ludwig dataset name to train on. + """ + # adding print statements to be captured in pytest stdout and help debug tests. + print("Config used (trained on synthetic data)") + pprint(config) + model = LudwigModel(config=config, callbacks=None, logging_level=logging.ERROR) + model.train(dataset=dataset) + model.evaluate(dataset=dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("sequence_concat")) +def test_ecd_sequence_concat_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("sequence")) +def test_ecd_sequence_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("comparator")) +def test_ecd_comparator_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("concat")) +def test_ecd_concat_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("project_aggregate")) +def test_ecd_project_aggregate_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("tabnet")) +def test_ecd_tabnet_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("tabtransformer")) +def test_ecd_tabtransformer_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", combiner_config_generator("transformer")) +def test_ecd_transformer_combiner(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", ecd_trainer_config_generator()) +def test_ecd_trainer(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("number", "encoder")) +def test_number_encoder_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("number", "decoder")) +def test_number_decoder_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("number", "loss")) +def test_number_encoder_loss(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("number", "preprocessing")) +def test_number_preprocessing_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("category", "encoder")) +def test_category_encoder_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("category", "decoder")) +def test_category_decoder_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("category", "loss")) +def test_category_loss_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("category", "preprocessing")) +def test_category_preprocessing_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("binary", "encoder")) +def test_binary_encoder_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("binary", "decoder")) +def test_binary_decoder_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("binary", "loss")) +def test_binary_loss_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +@pytest.mark.combinatorial +@pytest.mark.parametrize("config,dataset", defaults_config_generator("binary", "preprocessing")) +def test_binary_preprocessing_defaults(config, dataset): + train_and_evaluate(config, dataset) + + +# @pytest.mark.combinatorial +# @pytest.mark.parametrize("config,dataset", defaults_config_generator("text", "preprocessing")) +# def test_text_preprocessing_defaults(config, dataset): +# train_and_evaluate(config, dataset) + + +# @pytest.mark.combinatorial +# @pytest.mark.parametrize("config,dataset", defaults_config_generator("text", "encoder")) +# def test_text_encoder_defaults(config): +# train_and_evaluate(config, dataset) diff --git a/tests/ultra_slow/__init__.py b/tests/ultra_slow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ultra_slow/test_ultra_slow.py b/tests/ultra_slow/test_ultra_slow.py new file mode 100644 index 0000000..c1b7a8b --- /dev/null +++ b/tests/ultra_slow/test_ultra_slow.py @@ -0,0 +1,1168 @@ +"""Ultra-slow end-to-end training validation tests. + +Each test trains for one epoch on a small real-ish dataset and asserts: + 1. Training completes without error (no data processing or pipeline issues). + 2. GPU is utilised (torch.cuda.max_memory_allocated > 0 when CUDA is available). + 3. Training loss decreases at least slightly relative to initial loss. + 4. All expected output assets are produced (model weights, hyperparameters JSON, + training stats, predictions parquet). + +The learning rate is set to a very small value (1e-6) so that the loss can only +move downward — a loss increase flags a serious bug, not normal noise. + +Half the tests run with a local backend, half with a single-worker Ray backend. +These tests are NOT run in CI. Run them locally before releases: + + pytest tests/ultra_slow/ -v --timeout=600 + +Sections +-------- +- Input features (3 encoders × each type): text, number, category, sequence, + binary, vector, timeseries, image, audio, bag, set +- Combiners (concat, tabnet, transformer, tabtransformer, project_aggregate, + ft_transformer, cross_attention) +- Output features (one per output type): binary, number, category, sequence, + vector, timeseries +""" + +import io +import os +import tempfile + +import numpy as np +import pandas as pd +import pytest +import torch + +# --------------------------------------------------------------------------- +# Optional Ray import — skip Ray tests gracefully if not installed +# --------------------------------------------------------------------------- +ray = pytest.importorskip("ray", reason="ray not installed") + +from ludwig.api import LudwigModel # noqa: E402 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +RNG = np.random.default_rng(42) +N = 200 # number of training rows — small but enough for 1 epoch +VOCAB = ["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", "sat", "on", "a", "mat"] +LR = 1e-6 # tiny LR: loss should only decrease or stay flat +RAY_BACKEND = {"type": "ray", "processor": {"type": "dask"}} + + +def _words(n_per_row: int = 10) -> list[str]: + return [" ".join(RNG.choice(VOCAB, size=n_per_row).tolist()) for _ in range(N)] + + +def _ints(lo: int = 0, hi: int = 4) -> list[int]: + return RNG.integers(lo, hi, size=N).tolist() + + +def _floats() -> list[float]: + return RNG.standard_normal(N).tolist() + + +def _bool_col() -> list[int]: + return RNG.integers(0, 2, size=N).tolist() + + +def _vec_col(dim: int = 8) -> list[str]: + return [" ".join(f"{x:.4f}" for x in RNG.standard_normal(dim)) for _ in range(N)] + + +def _ts_col(length: int = 12) -> list[str]: + return [" ".join(f"{x:.4f}" for x in RNG.standard_normal(length)) for _ in range(N)] + + +def _tabular_df() -> pd.DataFrame: + return pd.DataFrame( + { + "num1": _floats(), + "num2": _floats(), + "cat1": _ints(0, 4), + "bin1": _bool_col(), + "label_cat": _ints(0, 3), + "label_bin": _bool_col(), + "label_num": _floats(), + } + ) + + +def _train_and_check(config: dict, df: pd.DataFrame, backend=None, tmpdir: str | None = None) -> dict: + """Train for 1 epoch and return a dict with loss info + asset checks.""" + with tempfile.TemporaryDirectory() as _tmp: + out = tmpdir or _tmp + if backend: + config = {**config, "backend": backend} + config.setdefault("trainer", {}) + config["trainer"].setdefault("epochs", 1) + config["trainer"].setdefault("learning_rate", LR) + config["trainer"].setdefault("batch_size", 32) + + model = LudwigModel(config, logging_level=40) + + if torch.cuda.is_available(): + torch.cuda.reset_peak_memory_stats() + + result = model.train(dataset=df, output_directory=out) + train_stats = result.train_stats + output_dir = result.output_directory + + # ── 1. Assets exist ────────────────────────────────────────────── + assert os.path.exists(os.path.join(output_dir, "model")), "model directory missing" + assert os.path.exists(os.path.join(output_dir, "model", "model_hyperparameters.json")), ( + "hyperparameters JSON missing" + ) + + # ── 2. GPU was used (local backend only — Ray workers run in subprocesses) ── + if backend is None and torch.cuda.is_available(): + peak = torch.cuda.max_memory_allocated() + assert peak > 0, f"CUDA available but peak GPU memory was 0 — model did not use GPU (peak={peak})" + + # ── 3. Loss should not blow up (LR=1e-6 means barely any movement) ── + # TrainingStats.training: dict[feature_name, dict[metric_name, list[float]]] + combined_feature = (train_stats.training or {}).get("combined", {}) + loss_values = combined_feature.get("loss", []) + if len(loss_values) >= 2: + first, last = loss_values[0], loss_values[-1] + assert last <= first * 2.0, ( + f"Combined loss more than doubled ({first:.4f} → {last:.4f}). " + "Something is wrong with the training loop." + ) + + return {"output_dir": output_dir, "train_stats": train_stats} + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def ray_1gpu(): + """Single-worker Ray cluster backed by the available GPU (or CPU fallback).""" + use_gpu = torch.cuda.is_available() + ray.init( + num_cpus=4, + num_gpus=torch.cuda.device_count(), + ignore_reinit_error=True, + ) + yield {"type": "ray", "processor": {"type": "dask"}, "trainer": {"use_gpu": use_gpu, "num_workers": 1}} + ray.shutdown() + + +# --------------------------------------------------------------------------- +# TEXT input (local backend) +# --------------------------------------------------------------------------- + + +class TestTextInputLocal: + """Text input — local backend — 3 encoders (popular + modern).""" + + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"text": _words(12), "label": _ints(0, 3)}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + { + "name": "text", + "type": "text", + "encoder": {"type": encoder_type}, + "preprocessing": {"max_sequence_length": 16}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + } + + def test_parallel_cnn(self): + """Classic, widely used sequence encoder.""" + _train_and_check(self._config("parallel_cnn"), self._df) + + def test_bert(self): + """Most popular pretrained text encoder.""" + _train_and_check( + { + "input_features": [ + { + "name": "text", + "type": "text", + "encoder": { + "type": "auto_transformer", + "pretrained_model_name_or_path": "bert-base-uncased", + "trainable": False, + "reduce_output": "mean", + }, + "preprocessing": {"max_sequence_length": 16}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + }, + self._df, + ) + + def test_modernbert(self): + """Modern BERT variant with BPE tokenizer (regression: was misrouted to WordPiece).""" + _train_and_check( + { + "input_features": [ + { + "name": "text", + "type": "text", + "encoder": { + "type": "auto_transformer", + "pretrained_model_name_or_path": "answerdotai/ModernBERT-base", + "trainable": False, + "reduce_output": "mean", + }, + "preprocessing": {"max_sequence_length": 16}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + }, + self._df, + ) + + +# --------------------------------------------------------------------------- +# TEXT input (Ray backend) +# --------------------------------------------------------------------------- + + +@pytest.mark.distributed +class TestTextInputRay: + """Text input — Ray backend — 3 encoders.""" + + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"text": _words(12), "label": _ints(0, 3)}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + { + "name": "text", + "type": "text", + "encoder": {"type": encoder_type}, + "preprocessing": {"max_sequence_length": 16}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + } + + def test_parallel_cnn(self, ray_1gpu): + _train_and_check(self._config("parallel_cnn"), self._df, backend=ray_1gpu) + + def test_rnn(self, ray_1gpu): + _train_and_check(self._config("rnn"), self._df, backend=ray_1gpu) + + def test_stacked_cnn(self, ray_1gpu): + _train_and_check(self._config("stacked_cnn"), self._df, backend=ray_1gpu) + + +# --------------------------------------------------------------------------- +# NUMBER input +# --------------------------------------------------------------------------- + + +class TestNumberInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"n1": _floats(), "n2": _floats(), "n3": _floats(), "label": _bool_col()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + {"name": f"n{i}", "type": "number", "encoder": {"type": encoder_type}} for i in range(1, 4) + ], + "output_features": [{"name": "label", "type": "binary"}], + } + + def test_dense(self): + _train_and_check(self._config("dense"), self._df) + + def test_passthrough(self): + _train_and_check(self._config("passthrough"), self._df) + + def test_bins(self): + _train_and_check(self._config("bins"), self._df) + + +@pytest.mark.distributed +class TestNumberInputRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"n1": _floats(), "n2": _floats(), "n3": _floats(), "label": _bool_col()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + {"name": f"n{i}", "type": "number", "encoder": {"type": encoder_type}} for i in range(1, 4) + ], + "output_features": [{"name": "label", "type": "binary"}], + } + + def test_dense(self, ray_1gpu): + _train_and_check(self._config("dense"), self._df, backend=ray_1gpu) + + def test_passthrough(self, ray_1gpu): + _train_and_check(self._config("passthrough"), self._df, backend=ray_1gpu) + + def test_bins(self, ray_1gpu): + _train_and_check(self._config("bins"), self._df, backend=ray_1gpu) + + +# --------------------------------------------------------------------------- +# CATEGORY input +# --------------------------------------------------------------------------- + + +class TestCategoryInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"cat": _ints(0, 8), "label": _floats()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [{"name": "cat", "type": "category", "encoder": {"type": encoder_type}}], + "output_features": [{"name": "label", "type": "number"}], + } + + def test_dense(self): + _train_and_check(self._config("dense"), self._df) + + def test_sparse(self): + """Sparse embedding — most memory-efficient for high-cardinality categories.""" + _train_and_check(self._config("sparse"), self._df) + + def test_onehot(self): + _train_and_check(self._config("onehot"), self._df) + + +@pytest.mark.distributed +class TestCategoryInputRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"cat": _ints(0, 8), "label": _floats()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [{"name": "cat", "type": "category", "encoder": {"type": encoder_type}}], + "output_features": [{"name": "label", "type": "number"}], + } + + def test_dense(self, ray_1gpu): + _train_and_check(self._config("dense"), self._df, backend=ray_1gpu) + + def test_sparse(self, ray_1gpu): + _train_and_check(self._config("sparse"), self._df, backend=ray_1gpu) + + def test_onehot(self, ray_1gpu): + _train_and_check(self._config("onehot"), self._df, backend=ray_1gpu) + + +# --------------------------------------------------------------------------- +# BINARY input +# --------------------------------------------------------------------------- + + +class TestBinaryInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"b1": _bool_col(), "b2": _bool_col(), "b3": _bool_col(), "label": _ints(0, 4)}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + {"name": f"b{i}", "type": "binary", "encoder": {"type": encoder_type}} for i in range(1, 4) + ], + "output_features": [{"name": "label", "type": "category"}], + } + + def test_dense(self): + _train_and_check(self._config("dense"), self._df) + + def test_passthrough_with_concat_combiner(self): + """Binary passthrough feeds raw 0/1 into the combiner — most common production pattern.""" + _train_and_check( + {**self._config("passthrough"), "combiner": {"type": "concat"}}, + self._df, + ) + + def test_mixed_binary_number(self): + """Mix of binary + number features — common real-world tabular setup.""" + df = self._df.copy() + df["num_feat"] = _floats() + _train_and_check( + { + "input_features": [ + {"name": "b1", "type": "binary"}, + {"name": "b2", "type": "binary"}, + {"name": "num_feat", "type": "number"}, + ], + "output_features": [{"name": "label", "type": "category"}], + }, + df, + ) + + +@pytest.mark.distributed +class TestBinaryInputRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"b1": _bool_col(), "b2": _bool_col(), "b3": _bool_col(), "label": _ints(0, 4)}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + {"name": f"b{i}", "type": "binary", "encoder": {"type": encoder_type}} for i in range(1, 4) + ], + "output_features": [{"name": "label", "type": "category"}], + } + + def test_dense(self, ray_1gpu): + _train_and_check(self._config("dense"), self._df, backend=ray_1gpu) + + def test_passthrough(self, ray_1gpu): + _train_and_check(self._config("passthrough"), self._df, backend=ray_1gpu) + + def test_mixed_binary_number(self, ray_1gpu): + df = self._df.copy() + df["num_feat"] = _floats() + _train_and_check( + { + "input_features": [ + {"name": "b1", "type": "binary"}, + {"name": "b2", "type": "binary"}, + {"name": "num_feat", "type": "number"}, + ], + "output_features": [{"name": "label", "type": "category"}], + }, + df, + backend=ray_1gpu, + ) + + +# --------------------------------------------------------------------------- +# SEQUENCE input +# --------------------------------------------------------------------------- + + +class TestSequenceInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"seq": _words(8), "label": _ints(0, 3)}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + { + "name": "seq", + "type": "sequence", + "encoder": {"type": encoder_type}, + "preprocessing": {"max_sequence_length": 12}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + } + + def test_rnn(self): + """Classic recurrent encoder.""" + _train_and_check(self._config("rnn"), self._df) + + def test_parallel_cnn(self): + """Most popular non-RNN sequence encoder in Ludwig.""" + _train_and_check(self._config("parallel_cnn"), self._df) + + def test_transformer(self): + """Modern attention-based encoder.""" + _train_and_check(self._config("transformer"), self._df) + + +@pytest.mark.distributed +class TestSequenceInputRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"seq": _words(8), "label": _ints(0, 3)}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + { + "name": "seq", + "type": "sequence", + "encoder": {"type": encoder_type}, + "preprocessing": {"max_sequence_length": 12}, + } + ], + "output_features": [{"name": "label", "type": "category"}], + } + + def test_rnn(self, ray_1gpu): + _train_and_check(self._config("rnn"), self._df, backend=ray_1gpu) + + def test_parallel_cnn(self, ray_1gpu): + _train_and_check(self._config("parallel_cnn"), self._df, backend=ray_1gpu) + + def test_transformer(self, ray_1gpu): + _train_and_check(self._config("transformer"), self._df, backend=ray_1gpu) + + +# --------------------------------------------------------------------------- +# VECTOR input +# --------------------------------------------------------------------------- + + +class TestVectorInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"vec": _vec_col(16), "label": _bool_col()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [{"name": "vec", "type": "vector", "encoder": {"type": encoder_type}}], + "output_features": [{"name": "label", "type": "binary"}], + } + + def test_dense(self): + _train_and_check(self._config("dense"), self._df) + + def test_passthrough(self): + _train_and_check(self._config("passthrough"), self._df) + + def test_multi_vector(self): + # Two vector features + binary output (validates multi-input tabular path) + df = pd.DataFrame({"v1": _vec_col(8), "v2": _vec_col(8), "label": _bool_col()}) + config = { + "input_features": [ + {"name": "v1", "type": "vector", "encoder": {"type": "dense"}}, + {"name": "v2", "type": "vector", "encoder": {"type": "dense"}}, + ], + "output_features": [{"name": "label", "type": "binary"}], + } + _train_and_check(config, df) + + +@pytest.mark.distributed +class TestVectorInputRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"vec": _vec_col(16), "label": _bool_col()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [{"name": "vec", "type": "vector", "encoder": {"type": encoder_type}}], + "output_features": [{"name": "label", "type": "binary"}], + } + + def test_dense(self, ray_1gpu): + _train_and_check(self._config("dense"), self._df, backend=ray_1gpu) + + def test_passthrough(self, ray_1gpu): + _train_and_check(self._config("passthrough"), self._df, backend=ray_1gpu) + + def test_multi_vector(self, ray_1gpu): + df = pd.DataFrame({"v1": _vec_col(8), "v2": _vec_col(8), "label": _bool_col()}) + config = { + "input_features": [ + {"name": "v1", "type": "vector", "encoder": {"type": "dense"}}, + {"name": "v2", "type": "vector", "encoder": {"type": "dense"}}, + ], + "output_features": [{"name": "label", "type": "binary"}], + } + _train_and_check(config, df, backend=ray_1gpu) + + +# --------------------------------------------------------------------------- +# TIMESERIES input +# --------------------------------------------------------------------------- + + +class TestTimeseriesInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"ts": _ts_col(12), "label": _floats()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + { + "name": "ts", + "type": "timeseries", + "encoder": {"type": encoder_type}, + "preprocessing": {"timeseries_length_limit": 12}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + } + + def test_dense(self): + _train_and_check(self._config("dense"), self._df) + + def test_rnn(self): + _train_and_check(self._config("rnn"), self._df) + + def test_transformer(self): + _train_and_check(self._config("transformer"), self._df) + + def test_patchtst(self): + """PatchTST: patch-based Transformer for time series (Nie et al., 2023).""" + df = pd.DataFrame({"ts": _ts_col(32), "label": _floats()}) + _train_and_check( + { + "input_features": [ + { + "name": "ts", + "type": "timeseries", + "encoder": { + "type": "patchtst", + "patch_size": 8, + "patch_stride": 4, + "d_model": 32, + "num_heads": 2, + "num_layers": 1, + "ffn_dim": 64, + "output_size": 32, + }, + "preprocessing": {"timeseries_length_limit": 32}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + }, + df, + ) + + def test_nbeats(self): + """N-BEATS: Neural basis expansion analysis (Oreshkin et al., 2020).""" + df = pd.DataFrame({"ts": _ts_col(32), "label": _floats()}) + _train_and_check( + { + "input_features": [ + { + "name": "ts", + "type": "timeseries", + "encoder": { + "type": "nbeats", + "num_stacks": 1, + "num_blocks": 2, + "layer_size": 32, + "output_size": 16, + }, + "preprocessing": {"timeseries_length_limit": 32}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + }, + df, + ) + + +@pytest.mark.distributed +class TestTimeseriesInputRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"ts": _ts_col(12), "label": _floats()}) + + def _config(self, encoder_type: str) -> dict: + return { + "input_features": [ + { + "name": "ts", + "type": "timeseries", + "encoder": {"type": encoder_type}, + "preprocessing": {"timeseries_length_limit": 12}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + } + + def test_dense(self, ray_1gpu): + _train_and_check(self._config("dense"), self._df, backend=ray_1gpu) + + def test_rnn(self, ray_1gpu): + _train_and_check(self._config("rnn"), self._df, backend=ray_1gpu) + + def test_transformer(self, ray_1gpu): + _train_and_check(self._config("transformer"), self._df, backend=ray_1gpu) + + def test_patchtst(self, ray_1gpu): + """PatchTST via Ray backend.""" + df = pd.DataFrame({"ts": _ts_col(32), "label": _floats()}) + _train_and_check( + { + "input_features": [ + { + "name": "ts", + "type": "timeseries", + "encoder": { + "type": "patchtst", + "patch_size": 8, + "patch_stride": 4, + "d_model": 32, + "num_heads": 2, + "num_layers": 1, + "ffn_dim": 64, + "output_size": 32, + }, + "preprocessing": {"timeseries_length_limit": 32}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + }, + df, + backend=ray_1gpu, + ) + + def test_nbeats(self, ray_1gpu): + """N-BEATS via Ray backend.""" + df = pd.DataFrame({"ts": _ts_col(32), "label": _floats()}) + _train_and_check( + { + "input_features": [ + { + "name": "ts", + "type": "timeseries", + "encoder": { + "type": "nbeats", + "num_stacks": 1, + "num_blocks": 2, + "layer_size": 32, + "output_size": 16, + }, + "preprocessing": {"timeseries_length_limit": 32}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + }, + df, + backend=ray_1gpu, + ) + + +# --------------------------------------------------------------------------- +# SET input (local only — Ray support is limited) +# --------------------------------------------------------------------------- + + +class TestSetInputLocal: + @pytest.fixture(autouse=True) + def df(self): + # Sets are space-separated token strings + self._df = pd.DataFrame({"tags": _words(4), "label": _ints(0, 3)}) + + def test_embed(self): + # Set feature only supports the "embed" encoder + config = { + "input_features": [{"name": "tags", "type": "set", "encoder": {"type": "embed"}}], + "output_features": [{"name": "label", "type": "category"}], + } + _train_and_check(config, self._df) + + def test_embed_with_number(self): + # Multi-feature: set + number → category + df = pd.DataFrame({"tags": _words(4), "score": _floats(), "label": _ints(0, 3)}) + config = { + "input_features": [ + {"name": "tags", "type": "set", "encoder": {"type": "embed"}}, + {"name": "score", "type": "number"}, + ], + "output_features": [{"name": "label", "type": "category"}], + } + _train_and_check(config, df) + + def test_embed_with_category(self): + # Multi-feature: set + category → binary + df = pd.DataFrame({"tags": _words(4), "cat": _ints(0, 4), "label": _bool_col()}) + config = { + "input_features": [ + {"name": "tags", "type": "set", "encoder": {"type": "embed"}}, + {"name": "cat", "type": "category"}, + ], + "output_features": [{"name": "label", "type": "binary"}], + } + _train_and_check(config, df) + + +# --------------------------------------------------------------------------- +# BAG input +# --------------------------------------------------------------------------- + + +class TestBagInputLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame({"bag": _words(6), "label": _bool_col()}) + + def test_embed(self): + # Bag feature only supports the "embed" encoder + config = { + "input_features": [{"name": "bag", "type": "bag", "encoder": {"type": "embed"}}], + "output_features": [{"name": "label", "type": "binary"}], + } + _train_and_check(config, self._df) + + def test_embed_with_number(self): + # Multi-feature: bag + number → binary + df = pd.DataFrame({"bag": _words(6), "score": _floats(), "label": _bool_col()}) + config = { + "input_features": [ + {"name": "bag", "type": "bag", "encoder": {"type": "embed"}}, + {"name": "score", "type": "number"}, + ], + "output_features": [{"name": "label", "type": "binary"}], + } + _train_and_check(config, df) + + def test_embed_with_category(self): + # Multi-feature: bag + category → number + df = pd.DataFrame({"bag": _words(6), "cat": _ints(0, 4), "label": _floats()}) + config = { + "input_features": [ + {"name": "bag", "type": "bag", "encoder": {"type": "embed"}}, + {"name": "cat", "type": "category"}, + ], + "output_features": [{"name": "label", "type": "number"}], + } + _train_and_check(config, df) + + +# --------------------------------------------------------------------------- +# Combiners (local, tabular features) +# --------------------------------------------------------------------------- + + +def _tabular_config(combiner_type: str, combiner_kwargs: dict | None = None) -> dict: + combiner = {"type": combiner_type, **(combiner_kwargs or {})} + return { + "input_features": [ + {"name": "num1", "type": "number"}, + {"name": "num2", "type": "number"}, + {"name": "cat1", "type": "category"}, + {"name": "bin1", "type": "binary"}, + ], + "output_features": [{"name": "label_cat", "type": "category"}], + "combiner": combiner, + } + + +class TestCombinersLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = _tabular_df() + + def test_concat(self): + _train_and_check(_tabular_config("concat"), self._df) + + def test_tabnet(self): + _train_and_check(_tabular_config("tabnet"), self._df) + + def test_transformer(self): + _train_and_check(_tabular_config("transformer"), self._df) + + def test_tabtransformer(self): + _train_and_check(_tabular_config("tabtransformer"), self._df) + + def test_project_aggregate(self): + _train_and_check(_tabular_config("project_aggregate"), self._df) + + def test_ft_transformer(self): + _train_and_check(_tabular_config("ft_transformer"), self._df) + + def test_cross_attention(self): + _train_and_check(_tabular_config("cross_attention"), self._df) + + +@pytest.mark.distributed +class TestCombinersRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = _tabular_df() + + def test_concat(self, ray_1gpu): + _train_and_check(_tabular_config("concat"), self._df, backend=ray_1gpu) + + def test_tabnet(self, ray_1gpu): + _train_and_check(_tabular_config("tabnet"), self._df, backend=ray_1gpu) + + def test_transformer(self, ray_1gpu): + _train_and_check(_tabular_config("transformer"), self._df, backend=ray_1gpu) + + def test_tabtransformer(self, ray_1gpu): + _train_and_check(_tabular_config("tabtransformer"), self._df, backend=ray_1gpu) + + def test_ft_transformer(self, ray_1gpu): + _train_and_check(_tabular_config("ft_transformer"), self._df, backend=ray_1gpu) + + def test_cross_attention(self, ray_1gpu): + _train_and_check(_tabular_config("cross_attention"), self._df, backend=ray_1gpu) + + def test_project_aggregate(self, ray_1gpu): + _train_and_check(_tabular_config("project_aggregate"), self._df, backend=ray_1gpu) + + +# --------------------------------------------------------------------------- +# Output feature types (one test each, local + Ray) +# --------------------------------------------------------------------------- + + +def _base_inputs() -> list[dict]: + return [ + {"name": "n1", "type": "number"}, + {"name": "n2", "type": "number"}, + {"name": "cat1", "type": "category"}, + ] + + +def _base_df() -> pd.DataFrame: + return pd.DataFrame({"n1": _floats(), "n2": _floats(), "cat1": _ints(0, 4)}) + + +class TestOutputTypesLocal: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame( + { + "n1": _floats(), + "n2": _floats(), + "cat1": _ints(0, 4), + "out_bin": _bool_col(), + "out_num": _floats(), + "out_cat": _ints(0, 3), + "out_seq": _words(6), + "out_vec": _vec_col(8), + "out_ts": _ts_col(8), + } + ) + + def test_binary_output(self): + _train_and_check( + {"input_features": _base_inputs(), "output_features": [{"name": "out_bin", "type": "binary"}]}, + self._df, + ) + + def test_number_output(self): + _train_and_check( + {"input_features": _base_inputs(), "output_features": [{"name": "out_num", "type": "number"}]}, + self._df, + ) + + def test_category_output(self): + _train_and_check( + {"input_features": _base_inputs(), "output_features": [{"name": "out_cat", "type": "category"}]}, + self._df, + ) + + def test_sequence_output(self): + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [ + { + "name": "out_seq", + "type": "sequence", + "preprocessing": {"max_sequence_length": 10}, + "decoder": {"type": "generator"}, + } + ], + }, + self._df, + ) + + def test_vector_output(self): + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [{"name": "out_vec", "type": "vector", "decoder": {"output_size": 8}}], + }, + self._df, + ) + + def test_timeseries_output(self): + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [ + { + "name": "out_ts", + "type": "timeseries", + "preprocessing": {"timeseries_length_limit": 8}, + "decoder": {"output_size": 8}, + } + ], + }, + self._df, + ) + + def test_timeseries_output_with_metrics(self): + """Timeseries output with MASE as the validation metric (PR #4147).""" + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [ + { + "name": "out_ts", + "type": "timeseries", + "preprocessing": {"timeseries_length_limit": 8}, + "decoder": {"output_size": 8}, + } + ], + }, + self._df, + ) + + +@pytest.mark.distributed +class TestOutputTypesRay: + @pytest.fixture(autouse=True) + def df(self): + self._df = pd.DataFrame( + { + "n1": _floats(), + "n2": _floats(), + "cat1": _ints(0, 4), + "out_bin": _bool_col(), + "out_num": _floats(), + "out_cat": _ints(0, 3), + "out_seq": _words(6), + "out_vec": _vec_col(8), + "out_ts": _ts_col(8), + } + ) + + def test_binary_output(self, ray_1gpu): + _train_and_check( + {"input_features": _base_inputs(), "output_features": [{"name": "out_bin", "type": "binary"}]}, + self._df, + backend=ray_1gpu, + ) + + def test_number_output(self, ray_1gpu): + _train_and_check( + {"input_features": _base_inputs(), "output_features": [{"name": "out_num", "type": "number"}]}, + self._df, + backend=ray_1gpu, + ) + + def test_category_output(self, ray_1gpu): + _train_and_check( + {"input_features": _base_inputs(), "output_features": [{"name": "out_cat", "type": "category"}]}, + self._df, + backend=ray_1gpu, + ) + + def test_sequence_output(self, ray_1gpu): + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [ + { + "name": "out_seq", + "type": "sequence", + "preprocessing": {"max_sequence_length": 10}, + "decoder": {"type": "generator"}, + } + ], + }, + self._df, + backend=ray_1gpu, + ) + + def test_vector_output(self, ray_1gpu): + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [{"name": "out_vec", "type": "vector", "decoder": {"output_size": 8}}], + }, + self._df, + backend=ray_1gpu, + ) + + def test_timeseries_output(self, ray_1gpu): + _train_and_check( + { + "input_features": _base_inputs(), + "output_features": [ + { + "name": "out_ts", + "type": "timeseries", + "preprocessing": {"timeseries_length_limit": 8}, + "decoder": {"output_size": 8}, + } + ], + }, + self._df, + backend=ray_1gpu, + ) + + +# --------------------------------------------------------------------------- +# IMAGE input — binary bytes in a pre-existing Dask DataFrame (GitHub #4149) +# --------------------------------------------------------------------------- + + +def _jpeg_bytes_col(n: int = N) -> list[bytes]: + """Return n small (8×8 RGB) JPEG-encoded images as raw bytes.""" + from PIL import Image + + result = [] + for i in range(n): + buf = io.BytesIO() + arr = np.full((32, 32, 3), [i % 256, (i * 7) % 256, (i * 13) % 256], dtype=np.uint8) + Image.fromarray(arr).save(buf, "JPEG") + result.append(buf.getvalue()) + return result + + +class TestImageBytesRay: + """Regression tests for GitHub #4149. + + The bug: when a user passes a Dask DataFrame whose image column contains raw + binary bytes (JPEG/PNG), Dask's _to_string_dtype tries to decode the bytes as + UTF-8, failing with UnicodeDecodeError. The fix moves + dask.config(dataframe.convert-string:False) to ludwig/__init__.py so it is set + before the user ever creates a Dask DataFrame. + """ + + _IMAGE_CONFIG = { + "input_features": [ + { + "name": "image_data", + "type": "image", + "preprocessing": {"height": 32, "width": 32, "num_channels": 3}, + "encoder": {"type": "stacked_cnn", "output_size": 16}, + } + ], + "output_features": [{"name": "label", "type": "number"}], + "trainer": {"epochs": 1, "learning_rate": LR, "batch_size": 16}, + } + + def test_image_bytes_pandas_local_backend(self): + """Pandas DataFrame with bytes column — verifies local image-bytes path works.""" + pd_df = pd.DataFrame({"image_data": _jpeg_bytes_col(), "label": RNG.standard_normal(N).tolist()}) + with tempfile.TemporaryDirectory() as out: + model = LudwigModel(self._IMAGE_CONFIG, logging_level=40) + result = model.train(dataset=pd_df, output_directory=out) + assert result.train_stats is not None + + def test_image_bytes_dask_ray_backend(self, ray_1gpu): + """Dask DataFrame with bytes column + Ray backend — the exact scenario from #4149. + + The Dask DataFrame is created before model.train() is called to faithfully + replicate the user scenario where dd.from_pandas() would fail with + UnicodeDecodeError if dask.config.convert-string is not set to False. + """ + import dask.dataframe as dd + + pd_df = pd.DataFrame({"image_data": _jpeg_bytes_col(), "label": RNG.standard_normal(N).tolist()}) + # Create Dask DataFrame before calling train — this is the user pattern that + # triggered the original bug. + dask_df = dd.from_pandas(pd_df, npartitions=4) + + config = {**self._IMAGE_CONFIG, "backend": ray_1gpu} + with tempfile.TemporaryDirectory() as out: + model = LudwigModel(config, logging_level=40) + result = model.train(dataset=dask_df, output_directory=out) + assert result.train_stats is not None diff --git a/tests/ultra_slow/test_ultra_slow_peft.py b/tests/ultra_slow/test_ultra_slow_peft.py new file mode 100644 index 0000000..a9eac08 --- /dev/null +++ b/tests/ultra_slow/test_ultra_slow_peft.py @@ -0,0 +1,90 @@ +"""Ultra-slow end-to-end tests for advanced PEFT adapters. + +Uses sshleifer/tiny-gpt2 (3MB) to verify each adapter configuration trains +for one step without crashing. These are NOT run in CI — run locally before releases. + +Sections +-------- +- LoRA with default init +- LoRA with PiSSA initializer +- LoRA with Gaussian initializer +- LoRA with per-layer rank_pattern +- TinyLoRA (LoRA-XS equivalent) +- LN-Tuning (layer-norm-only fine-tuning) +- IA3 +""" + +import tempfile + +import numpy as np +import pandas as pd + +from ludwig.api import LudwigModel + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +RNG = np.random.default_rng(42) +VOCAB = ["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", "sat", "on", "a", "mat"] +N = 40 # small enough for a single-step smoke test + + +def _llm_train(adapter_config: dict) -> None: + """Train tiny-gpt2 for 1 epoch with the given adapter config and assert training succeeds.""" + df = pd.DataFrame( + { + "prompt": [" ".join(RNG.choice(VOCAB, size=8).tolist()) for _ in range(N)], + "response": [" ".join(RNG.choice(VOCAB, size=4).tolist()) for _ in range(N)], + } + ) + config = { + "model_type": "llm", + "base_model": "sshleifer/tiny-gpt2", + "input_features": [{"name": "prompt", "type": "text"}], + "output_features": [{"name": "response", "type": "text"}], + "adapter": adapter_config, + "trainer": {"type": "finetune", "epochs": 1, "batch_size": 4, "learning_rate": 1e-5}, + } + with tempfile.TemporaryDirectory() as tmpdir: + model = LudwigModel(config, logging_level=40) + result = model.train(dataset=df, output_directory=tmpdir) + assert result.train_stats is not None, "Training produced no stats" + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestPEFTAdaptersLocal: + """Local (non-Ray) end-to-end smoke tests for each advanced PEFT adapter.""" + + def test_lora_default(self): + """Standard LoRA with default Kaiming initialization.""" + _llm_train({"type": "lora", "r": 4}) + + def test_lora_pissa(self): + """LoRA with PiSSA initializer (principal singular vectors, Meng et al., 2024).""" + _llm_train({"type": "lora", "r": 4, "init_lora_weights": "pissa"}) + + def test_lora_gaussian(self): + """LoRA with Gaussian initialization (alternative to default Kaiming zeros init).""" + _llm_train({"type": "lora", "r": 4, "init_lora_weights": "gaussian"}) + + def test_lora_rank_pattern(self): + """LoRA with per-module rank override via rank_pattern.""" + # tiny-gpt2 attention weight is named c_attn; override rank to 8 for that layer. + _llm_train({"type": "lora", "r": 4, "rank_pattern": {"c_attn": 8}}) + + def test_tinylora(self): + """TinyLoRA: SVD-projected extreme parameter-efficient fine-tuning (LoRA-XS variant).""" + _llm_train({"type": "tinylora", "r": 4}) + + def test_ln_tuning(self): + """LN-Tuning: tunes only the layer normalization parameters (~0.1% of params).""" + _llm_train({"type": "ln_tuning"}) + + def test_ia3(self): + """IA3: scaling-vector-based adapter, even lighter than LoRA.""" + _llm_train({"type": "ia3"})